gallery/vendor/i-slint-backend-android-activity-1.18.1/lib.rs
Greg Pomerantz b97b6515d5 120Hz: Choreographer chain self-sustains on (animating||dirty); EW gates the wake; MISS v2 forensics; ctxsw discriminator + present CPU split
vsync.rs:
- set_state(animating, dirty) replaces set_animating. The Choreographer
  callback chain re-posts while animating||dirty: gating on animating
  alone let the chain die during flings (pointer inactivity >200ms) —
  with EW disabled the loop then froze (measured: 30 frames in 4s,
  then silence). The dirty mirror is pushed every main-loop iteration
  (<=8ms stale); the 6ms poll backstop bounds chain-break damage.
- EW_ON now gates the WAKE itself: the early waker raced the
  Choreographer (fires at edge+0.52ms measured) and its independent
  phase caused the -4..+4ms render-phase wander. Choreographer is the
  single wake source.
- MISS lines carry forensics: sw/sn (wake slot vs last rendered slot),
  eage (ms since last observed vsync edge), lastage (ms since last
  render) — distinguishes chain-stall / loop-stuck / guard-reject /
  no-wake-iteration without a second run.
stg.rs:
- ctxsw_sample() reads /proc/self/task/<pid>/stat for voluntary/involuntary
  context switches, CONDITIONAL on iteration >5ms (per-iteration procfs
  reads were a measured confounder: 20-50us each, pushed phase across
  the slot boundary, 10x worse drops). vsw/nvol on the IT line.
- precpu: CPU time inside Queue::present on the PRES line (pre_us >>
  precpu = blocked on binder/fence/SF; pre_us ~= precpu = CPU-bound).
wgpu_30_surface.rs: EW_ON moved here (vkprobe) so vsync.rs can gate the
wake without a new crate dependency.
lib.rs: poll backstop uses the dirty state; 6ms cap during active phases.
2026-10-30 14:51:06 -04:00

