Add Iris Android APK tooling
This commit is contained in:
1 parent
3246f397b5
commit
37f956707e
20 files changed
+1766
-54
No files matched your search
+9
-9
@@ -31,14 +31,14 @@ fn utf16_to_byte(text: &str, utf16_idx: usize) -> usize {
|
||||
|
||||
impl<State: AndroidAppState> IrisViewPeer<State> {
|
||||
fn focus(&self) -> Option<WeakWidget<TextEdit>> {
|
||||
(!self.rsc.events.controllers.command_target_blocks_input())
|
||||
(!self.rsc.events().controllers.command_target_blocks_input())
|
||||
.then_some(self.state.android_state().focus)
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub(super) fn update_ime_selection(&mut self, ctx: &mut CallbackCtx) {
|
||||
let Some(focus) = self.focus() else { return };
|
||||
let text = &self.rsc[focus];
|
||||
let text = focus.get(&self.rsc);
|
||||
let Some(sel) = text.selection_range() else {
|
||||
return;
|
||||
};
|
||||
@@ -87,7 +87,7 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
||||
IME_FLAG_NO_FULLSCREEN | IME_FLAG_NO_EXTRACT_UI | IME_FLAG_NO_ENTER_ACTION,
|
||||
);
|
||||
if let Some(focus) = self.focus() {
|
||||
let text = &self.rsc[focus];
|
||||
let text = focus.get(&self.rsc);
|
||||
let sel = text.selection_range().unwrap_or(0..0);
|
||||
let start = byte_to_utf16(text.text(), sel.start) as i32;
|
||||
let end = byte_to_utf16(text.text(), sel.end) as i32;
|
||||
@@ -112,7 +112,7 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
||||
return None;
|
||||
}
|
||||
let focus = self.focus()?;
|
||||
let text = &self.rsc[focus];
|
||||
let text = focus.get(&self.rsc);
|
||||
let sel = text.selection_range()?;
|
||||
let end_16 = byte_to_utf16(text.text(), sel.start);
|
||||
let start_16 = end_16.saturating_sub(n as usize);
|
||||
@@ -129,7 +129,7 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
||||
return None;
|
||||
}
|
||||
let focus = self.focus()?;
|
||||
let text = &self.rsc[focus];
|
||||
let text = focus.get(&self.rsc);
|
||||
let sel = text.selection_range()?;
|
||||
let len_16 = byte_to_utf16(text.text(), text.text().len());
|
||||
let start_16 = byte_to_utf16(text.text(), sel.end);
|
||||
@@ -140,14 +140,14 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
||||
|
||||
fn selected_text<'slf>(&'slf mut self, _ctx: &mut CallbackCtx) -> Option<Cow<'slf, str>> {
|
||||
let focus = self.focus()?;
|
||||
Some(Cow::Owned(self.rsc[focus].selected_text()?))
|
||||
Some(Cow::Owned(focus.get(&self.rsc).selected_text()?))
|
||||
}
|
||||
|
||||
fn cursor_caps_mode(&mut self, ctx: &mut CallbackCtx, req_modes: u32) -> u32 {
|
||||
let Some(focus) = self.focus() else {
|
||||
return 0;
|
||||
};
|
||||
let text = &self.rsc[focus];
|
||||
let text = focus.get(&self.rsc);
|
||||
let Some(caret) = text.caret() else {
|
||||
return 0;
|
||||
};
|
||||
@@ -164,7 +164,7 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
||||
let Some(focus) = self.focus() else {
|
||||
return false;
|
||||
};
|
||||
let text = &self.rsc[focus];
|
||||
let text = focus.get(&self.rsc);
|
||||
let Some(sel) = text.selection_range() else {
|
||||
return false;
|
||||
};
|
||||
@@ -223,7 +223,7 @@ impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
||||
let Some(focus) = self.focus() else {
|
||||
return false;
|
||||
};
|
||||
let text = &self.rsc[focus];
|
||||
let text = focus.get(&self.rsc);
|
||||
let content = text.text();
|
||||
let byte = utf16_to_byte(content, end.max(0) as usize);
|
||||
focus.edit(&mut self.rsc).set_cursor_byte(byte);
|
||||
|
||||
@@ -10,7 +10,7 @@ use super::view::AndroidAppState;
|
||||
/// the arrow keys on a physical keyboard) plus whatever `unicode_char`
|
||||
/// reports for a plain key press. Returns whether anything used the event.
|
||||
pub(super) fn on_key<'local, State: AndroidAppState>(
|
||||
rsc: &mut StdRsc<State>,
|
||||
rsc: &mut State::Resources,
|
||||
state: &mut State,
|
||||
env: &mut JNIEnv<'local>,
|
||||
key_code: Keycode,
|
||||
|
||||
+33
-1
@@ -10,9 +10,41 @@ mod view;
|
||||
pub use insets::Insets;
|
||||
pub use render::AndroidRenderer;
|
||||
pub use view::{
|
||||
AndroidAppState, AndroidUiState, HasAndroidUiState, IrisViewPeer, WindowInsets, new_peer,
|
||||
AndroidAppState, AndroidResources, AndroidUiState, HasAndroidUiState, IrisViewPeer,
|
||||
WindowInsets, new_peer,
|
||||
};
|
||||
|
||||
/// Types used by `#[iris::app_init]`'s generated JNI boundary.
|
||||
#[doc(hidden)]
|
||||
pub mod __private {
|
||||
pub use android_view::{Context, View, jni::JNIEnv};
|
||||
pub type JInt = android_view::jni::sys::jint;
|
||||
pub type JLong = android_view::jni::sys::jlong;
|
||||
pub type RawJavaVM = android_view::jni::sys::JavaVM;
|
||||
|
||||
/// Registers the fixed view class shipped by `cargo-iris`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `vm` must be the live VM pointer Android passed to `JNI_OnLoad`.
|
||||
pub unsafe fn on_load(
|
||||
vm: *mut RawJavaVM,
|
||||
new_peer: for<'local> extern "system" fn(
|
||||
JNIEnv<'local>,
|
||||
View<'local>,
|
||||
Context<'local>,
|
||||
) -> JLong,
|
||||
) -> JInt {
|
||||
use android_view::{jni::JavaVM, register_view_class};
|
||||
|
||||
let vm = unsafe { JavaVM::from_raw(vm) }.unwrap();
|
||||
let mut env = vm.get_env().unwrap();
|
||||
const VIEW_CLASS: &str = "dev/iris/android/IrisView";
|
||||
register_view_class(&mut env, VIEW_CLASS, new_peer);
|
||||
super::register_native_methods(&mut env, VIEW_CLASS);
|
||||
android_view::jni::sys::JNI_VERSION_1_6
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers the extra native methods this backend needs beyond what
|
||||
/// `android_view::register_view_class` covers (window insets -- see
|
||||
/// `insets.rs`'s doc comment for why that one could not ride along on an
|
||||
|
||||
+19
-1
@@ -191,7 +191,25 @@ impl AndroidRenderer {
|
||||
);
|
||||
|
||||
let surface_caps = surface.get_capabilities(&adapter);
|
||||
let formats = iris_core::srgb_surface_format(&surface_caps)?;
|
||||
let mut formats = iris_core::srgb_surface_format(&surface_caps)?;
|
||||
let downlevel = adapter.get_downlevel_capabilities();
|
||||
if formats.view != formats.surface
|
||||
&& !downlevel
|
||||
.flags
|
||||
.contains(DownlevelFlags::SURFACE_VIEW_FORMATS)
|
||||
{
|
||||
// GLES implementations are allowed to lack texture-view format
|
||||
// reinterpretation. Configuring the advertised non-sRGB format
|
||||
// with an sRGB view then fails validation and leaves the surface
|
||||
// unusable. Rendering through the advertised format is the only
|
||||
// viable fallback on those devices.
|
||||
log::warn!(
|
||||
"iris renderer: {:?} cannot use an sRGB surface view; rendering through {:?}",
|
||||
formats.surface,
|
||||
formats.surface,
|
||||
);
|
||||
formats.view = formats.surface;
|
||||
}
|
||||
log::info!(
|
||||
"iris renderer: surface={:?}, view={:?}, color_space=Srgb",
|
||||
formats.surface,
|
||||
|
||||
+60
-34
@@ -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);
|
||||
|
||||
@@ -29,6 +29,7 @@ mod layout_tests;
|
||||
|
||||
pub use iris_core as core;
|
||||
pub use iris_macro as macros;
|
||||
pub use iris_macro::app_init;
|
||||
|
||||
pub mod prelude {
|
||||
use super::*;
|
||||
|
||||
Reference in new issue
Block a user