857 lines
32 KiB
Rust
857 lines
32 KiB
Rust
use crate::prelude::*;
|
|
use crate::task::RequestRedraw;
|
|
use accesskit_android::Adapter as AccessAdapter;
|
|
use android_view::{
|
|
AccessibilityNodeInfo, AccessibilityNodeProvider, Bundle, CallbackCtx, Context,
|
|
InputConnection, KeyEvent, MotionEvent, Rect, View, ViewPeer,
|
|
jni::{
|
|
JNIEnv, JavaVM,
|
|
objects::{GlobalRef, JValue},
|
|
sys::jint,
|
|
},
|
|
ndk::event::{Axis, Keycode, MotionAction},
|
|
};
|
|
use std::{cell::RefCell, marker::Sized, rc::Rc, sync::Arc, time::Instant};
|
|
|
|
use super::{
|
|
access::{AndroidAccessSource, NullActionHandler, raise_if_enabled},
|
|
insets::{Insets, Shared},
|
|
render::{AndroidRedrawHandle, AndroidRenderer},
|
|
};
|
|
|
|
/// Android host state. The renderer follows the `SurfaceView` lifecycle.
|
|
/// How many frames after each `surface_changed` `render()` logs a full
|
|
/// diagnostic line for -- see the log site's own comment.
|
|
const DIAGNOSTIC_FRAMES: u64 = 10;
|
|
|
|
pub struct AndroidUiState {
|
|
pub root: Option<StrongWidget>,
|
|
pub renderer: Option<AndroidRenderer>,
|
|
pub focus: Option<WeakWidget<TextEdit>>,
|
|
pub cursor: CursorState,
|
|
pub last_click: Instant,
|
|
/// Previous IME preedit length, in characters.
|
|
pub compose_len: usize,
|
|
/// Deferred until the input callback regains access to JNI.
|
|
pub pending_show_keyboard: bool,
|
|
/// Also deferred until a JNI callback is available.
|
|
pub pending_open_url: Option<String>,
|
|
/// Filled by the native insets callback registered in `android/insets.rs`.
|
|
shared: Rc<RefCell<Shared>>,
|
|
pub access_adapter: AccessAdapter,
|
|
pub access: AccessTree,
|
|
pub frame_report: FrameReport,
|
|
pub content_scale: f32,
|
|
/// The last insets `render()` saw -- compared each frame so
|
|
/// `AndroidAppState::on_insets_changed` fires only when they actually
|
|
/// change (once at startup for the status bar, again if the device
|
|
/// rotates), not every frame.
|
|
last_insets: Insets,
|
|
}
|
|
|
|
impl AndroidUiState {
|
|
fn new(shared: Rc<RefCell<Shared>>, content_scale: f32) -> Self {
|
|
Self {
|
|
root: None,
|
|
renderer: None,
|
|
focus: None,
|
|
cursor: Default::default(),
|
|
last_click: Instant::now(),
|
|
compose_len: 0,
|
|
pending_show_keyboard: false,
|
|
pending_open_url: None,
|
|
shared,
|
|
access_adapter: Default::default(),
|
|
access: AccessTree::new(),
|
|
frame_report: FrameReport::new(),
|
|
content_scale,
|
|
last_insets: Insets::default(),
|
|
}
|
|
}
|
|
|
|
pub fn insets(&self) -> Insets {
|
|
self.shared.borrow().insets
|
|
}
|
|
|
|
pub fn insets_report(&self) -> String {
|
|
let shared = self.shared.borrow();
|
|
let i = shared.insets;
|
|
if shared.updates == 0 {
|
|
return "insets: dispatches=0 -- the platform has never called \
|
|
onApplyWindowInsets, so nothing below was measured"
|
|
.to_string();
|
|
}
|
|
format!(
|
|
"insets: dispatches={} left={} top={} right={} bottom={} ime_bottom={} \
|
|
ime_visible={}",
|
|
shared.updates, i.left, i.top, i.right, i.bottom, i.ime_bottom, i.ime_visible,
|
|
)
|
|
}
|
|
}
|
|
|
|
impl<Rsc: HasEvents> HasRoot<Rsc> for AndroidUiState {
|
|
fn set_root(&mut self, rsc: &mut Rsc, root: StrongWidget) {
|
|
self.root = Some(crate::overlay::default_overlay_root(rsc, root));
|
|
}
|
|
}
|
|
|
|
pub trait HasAndroidUiState: Sized + 'static {
|
|
fn android_state(&self) -> &AndroidUiState;
|
|
fn android_state_mut(&mut self) -> &mut AndroidUiState;
|
|
}
|
|
|
|
/// Application state retained for the lifetime of one Android `View`.
|
|
///
|
|
/// [`StdRsc`] is the usual [`AndroidResources`] implementation, but the host only
|
|
/// requires the capabilities in [`AndroidResources`]. An application may add
|
|
/// its own resources by supplying another implementation.
|
|
pub trait AndroidAppState: HasAndroidUiState {
|
|
type Resources: AndroidResources<Self>;
|
|
|
|
#[allow(unused_variables)]
|
|
fn back_pressed(&mut self, rsc: &mut Self::Resources) -> bool {
|
|
false
|
|
}
|
|
#[allow(unused_variables)]
|
|
fn platform_ready(&mut self, rsc: &mut Self::Resources, vm: JavaVM, view: GlobalRef) {}
|
|
#[allow(unused_variables)]
|
|
fn on_insets_changed(&mut self, rsc: &mut Self::Resources, insets: WindowInsets) {}
|
|
}
|
|
|
|
/// Resources the Android host needs to draw and dispatch application events.
|
|
///
|
|
/// This deliberately names capabilities rather than storage. Custom bundles
|
|
/// can embed or replace [`StdRsc`] as long as they implement these traits and
|
|
/// create the task receiver paired with their [`Tasks`] value.
|
|
pub trait AndroidResources<State>:
|
|
HasTasks<State = State> + HasWidgetState + Sized + 'static
|
|
where
|
|
State: 'static,
|
|
{
|
|
fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>);
|
|
}
|
|
|
|
impl<State: 'static> AndroidResources<State> for StdRsc<State> {
|
|
fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>) {
|
|
StdRsc::new(redraw)
|
|
}
|
|
}
|
|
|
|
/// Widget-facing insets in physical pixels, decoupled from JNI's integer shape.
|
|
#[derive(Clone, Copy, Default, Debug, PartialEq)]
|
|
pub struct WindowInsets {
|
|
pub left: f32,
|
|
pub top: f32,
|
|
pub right: f32,
|
|
pub bottom: f32,
|
|
/// How much of the window the keyboard covers, in physical pixels --
|
|
/// what a layout pads by. See `insets::Insets::ime_visible` for why
|
|
/// "is the keyboard up" is a separate field rather than this one
|
|
/// compared against zero.
|
|
pub ime_bottom: f32,
|
|
pub ime_visible: bool,
|
|
}
|
|
|
|
impl WindowInsets {
|
|
fn from_physical(insets: Insets) -> Self {
|
|
Self {
|
|
left: insets.left as f32,
|
|
top: insets.top as f32,
|
|
right: insets.right as f32,
|
|
bottom: insets.bottom as f32,
|
|
ime_bottom: insets.ime_bottom as f32,
|
|
ime_visible: insets.ime_visible,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The `ViewPeer` android-view dispatches every callback to. One per
|
|
/// `RustView` instance; `new_peer` (below) builds it and hands the id to
|
|
/// Java the same way android-view's own demo does.
|
|
pub struct IrisViewPeer<State: AndroidAppState> {
|
|
pub(super) rsc: State::Resources,
|
|
pub(super) state: State,
|
|
task_recv: TaskMsgReceiver<State::Resources>,
|
|
/// Converts input and Choreographer timestamps onto one monotonic clock.
|
|
device_clock: Option<DeviceClock>,
|
|
}
|
|
|
|
impl<State: AndroidAppState> IrisViewPeer<State> {
|
|
fn drain_tasks(&mut self) {
|
|
while let Ok(update) = self.task_recv.try_recv() {
|
|
update(&mut self.state, &mut self.rsc);
|
|
}
|
|
}
|
|
|
|
/// One pointer sample through the sensors, plus the platform calls a
|
|
/// handler can only ask for by raising a flag. Split out of
|
|
/// [`Self::after_input`] because a batched `MotionEvent` carries
|
|
/// several samples that all belong to the same *frame*
|
|
/// (`on_touch_event`): each one is a real input frame the widgets must
|
|
/// see, but only the last one ends the frame and asks for a redraw.
|
|
fn run_input_frame(&mut self, ctx: &mut CallbackCtx) {
|
|
let window_size = self.window_size();
|
|
let ui_state = self.state.android_state_mut();
|
|
let cursor = ui_state.cursor.clone();
|
|
let old_focus = ui_state.focus;
|
|
let render_state = self.rsc.ui().render_state();
|
|
render_state
|
|
.get()
|
|
.run_sensors(&mut self.rsc, &mut self.state, cursor, window_size);
|
|
|
|
let ui_state = self.state.android_state_mut();
|
|
if old_focus != ui_state.focus
|
|
&& let Some(old) = old_focus
|
|
{
|
|
old.edit(&mut self.rsc).deselect();
|
|
}
|
|
if std::mem::take(&mut ui_state.pending_show_keyboard) {
|
|
show_soft_input(&mut ctx.env, &ctx.view);
|
|
}
|
|
if let Some(url) = ui_state.pending_open_url.take() {
|
|
super::platform::open_url(&mut ctx.env, &ctx.view, &url);
|
|
}
|
|
}
|
|
|
|
/// Common tail for every callback that might have changed the cursor,
|
|
/// the text focus, or the widget tree: run the sensors that touch
|
|
/// input feeds, then ask for a frame if the result needs drawing.
|
|
/// Mirrors `desktop::DesktopApp::window_event`'s tail, split across
|
|
/// android-view's several entry points instead of winit's one.
|
|
pub(super) fn after_input(&mut self, ctx: &mut CallbackCtx) {
|
|
self.run_input_frame(ctx);
|
|
|
|
self.update_ime_selection(ctx);
|
|
|
|
let ui_state = self.state.android_state_mut();
|
|
ui_state.cursor.end_frame();
|
|
let render_state = self.rsc.ui().render_state();
|
|
if render_state
|
|
.get()
|
|
.needs_redraw(&ui_state.root, self.rsc.widgets())
|
|
{
|
|
ctx.view.post_frame_callback(&mut ctx.env);
|
|
}
|
|
}
|
|
|
|
fn device_clock(&mut self, event_time: i64, oldest: i64) -> DeviceClock {
|
|
*self
|
|
.device_clock
|
|
.get_or_insert_with(|| DeviceClock::anchored(Instant::now(), event_time, oldest))
|
|
}
|
|
|
|
fn generic_motion<'local>(
|
|
&mut self,
|
|
ctx: &mut CallbackCtx<'local>,
|
|
event: &MotionEvent<'local>,
|
|
) -> bool {
|
|
let action = event.action_masked(&mut ctx.env);
|
|
let event_time = event.event_time_nanos(&mut ctx.env);
|
|
let mut clock = self.device_clock(event_time, event_time);
|
|
let at = clock.sample(event_time);
|
|
self.device_clock = Some(clock);
|
|
|
|
let ui = self.state.android_state_mut();
|
|
ui.cursor.time = at;
|
|
ui.cursor.pos = vec2(event.x(&mut ctx.env), event.y(&mut ctx.env));
|
|
ui.cursor.exists = !matches!(action, MotionAction::HoverExit);
|
|
|
|
let buttons = event.button_state(&mut ctx.env);
|
|
ui.cursor.buttons.left.update(buttons.primary());
|
|
ui.cursor.buttons.right.update(buttons.secondary());
|
|
ui.cursor.buttons.middle.update(buttons.teriary());
|
|
|
|
match action {
|
|
MotionAction::HoverEnter
|
|
| MotionAction::HoverMove
|
|
| MotionAction::HoverExit
|
|
| MotionAction::ButtonPress
|
|
| MotionAction::ButtonRelease => {}
|
|
MotionAction::Scroll => {
|
|
ui.cursor.scroll_delta = vec2(
|
|
event.axis(&mut ctx.env, Axis::Hscroll, 0),
|
|
event.axis(&mut ctx.env, Axis::Vscroll, 0),
|
|
);
|
|
}
|
|
_ => return false,
|
|
}
|
|
|
|
self.after_input(ctx);
|
|
true
|
|
}
|
|
|
|
fn window_size(&self) -> Vec2 {
|
|
let ui_state = self.state.android_state();
|
|
match &ui_state.renderer {
|
|
Some(r) => r.size(),
|
|
None => Vec2::ZERO,
|
|
}
|
|
}
|
|
|
|
fn render(&mut self, ctx: &mut CallbackCtx, now: Instant) {
|
|
if self.state.android_state().renderer.is_none() {
|
|
return;
|
|
}
|
|
// See `AndroidAppState::on_insets_changed`'s doc comment: fires
|
|
// exactly when insets actually differ from last frame, not every
|
|
// frame -- most frames this is one `Insets` equality check against
|
|
// a `Copy` struct. Done before `ui_state` is bound below, since
|
|
// `on_insets_changed` needs `&mut self.state`/`&mut self.rsc` both.
|
|
let ui_state = self.state.android_state();
|
|
let current_insets = ui_state.insets();
|
|
if current_insets != ui_state.last_insets {
|
|
let physical = WindowInsets::from_physical(current_insets);
|
|
log::info!(
|
|
"iris insets: left={} top={} right={} bottom={} ime_bottom={} \
|
|
ime_visible={} window={:?}",
|
|
physical.left,
|
|
physical.top,
|
|
physical.right,
|
|
physical.bottom,
|
|
physical.ime_bottom,
|
|
physical.ime_visible,
|
|
self.window_size(),
|
|
);
|
|
self.state.android_state_mut().last_insets = current_insets;
|
|
self.state.on_insets_changed(&mut self.rsc, physical);
|
|
}
|
|
|
|
if crate::diagnostics::trace_enabled() {
|
|
let ui_state = self.state.android_state();
|
|
log::debug!(
|
|
target: "iris::frame",
|
|
"render(): root={:?} widgets={} active={} root_px={:?} out_size={:?}",
|
|
ui_state.root.is_some(),
|
|
self.rsc.widgets().len(),
|
|
self.rsc.ui().render_state().get().active_widgets(),
|
|
ui_state
|
|
.root
|
|
.as_ref()
|
|
.and_then(|r| {
|
|
self.rsc
|
|
.ui()
|
|
.render_state()
|
|
.get()
|
|
.window_region(r, &self.rsc)
|
|
}),
|
|
self.window_size(),
|
|
);
|
|
}
|
|
let frame_start = Instant::now();
|
|
let animating = self.rsc.ui_mut().tick_animations(now);
|
|
if animating {
|
|
ctx.view.post_frame_callback(&mut ctx.env);
|
|
}
|
|
let ui_state = self.state.android_state_mut();
|
|
self.rsc.draw(&ui_state.root);
|
|
let ui_state = self.state.android_state_mut();
|
|
let Some(renderer) = &mut ui_state.renderer else {
|
|
return;
|
|
};
|
|
let frame_diagnostics = renderer.update(self.rsc.ui_mut());
|
|
if renderer.frame_count() <= DIAGNOSTIC_FRAMES {
|
|
log::info!(
|
|
"iris frame diagnostics: frame={} masks_resized={} moves_resized={} \
|
|
paints_resized={} \
|
|
atlas_pages_grown_prev={} image_bind_group_creates_prev={} wgpu_errors={}",
|
|
renderer.frame_count(),
|
|
frame_diagnostics.masks_resized,
|
|
frame_diagnostics.moves_resized,
|
|
frame_diagnostics.paints_resized,
|
|
frame_diagnostics.atlas_pages_grown_prev,
|
|
frame_diagnostics.image_bind_group_creates_prev,
|
|
renderer.wgpu_errors.snapshot().len(),
|
|
);
|
|
}
|
|
let mut parts = renderer.draw();
|
|
parts.total = frame_start.elapsed();
|
|
self.state
|
|
.android_state_mut()
|
|
.frame_report
|
|
.record(now, parts, animating);
|
|
let render_state = self.rsc.ui().render_state();
|
|
crate::diagnostics::log_frame(&render_state.get(), now, parts, animating);
|
|
if crate::diagnostics::trace_enabled() {
|
|
let ui_state = self.state.android_state();
|
|
log::debug!(
|
|
target: "iris::frame",
|
|
"render(): after update active={} root_px={:?}",
|
|
self.rsc.ui().render_state().get().active_widgets(),
|
|
ui_state
|
|
.root
|
|
.as_ref()
|
|
.and_then(|r| {
|
|
self.rsc
|
|
.ui()
|
|
.render_state()
|
|
.get()
|
|
.window_region(r, &self.rsc)
|
|
}),
|
|
);
|
|
}
|
|
|
|
let ui_state = self.state.android_state_mut();
|
|
if let Some(tree_update) = ui_state.access.update(
|
|
self.rsc.widgets(),
|
|
&self.rsc.ui().render_state().get(),
|
|
&self.rsc,
|
|
) {
|
|
let ui_state = self.state.android_state_mut();
|
|
if let Some(events) = ui_state.access_adapter.update_if_active(|| tree_update) {
|
|
ctx.push_dynamic_deferred_callback(move |env, view| {
|
|
raise_if_enabled(env, view, events);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn show_soft_input<'local>(env: &mut JNIEnv<'local>, view: &View<'local>) {
|
|
let imm = view.input_method_manager(env);
|
|
imm.show_soft_input(env, view, 0);
|
|
}
|
|
|
|
fn show_renderer_error<'local>(env: &mut JNIEnv<'local>, view: &View<'local>, report: &str) {
|
|
let Ok(message) = env.new_string(report) else {
|
|
return;
|
|
};
|
|
let _ = env.call_method(
|
|
&view.0,
|
|
"showRendererError",
|
|
"(Ljava/lang/String;)V",
|
|
&[JValue::Object(message.as_ref())],
|
|
);
|
|
}
|
|
|
|
impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
|
fn on_key_down<'local>(
|
|
&mut self,
|
|
ctx: &mut CallbackCtx<'local>,
|
|
key_code: Keycode,
|
|
event: &KeyEvent<'local>,
|
|
) -> bool {
|
|
self.drain_tasks();
|
|
// With no `OnBackPressedCallback` registered on the Java side, the
|
|
// system still delivers the back gesture as a synthetic
|
|
// `KEYCODE_BACK` through this same path -- the legacy behaviour
|
|
// every view-based app gets by default, and enough for "the back
|
|
// gesture as an event" without a second JNI registry. See
|
|
// `android/insets.rs`'s doc comment for why insets could not take
|
|
// the same shortcut.
|
|
if key_code == Keycode::Back {
|
|
if self.rsc.run_command(Command::Escape) != CommandResult::Unused {
|
|
self.after_input(ctx);
|
|
return true;
|
|
}
|
|
let handled = self.state.back_pressed(&mut self.rsc);
|
|
if handled {
|
|
self.after_input(ctx);
|
|
}
|
|
return handled;
|
|
}
|
|
if self.rsc.events().controllers.command_target_blocks_input() {
|
|
return true;
|
|
}
|
|
let handled = super::input::on_key(
|
|
&mut self.rsc,
|
|
&mut self.state,
|
|
&mut ctx.env,
|
|
key_code,
|
|
event,
|
|
);
|
|
if handled {
|
|
self.after_input(ctx);
|
|
}
|
|
handled
|
|
}
|
|
|
|
fn on_touch_event<'local>(
|
|
&mut self,
|
|
ctx: &mut CallbackCtx<'local>,
|
|
event: &MotionEvent<'local>,
|
|
) -> bool {
|
|
self.drain_tasks();
|
|
let action = event.action_masked(&mut ctx.env);
|
|
// MotionEvent and layout both use physical pixels.
|
|
let x = event.x(&mut ctx.env);
|
|
let y = event.y(&mut ctx.env);
|
|
// Use the event clock so batched movement keeps its real timing.
|
|
let event_time = event.event_time_nanos(&mut ctx.env);
|
|
let history = event.history_size(&mut ctx.env);
|
|
let mut clock = match self.device_clock {
|
|
Some(clock) => clock,
|
|
None => {
|
|
let oldest = if history > 0 {
|
|
event.historical_event_time_nanos(&mut ctx.env, 0)
|
|
} else {
|
|
event_time
|
|
};
|
|
self.device_clock(event_time, oldest)
|
|
}
|
|
};
|
|
// Avoid allocating trace history when input tracing is disabled.
|
|
let trace_input = crate::diagnostics::trace_enabled();
|
|
let mut historical_ms: Vec<(u64, f32, f32)> = Vec::new();
|
|
|
|
if matches!(action, MotionAction::Move) {
|
|
// Android orders history oldest-first; `sample` checks monotonicity.
|
|
for pos in 0..history {
|
|
let hx = event.historical_axis(&mut ctx.env, Axis::X, 0, pos);
|
|
let hy = event.historical_axis(&mut ctx.env, Axis::Y, 0, pos);
|
|
let ht = event.historical_event_time_nanos(&mut ctx.env, pos);
|
|
let sample_at = clock.sample(ht);
|
|
if trace_input {
|
|
historical_ms.push((clock.ms_since_anchor(ht), hx, hy));
|
|
}
|
|
let ui_state = self.state.android_state_mut();
|
|
ui_state.cursor.pos = vec2(hx, hy);
|
|
ui_state.cursor.time = sample_at;
|
|
self.run_input_frame(ctx);
|
|
}
|
|
}
|
|
|
|
let event_at = clock.sample(event_time);
|
|
let event_ms = clock.ms_since_anchor(event_time);
|
|
self.device_clock = Some(clock);
|
|
let ui_state = self.state.android_state_mut();
|
|
ui_state.cursor.time = event_at;
|
|
match action {
|
|
MotionAction::Down => {
|
|
ui_state.cursor.pos = vec2(x, y);
|
|
ui_state.cursor.exists = true;
|
|
ui_state.cursor.buttons.left.update(true);
|
|
}
|
|
MotionAction::Move => {
|
|
ui_state.cursor.pos = vec2(x, y);
|
|
}
|
|
MotionAction::Up => {
|
|
ui_state.cursor.pos = vec2(x, y);
|
|
ui_state.cursor.buttons.left.update(false);
|
|
}
|
|
MotionAction::Cancel => {
|
|
ui_state.cursor.pos = vec2(x, y);
|
|
ui_state.cursor.buttons.left.update(false);
|
|
ui_state.cursor.cancelled = true;
|
|
}
|
|
_ => return false,
|
|
}
|
|
if trace_input {
|
|
let action_word = match action {
|
|
MotionAction::Down => "down",
|
|
MotionAction::Move => "move",
|
|
MotionAction::Up => "up",
|
|
MotionAction::Cancel => "cancel",
|
|
_ => "other",
|
|
};
|
|
crate::sense::log_input_event(action_word, x, y, event_ms, &historical_ms);
|
|
}
|
|
self.after_input(ctx);
|
|
true
|
|
}
|
|
|
|
fn on_generic_motion_event<'local>(
|
|
&mut self,
|
|
ctx: &mut CallbackCtx<'local>,
|
|
event: &MotionEvent<'local>,
|
|
) -> bool {
|
|
self.drain_tasks();
|
|
self.generic_motion(ctx, event)
|
|
}
|
|
|
|
fn on_hover_event<'local>(
|
|
&mut self,
|
|
ctx: &mut CallbackCtx<'local>,
|
|
event: &MotionEvent<'local>,
|
|
) -> bool {
|
|
let action = event.action(&mut ctx.env);
|
|
let x = event.x(&mut ctx.env);
|
|
let y = event.y(&mut ctx.env);
|
|
let access_events = {
|
|
let render_handle = self.rsc.ui().render_state();
|
|
let render_state = render_handle.get();
|
|
let mut source = AndroidAccessSource {
|
|
widgets: self.rsc.widgets(),
|
|
render: &render_state,
|
|
rsc: &self.rsc,
|
|
};
|
|
self.state
|
|
.android_state_mut()
|
|
.access_adapter
|
|
.on_hover_event(&mut source, action, x, y)
|
|
};
|
|
if let Some(events) = access_events {
|
|
ctx.push_dynamic_deferred_callback(move |env, view| {
|
|
raise_if_enabled(env, view, events);
|
|
});
|
|
true
|
|
} else {
|
|
self.drain_tasks();
|
|
self.generic_motion(ctx, event)
|
|
}
|
|
}
|
|
|
|
fn on_focus_changed<'local>(
|
|
&mut self,
|
|
ctx: &mut CallbackCtx<'local>,
|
|
gain_focus: bool,
|
|
_direction: i32,
|
|
_previously_focused_rect: Option<&Rect<'local>>,
|
|
) {
|
|
self.drain_tasks();
|
|
if !gain_focus {
|
|
let ui_state = self.state.android_state_mut();
|
|
if let Some(focus) = ui_state.focus.take() {
|
|
focus.edit(&mut self.rsc).deselect();
|
|
}
|
|
}
|
|
self.after_input(ctx);
|
|
}
|
|
|
|
fn on_attached_to_window(&mut self, _ctx: &mut CallbackCtx) {
|
|
self.drain_tasks();
|
|
}
|
|
|
|
fn surface_changed<'local>(
|
|
&mut self,
|
|
ctx: &mut CallbackCtx<'local>,
|
|
holder: &android_view::SurfaceHolder<'local>,
|
|
_format: i32,
|
|
width: i32,
|
|
height: i32,
|
|
) {
|
|
self.drain_tasks();
|
|
// The layout canvas and wgpu surface are separate and both use physical pixels.
|
|
self.rsc.ui_mut().resize((width as f32, height as f32));
|
|
|
|
// Resizing preserves GPU resources; recreating a destroyed surface does not.
|
|
let already_live = self.state.android_state().renderer.is_some();
|
|
log::info!(
|
|
"iris surface: surface_changed {width}x{height} already_live={already_live} \
|
|
glyphs_cached={} atlas_pages={}",
|
|
self.rsc.ui().text.atlas.glyph_count(),
|
|
self.rsc.ui().text.atlas.page_count(),
|
|
);
|
|
if already_live {
|
|
let ui_state = self.state.android_state_mut();
|
|
ui_state
|
|
.renderer
|
|
.as_mut()
|
|
.expect("checked Some above")
|
|
.resize(width as u32, height as u32);
|
|
self.render(ctx, Instant::now());
|
|
return;
|
|
}
|
|
|
|
let window = holder.surface(&mut ctx.env).to_native_window(&mut ctx.env);
|
|
// `content_scale` reaches `AndroidRenderer` only for the
|
|
// Diagnostics page's report text now -- window size and the
|
|
// shader's window uniform are physical pixels throughout (see the
|
|
// `resize` call above), not divided by it.
|
|
let content_scale = self.state.android_state().content_scale;
|
|
match AndroidRenderer::new(window, width as u32, height as u32, content_scale) {
|
|
Ok(renderer) => {
|
|
// A genuinely new renderer means a genuinely new GPU
|
|
// device, holding none of the textures the old one did --
|
|
// while the CPU side of them (`UiData::textures`, and the
|
|
// glyph atlas built on it) lives on `self.rsc` and
|
|
// survives. So every slot has to be uploaded again, and
|
|
// `Textures::reupload` queues exactly that, in slot order.
|
|
//
|
|
// It replaces clearing them, which threw away the *slot
|
|
// numbering* as well as the pixels: every `TextureHandle`
|
|
// a live widget still held -- one per icon or image on
|
|
// screen, and one per folded card at the time -- then
|
|
// named a slot nothing recognised, and the next frame
|
|
// panicked in `image_bind_group` ("texture slot 89 is not
|
|
// a live standalone image: None"). Re-uploading also keeps
|
|
// the glyph atlas, so an app switch no longer re-rasterises
|
|
// every glyph on screen. This only runs on the branch that
|
|
// actually builds a new renderer, never on the reuse
|
|
// branch above, where the textures are still on the device
|
|
// that holds them.
|
|
log::info!(
|
|
"iris surface: new renderer built ({:?}), re-uploading textures: \
|
|
glyphs={} pages={}",
|
|
renderer.adapter_backend,
|
|
self.rsc.ui().text.atlas.glyph_count(),
|
|
self.rsc.ui().text.atlas.page_count(),
|
|
);
|
|
self.rsc.ui_mut().textures.reupload();
|
|
self.rsc.ui_mut().paints.reupload();
|
|
self.state.android_state_mut().renderer = Some(renderer);
|
|
self.render(ctx, Instant::now());
|
|
}
|
|
Err(report) => {
|
|
log::error!("iris renderer init failed: {}", report.replace('\n', " | "));
|
|
ctx.push_dynamic_deferred_callback(move |env, view| {
|
|
show_renderer_error(env, view, &report);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
fn surface_destroyed<'local>(
|
|
&mut self,
|
|
_ctx: &mut CallbackCtx<'local>,
|
|
_holder: &android_view::SurfaceHolder<'local>,
|
|
) {
|
|
log::info!(
|
|
"iris surface: surface_destroyed, tearing the renderer down \
|
|
(glyphs_cached={} atlas_pages={})",
|
|
self.rsc.ui().text.atlas.glyph_count(),
|
|
self.rsc.ui().text.atlas.page_count(),
|
|
);
|
|
self.state.android_state_mut().renderer = None;
|
|
}
|
|
|
|
fn do_frame(&mut self, ctx: &mut CallbackCtx, frame_time_nanos: i64) {
|
|
self.drain_tasks();
|
|
// The vsync this frame is for, dated on the same ruler touch
|
|
// samples are (`DeviceClock`), rather than `Instant::now()` here:
|
|
// this callback runs some variable distance after that vsync --
|
|
// behind `drain_tasks`, behind whatever else the UI thread was
|
|
// doing -- and anything advanced by that variable amount moves
|
|
// unevenly between frames the display shows evenly. `at` rather
|
|
// than `sample`, since a frame time is not part of the touch
|
|
// samples' own ordering.
|
|
let now = self
|
|
.device_clock(frame_time_nanos, frame_time_nanos)
|
|
.at(frame_time_nanos);
|
|
self.render(ctx, now);
|
|
}
|
|
|
|
/// Where `AndroidRedrawHandle::request_redraw` (`android/render.rs`)
|
|
/// actually lands: `View.postDelayed`'s Runnable resolves to this, on
|
|
/// the UI thread, which is what makes it safe to call from a background
|
|
/// task's own thread when `post_frame_callback`'s `Choreographer`
|
|
/// requirement (a `Looper` on the *calling* thread) is not. Same body
|
|
/// as `do_frame` -- draining tasks and rendering immediately is a
|
|
/// perfectly good answer to "a background fetch has new state," and
|
|
/// avoids a second frame-scheduling path to keep in sync with the real
|
|
/// one.
|
|
fn delayed_callback(&mut self, ctx: &mut CallbackCtx) {
|
|
self.drain_tasks();
|
|
self.render(ctx, Instant::now());
|
|
}
|
|
|
|
fn as_input_connection(&mut self) -> Option<&mut dyn InputConnection> {
|
|
Some(self)
|
|
}
|
|
|
|
fn as_accessibility_node_provider(&mut self) -> Option<&mut dyn AccessibilityNodeProvider> {
|
|
Some(self)
|
|
}
|
|
}
|
|
|
|
impl<State: AndroidAppState> AccessibilityNodeProvider for IrisViewPeer<State> {
|
|
fn create_accessibility_node_info<'local>(
|
|
&mut self,
|
|
ctx: &mut CallbackCtx<'local>,
|
|
virtual_view_id: jint,
|
|
) -> AccessibilityNodeInfo<'local> {
|
|
let render_handle = self.rsc.ui().render_state();
|
|
let render_state = render_handle.get();
|
|
let mut source = AndroidAccessSource {
|
|
widgets: self.rsc.widgets(),
|
|
render: &render_state,
|
|
rsc: &self.rsc,
|
|
};
|
|
let ui_state = self.state.android_state_mut();
|
|
AccessibilityNodeInfo(ui_state.access_adapter.create_accessibility_node_info(
|
|
&mut source,
|
|
&mut ctx.env,
|
|
&ctx.view.0,
|
|
virtual_view_id,
|
|
))
|
|
}
|
|
|
|
fn find_focus<'local>(
|
|
&mut self,
|
|
ctx: &mut CallbackCtx<'local>,
|
|
focus_type: jint,
|
|
) -> AccessibilityNodeInfo<'local> {
|
|
let render_handle = self.rsc.ui().render_state();
|
|
let render_state = render_handle.get();
|
|
let mut source = AndroidAccessSource {
|
|
widgets: self.rsc.widgets(),
|
|
render: &render_state,
|
|
rsc: &self.rsc,
|
|
};
|
|
let ui_state = self.state.android_state_mut();
|
|
AccessibilityNodeInfo(ui_state.access_adapter.find_focus(
|
|
&mut source,
|
|
&mut ctx.env,
|
|
&ctx.view.0,
|
|
focus_type,
|
|
))
|
|
}
|
|
|
|
fn perform_action<'local>(
|
|
&mut self,
|
|
ctx: &mut CallbackCtx<'local>,
|
|
virtual_view_id: jint,
|
|
action: jint,
|
|
arguments: &Bundle<'local>,
|
|
) -> bool {
|
|
let Some(action) =
|
|
accesskit_android::PlatformAction::from_java(&mut ctx.env, action, &arguments.0)
|
|
else {
|
|
return false;
|
|
};
|
|
let ui_state = self.state.android_state_mut();
|
|
let Some(events) = ui_state.access_adapter.perform_action(
|
|
&mut NullActionHandler,
|
|
virtual_view_id,
|
|
&action,
|
|
) else {
|
|
return false;
|
|
};
|
|
ctx.push_dynamic_deferred_callback(move |env, view| {
|
|
raise_if_enabled(env, view, events);
|
|
});
|
|
true
|
|
}
|
|
}
|
|
|
|
/// Registers `IrisViewPeer<State>`'s native methods and builds one on every
|
|
/// `newViewPeer` call from Java. `State`'s app crate wraps this in a
|
|
/// concrete `extern "system" fn` (a generic function cannot be handed to
|
|
/// `register_view_class`, which wants a plain function pointer) -- see
|
|
/// `iris/android-app/src/lib.rs`.
|
|
pub fn new_peer<'local, State: AndroidAppState>(
|
|
mut env: JNIEnv<'local>,
|
|
view: View<'local>,
|
|
context: Context<'local>,
|
|
init: fn(AndroidUiState, &mut State::Resources) -> State,
|
|
) -> android_view::jni::sys::jlong {
|
|
// `DisplayMetrics.density` -- physical pixels per dp on this device.
|
|
// Read once here, at the one point in this file already handed a
|
|
// `Context`, and carried on `AndroidUiState` from then on (see
|
|
// `content_scale`'s field comment for what depends on it).
|
|
let content_scale = context
|
|
.resources(&mut env)
|
|
.display_metrics(&mut env)
|
|
.density(&mut env);
|
|
log::info!("iris: new_peer content_scale={content_scale}");
|
|
let vm = env.get_java_vm().unwrap();
|
|
let global_view = env.new_global_ref(&view.0).unwrap();
|
|
let redraw: Arc<dyn RequestRedraw> = Arc::new(AndroidRedrawHandle::new(vm, global_view));
|
|
let (mut rsc, task_recv) = State::Resources::new(redraw);
|
|
rsc.ui_mut().set_density(content_scale);
|
|
let shared = Rc::new(RefCell::new(Shared::default()));
|
|
let ui_state = AndroidUiState::new(shared.clone(), content_scale);
|
|
let mut state = init(ui_state, &mut rsc);
|
|
let platform_vm = env.get_java_vm().unwrap();
|
|
let platform_view = env.new_global_ref(&view.0).unwrap();
|
|
state.platform_ready(&mut rsc, platform_vm, platform_view);
|
|
let peer = IrisViewPeer {
|
|
rsc,
|
|
state,
|
|
task_recv,
|
|
device_clock: None,
|
|
};
|
|
let id = android_view::register_view_peer(peer);
|
|
super::insets::register(id, shared);
|
|
id
|
|
}
|