547 lines
27 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright © SixtyFPS GmbH <info@slint.dev>
// 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<Weak<AndroidWindowAdapter>> = RefCell::new(Default::default());
}
pub struct AndroidPlatform {
app: AndroidApp,
window: Rc<AndroidWindowAdapter>,
event_listener: Option<Box<dyn Fn(&PollEvent<'_>)>>,
context: core::cell::OnceCell<i_slint_core::SlintContextWeak>,
}
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<Rc<dyn WindowAdapter>, 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");
// 120Hz scroll fix (docs/BUGS/scroll-jank-120hz.md): enable the
// render-ahead pipeline in the wgpu surface. It is only ACTIVE per-
// frame while `locked` (VSYNC_LOCKED_RENDER && animating); unlocked
// renders always present immediately. See RENDER_AHEAD / RENDER_AHEAD_ACTIVE.
i_slint_renderer_skia::wgpu_30_surface::vkprobe::RENDER_AHEAD
.store(true, std::sync::atomic::Ordering::Relaxed);
let vsync = vsync::VsyncDriver::new(self.app.create_waker());
// RENDER_AHEAD flush: tracks the previous iteration's locked state so
// that when animation ends we do one final render() — which presents
// the frame held by the render-ahead pipeline (otherwise the last
// content change would never be displayed).
let mut prev_locked = false;
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.
//
// We also treat an ACTIVE POINTER DRAG as "animating" (see
// stg::pointer_recently_active): during a direct drag Slint has no
// physics animation, so has_active_animations() is false and, without
// this, the loop would sleep until the next timer (16 ms) between
// pointer events → 16-30 ms frame gaps. Keeping the vsync driver
// alive makes poll_events wake at each display refresh instead.
let animating = self.window.window.has_active_animations()
|| stg::pointer_recently_active();
// Push (animating, dirty) to the vsync driver: the Choreographer
// callback chain self-sustains while animating OR dirty
// (vsync.rs) — the dirty leg is what keeps flings alive after
// pointer inactivity expires the `animating` window.
let dirty = self.window.pending_redraw.get();
vsync.set_state(animating, dirty);
if animating || dirty {
// When animating (scrolling/flinging) or dirty, cap the poll
// timeout to 6ms as a safety net in case a vsync wake is
// lost (chain break self-heals within one backstop cycle).
// Rendering still only happens on wakes (locked) or when
// `pending_redraw` is set (unlocked).
let frame_duration = Duration::from_millis(6);
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.
// EXCEPT in vsync-locked mode: the render is deliberately deferred
// to the vsync WAKE, so a zero timeout here would busy-spin the
// loop at ~100 kHz (timeout event, no wake, flag never consumed)
// for up to 8.3 ms after every input event. Block up to the cap
// instead and let the wake arrive.
let pr_before = self.window.pending_redraw.get();
let locked = stg::VSYNC_LOCKED_RENDER
.load(std::sync::atomic::Ordering::Relaxed)
&& animating;
if pr_before && !locked {
timeout = Some(Duration::ZERO);
}
// Jank-hunt: classify the timeout path to explain the 1451 iters/s
// busy-spin. ZERO = forced by pending_redraw; CAP = 6 ms anim cap;
// NATURAL = the next timer (>= ~7 ms).
if stg::IT_ON.load(std::sync::atomic::Ordering::Relaxed) {
let to_ms = timeout.map(|d| d.as_secs_f64() * 1000.0).unwrap_or(f64::MAX);
let kind = if pr_before {
0
} else if animating && to_ms <= 6.0 {
1
} else {
2
};
stg::note_timeout_class(kind);
}
// 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);
}
}
let t_start = stg::now_us();
let timeout_ms = timeout.map(|d| d.as_millis() as u64).unwrap_or(u64::MAX);
stg::S_START.store(t_start, 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(()));
// Jank-hunt / 120Hz: did this poll return because the vsync driver
// (or any waker) fired? Used to lock rendering to the vsync edge
// (see the render decision below) instead of rendering in reaction
// to input events (which arrive at a random phase in the slot and
// push the present past the ~5.5 ms compositor deadline → every
// frame shown one vsync late → effective 60 Hz + judder).
let mut saw_wake = false;
// H5 (hypotheses-120hz.md): CPU time inside poll_events. wall≫cpu
// ⇒ the UI thread slept/descheduled while the wake byte was
// pending (epoll not woken, or the thread lost the CPU); wall≈cpu
// ⇒ the loop spun inside ALooper.
let __cpu_p0 = stg::thread_cpu_us();
self.app.poll_events(timeout, |e| {
if matches!(e, PollEvent::Wake) {
saw_wake = true;
}
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 t3_start = stg::now_us();
let t3_cpu_start = stg::thread_cpu_us();
r = self.window.process_event(&e);
let t3_end = stg::now_us();
let t3_cpu_end = stg::thread_cpu_us();
let __dt3 = t3_end - t3_start;
// Jank-hunt: count wake sources. If WAKE events dominate, the
// loop is being woken far more often than vsync (120/s) or input,
// which explains the 1600 iters/s busy-spin.
match e {
PollEvent::Wake => { stg::WAKE_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); }
PollEvent::Timeout => { stg::TIMEOUT_EV_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); }
PollEvent::Main(android_activity::MainEvent::InputAvailable) => {
stg::INPUT_EV_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
_ => {}
}
// Jank-hunt: identify which MainEvent variant causes the big
// `ev` spikes (pointer input is ~2.5 ms max, so a 20 ms event
// is something else entirely).
if __dt3 > 5_000 && stg::IT_ON.load(std::sync::atomic::Ordering::Relaxed) {
let nm = match e {
PollEvent::Main(android_activity::MainEvent::InputAvailable) => "Input",
PollEvent::Main(android_activity::MainEvent::InitWindow { .. }) => "InitWin",
PollEvent::Main(android_activity::MainEvent::TerminateWindow { .. }) => "TermWin",
PollEvent::Main(android_activity::MainEvent::WindowResized { .. }) => "WinResized",
PollEvent::Main(android_activity::MainEvent::RedrawNeeded { .. }) => "RedrawNeeded",
PollEvent::Main(_) => "Main-other",
_ => "NonMain",
};
stg::log_slow_event(nm, __dt3, t3_cpu_end.saturating_sub(t3_cpu_start));
}
stg::S_EVENT_US.fetch_add(__dt3, 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)
}
});
let t_poll1 = stg::now_us();
let __poll_cpu = stg::thread_cpu_us().saturating_sub(__cpu_p0);
let lv1 = stg::lv_snapshot();
// Unconditionally update timers and animations on every event-loop wakeup
ctx.update_timers_and_animations();
let t2 = stg::now_us();
stg::S_TIMER_US.store(t2 - t_poll1, std::sync::atomic::Ordering::Relaxed);
stg::S_POLL_END.store(t2, std::sync::atomic::Ordering::Relaxed);
if r?.is_break() {
break;
}
let mut render_us = 0u64;
let mut __render_cpu = 0u64;
// Jank-hunt / 120Hz: vsync-locked rendering (the native-Android
// model). While animating, input events only MARK the window dirty;
// the render happens on the next vsync WAKE, so every present
// starts at a deterministic phase (vsync edge + wake latency) and
// lands well inside the compositor's ~5.5 ms deadline. Cost: up
// to one slot of extra input latency.
let do_render = if locked {
// Present EVERY vsync while animating (not only on content
// change): the regular 120 Hz present stream locks the
// display cadence. Re-rendering unchanged content is cheap
// with Skia's incremental pipeline.
//
// IMPORTANT: do NOT `pending_redraw.take()` here. The render
// is driven by the vsync wake, but the pending flag also
// carries the motion events' invalidations. Consuming it
// here starves the follow-up unlocked iteration (which runs
// 0.4 ms later, before the next motion event arrives) of
// anything to render, collapsing the cadence to 60 Hz
// (one render per two vsyncs). Letting the flag survive
// means the next iteration renders too; if the content is
// unchanged the extra render is invisible.
//
// SLOT-BASED WAKE GUARD (hypotheses-120hz.md, 2026-07-09 (3)).
// Replaces the interval freshness guard, which had a 0.2 ms
// false-reject margin (threshold 6.83 ms vs earliest
// legitimate interval 7.03 ms after a Choreographer-phase
// render) and — worse — could not express the real rule.
// The rule: at most ONE render per display slot. A wake
// belongs to slot floor((t − E)/P) (E = last observed vsync
// edge, P = period; a stale E only shifts all indices by a
// constant, so the comparison is exact). Reject only when
// the wake's slot is the one already rendered; a wake N
// slots later (after a spike or lost wakes) MUST render —
// and the gap is logged as MISS with the wake state, which
// is the missing evidence for the stall slots.
let __now_ns = i_slint_renderer_skia::wgpu_30_surface::vkprobe::monotonic_ns();
let __p = i_slint_renderer_skia::wgpu_30_surface::vkprobe::VSYNC_PERIOD_NS
.load(std::sync::atomic::Ordering::Relaxed);
let __e = i_slint_renderer_skia::wgpu_30_surface::vkprobe::VSYNC_EDGE_NS
.load(std::sync::atomic::Ordering::Relaxed);
let __last = i_slint_renderer_skia::wgpu_30_surface::vkprobe::LAST_RENDER_START_NS
.load(std::sync::atomic::Ordering::Relaxed);
let __slot = |t: u64| -> i128 {
if t >= __e { ((t - __e) as i128) / (__p as i128) }
else { -((( __e - t) as i128 + __p as i128 - 1) / (__p as i128)) }
};
let __sw = if __p == 0 { 0 } else { __slot(__now_ns) };
let __sn = if __p == 0 || __last == 0 { i128::MIN } else { __slot(__last) };
if __p != 0 && __last != 0 && __sw >= __sn + 2
&& stg::IT_ON.load(std::sync::atomic::Ordering::Relaxed)
{
stg::log_miss(__sw - __sn - 1, saw_wake, __now_ns,
__sw, __sn,
__now_ns.saturating_sub(__e) / 1000,
__now_ns.saturating_sub(__last) / 1000);
}
let __accept = saw_wake && (__p == 0 || __sw > __sn);
if __accept {
i_slint_renderer_skia::wgpu_30_surface::vkprobe::LAST_RENDER_START_NS
.store(__now_ns, std::sync::atomic::Ordering::Relaxed);
}
__accept
} else {
self.window.pending_redraw.get()
};
if do_render {
// Locked (vsync-wake) renders leave the flag set so the
// follow-up unlocked iteration renders too (60 Hz collapse,
// see the do_render note); unlocked renders consume it.
if !locked {
self.window.pending_redraw.take();
}
// RENDER_AHEAD (hold-without-present) is DISABLED: the held
// frame is presented at the start of the NEXT render, i.e.
// after the next vsync wake's Choreographer latency
// (measured 3.2–6 ms on this device) plus the present cost —
// landing at vsync+12.4–15.2 ms against a 13.8 ms latch
// deadline: a 1.4 ms margin that missed 52% of frames
// (one slot late) during real slow scrolls. Presenting at
// the END of the render instead still left 51% one slot
// late — under the model THEN believed (callback 3-6 ms
// late). That model was WRONG: the callback fires at
// edge+0.5 ms (measured, VSYNC mon−ftn). See
// docs/BUGS/hypotheses-120hz.md.
//
// RENDER-AHEAD (re-enabled under the corrected model): the
// wake arrives ON the edge, so the held frame — presented
// at the START of this render — lands at edge+~1.5 ms:
// constant phase, ~4 ms of deadline margin, and render
// SPIKES (Skia 5-10 ms) stop delaying visible frames: a
// spike only stretches the PREPARE phase of the frame held
// for the NEXT slot (presented at the NEXT edge+1.5 ms,
// same phase). Cost: one slot of constant latency
// (imperceptible).
i_slint_renderer_skia::wgpu_30_surface::vkprobe::RENDER_AHEAD_ACTIVE
.store(locked, std::sync::atomic::Ordering::Relaxed);
// PRESENT GATE: DISABLED (UI-thread sleep ate the slot
// budget — history in hypotheses-120hz.md). Reference only.
i_slint_renderer_skia::wgpu_30_surface::vkprobe::PRESENT_GATE_ON
.store(false, std::sync::atomic::Ordering::Relaxed);
stg::S_RENDER_START.store(t2, std::sync::atomic::Ordering::Relaxed);
// Spike classifier (hypotheses-120hz.md): CPU time inside
// do_render. rcpu≈render ⇒ Skia/wgpu CPU work; rcpu≪render ⇒
// blocked on the GPU/driver (queue back-pressure, fences).
let __rc0 = stg::thread_cpu_us();
self.window.do_render()?;
let t3 = stg::now_us();
stg::S_RENDER_END.store(t3, std::sync::atomic::Ordering::Relaxed);
render_us = t3 - t2;
__render_cpu = stg::thread_cpu_us().saturating_sub(__rc0);
} else {
stg::S_RENDER_END.store(0, std::sync::atomic::Ordering::Relaxed);
}
// RENDER_AHEAD flush (see above): on the locked → unlocked
// transition, render once more to present the held frame.
if prev_locked && !locked {
self.window.pending_redraw.take();
i_slint_renderer_skia::wgpu_30_surface::vkprobe::RENDER_AHEAD_ACTIVE
.store(false, std::sync::atomic::Ordering::Relaxed);
self.window.do_render()?;
}
prev_locked = locked;
// Miss forensics (CONDITIONAL — 2026-07-09): the per-iteration
// /proc read was itself a confounder (procfs reads cost tens of
// µs on the UI thread; with render completion near the slot
// boundary that pushed the present phase across it — 10x worse
// drops/doubles measured). Sample only around SLOW iterations
// (>5 ms wall): those are exactly the miss candidates, and the
// common path pays nothing.
let __iter_wall = stg::now_us().saturating_sub(t_start);
let (__vsw, __nvol):
(u64, u64) = if __iter_wall > 5_000 && stg::IT_ON.load(std::sync::atomic::Ordering::Relaxed)
{
// Delta against the rolling baseline (previous slow sample).
let c = stg::ctxsw_sample();
let pv = stg::CTX_PREV_V.load(std::sync::atomic::Ordering::Relaxed);
let pn = stg::CTX_PREV_NV.load(std::sync::atomic::Ordering::Relaxed);
let first = !stg::CTX_SAMPLED.swap(true, std::sync::atomic::Ordering::Relaxed);
stg::CTX_PREV_V.store(c.0, std::sync::atomic::Ordering::Relaxed);
stg::CTX_PREV_NV.store(c.1, std::sync::atomic::Ordering::Relaxed);
if first { (0, 0) } else { (c.0.saturating_sub(pv), c.1.saturating_sub(pn)) }
} else {
(0, 0)
};
if stg::IT_ON.load(std::sync::atomic::Ordering::Relaxed) {
let first_ev = stg::S_FIRST_EV.load(std::sync::atomic::Ordering::Relaxed);
let lv2 = stg::lv_snapshot();
stg::log_iter(
t_start,
t_poll1 - t_start,
stg::S_N_EVENTS.load(std::sync::atomic::Ordering::Relaxed),
if first_ev == 0 { 0 } else { first_ev - t_start },
t2 - t_poll1,
stg::S_EVENT_US.load(std::sync::atomic::Ordering::Relaxed),
render_us,
animating,
timeout_ms,
lv1,
lv2,
stg::flick_snapshot(),
stg::input_snapshot(),
stg::timeout_snapshot(),
stg::wake_snapshot(),
(locked, saw_wake, pr_before, do_render),
__poll_cpu,
__render_cpu,
__vsw,
__nvol,
);
}
}
Ok(())
}
fn new_event_loop_proxy(&self) -> Option<Box<dyn i_slint_core::platform::EventLoopProxy>> {
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<String> {
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<dyn FnOnce() + Send + 'static>),
}
type EventQueue = Arc<Mutex<Vec<Event>>>;
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<dyn FnOnce() + Send>,
) -> Result<(), EventLoopError> {
let n = crate::stg::INVK_PUSH_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
crate::stg::log_line(&format!("INVK n={n}"));
self.event_queue.lock().unwrap().push(Event::Other(event));
self.waker.wake();
Ok(())
}
}
pub fn set_requested_graphics_api(
requested_graphics_api: Option<i_slint_core::graphics::RequestedGraphicsAPI>,
) -> 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(())
}