Files
iris/src/android/view.rs
T

831 lines
31 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<State: 'static> HasRoot<StdRsc<State>> for AndroidUiState {
fn set_root(&mut self, rsc: &mut StdRsc<State>, 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;
}
pub trait AndroidAppState: HasAndroidUiState {
fn new(ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self;
#[allow(unused_variables)]
fn back_pressed(&mut self, rsc: &mut StdRsc<Self>) -> bool {
false
}
#[allow(unused_variables)]
fn platform_ready(&mut self, rsc: &mut StdRsc<Self>, vm: JavaVM, view: GlobalRef) {}
#[allow(unused_variables)]
fn on_insets_changed(&mut self, rsc: &mut StdRsc<Self>, insets: WindowInsets) {}
}
/// 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: StdRsc<State>,
pub(super) state: State,
task_recv: TaskMsgReceiver<StdRsc<State>>,
/// 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.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(&mut self.rsc.ui);
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.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.textures.reupload();
self.rsc.ui.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>,
) -> 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) = StdRsc::new(redraw);
rsc.ui.set_density(content_scale);
let shared = Rc::new(RefCell::new(Shared::default()));
let ui_state = AndroidUiState::new(shared.clone(), content_scale);
let mut state = State::new(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
}