diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d73e0fb850..8235ff18e34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ And please only add new entries to the top of this list, right below the `# Unre - On macOS, added support for `WindowEvent::ThemeChanged`. - **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`. +- **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.4 diff --git a/Cargo.toml b/Cargo.toml index e6464fba70b..32bfd37ebc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,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/README.md b/README.md index 00a9d8586a4..a491e1d44b3 100644 --- a/README.md +++ b/README.md @@ -99,36 +99,77 @@ 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.3.0"` | +| 0.27 | `ndk-glue = "0.7.0"` | +| 0.26 | `ndk-glue = "0.5.0"` | +| 0.25 | `ndk-glue = "0.3.0"` | +| 0.24 | `ndk-glue = "0.2.0"` | + +_Note: There is unfortunately no ergonomic way to configure Cargo so that it understands that the glue crate is special on Android and to give a clear error if multiple versions are resolved. With `ndk-glue` you will get a runtime failure caused by clashing, global static variables. With `android-activity` you will see a compile-time error due to missing feature flags._ Running on an Android device needs a dynamic system library, add this to Cargo.toml: ```toml [[example]] -name = "request_redraw_threaded" +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` | `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`] | `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 +[target.'cfg(target_os = "android")'.dependencies] +android_logger = "0.11.0" +android-activity = { version = "0.3", features = [ "game-activity" ] } +``` + +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")] +#[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` and add dependency on `android-activity` that specifies the `"native-activity"` feature like: `android-activity = { version = "0.3", features = [ "native-activity" ] }` +2. 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). +3. 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..671da618dac 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,17 @@ 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 + } +} diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index 5f402cf9db0..bfdc623a084 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,597 @@ 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) -> ! - 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 event_handler: F) -> i32 + 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: FnMut(event::Event<'_, T>, &event_loop::EventLoopWindowTarget, &mut ControlFlow), + F: FnMut(event::Event<'_, T>, &RootELW, &mut ControlFlow), { - let mut control_flow = ControlFlow::default(); + 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"); + } - '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"); + // Process input events + // + // Note that GameActivity doesn't integrate input events with the mainloop, + // they are double buffered and it's assumed that the application will be + // rendering continuously. + + 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 +791,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 +843,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 +875,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 +899,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 +941,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 +1031,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 +1042,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 +1067,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 +1104,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)