From 713bb4ef5eea34461cb67dacdfd3a866e4ded763 Mon Sep 17 00:00:00 2001 From: Robert Bragg Date: Tue, 10 May 2022 01:33:57 +0100 Subject: [PATCH] Android: rework backend to use android-activity crate This updates the Android backend to use the android-activity crate instead of ndk-glue. This solves a few issues: 1. The backend is agnostic of the application's choice of Activity base class 2. Winit is no longer responsible for handling any Java synchronization details, since these are encapsulated by the design of android_activity 3. The backend no longer depends on global / static getters for state such as the native_window() which puts it in a better position to support running multiple activities within a single Android process. 4. Redraw requests are flagged, not queued, in a way that avoids taking priority over user events (resolves #2299) To make it possible for application crates to avoid explicitly depending on the `android-activity` crate (and avoid version conflicts) this re-exports the android-activity crate under: `winit::platform::android::activity::*` This also adds `android-native-activity` and `android-game-activity` features that set the corresponding android-activity features. Addresses: PR https://github.com/rust-windowing/winit/pull/1892 Addresses: PR https://github.com/rust-windowing/winit/pull/2307 Addresses: PR https://github.com/rust-windowing/winit/pull/2343 Addresses: #2293 Resolves: #2299 --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 1 + Cargo.toml | 4 +- FEATURES.md | 22 +- README.md | 89 ++- src/event_loop.rs | 10 + src/platform/android.rs | 50 +- src/platform_impl/android/mod.rs | 993 +++++++++++++++++++------------ 8 files changed, 744 insertions(+), 427 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9504d561de0..a797dc6b578 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: - { target: x86_64-unknown-linux-gnu, os: ubuntu-latest, } - { target: x86_64-unknown-linux-gnu, os: ubuntu-latest, options: --no-default-features, features: x11 } - { target: x86_64-unknown-linux-gnu, os: ubuntu-latest, options: --no-default-features, features: "wayland,wayland-dlopen" } - - { target: aarch64-linux-android, os: ubuntu-latest, cmd: 'apk --' } + - { target: aarch64-linux-android, os: ubuntu-latest, cmd: 'apk --', features: "android-native-activity" } - { target: x86_64-apple-darwin, os: macos-latest, } - { target: x86_64-apple-ios, os: macos-latest, } - { target: aarch64-apple-ios, os: macos-latest, } diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b745bc1487..4207fdab1ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ And please only add new entries to the top of this list, right below the `# Unre - **Breaking:** Removed `WindowBuilderExtWindows::with_theme` and `WindowBuilderExtWayland::with_wayland_csd_theme` in favour of `WindowBuilder::with_theme`. - **Breaking:** Removed `WindowExtWindows::theme` in favour of `Window::theme`. - Enabled `doc_auto_cfg` when generating docs on docs.rs for feature labels. +- **Breaking:** On Android, switched to using [`android-activity`](https://github.com/rib/android-activity) crate as a glue layer instead of [`ndk-glue](https://github.com/rust-windowing/android-ndk-rs/tree/master/ndk-glue). See [README.md#Android](https://github.com/rust-windowing/winit#Android) for more details. ([#2444](https://github.com/rust-windowing/winit/pull/2444)) # 0.27.5 diff --git a/Cargo.toml b/Cargo.toml index 40f56ffa551..5bc82863496 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,8 @@ wayland-dlopen = ["sctk/dlopen", "wayland-client/dlopen"] wayland-csd-adwaita = ["sctk-adwaita", "sctk-adwaita/ab_glyph"] wayland-csd-adwaita-crossfont = ["sctk-adwaita", "sctk-adwaita/crossfont"] wayland-csd-adwaita-notitle = ["sctk-adwaita"] +android-native-activity = [ "android-activity/native-activity" ] +android-game-activity = [ "android-activity/game-activity" ] [dependencies] instant = { version = "0.1", features = ["wasm-bindgen"] } @@ -60,7 +62,7 @@ simple_logger = { version = "2.1.0", default_features = false } [target.'cfg(target_os = "android")'.dependencies] # Coordinate the next winit release with android-ndk-rs: https://github.com/rust-windowing/winit/issues/1995 ndk = "0.7.0" -ndk-glue = "0.7.0" +android-activity = "0.4.0-beta.1" [target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] objc2 = "=0.3.0-beta.3" diff --git a/FEATURES.md b/FEATURES.md index 580d05555cd..23356189891 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -188,41 +188,41 @@ Legend: |Fullscreen |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ |✔️ | |Fullscreen toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ |✔️ | |Exclusive fullscreen |✔️ |✔️ |✔️ |**N/A** |❌ |✔️ |**N/A**| -|HiDPI support |✔️ |✔️ |✔️ |✔️ |▢[#721]|✔️ |✔️ | +|HiDPI support |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ | |Popup windows |❌ |❌ |❌ |❌ |❌ |❌ |**N/A**| ### System information |Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |WASM | |---------------- | ----- | ---- | ------- | ----------- | ----- | ------- | -------- | -|Monitor list |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ |**N/A**| -|Video mode query |✔️ |✔️ |✔️ |✔️ |❌ |✔️ |**N/A**| +|Monitor list |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |**N/A**| +|Video mode query |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |**N/A**| ### Input handling |Feature |Windows |MacOS |Linux x11|Linux Wayland|Android|iOS |WASM | |----------------------- | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | |Mouse events |✔️ |▢[#63] |✔️ |✔️ |**N/A**|**N/A**|✔️ | |Mouse set location |✔️ |✔️ |✔️ |✔️(when locked) |**N/A**|**N/A**|**N/A**| -|Cursor locking |❌ |✔️ |❌ |✔️ |**N/A**|**N/A**|✔️ | -|Cursor confining |✔️ |❌ |✔️ |✔️ |**N/A**|**N/A**|❌ | -|Cursor icon |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ | -|Cursor hittest |✔️ |✔️ |❌ |✔️ |**N/A**|**N/A**|❌ | +|Cursor locking |❌ |✔️ |❌ |✔️ |**N/A**|**N/A**|✔️ | +|Cursor confining |✔️ |❌ |✔️ |✔️ |**N/A**|**N/A**|❌ | +|Cursor icon |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ | +|Cursor hittest |✔️ |✔️ |❌ |✔️ |**N/A**|**N/A**|❌ | |Touch events |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |❌ | |Touch pressure |✔️ |❌ |❌ |❌ |❌ |✔️ |❌ | |Multitouch |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |❌ | -|Keyboard events |✔️ |✔️ |✔️ |✔️ |❓ |❌ |✔️ | +|Keyboard events |✔️ |✔️ |✔️ |✔️ |✔️ |❌ |✔️ | |Drag & Drop |▢[#720] |▢[#720] |▢[#720] |❌[#306] |**N/A**|**N/A**|❓ | |Raw Device Events |▢[#750] |▢[#750] |▢[#750] |❌ |❌ |❌ |❓ | |Gamepad/Joystick events |❌[#804] |❌ |❌ |❌ |❌ |❌ |❓ | |Device movement events |❓ |❓ |❓ |❓ |❌ |❌ |❓ | -|Drag window with cursor |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A** | +|Drag window with cursor |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A** | ### Pending API Reworks Changes in the API that have been agreed upon but aren't implemented across all platforms. |Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |WASM | |------------------------------ | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | -|New API for HiDPI ([#315] [#319]) |✔️ |✔️ |✔️ |✔️ |▢[#721]|✔️ |❓ | -|Event Loop 2.0 ([#459]) |✔️ |✔️ |❌ |✔️ |❌ |✔️ |❓ | +|New API for HiDPI ([#315] [#319]) |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |❓ | +|Event Loop 2.0 ([#459]) |✔️ |✔️ |❌ |✔️ |✔️ |✔️ |❓ | |Keyboard Input ([#812]) |❌ |❌ |❌ |❌ |❌ |❌ |❓ | ### Completed API Reworks diff --git a/README.md b/README.md index 45f35c0eddc..e60424c307d 100644 --- a/README.md +++ b/README.md @@ -99,36 +99,93 @@ book]. #### Android -This library makes use of the [ndk-rs](https://github.com/rust-windowing/android-ndk-rs) crates, refer to that repo for more documentation. +The Android backend builds on (and exposes types from) the [`ndk`](https://docs.rs/ndk/0.7.0/ndk/) crate. -The `ndk-glue` version needs to match the version used by `winit`. Otherwise, the application will not start correctly as `ndk-glue`'s internal `NativeActivity` static is not the same due to version mismatch. +Native Android applications need some form of "glue" crate that is responsible +for defining the main entry point for your Rust application as well as tracking +various life-cycle events and synchronizing with the main JVM thread. -`winit` compatibility table with `ndk-glue`: +Winit uses the [android-activity](https://github.com/rib/android-activity) as a +glue crate (prior to `0.28` it used +[ndk-glue](https://github.com/rust-windowing/android-ndk-rs/tree/master/ndk-glue). -| winit | ndk-glue | -| :---: | :------------------: | -| 0.24 | `ndk-glue = "0.2.0"` | -| 0.25 | `ndk-glue = "0.3.0"` | -| 0.26 | `ndk-glue = "0.5.0"` | -| 0.27 | `ndk-glue = "0.7.0"` | +The version of the glue crate that your application depends on _must_ match the +version that Winit depends on because the glue crate is responsible for your +application's main entrypoint. If Cargo resolves multiple versions they will +clash. + +`winit` glue compatibility table: + +| winit | ndk-glue | +| :---: | :--------------------------: | +| 0.28 | `android-activity = "0.4"` | +| 0.27 | `ndk-glue = "0.7"` | +| 0.26 | `ndk-glue = "0.5"` | +| 0.25 | `ndk-glue = "0.3"` | +| 0.24 | `ndk-glue = "0.2"` | + +The recommended way to avoid a conflict with the glue version is to avoid explicitly +depending on the `android-activity` crate, and instead consume the API that +is re-exported by Winit under `winit::platform::android::activity::*` Running on an Android device needs a dynamic system library, add this to Cargo.toml: ```toml -[[example]] -name = "request_redraw_threaded" +[lib] +name = "main" crate-type = ["cdylib"] ``` -And add this to the example file to add the native activity glue: +All Android applications are based on an `Activity` subclass and the +`android-activity` crate is designed to support different choices for this base +class. You application must specify the base class it needs via a feature flag: + +| Base Class | Feature Flag | Notes | +| :--------------: | :---------------: | :-----: | +| `NativeActivity` | `android-native-activity` | Built-in to Android - making it possible to build some tests/demos without needing to compile any JVM code. Can give a false sense of convenience because it's often not really possible to avoid needing a build system that can compile some JVM code, to at least subclass `NativeActivity` | +| [`GameActivity`] | `android-game-activity` | Derives from [`AndroidAppCompat`] which is a defacto standard `Activity` base class that helps support a wider range of Android versions. Will offer integration with [`GameTextInput`] library for soft keyboard support. Requires a build system that can compile Java and fetch Android dependencies from a Maven repository (with [Gradle] being the defacto standard build system for Android applications) | + +[`GameActivity`]: https://developer.android.com/games/agdk/game-activity +[`GameTextInput`]: https://developer.android.com/games/agdk/add-support-for-text-input +[`AndroidAppCompat`]: https://developer.android.com/reference/androidx/appcompat/app/AppCompatActivity +[Gradle]: https://developer.android.com/studio/build + +For example, add this to Cargo.toml: +```toml +winit = { version = "0.28", features = [ "android-game-activity" ] } + +[target.'cfg(target_os = "android")'.dependencies] +android_logger = "0.11.0" +``` + +And, for example, define an entry point for your library like this: ```rust -#[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on"))] -fn main() { - ... +#[cfg(target_os = "android")] +use winit::platform::android::activity::AndroidApp; + +#[cfg(target_os = "android")] +#[no_mangle] +fn android_main(app: AndroidApp) { + use winit::platform::android::EventLoopBuilderExtAndroid; + + android_logger::init_once(android_logger::Config::default().with_min_level(log::Level::Trace)); + + let event_loop = EventLoopBuilder::with_user_event() + .with_android_app(app) + .build(); + _main(event_loop); } ``` -And run the application with `cargo apk run --example request_redraw_threaded` +For more details, refer to these `android-activity` [example applications](https://github.com/rib/android-activity/tree/main/examples). + +##### Converting from `ndk-glue` to `android-activity` + +If your application is currently based on `NativeActivity` via the `ndk-glue` crate and building with `cargo apk` then the minimal changes would be: +1. Remove `ndk-glue` from your `Cargo.toml` +2. Enable the `"android-native-activity"` feature for Winit: `winit = { version = "0.28", features = [ "android-native-activity" ] }` +3. Add an `android_main` entrypoint (as above), instead of using the '`[ndk_glue::main]` proc macro from `ndk-macros` (optionally add a dependency on `android_logger` and initialize logging as above). +4. Pass a clone of the `AndroidApp` that your application receives to Winit when building your event loop (as shown above). #### MacOS diff --git a/src/event_loop.rs b/src/event_loop.rs index 8cb946f7aa2..92861cad9d3 100644 --- a/src/event_loop.rs +++ b/src/event_loop.rs @@ -97,8 +97,18 @@ impl EventLoopBuilder { /// `WINIT_UNIX_BACKEND`. Legal values are `x11` and `wayland`. /// If it is not set, winit will try to connect to a Wayland connection, and if that fails, /// will fall back on X11. If this variable is set with any other value, winit will panic. + /// - **Android:** Must be configured with an `AndroidApp` from `android_main()` by calling + /// [`.with_android_app(app)`] before calling `.build()`. /// /// [`platform`]: crate::platform + #[cfg_attr( + target_os = "android", + doc = "[`.with_android_app(app)`]: crate::platform::android::EventLoopBuilderExtAndroid::with_android_app" + )] + #[cfg_attr( + not(target_os = "android"), + doc = "[`.with_android_app(app)`]: #only-available-on-android" + )] #[inline] pub fn build(&mut self) -> EventLoop { static EVENT_LOOP_CREATED: OnceCell<()> = OnceCell::new(); diff --git a/src/platform/android.rs b/src/platform/android.rs index aae8c826163..a7cc7c8eae6 100644 --- a/src/platform/android.rs +++ b/src/platform/android.rs @@ -1,9 +1,9 @@ use crate::{ - event_loop::{EventLoop, EventLoopWindowTarget}, + event_loop::{EventLoop, EventLoopBuilder, EventLoopWindowTarget}, window::{Window, WindowBuilder}, }; -use ndk::configuration::Configuration; -use ndk_glue::Rect; + +use android_activity::{AndroidApp, ConfigurationRef, Rect}; /// Additional methods on [`EventLoop`] that are specific to Android. pub trait EventLoopExtAndroid {} @@ -17,7 +17,7 @@ pub trait EventLoopWindowTargetExtAndroid {} pub trait WindowExtAndroid { fn content_rect(&self) -> Rect; - fn config(&self) -> Configuration; + fn config(&self) -> ConfigurationRef; } impl WindowExtAndroid for Window { @@ -25,7 +25,7 @@ impl WindowExtAndroid for Window { self.window.content_rect() } - fn config(&self) -> Configuration { + fn config(&self) -> ConfigurationRef { self.window.config() } } @@ -36,3 +36,43 @@ impl EventLoopWindowTargetExtAndroid for EventLoopWindowTarget {} pub trait WindowBuilderExtAndroid {} impl WindowBuilderExtAndroid for WindowBuilder {} + +pub trait EventLoopBuilderExtAndroid { + /// Associates the `AndroidApp` that was passed to `android_main()` with the event loop + /// + /// This must be called on Android since the `AndroidApp` is not global state. + fn with_android_app(&mut self, app: AndroidApp) -> &mut Self; +} + +impl EventLoopBuilderExtAndroid for EventLoopBuilder { + fn with_android_app(&mut self, app: AndroidApp) -> &mut Self { + self.platform_specific.android_app = Some(app); + self + } +} + +/// Re-export of the `android_activity` API +/// +/// Winit re-exports the `android_activity` API for convenience so that most +/// applications can rely on the Winit crate to resolve the required version of +/// `android_activity` and avoid any chance of a conflict between Winit and the +/// application crate. +/// +/// Unlike most libraries there can only be a single implementation +/// of the `android_activity` glue crate linked with an application because +/// it is responsible for the application's `android_main()` entry point. +/// +/// Since Winit depends on a specific version of `android_activity` the simplest +/// way to avoid creating a conflict is for applications to avoid explicitly +/// depending on the `android_activity` crate, and instead consume the API that +/// is re-exported by Winit. +/// +/// For compatibility applications should then import the `AndroidApp` type for +/// their `android_main(app: AndroidApp)` function like: +/// ```rust +/// #[cfg(target_os="android")] +/// use winit::platform::android::activity::AndroidApp; +/// ``` +pub mod activity { + pub use android_activity::*; +} diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index 5f402cf9db0..11b6a6e7f30 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -2,18 +2,18 @@ use std::{ collections::VecDeque, - sync::{mpsc, RwLock}, + hash::Hash, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc, Arc, + }, time::{Duration, Instant}, }; -use ndk::{ - configuration::Configuration, - event::{InputEvent, KeyAction, Keycode, MotionAction}, - looper::{ForeignLooper, Poll, ThreadLooper}, - native_window::NativeWindow, +use android_activity::input::{InputEvent, KeyAction, Keycode, MotionAction}; +use android_activity::{ + AndroidApp, AndroidAppWaker, ConfigurationRef, InputStatus, MainEvent, Rect, }; -use ndk_glue::{Event, LockReadGuard, Rect}; -use once_cell::sync::Lazy; use raw_window_handle::{ AndroidDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, }; @@ -22,36 +22,11 @@ use crate::platform_impl::Fullscreen; use crate::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error, - event::{self, VirtualKeyCode}, - event_loop::{self, ControlFlow}, + event::{self, StartCause, VirtualKeyCode}, + event_loop::{self, ControlFlow, EventLoopWindowTarget as RootELW}, window::{self, CursorGrabMode, Theme}, }; -static CONFIG: Lazy> = Lazy::new(|| { - RwLock::new(Configuration::from_asset_manager( - #[allow(deprecated)] // TODO: rust-windowing/winit#2196 - &ndk_glue::native_activity().asset_manager(), - )) -}); -// If this is `Some()` a `Poll::Wake` is considered an `EventSource::Internal` with the event -// contained in the `Option`. The event is moved outside of the `Option` replacing it with a -// `None`. -// -// This allows us to inject event into the event loop without going through `ndk-glue` and -// calling unsafe function that should only be called by Android. -static INTERNAL_EVENT: Lazy>> = Lazy::new(|| RwLock::new(None)); - -enum InternalEvent { - RedrawRequested, -} - -enum EventSource { - Callback, - InputQueue, - User, - Internal(InternalEvent), -} - fn ndk_keycode_to_virtualkeycode(keycode: Keycode) -> Option { match keycode { Keycode::A => Some(VirtualKeyCode::A), @@ -216,383 +191,593 @@ fn ndk_keycode_to_virtualkeycode(keycode: Keycode) -> Option Option { - match poll { - Poll::Event { ident, .. } => match ident { - ndk_glue::NDK_GLUE_LOOPER_EVENT_PIPE_IDENT => Some(EventSource::Callback), - ndk_glue::NDK_GLUE_LOOPER_INPUT_QUEUE_IDENT => Some(EventSource::InputQueue), - _ => unreachable!(), - }, - Poll::Timeout => None, - Poll::Wake => Some( - INTERNAL_EVENT - .write() - .unwrap() - .take() - .map_or(EventSource::User, EventSource::Internal), - ), - Poll::Callback => unreachable!(), +struct PeekableReceiver { + recv: mpsc::Receiver, + first: Option, +} + +impl PeekableReceiver { + pub fn from_recv(recv: mpsc::Receiver) -> Self { + Self { recv, first: None } + } + pub fn has_incoming(&mut self) -> bool { + if self.first.is_some() { + return true; + } + match self.recv.try_recv() { + Ok(v) => { + self.first = Some(v); + true + } + Err(mpsc::TryRecvError::Empty) => false, + Err(mpsc::TryRecvError::Disconnected) => { + warn!("Channel was disconnected when checking incoming"); + false + } + } + } + pub fn try_recv(&mut self) -> Result { + if let Some(first) = self.first.take() { + return Ok(first); + } + self.recv.try_recv() + } +} + +#[derive(Clone)] +struct SharedFlagSetter { + flag: Arc, +} +impl SharedFlagSetter { + pub fn set(&self) -> bool { + self.flag + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed) + .is_ok() + } +} + +struct SharedFlag { + flag: Arc, +} + +// Used for queuing redraws from arbitrary threads. We don't care how many +// times a redraw is requested (so don't actually need to queue any data, +// we just need to know at the start of a main loop iteration if a redraw +// was queued and be able to read and clear the state atomically) +impl SharedFlag { + pub fn new() -> Self { + Self { + flag: Arc::new(AtomicBool::new(false)), + } + } + pub fn setter(&self) -> SharedFlagSetter { + SharedFlagSetter { + flag: self.flag.clone(), + } + } + pub fn get_and_reset(&self) -> bool { + self.flag.swap(false, std::sync::atomic::Ordering::AcqRel) + } +} + +#[derive(Clone)] +pub struct RedrawRequester { + flag: SharedFlagSetter, + waker: AndroidAppWaker, +} + +impl RedrawRequester { + fn new(flag: &SharedFlag, waker: AndroidAppWaker) -> Self { + RedrawRequester { + flag: flag.setter(), + waker, + } + } + pub fn request_redraw(&self) { + if self.flag.set() { + // Only explicitly try to wake up the main loop when the flag + // value changes + self.waker.wake(); + } } } pub struct EventLoop { + android_app: AndroidApp, window_target: event_loop::EventLoopWindowTarget, + redraw_flag: SharedFlag, user_events_sender: mpsc::Sender, - user_events_receiver: mpsc::Receiver, - first_event: Option, - start_cause: event::StartCause, - looper: ThreadLooper, + user_events_receiver: PeekableReceiver, //must wake looper whenever something gets sent running: bool, - window_lock: Option>, } -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub(crate) struct PlatformSpecificEventLoopAttributes {} +#[derive(Default, Debug, Clone, PartialEq)] +pub(crate) struct PlatformSpecificEventLoopAttributes { + pub(crate) android_app: Option, +} -macro_rules! call_event_handler { - ( $event_handler:expr, $window_target:expr, $cf:expr, $event:expr ) => {{ - if let ControlFlow::ExitWithCode(code) = $cf { - $event_handler($event, $window_target, &mut ControlFlow::ExitWithCode(code)); - } else { - $event_handler($event, $window_target, &mut $cf); - } - }}; +fn sticky_exit_callback( + evt: event::Event<'_, T>, + target: &RootELW, + control_flow: &mut ControlFlow, + callback: &mut F, +) where + F: FnMut(event::Event<'_, T>, &RootELW, &mut ControlFlow), +{ + // make ControlFlow::ExitWithCode sticky by providing a dummy + // control flow reference if it is already ExitWithCode. + if let ControlFlow::ExitWithCode(code) = *control_flow { + callback(evt, target, &mut ControlFlow::ExitWithCode(code)) + } else { + callback(evt, target, control_flow) + } +} + +struct IterationResult { + deadline: Option, + timeout: Option, + wait_start: Instant, } impl EventLoop { - pub(crate) fn new(_: &PlatformSpecificEventLoopAttributes) -> Self { + pub(crate) fn new(attributes: &PlatformSpecificEventLoopAttributes) -> Self { let (user_events_sender, user_events_receiver) = mpsc::channel(); + + let android_app = attributes.android_app.as_ref().expect("An `AndroidApp` as passed to android_main() is required to create an `EventLoop` on Android"); + let redraw_flag = SharedFlag::new(); + Self { + android_app: android_app.clone(), window_target: event_loop::EventLoopWindowTarget { p: EventLoopWindowTarget { + app: android_app.clone(), + redraw_requester: RedrawRequester::new( + &redraw_flag, + android_app.create_waker(), + ), _marker: std::marker::PhantomData, }, _marker: std::marker::PhantomData, }, + redraw_flag, user_events_sender, - user_events_receiver, - first_event: None, - start_cause: event::StartCause::Init, - looper: ThreadLooper::for_thread().unwrap(), + user_events_receiver: PeekableReceiver::from_recv(user_events_receiver), running: false, - window_lock: None, } } - pub fn run(mut self, event_handler: F) -> ! + fn single_iteration<'a, F>( + &mut self, + //this: &mut EventLoop, + control_flow: &mut ControlFlow, + main_event: Option>, + pending_redraw: &mut bool, + cause: &mut StartCause, + callback: &mut F, + ) -> IterationResult where - F: 'static - + FnMut(event::Event<'_, T>, &event_loop::EventLoopWindowTarget, &mut ControlFlow), + F: FnMut(event::Event<'_, T>, &RootELW, &mut ControlFlow), { - let exit_code = self.run_return(event_handler); - ::std::process::exit(exit_code); - } + trace!("Mainloop iteration"); + + sticky_exit_callback( + event::Event::NewEvents(*cause), + self.window_target(), + control_flow, + callback, + ); + + //let mut redraw = false; + let mut resized = false; + + if let Some(event) = main_event { + trace!("Handling main event {:?}", event); + + match event { + MainEvent::InitWindow { .. } => { + sticky_exit_callback( + event::Event::Resumed, + self.window_target(), + control_flow, + callback, + ); + } + MainEvent::TerminateWindow { .. } => { + sticky_exit_callback( + event::Event::Suspended, + self.window_target(), + control_flow, + callback, + ); + } + MainEvent::WindowResized { .. } => resized = true, + MainEvent::RedrawNeeded { .. } => *pending_redraw = true, + //{ + // self.redraw_flag.setter().set(); + //}, + MainEvent::ContentRectChanged { .. } => { + warn!("TODO: find a way to notify application of content rect change"); + } + MainEvent::GainedFocus => { + sticky_exit_callback( + event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::Focused(true), + }, + self.window_target(), + control_flow, + callback, + ); + } + MainEvent::LostFocus => { + sticky_exit_callback( + event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::Focused(false), + }, + self.window_target(), + control_flow, + callback, + ); + } + MainEvent::ConfigChanged { .. } => { + let monitor = MonitorHandle::new(self.android_app.clone()); + let old_scale_factor = monitor.scale_factor(); + let scale_factor = monitor.scale_factor(); + if (scale_factor - old_scale_factor).abs() < f64::EPSILON { + let mut size = MonitorHandle::new(self.android_app.clone()).size(); + let event = event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::ScaleFactorChanged { + new_inner_size: &mut size, + scale_factor, + }, + }; + sticky_exit_callback(event, self.window_target(), control_flow, callback); + } + } + MainEvent::LowMemory => { + // XXX: how to forward this state to applications? + // It seems like ideally winit should support lifecycle and + // low-memory events, especially for mobile platforms. + warn!("TODO: handle Android LowMemory notification"); + } + MainEvent::Start => { + // XXX: how to forward this state to applications? + warn!("TODO: forward onStart notification to application"); + } + MainEvent::Resume { .. } => { + debug!("App Resumed - is running"); + self.running = true; + } + MainEvent::SaveState { .. } => { + // XXX: how to forward this state to applications? + // XXX: also how do we expose state restoration to apps? + warn!("TODO: forward saveState notification to application"); + } + MainEvent::Pause => { + debug!("App Paused - stopped running"); + self.running = false; + } + MainEvent::Stop => { + // XXX: how to forward this state to applications? + warn!("TODO: forward onStop notification to application"); + } + MainEvent::Destroy => { + // XXX: maybe exit mainloop to drop things before being + // killed by the OS? + warn!("TODO: forward onDestroy notification to application"); + } + MainEvent::InsetsChanged { .. } => { + // XXX: how to forward this state to applications? + warn!("TODO: handle Android InsetsChanged notification"); + } + unknown => { + trace!("Unknown MainEvent {unknown:?} (ignored)"); + } + } + } else { + trace!("No main event to handle"); + } - pub fn run_return(&mut self, mut event_handler: F) -> i32 - where - F: FnMut(event::Event<'_, T>, &event_loop::EventLoopWindowTarget, &mut ControlFlow), - { - let mut control_flow = ControlFlow::default(); + // Process input events - 'event_loop: loop { - call_event_handler!( - event_handler, - self.window_target(), - control_flow, - event::Event::NewEvents(self.start_cause) - ); - - let mut redraw = false; - let mut resized = false; - - match self.first_event.take() { - Some(EventSource::Callback) => match ndk_glue::poll_events().unwrap() { - Event::WindowCreated => { - // Acquire a lock on the window to prevent Android from destroying - // it until we've notified and waited for the user in Event::Suspended. - // WARNING: ndk-glue is inherently racy (https://github.com/rust-windowing/winit/issues/2293) - // and may have already received onNativeWindowDestroyed while this thread hasn't yet processed - // the event, and would see a `None` lock+window in that case. - if let Some(next_window_lock) = ndk_glue::native_window() { - assert!( - self.window_lock.replace(next_window_lock).is_none(), - "Received `Event::WindowCreated` while we were already holding a lock" - ); - call_event_handler!( - event_handler, - self.window_target(), - control_flow, - event::Event::Resumed - ); - } else { - warn!("Received `Event::WindowCreated` while `ndk_glue::native_window()` provides no window"); + self.android_app.input_events(|event| { + match event { + InputEvent::MotionEvent(motion_event) => { + let window_id = window::WindowId(WindowId); + let device_id = event::DeviceId(DeviceId); + + let phase = match motion_event.action() { + MotionAction::Down | MotionAction::PointerDown => { + Some(event::TouchPhase::Started) } - } - Event::WindowResized => resized = true, - Event::WindowRedrawNeeded => redraw = true, - Event::WindowDestroyed => { - // Release the lock, allowing Android to clean up this surface - // WARNING: See above - if ndk-glue is racy, this event may be called - // without having a `self.window_lock` in place. - if self.window_lock.take().is_some() { - call_event_handler!( - event_handler, - self.window_target(), - control_flow, - event::Event::Suspended - ); - } else { - warn!("Received `Event::WindowDestroyed` while we were not holding a window lock"); + MotionAction::Up | MotionAction::PointerUp => { + Some(event::TouchPhase::Ended) } - } - Event::Pause => self.running = false, - Event::Resume => self.running = true, - Event::ConfigChanged => { - #[allow(deprecated)] // TODO: rust-windowing/winit#2196 - let am = ndk_glue::native_activity().asset_manager(); - let config = Configuration::from_asset_manager(&am); - let old_scale_factor = MonitorHandle.scale_factor(); - *CONFIG.write().unwrap() = config; - let scale_factor = MonitorHandle.scale_factor(); - if (scale_factor - old_scale_factor).abs() < f64::EPSILON { - let mut size = MonitorHandle.size(); + MotionAction::Move => Some(event::TouchPhase::Moved), + MotionAction::Cancel => { + Some(event::TouchPhase::Cancelled) + } + _ => { + None // TODO mouse events + } + }; + if let Some(phase) = phase { + let pointers: Box< + dyn Iterator>, + > = match phase { + event::TouchPhase::Started + | event::TouchPhase::Ended => { + Box::new( + std::iter::once(motion_event.pointer_at_index( + motion_event.pointer_index(), + )) + ) + }, + event::TouchPhase::Moved + | event::TouchPhase::Cancelled => { + Box::new(motion_event.pointers()) + } + }; + + for pointer in pointers { + let location = PhysicalPosition { + x: pointer.x() as _, + y: pointer.y() as _, + }; + trace!("Input event {device_id:?}, {phase:?}, loc={location:?}, pointer={pointer:?}"); let event = event::Event::WindowEvent { - window_id: window::WindowId(WindowId), - event: event::WindowEvent::ScaleFactorChanged { - new_inner_size: &mut size, - scale_factor, - }, + window_id, + event: event::WindowEvent::Touch( + event::Touch { + device_id, + phase, + location, + id: pointer.pointer_id() as u64, + force: None, + }, + ), }; - call_event_handler!( - event_handler, + sticky_exit_callback( + event, self.window_target(), control_flow, - event + callback ); } } - Event::WindowHasFocus => { - call_event_handler!( - event_handler, - self.window_target(), - control_flow, - event::Event::WindowEvent { - window_id: window::WindowId(WindowId), - event: event::WindowEvent::Focused(true), - } - ); - } - Event::WindowLostFocus => { - call_event_handler!( - event_handler, - self.window_target(), - control_flow, - event::Event::WindowEvent { - window_id: window::WindowId(WindowId), - event: event::WindowEvent::Focused(false), - } - ); - } - _ => {} - }, - Some(EventSource::InputQueue) => { - if let Some(input_queue) = ndk_glue::input_queue().as_ref() { - while let Some(event) = input_queue.get_event().expect("get_event") { - if let Some(event) = input_queue.pre_dispatch(event) { - let mut handled = true; - let window_id = window::WindowId(WindowId); - let device_id = event::DeviceId(DeviceId); - match &event { - InputEvent::MotionEvent(motion_event) => { - let phase = match motion_event.action() { - MotionAction::Down | MotionAction::PointerDown => { - Some(event::TouchPhase::Started) - } - MotionAction::Up | MotionAction::PointerUp => { - Some(event::TouchPhase::Ended) - } - MotionAction::Move => Some(event::TouchPhase::Moved), - MotionAction::Cancel => { - Some(event::TouchPhase::Cancelled) - } - _ => { - handled = false; - None // TODO mouse events - } - }; - if let Some(phase) = phase { - let pointers: Box< - dyn Iterator>, - > = match phase { - event::TouchPhase::Started - | event::TouchPhase::Ended => Box::new( - std::iter::once(motion_event.pointer_at_index( - motion_event.pointer_index(), - )), - ), - event::TouchPhase::Moved - | event::TouchPhase::Cancelled => { - Box::new(motion_event.pointers()) - } - }; - - for pointer in pointers { - let location = PhysicalPosition { - x: pointer.x() as _, - y: pointer.y() as _, - }; - let event = event::Event::WindowEvent { - window_id, - event: event::WindowEvent::Touch( - event::Touch { - device_id, - phase, - location, - id: pointer.pointer_id() as u64, - force: None, - }, - ), - }; - call_event_handler!( - event_handler, - self.window_target(), - control_flow, - event - ); - } - } - } - InputEvent::KeyEvent(key) => { - let state = match key.action() { - KeyAction::Down => event::ElementState::Pressed, - KeyAction::Up => event::ElementState::Released, - _ => event::ElementState::Released, - }; - #[allow(deprecated)] - let event = event::Event::WindowEvent { - window_id, - event: event::WindowEvent::KeyboardInput { - device_id, - input: event::KeyboardInput { - scancode: key.scan_code() as u32, - state, - virtual_keycode: ndk_keycode_to_virtualkeycode( - key.key_code(), - ), - modifiers: event::ModifiersState::default(), - }, - is_synthetic: false, - }, - }; - call_event_handler!( - event_handler, - self.window_target(), - control_flow, - event - ); - } - }; - input_queue.finish_event(event, handled); - } - } - } } - Some(EventSource::User) => { - // try_recv only errors when empty (expected) or disconnect. But because Self - // contains a Sender it will never disconnect, so no error handling need. - while let Ok(event) = self.user_events_receiver.try_recv() { - call_event_handler!( - event_handler, - self.window_target(), - control_flow, - event::Event::UserEvent(event) - ); - } + InputEvent::KeyEvent(key) => { + let device_id = event::DeviceId(DeviceId); + + let state = match key.action() { + KeyAction::Down => event::ElementState::Pressed, + KeyAction::Up => event::ElementState::Released, + _ => event::ElementState::Released, + }; + #[allow(deprecated)] + let event = event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::KeyboardInput { + device_id, + input: event::KeyboardInput { + scancode: key.scan_code() as u32, + state, + virtual_keycode: ndk_keycode_to_virtualkeycode( + key.key_code(), + ), + modifiers: event::ModifiersState::default(), + }, + is_synthetic: false, + }, + }; + sticky_exit_callback( + event, + self.window_target(), + control_flow, + callback + ); + } + _ => { + warn!("Unknown android_activity input event {event:?}") } - Some(EventSource::Internal(internal)) => match internal { - InternalEvent::RedrawRequested => redraw = true, - }, - None => {} } - call_event_handler!( - event_handler, - self.window_target(), - control_flow, - event::Event::MainEventsCleared - ); + // Assume all events are handled, while Winit doesn't currently give a way for + // applications to report whether they handled an input event. + InputStatus::Handled + }); + + // Empty the user event buffer + { + while let Ok(event) = self.user_events_receiver.try_recv() { + sticky_exit_callback( + crate::event::Event::UserEvent(event), + self.window_target(), + control_flow, + callback, + ); + } + } - if resized && self.running { - let size = MonitorHandle.size(); + sticky_exit_callback( + event::Event::MainEventsCleared, + self.window_target(), + control_flow, + callback, + ); + + if self.running { + if resized { + let size = if let Some(native_window) = self.android_app.native_window().as_ref() { + let width = native_window.width() as _; + let height = native_window.height() as _; + PhysicalSize::new(width, height) + } else { + PhysicalSize::new(0, 0) + }; let event = event::Event::WindowEvent { window_id: window::WindowId(WindowId), event: event::WindowEvent::Resized(size), }; - call_event_handler!(event_handler, self.window_target(), control_flow, event); + sticky_exit_callback(event, self.window_target(), control_flow, callback); } - if redraw && self.running { + *pending_redraw |= self.redraw_flag.get_and_reset(); + if *pending_redraw { + *pending_redraw = false; let event = event::Event::RedrawRequested(window::WindowId(WindowId)); - call_event_handler!(event_handler, self.window_target(), control_flow, event); + sticky_exit_callback(event, self.window_target(), control_flow, callback); } + } - call_event_handler!( - event_handler, - self.window_target(), - control_flow, - event::Event::RedrawEventsCleared - ); - - match control_flow { - ControlFlow::ExitWithCode(code) => { - self.first_event = poll( - self.looper - .poll_once_timeout(Duration::from_millis(0)) - .unwrap(), - ); - self.start_cause = event::StartCause::WaitCancelled { - start: Instant::now(), - requested_resume: None, - }; - break 'event_loop code; - } - ControlFlow::Poll => { - self.first_event = poll( - self.looper - .poll_all_timeout(Duration::from_millis(0)) - .unwrap(), - ); - self.start_cause = event::StartCause::Poll; - } - ControlFlow::Wait => { - self.first_event = poll(self.looper.poll_all().unwrap()); - self.start_cause = event::StartCause::WaitCancelled { - start: Instant::now(), - requested_resume: None, + sticky_exit_callback( + event::Event::RedrawEventsCleared, + self.window_target(), + control_flow, + callback, + ); + + let start = Instant::now(); + let (deadline, timeout); + + match control_flow { + ControlFlow::ExitWithCode(_) => { + deadline = None; + timeout = None; + } + ControlFlow::Poll => { + *cause = StartCause::Poll; + deadline = None; + timeout = Some(Duration::from_millis(0)); + } + ControlFlow::Wait => { + *cause = StartCause::WaitCancelled { + start, + requested_resume: None, + }; + deadline = None; + timeout = None; + } + ControlFlow::WaitUntil(wait_deadline) => { + *cause = StartCause::ResumeTimeReached { + start, + requested_resume: *wait_deadline, + }; + timeout = if *wait_deadline > start { + Some(*wait_deadline - start) + } else { + Some(Duration::from_millis(0)) + }; + deadline = Some(*wait_deadline); + } + } + + IterationResult { + wait_start: start, + deadline, + timeout, + } + } + + pub fn run(mut self, event_handler: F) -> ! + where + F: 'static + + FnMut(event::Event<'_, T>, &event_loop::EventLoopWindowTarget, &mut ControlFlow), + { + let exit_code = self.run_return(event_handler); + ::std::process::exit(exit_code); + } + + pub fn run_return(&mut self, mut callback: F) -> i32 + where + F: FnMut(event::Event<'_, T>, &RootELW, &mut ControlFlow), + { + let mut control_flow = ControlFlow::default(); + let mut cause = StartCause::Init; + let mut pending_redraw = false; + + // run the initial loop iteration + let mut iter_result = self.single_iteration( + &mut control_flow, + None, + &mut pending_redraw, + &mut cause, + &mut callback, + ); + + let exit_code = loop { + if let ControlFlow::ExitWithCode(code) = control_flow { + break code; + } + + let mut timeout = iter_result.timeout; + + // If we already have work to do then we don't want to block on the next poll... + pending_redraw |= self.redraw_flag.get_and_reset(); + if self.running && (pending_redraw || self.user_events_receiver.has_incoming()) { + timeout = Some(Duration::from_millis(0)) + } + + let app = self.android_app.clone(); // Don't borrow self as part of poll expression + app.poll_events(timeout, |poll_event| { + let mut main_event = None; + + match poll_event { + android_activity::PollEvent::Wake => { + // In the X11 backend it's noted that too many false-positive wake ups + // would cause the event loop to run continuously. They handle this by re-checking + // for pending events (assuming they cover all valid reasons for a wake up). + // + // For now, user_events and redraw_requests are the only reasons to expect + // a wake up here so we can ignore the wake up if there are no events/requests. + // We also ignore wake ups while suspended. + pending_redraw |= self.redraw_flag.get_and_reset(); + if !self.running + || (!pending_redraw && !self.user_events_receiver.has_incoming()) + { + return; + } + } + android_activity::PollEvent::Timeout => {} + android_activity::PollEvent::Main(event) => { + main_event = Some(event); + } + unknown_event => { + warn!("Unknown poll event {unknown_event:?} (ignored)"); } } - ControlFlow::WaitUntil(instant) => { - let start = Instant::now(); - let duration = if instant <= start { - Duration::default() - } else { - instant - start + + let wait_cancelled = iter_result + .deadline + .map_or(false, |deadline| Instant::now() < deadline); + + if wait_cancelled { + cause = StartCause::WaitCancelled { + start: iter_result.wait_start, + requested_resume: iter_result.deadline, }; - self.first_event = poll(self.looper.poll_all_timeout(duration).unwrap()); - self.start_cause = if self.first_event.is_some() { - event::StartCause::WaitCancelled { - start, - requested_resume: Some(instant), - } - } else { - event::StartCause::ResumeTimeReached { - start, - requested_resume: instant, - } - } } - } - } + + iter_result = self.single_iteration( + &mut control_flow, + main_event, + &mut pending_redraw, + &mut cause, + &mut callback, + ); + }); + }; + + sticky_exit_callback( + event::Event::LoopDestroyed, + self.window_target(), + &mut control_flow, + &mut callback, + ); + + exit_code } pub fn window_target(&self) -> &event_loop::EventLoopWindowTarget { @@ -602,47 +787,49 @@ impl EventLoop { pub fn create_proxy(&self) -> EventLoopProxy { EventLoopProxy { user_events_sender: self.user_events_sender.clone(), - looper: ForeignLooper::for_thread().expect("called from event loop thread"), + waker: self.android_app.create_waker(), } } } pub struct EventLoopProxy { user_events_sender: mpsc::Sender, - looper: ForeignLooper, + waker: AndroidAppWaker, +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + EventLoopProxy { + user_events_sender: self.user_events_sender.clone(), + waker: self.waker.clone(), + } + } } impl EventLoopProxy { pub fn send_event(&self, event: T) -> Result<(), event_loop::EventLoopClosed> { self.user_events_sender .send(event) - .map_err(|mpsc::SendError(x)| event_loop::EventLoopClosed(x))?; - self.looper.wake(); + .map_err(|err| event_loop::EventLoopClosed(err.0))?; + self.waker.wake(); Ok(()) } } -impl Clone for EventLoopProxy { - fn clone(&self) -> Self { - EventLoopProxy { - user_events_sender: self.user_events_sender.clone(), - looper: self.looper.clone(), - } - } -} - pub struct EventLoopWindowTarget { + app: AndroidApp, + redraw_requester: RedrawRequester, _marker: std::marker::PhantomData, } impl EventLoopWindowTarget { pub fn primary_monitor(&self) -> Option { - Some(MonitorHandle) + Some(MonitorHandle::new(self.app.clone())) } pub fn available_monitors(&self) -> VecDeque { let mut v = VecDeque::with_capacity(1); - v.push_back(MonitorHandle); + v.push_back(MonitorHandle::new(self.app.clone())); v } @@ -652,7 +839,7 @@ impl EventLoopWindowTarget { } #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -pub struct WindowId; +pub(crate) struct WindowId; impl WindowId { pub const fn dummy() -> Self { @@ -684,16 +871,23 @@ impl DeviceId { #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct PlatformSpecificWindowBuilderAttributes; -pub(crate) struct Window; +pub(crate) struct Window { + app: AndroidApp, + redraw_requester: RedrawRequester, +} impl Window { pub(crate) fn new( - _el: &EventLoopWindowTarget, + el: &EventLoopWindowTarget, _window_attrs: window::WindowAttributes, _: PlatformSpecificWindowBuilderAttributes, ) -> Result { // FIXME this ignores requested window attributes - Ok(Self) + + Ok(Self { + app: el.app.clone(), + redraw_requester: el.redraw_requester.clone(), + }) } pub fn id(&self) -> WindowId { @@ -701,26 +895,25 @@ impl Window { } pub fn primary_monitor(&self) -> Option { - Some(MonitorHandle) + Some(MonitorHandle::new(self.app.clone())) } pub fn available_monitors(&self) -> VecDeque { let mut v = VecDeque::with_capacity(1); - v.push_back(MonitorHandle); + v.push_back(MonitorHandle::new(self.app.clone())); v } pub fn current_monitor(&self) -> Option { - Some(MonitorHandle) + Some(MonitorHandle::new(self.app.clone())) } pub fn scale_factor(&self) -> f64 { - MonitorHandle.scale_factor() + MonitorHandle::new(self.app.clone()).scale_factor() } pub fn request_redraw(&self) { - *INTERNAL_EVENT.write().unwrap() = Some(InternalEvent::RedrawRequested); - ForeignLooper::for_thread().unwrap().wake(); + self.redraw_requester.request_redraw() } pub fn inner_position(&self) -> Result, error::NotSupportedError> { @@ -744,7 +937,7 @@ impl Window { } pub fn outer_size(&self) -> PhysicalSize { - MonitorHandle.size() + MonitorHandle::new(self.app.clone()).size() } pub fn set_min_inner_size(&self, _: Option) {} @@ -834,7 +1027,7 @@ impl Window { } pub fn raw_window_handle(&self) -> RawWindowHandle { - if let Some(native_window) = ndk_glue::native_window() { + if let Some(native_window) = self.app.native_window().as_ref() { native_window.raw_window_handle() } else { panic!("Cannot get the native window, it's null and will always be null before Event::Resumed and after Event::Suspended. Make sure you only call this function between those events."); @@ -845,12 +1038,12 @@ impl Window { RawDisplayHandle::Android(AndroidDisplayHandle::empty()) } - pub fn config(&self) -> Configuration { - CONFIG.read().unwrap().clone() + pub fn config(&self) -> ConfigurationRef { + self.app.config() } pub fn content_rect(&self) -> Rect { - ndk_glue::content_rect() + self.app.content_rect() } pub fn theme(&self) -> Option { @@ -870,19 +1063,33 @@ impl Display for OsError { pub(crate) use crate::icon::NoIcon as PlatformIcon; -#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -pub struct MonitorHandle; +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct MonitorHandle { + app: AndroidApp, +} +impl PartialOrd for MonitorHandle { + fn partial_cmp(&self, _other: &Self) -> Option { + Some(std::cmp::Ordering::Equal) + } +} +impl Ord for MonitorHandle { + fn cmp(&self, _other: &Self) -> std::cmp::Ordering { + std::cmp::Ordering::Equal + } +} impl MonitorHandle { + pub(crate) fn new(app: AndroidApp) -> Self { + Self { app } + } + pub fn name(&self) -> Option { Some("Android Device".to_owned()) } pub fn size(&self) -> PhysicalSize { - if let Some(native_window) = ndk_glue::native_window().as_ref() { - let width = native_window.width() as _; - let height = native_window.height() as _; - PhysicalSize::new(width, height) + if let Some(native_window) = self.app.native_window() { + PhysicalSize::new(native_window.width() as _, native_window.height() as _) } else { PhysicalSize::new(0, 0) } @@ -893,8 +1100,8 @@ impl MonitorHandle { } pub fn scale_factor(&self) -> f64 { - let config = CONFIG.read().unwrap(); - config + self.app + .config() .density() .map(|dpi| dpi as f64 / 160.0) .unwrap_or(1.0)