Add Iris Android APK tooling

This commit is contained in:
iris committed 2026-09-11 03:44:06 -04:00
1 parent 3246f397b5
commit 37f956707e
20 files changed
+1766 -54

No files matched your search

+60 -34
View File
@@ -89,8 +89,8 @@ impl AndroidUiState {
}
}
impl<State: 'static> HasRoot<StdRsc<State>> for AndroidUiState {
fn set_root(&mut self, rsc: &mut StdRsc<State>, root: StrongWidget) {
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));
}
}
@@ -100,16 +100,41 @@ pub trait HasAndroidUiState: Sized + 'static {
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 {
fn new(ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self;
type Resources: AndroidResources<Self>;
#[allow(unused_variables)]
fn back_pressed(&mut self, rsc: &mut StdRsc<Self>) -> bool {
fn back_pressed(&mut self, rsc: &mut Self::Resources) -> bool {
false
}
#[allow(unused_variables)]
fn platform_ready(&mut self, rsc: &mut StdRsc<Self>, vm: JavaVM, view: GlobalRef) {}
fn platform_ready(&mut self, rsc: &mut Self::Resources, vm: JavaVM, view: GlobalRef) {}
#[allow(unused_variables)]
fn on_insets_changed(&mut self, rsc: &mut StdRsc<Self>, insets: WindowInsets) {}
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.
@@ -144,9 +169,9 @@ impl WindowInsets {
/// `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) rsc: State::Resources,
pub(super) state: State,
task_recv: TaskMsgReceiver<StdRsc<State>>,
task_recv: TaskMsgReceiver<State::Resources>,
/// Converts input and Choreographer timestamps onto one monotonic clock.
device_clock: Option<DeviceClock>,
}
@@ -169,7 +194,7 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
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();
let render_state = self.rsc.ui().render_state();
render_state
.get()
.run_sensors(&mut self.rsc, &mut self.state, cursor, window_size);
@@ -200,7 +225,7 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
let ui_state = self.state.android_state_mut();
ui_state.cursor.end_frame();
let render_state = self.rsc.ui.render_state();
let render_state = self.rsc.ui().render_state();
if render_state
.get()
.needs_redraw(&ui_state.root, self.rsc.widgets())
@@ -298,13 +323,13 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
"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(),
self.rsc.ui().render_state().get().active_widgets(),
ui_state
.root
.as_ref()
.and_then(|r| {
self.rsc
.ui
.ui()
.render_state()
.get()
.window_region(r, &self.rsc)
@@ -313,7 +338,7 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
);
}
let frame_start = Instant::now();
let animating = self.rsc.ui.tick_animations(now);
let animating = self.rsc.ui_mut().tick_animations(now);
if animating {
ctx.view.post_frame_callback(&mut ctx.env);
}
@@ -323,7 +348,7 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
let Some(renderer) = &mut ui_state.renderer else {
return;
};
let frame_diagnostics = renderer.update(&mut self.rsc.ui);
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={} \
@@ -344,20 +369,20 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
.android_state_mut()
.frame_report
.record(now, parts, animating);
let render_state = self.rsc.ui.render_state();
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(),
self.rsc.ui().render_state().get().active_widgets(),
ui_state
.root
.as_ref()
.and_then(|r| {
self.rsc
.ui
.ui()
.render_state()
.get()
.window_region(r, &self.rsc)
@@ -368,7 +393,7 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
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.ui().render_state().get(),
&self.rsc,
) {
let ui_state = self.state.android_state_mut();
@@ -424,7 +449,7 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
}
return handled;
}
if self.rsc.events.controllers.command_target_blocks_input() {
if self.rsc.events().controllers.command_target_blocks_input() {
return true;
}
let handled = super::input::on_key(
@@ -542,7 +567,7 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
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_handle = self.rsc.ui().render_state();
let render_state = render_handle.get();
let mut source = AndroidAccessSource {
widgets: self.rsc.widgets(),
@@ -596,15 +621,15 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
) {
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));
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(),
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();
@@ -648,11 +673,11 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
"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().text.atlas.glyph_count(),
self.rsc.ui().text.atlas.page_count(),
);
self.rsc.ui.textures.reupload();
self.rsc.ui.paints.reupload();
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());
}
@@ -673,8 +698,8 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
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.rsc.ui().text.atlas.glyph_count(),
self.rsc.ui().text.atlas.page_count(),
);
self.state.android_state_mut().renderer = None;
}
@@ -724,7 +749,7 @@ impl<State: AndroidAppState> AccessibilityNodeProvider for IrisViewPeer<State> {
ctx: &mut CallbackCtx<'local>,
virtual_view_id: jint,
) -> AccessibilityNodeInfo<'local> {
let render_handle = self.rsc.ui.render_state();
let render_handle = self.rsc.ui().render_state();
let render_state = render_handle.get();
let mut source = AndroidAccessSource {
widgets: self.rsc.widgets(),
@@ -745,7 +770,7 @@ impl<State: AndroidAppState> AccessibilityNodeProvider for IrisViewPeer<State> {
ctx: &mut CallbackCtx<'local>,
focus_type: jint,
) -> AccessibilityNodeInfo<'local> {
let render_handle = self.rsc.ui.render_state();
let render_handle = self.rsc.ui().render_state();
let render_state = render_handle.get();
let mut source = AndroidAccessSource {
widgets: self.rsc.widgets(),
@@ -797,6 +822,7 @@ 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
@@ -810,11 +836,11 @@ pub fn new_peer<'local, State: AndroidAppState>(
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 (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 = State::new(ui_state, &mut rsc);
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);