// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 // cSpell: ignore androidwindowadapter javahelper #![doc = include_str!("README.md")] #![doc(html_logo_url = "https://slint.dev/logo/slint-logo-square-light.svg")] #![cfg_attr(not(target_os = "android"), allow(rustdoc::broken_intra_doc_links))] #![cfg(target_os = "android")] #![cfg_attr(slint_nightly_test, feature(non_exhaustive_omitted_patterns_lint))] #![cfg_attr(slint_nightly_test, warn(non_exhaustive_omitted_patterns))] mod androidwindowadapter; mod javahelper; mod vsync; pub mod stg; #[cfg(all(not(feature = "aa-06"), feature = "aa-05"))] pub use android_activity_05 as android_activity; #[cfg(feature = "aa-06")] pub use android_activity_06 as android_activity; #[cfg(all(not(feature = "aa-06"), feature = "aa-05"))] use ndk_08 as ndk; #[cfg(feature = "aa-06")] use ndk_09 as ndk; pub use android_activity::AndroidApp; use android_activity::PollEvent; use androidwindowadapter::AndroidWindowAdapter; use core::ops::ControlFlow; use core::time::Duration; use i_slint_core::api::{EventLoopError, PlatformError}; use i_slint_core::platform::{Clipboard, WindowAdapter}; use i_slint_renderer_skia::SkiaRendererExt; use std::cell::RefCell; use std::rc::{Rc, Weak}; use std::sync::{Arc, Mutex}; thread_local! { static CURRENT_WINDOW: RefCell> = RefCell::new(Default::default()); } pub struct AndroidPlatform { app: AndroidApp, window: Rc, event_listener: Option)>>, context: core::cell::OnceCell, } impl AndroidPlatform { /// Instantiate a new Android backend given the [`android_activity::AndroidApp`] /// /// Pass the returned value to [`slint::platform::set_platform()`](`i_slint_core::platform::set_platform()`) /// /// # Example /// ``` /// #[cfg(target_os = "android")] /// #[unsafe(no_mangle)] /// fn android_main(app: i_slint_backend_android_activity::AndroidApp) { /// slint::platform::set_platform(Box::new( /// i_slint_backend_android_activity::AndroidPlatform::new(app), /// )) /// .unwrap(); /// // ... your slint application ... /// } /// ``` pub fn new(app: AndroidApp) -> Self { let window = AndroidWindowAdapter::new(app.clone()); CURRENT_WINDOW.set(Rc::downgrade(&window)); Self { app, window, event_listener: None, context: Default::default() } } /// Instantiate a new Android backend given the [`android_activity::AndroidApp`] /// and a function to process the events. /// /// This is the same as [`AndroidPlatform::new()`], but it allow you to get notified /// of events. /// /// Pass the returned value to [`slint::platform::set_platform()`](`i_slint_core::platform::set_platform()`) /// /// # Example /// ``` /// #[cfg(target_os = "android")] /// #[unsafe(no_mangle)] /// fn android_main(app: i_slint_backend_android_activity::AndroidApp) { /// slint::platform::set_platform(Box::new( /// i_slint_backend_android_activity::AndroidPlatform::new_with_event_listener( /// app, /// |event| { eprintln!("got event {event:?}") } /// ), /// )) /// .unwrap(); /// // ... your slint application ... /// } /// ``` pub fn new_with_event_listener( app: AndroidApp, listener: impl Fn(&PollEvent<'_>) + 'static, ) -> Self { let mut this = Self::new(app); this.event_listener = Some(Box::new(listener)); this } } impl i_slint_core::platform::Platform for AndroidPlatform { fn create_window_adapter(&self) -> Result, PlatformError> { Ok(self.window.clone()) } fn run_event_loop(&self) -> Result<(), PlatformError> { let ctx = self .context .get() .and_then(|ctx| ctx.upgrade()) .expect("the event loop runs inside the context that owns this platform"); let vsync = vsync::VsyncDriver::new(self.app.create_waker()); loop { let mut timeout = ctx.duration_until_next_timer_update(); // While animating, the vsync thread wakes this loop at each display refresh // so rendering tracks the refresh rate instead of a fixed poll timeout. let animating = self.window.window.has_active_animations(); vsync.set_animating(animating); if animating && !vsync.is_driving() { // The vsync thread is not driving frames (still starting, or unavailable); // fall back to a periodic wakeup so animations still advance. let frame_duration = Duration::from_millis(10); timeout = Some(timeout.map_or(frame_duration, |x| x.min(frame_duration))); } // `request_redraw()` only raises the flag, so a request issued outside the // poll would otherwise wait here for an unrelated wakeup. if self.window.pending_redraw.get() { timeout = Some(Duration::ZERO); } // STG (jank-hunt instrumentation): reset per-iteration marks. { let pr = stg::S_RENDER_START.load(std::sync::atomic::Ordering::Relaxed); let pe = stg::S_RENDER_END.load(std::sync::atomic::Ordering::Relaxed); if pr != 0 && pe != 0 { stg::S_LAST_RENDER_US .store(pe - pr, std::sync::atomic::Ordering::Relaxed); } } stg::S_START.store(stg::now_us(), std::sync::atomic::Ordering::Relaxed); stg::S_FIRST_EV.store(0, std::sync::atomic::Ordering::Relaxed); stg::S_TIMER_US.store(0, std::sync::atomic::Ordering::Relaxed); stg::S_EVENT_US.store(0, std::sync::atomic::Ordering::Relaxed); stg::S_N_EVENTS.store(0, std::sync::atomic::Ordering::Relaxed); let mut r = Ok(ControlFlow::Continue(())); self.app.poll_events(timeout, |e| { if stg::S_FIRST_EV.load(std::sync::atomic::Ordering::Relaxed) == 0 { stg::S_FIRST_EV .store(stg::now_us(), std::sync::atomic::Ordering::Relaxed); } let t1 = stg::now_us(); ctx.update_timers_and_animations(); let t2 = stg::now_us(); r = self.window.process_event(&e); let t3 = stg::now_us(); stg::S_TIMER_US.fetch_add(t2 - t1, std::sync::atomic::Ordering::Relaxed); stg::S_EVENT_US.fetch_add(t3 - t2, std::sync::atomic::Ordering::Relaxed); stg::S_N_EVENTS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); if let Some(event_listener) = &self.event_listener { event_listener(&e) } }); stg::S_POLL_END.store(stg::now_us(), std::sync::atomic::Ordering::Relaxed); if r?.is_break() { break; } if self.window.pending_redraw.take() { stg::S_RENDER_START.store(stg::now_us(), std::sync::atomic::Ordering::Relaxed); self.window.do_render()?; stg::S_RENDER_END.store(stg::now_us(), std::sync::atomic::Ordering::Relaxed); } else { stg::S_RENDER_END.store(0, std::sync::atomic::Ordering::Relaxed); } } Ok(()) } fn new_event_loop_proxy(&self) -> Option> { Some(Box::new(AndroidEventLoopProxy { event_queue: self.window.event_queue.clone(), waker: self.app.create_waker(), })) } fn set_clipboard_text(&self, text: &str, clipboard: Clipboard) { if clipboard == Clipboard::DefaultClipboard { self.window .java_helper .set_clipboard(text) .unwrap_or_else(|e| javahelper::print_jni_error(&self.app, e)); } } fn clipboard_text(&self, clipboard: Clipboard) -> Option { if clipboard == Clipboard::DefaultClipboard { Some( self.window .java_helper .get_clipboard() .unwrap_or_else(|e| javahelper::print_jni_error(&self.app, e)), ) } else { None } } fn bind_context(&self, ctx: i_slint_core::SlintContextWeak, _: i_slint_core::InternalToken) { let _ = self.context.set(ctx.clone()); let ctx = ctx.upgrade().expect("bind_context called while the SlintContext is still alive"); let color_scheme = match self .window .java_helper .color_scheme() .unwrap_or_else(|e| javahelper::print_jni_error(&self.app, e)) { 0x10 => i_slint_core::items::ColorScheme::Light, // UI_MODE_NIGHT_NO 0x20 => i_slint_core::items::ColorScheme::Dark, // UI_MODE_NIGHT_YES _ => i_slint_core::items::ColorScheme::Unknown, }; ctx.set_color_scheme(color_scheme); if let Ok(accent) = self.window.java_helper.accent_color() { ctx.set_accent_color(accent); } if let Ok(scale) = self.window.java_helper.font_scale() && let Some(size) = javahelper::font_scale_to_logical_length(scale) { ctx.set_platform_default_font_size(Some(size)); } } fn long_press_interval(&self, _: i_slint_core::InternalToken) -> Duration { self.window.java_helper.long_press_timeout().unwrap_or(Duration::from_millis(500)) } } enum Event { Quit, Other(Box), } type EventQueue = Arc>>; struct AndroidEventLoopProxy { event_queue: EventQueue, waker: android_activity::AndroidAppWaker, } impl i_slint_core::platform::EventLoopProxy for AndroidEventLoopProxy { fn quit_event_loop(&self) -> Result<(), EventLoopError> { self.event_queue.lock().unwrap().push(Event::Quit); self.waker.wake(); Ok(()) } fn invoke_from_event_loop( &self, event: Box, ) -> Result<(), EventLoopError> { self.event_queue.lock().unwrap().push(Event::Other(event)); self.waker.wake(); Ok(()) } } pub fn set_requested_graphics_api( requested_graphics_api: Option, ) -> Result<(), PlatformError> { let Some(adapter) = CURRENT_WINDOW.with_borrow(|x| x.upgrade()) else { return Err(format!("On Android a graphics API for Slint can only be requested after calling slint::android::init()").into()); }; adapter.set_requested_graphics_api(requested_graphics_api); Ok(()) }