Clean up shared UI runtime state

This commit is contained in:
iris committed 2026-09-11 00:55:33 -04:00
1 parent 9b4c690916
commit 5ca244528f
27 files changed
+355 -499

No files matched your search

+36 -6
View File
@@ -68,7 +68,12 @@ fn bench_first_frame(n: usize) {
let start = Instant::now(); let start = Instant::now();
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
let elapsed = start.elapsed(); let elapsed = start.elapsed();
let (draws, rewrites, moves, _shapes) = render.take_counters(); let RenderCounters {
draws,
region_rewrites: rewrites,
moves,
..
} = render.take_counters();
report( report(
&format!("(a) first frame, N={n}"), &format!("(a) first frame, N={n}"),
elapsed, elapsed,
@@ -97,7 +102,12 @@ fn bench_scroll(n: usize, ticks: usize) {
let start = Instant::now(); let start = Instant::now();
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
total += start.elapsed(); total += start.elapsed();
let (draws, rewrites, moves, _shapes) = render.take_counters(); let RenderCounters {
draws,
region_rewrites: rewrites,
moves,
..
} = render.take_counters();
total_draws += draws; total_draws += draws;
total_rewrites += rewrites; total_rewrites += rewrites;
total_moves += moves; total_moves += moves;
@@ -155,7 +165,12 @@ fn bench_input_grows(n: usize, lines: usize) {
let start = Instant::now(); let start = Instant::now();
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
total += start.elapsed(); total += start.elapsed();
let (draws, rewrites, moves, _shapes) = render.take_counters(); let RenderCounters {
draws,
region_rewrites: rewrites,
moves,
..
} = render.take_counters();
total_draws += draws; total_draws += draws;
total_rewrites += rewrites; total_rewrites += rewrites;
total_moves += moves; total_moves += moves;
@@ -200,7 +215,12 @@ fn bench_insert_above_anchor(n: usize, inserts: usize) {
let start = Instant::now(); let start = Instant::now();
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
total += start.elapsed(); total += start.elapsed();
let (draws, rewrites, moves, _shapes) = render.take_counters(); let RenderCounters {
draws,
region_rewrites: rewrites,
moves,
..
} = render.take_counters();
total_draws += draws; total_draws += draws;
total_rewrites += rewrites; total_rewrites += rewrites;
total_moves += moves; total_moves += moves;
@@ -270,7 +290,12 @@ fn bench_expand_holds_edge(n: usize, growths: usize) {
let start = Instant::now(); let start = Instant::now();
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
total += start.elapsed(); total += start.elapsed();
let (draws, rewrites, moves, _shapes) = render.take_counters(); let RenderCounters {
draws,
region_rewrites: rewrites,
moves,
..
} = render.take_counters();
total_draws += draws; total_draws += draws;
total_rewrites += rewrites; total_rewrites += rewrites;
total_moves += moves; total_moves += moves;
@@ -314,7 +339,12 @@ fn bench_redraw_big_text(chars: usize, redraws: usize) {
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
total += start.elapsed(); total += start.elapsed();
} }
let (draws, rewrites, moves, _shapes) = render.take_counters(); let RenderCounters {
draws,
region_rewrites: rewrites,
moves,
..
} = render.take_counters();
report( report(
&format!("(g) redraw one {chars}-glyph text, {redraws}x (totals)"), &format!("(g) redraw one {chars}-glyph text, {redraws}x (totals)"),
total, total,
+1 -1
View File
@@ -32,7 +32,7 @@ fn entry_node(entry: &Entry) -> Node {
/// Owns the last tree pushed out, so `update` can tell "nothing /// Owns the last tree pushed out, so `update` can tell "nothing
/// accessibility-relevant changed" from "something did" without asking /// accessibility-relevant changed" from "something did" without asking
/// the platform adapter to diff two `Node`s itself. One of these per /// the platform adapter to diff two `Node`s itself. One of these per
/// window/view -- `default::DefaultUiState` and `android::AndroidUiState` /// window/view -- `desktop::DesktopUiState` and `android::AndroidUiState`
/// each keep one. /// each keep one.
#[derive(Default)] #[derive(Default)]
pub struct AccessTree { pub struct AccessTree {
+15 -7
View File
@@ -18,6 +18,14 @@ pub enum RedrawKind {
Updates, Updates,
} }
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct RenderCounters {
pub draws: u64,
pub region_rewrites: u64,
pub moves: u64,
pub shapes: u64,
}
pub struct UiRenderState { pub struct UiRenderState {
pub active: HashMap<WidgetId, ActiveData>, pub active: HashMap<WidgetId, ActiveData>,
pub primitives: Primitives, pub primitives: Primitives,
@@ -115,13 +123,13 @@ impl UiRenderState {
} }
} }
pub fn take_counters(&mut self) -> (u64, u64, u64, u64) { pub fn take_counters(&mut self) -> RenderCounters {
( RenderCounters {
std::mem::take(&mut self.draw_count), draws: std::mem::take(&mut self.draw_count),
std::mem::take(&mut self.region_mut_count), region_rewrites: std::mem::take(&mut self.region_mut_count),
std::mem::take(&mut self.mov_count), moves: std::mem::take(&mut self.mov_count),
std::mem::take(&mut self.shape_count), shapes: std::mem::take(&mut self.shape_count),
) }
} }
pub(super) fn note_move(&mut self) { pub(super) fn note_move(&mut self) {
+9 -9
View File
@@ -4,24 +4,24 @@ const ROWS: usize = 1000;
const SETTLE_FRAMES: usize = 4; const SETTLE_FRAMES: usize = 4;
const FRAMES: usize = 6; const FRAMES: usize = 6;
#[derive(DefaultUiState)] #[derive(DesktopUiState)]
struct State { struct State {
ui_state: DefaultUiState, ui_state: DesktopUiState,
span: WeakWidget<Span>, span: WeakWidget<Span>,
frame: usize, frame: usize,
appended: bool, appended: bool,
} }
impl DefaultAppState for State { impl DesktopAppState for State {
fn new( fn new(
mut ui_state: DefaultUiState, mut ui_state: DesktopUiState,
rsc: &mut DefaultRsc<Self>, rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>, _: Proxy<Self::Event>,
) -> Self { ) -> Self {
let mut span = Span::empty(Dir::DOWN); let mut span = Span::empty(Dir::DOWN);
for _ in 0..ROWS { for _ in 0..ROWS {
let img = image::DynamicImage::new_rgba8(32, 32); let img = image::DynamicImage::new_rgba8(32, 32);
let widget = image::<DefaultRsc<Self>>(img)(rsc); let widget = image::<DesktopRsc<Self>>(img)(rsc);
let widget = rsc.ui.widgets.add_strong(widget); let widget = rsc.ui.widgets.add_strong(widget);
span.push(widget.any()); span.push(widget.any());
} }
@@ -40,7 +40,7 @@ impl DefaultAppState for State {
} }
} }
fn window_event(&mut self, event: winit::event::WindowEvent, rsc: &mut DefaultRsc<Self>) { fn window_event(&mut self, event: winit::event::WindowEvent, rsc: &mut DesktopRsc<Self>) {
if !matches!(event, winit::event::WindowEvent::RedrawRequested) { if !matches!(event, winit::event::WindowEvent::RedrawRequested) {
return; return;
} }
@@ -53,7 +53,7 @@ impl DefaultAppState for State {
if self.frame == SETTLE_FRAMES && !self.appended { if self.frame == SETTLE_FRAMES && !self.appended {
self.appended = true; self.appended = true;
let img = image::DynamicImage::new_rgba8(32, 32); let img = image::DynamicImage::new_rgba8(32, 32);
let widget = image::<DefaultRsc<Self>>(img)(rsc); let widget = image::<DesktopRsc<Self>>(img)(rsc);
let widget = rsc.ui.widgets.add_strong(widget); let widget = rsc.ui.widgets.add_strong(widget);
rsc.ui rsc.ui
.widgets .widgets
@@ -71,5 +71,5 @@ impl DefaultAppState for State {
} }
fn main() { fn main() {
DefaultApp::<State>::run(); DesktopApp::<State>::run();
} }
+6 -6
View File
@@ -2,12 +2,12 @@ use iris::prelude::*;
use winit::{dpi::LogicalSize, window::WindowAttributes}; use winit::{dpi::LogicalSize, window::WindowAttributes};
fn main() { fn main() {
DefaultApp::<State>::run(); DesktopApp::<State>::run();
} }
#[derive(DefaultUiState)] #[derive(DesktopUiState)]
struct State { struct State {
ui_state: DefaultUiState, ui_state: DesktopUiState,
} }
const ROWS: usize = 800; const ROWS: usize = 800;
@@ -58,14 +58,14 @@ fn build_row<Rsc: UiRsc + 'static>(rsc: &mut Rsc, i: usize) -> StrongWidget {
} }
} }
impl DefaultAppState for State { impl DesktopAppState for State {
fn window_attributes() -> WindowAttributes { fn window_attributes() -> WindowAttributes {
WindowAttributes::default().with_inner_size(LogicalSize::new(420.0, 900.0)) WindowAttributes::default().with_inner_size(LogicalSize::new(420.0, 900.0))
} }
fn new( fn new(
mut ui_state: DefaultUiState, mut ui_state: DesktopUiState,
rsc: &mut DefaultRsc<Self>, rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>, _: Proxy<Self::Event>,
) -> Self { ) -> Self {
let mut list = LazySpan::new(Dir::DOWN, Pin::End); let mut list = LazySpan::new(Dir::DOWN, Pin::End);
+6 -6
View File
@@ -1,18 +1,18 @@
use iris::prelude::*; use iris::prelude::*;
fn main() { fn main() {
DefaultApp::<State>::run(); DesktopApp::<State>::run();
} }
#[derive(DefaultUiState)] #[derive(DesktopUiState)]
struct State { struct State {
ui_state: DefaultUiState, ui_state: DesktopUiState,
} }
impl DefaultAppState for State { impl DesktopAppState for State {
fn new( fn new(
mut ui_state: DefaultUiState, mut ui_state: DesktopUiState,
rsc: &mut DefaultRsc<Self>, rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>, _: Proxy<Self::Event>,
) -> Self { ) -> Self {
rect(PaintId::RED).set_root(rsc, &mut ui_state); rect(PaintId::RED).set_root(rsc, &mut ui_state);
+7 -7
View File
@@ -2,19 +2,19 @@ use iris::prelude::*;
use winit::event::WindowEvent; use winit::event::WindowEvent;
fn main() { fn main() {
DefaultApp::<Client>::run(); DesktopApp::<Client>::run();
} }
#[derive(DefaultUiState)] #[derive(DesktopUiState)]
pub struct Client { pub struct Client {
ui_state: DefaultUiState, ui_state: DesktopUiState,
info: WeakWidget<Text>, info: WeakWidget<Text>,
} }
impl DefaultAppState for Client { impl DesktopAppState for Client {
fn new( fn new(
mut ui_state: DefaultUiState, mut ui_state: DesktopUiState,
rsc: &mut DefaultRsc<Self>, rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>, _: Proxy<Self::Event>,
) -> Self { ) -> Self {
let widgets = tabs_ui::build(rsc, &mut ui_state); let widgets = tabs_ui::build(rsc, &mut ui_state);
@@ -24,7 +24,7 @@ impl DefaultAppState for Client {
} }
} }
fn window_event(&mut self, _: WindowEvent, rsc: &mut DefaultRsc<Self>) { fn window_event(&mut self, _: WindowEvent, rsc: &mut DesktopRsc<Self>) {
let render_state = rsc.ui.render_state(); let render_state = rsc.ui.render_state();
let new = format!( let new = format!(
"widgets: {}\nactive: {}\nviews: {}", "widgets: {}\nactive: {}\nviews: {}",
+6 -6
View File
@@ -2,18 +2,18 @@ use iris::prelude::*;
use std::time::Duration; use std::time::Duration;
fn main() { fn main() {
DefaultApp::<State>::run(); DesktopApp::<State>::run();
} }
#[derive(DefaultUiState)] #[derive(DesktopUiState)]
struct State { struct State {
ui_state: DefaultUiState, ui_state: DesktopUiState,
} }
impl DefaultAppState for State { impl DesktopAppState for State {
fn new( fn new(
mut ui_state: DefaultUiState, mut ui_state: DesktopUiState,
rsc: &mut DefaultRsc<Self>, rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>, _: Proxy<Self::Event>,
) -> Self { ) -> Self {
let rect = rect(PaintId::RED).add(rsc); let rect = rect(PaintId::RED).add(rsc);
+7 -7
View File
@@ -1,15 +1,15 @@
use iris::prelude::*; use iris::prelude::*;
fn main() { fn main() {
DefaultApp::<State>::run(); DesktopApp::<State>::run();
} }
#[derive(DefaultUiState)] #[derive(DesktopUiState)]
struct State { struct State {
ui_state: DefaultUiState, ui_state: DesktopUiState,
} }
type Rsc = DefaultRsc<State>; type Rsc = DesktopRsc<State>;
#[derive(Clone, Copy, WidgetView)] #[derive(Clone, Copy, WidgetView)]
struct Test { struct Test {
@@ -35,10 +35,10 @@ impl Test {
} }
} }
impl DefaultAppState for State { impl DesktopAppState for State {
fn new( fn new(
mut ui_state: DefaultUiState, mut ui_state: DesktopUiState,
rsc: &mut DefaultRsc<Self>, rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>, _: Proxy<Self::Event>,
) -> Self { ) -> Self {
let test = Test::new(rsc); let test = Test::new(rsc);
+9 -9
View File
@@ -101,8 +101,8 @@ pub fn widget_trait(input: TokenStream) -> TokenStream {
.into() .into()
} }
#[proc_macro_derive(DefaultUiState, attributes(default_ui_state))] #[proc_macro_derive(DesktopUiState, attributes(desktop_ui_state))]
pub fn derive_default_ui_state(input: TokenStream) -> TokenStream { pub fn derive_desktop_ui_state(input: TokenStream) -> TokenStream {
let mut output = proc_macro2::TokenStream::new(); let mut output = proc_macro2::TokenStream::new();
let state: ItemStruct = parse_macro_input!(input); let state: ItemStruct = parse_macro_input!(input);
@@ -112,14 +112,14 @@ pub fn derive_default_ui_state(input: TokenStream) -> TokenStream {
for field in &state.fields { for field in &state.fields {
if !found_attr if !found_attr
&& let Type::Path(path) = &field.ty && let Type::Path(path) = &field.ty
&& path.path.is_ident("DefaultUiState") && path.path.is_ident("DesktopUiState")
{ {
state_field = Some(field); state_field = Some(field);
} }
let Some(attr) = field let Some(attr) = field
.attrs .attrs
.iter() .iter()
.find(|a| a.path().is_ident("default_ui_state")) .find(|a| a.path().is_ident("desktop_ui_state"))
else { else {
continue; continue;
}; };
@@ -127,7 +127,7 @@ pub fn derive_default_ui_state(input: TokenStream) -> TokenStream {
output.extend( output.extend(
Error::new( Error::new(
attr.span(), attr.span(),
"cannot have more than one default_ui_state attribute", "cannot have more than one desktop_ui_state attribute",
) )
.into_compile_error(), .into_compile_error(),
); );
@@ -138,18 +138,18 @@ pub fn derive_default_ui_state(input: TokenStream) -> TokenStream {
} }
let Some(field) = state_field else { let Some(field) = state_field else {
output.extend( output.extend(
Error::new(state.ident.span(), "no DefaultUiState field found").into_compile_error(), Error::new(state.ident.span(), "no DesktopUiState field found").into_compile_error(),
); );
return output.into(); return output.into();
}; };
let sname = &state.ident; let sname = &state.ident;
let fname = field.ident.as_ref().unwrap(); let fname = field.ident.as_ref().unwrap();
output.extend(quote! { output.extend(quote! {
impl iris::default::HasDefaultUiState for #sname { impl iris::desktop::HasDesktopUiState for #sname {
fn default_state(&self) -> &iris::default::DefaultUiState { fn desktop_state(&self) -> &iris::desktop::DesktopUiState {
&self.#fname &self.#fname
} }
fn default_state_mut(&mut self) -> &mut iris::default::DefaultUiState { fn desktop_state_mut(&mut self) -> &mut iris::desktop::DesktopUiState {
&mut self.#fname &mut self.#fname
} }
} }
+1 -1
View File
@@ -18,7 +18,7 @@
# `content_scale` 2.55, from docs/bench/iris-phone-v2-2026-09-06.md, # `content_scale` 2.55, from docs/bench/iris-phone-v2-2026-09-06.md,
# carried in `ai_app::ui::fixture::PHONE_*`), and `IRIS_SCALE` hands that # carried in `ai_app::ui::fixture::PHONE_*`), and `IRIS_SCALE` hands that
# density to iris the way `DisplayMetrics.density` does on Android # density to iris the way `DisplayMetrics.density` does on Android
# (`iris::default::content_scale`). So a screenshot from here and one # (`iris::desktop::content_scale`). So a screenshot from here and one
# from the phone are the same layout at the same density, and what # from the phone are the same layout at the same density, and what
# differs is only the renderer. Without it the output stays desktop- # differs is only the renderer. Without it the output stays desktop-
# shaped, which is what every other example wants. # shaped, which is what every other example wants.
+1 -1
View File
@@ -176,7 +176,7 @@ impl AndroidRenderer {
.filter(|part| !part.is_empty()) .filter(|part| !part.is_empty())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" "); .join(" ");
// Say which adapter won, in the same words `default::render` uses, // Say which adapter won, in the same words `desktop::render` uses,
// and at startup rather than only on the Diagnostics page: the // and at startup rather than only on the Diagnostics page: the
// backend alone (logged by `view.rs` when a renderer is built) does // backend alone (logged by `view.rs` when a renderer is built) does
// not separate the cases that matter. In this checkout's emulator // not separate the cases that matter. In this checkout's emulator
+85 -176
View File
@@ -11,13 +11,7 @@ use android_view::{
}, },
ndk::event::{Axis, Keycode, MotionAction}, ndk::event::{Axis, Keycode, MotionAction},
}; };
use std::{ use std::{cell::RefCell, marker::Sized, rc::Rc, sync::Arc, time::Instant};
cell::RefCell,
marker::{PhantomData, Sized},
rc::Rc,
sync::Arc,
time::Instant,
};
use super::{ use super::{
access::{AndroidAccessSource, NullActionHandler, raise_if_enabled}, access::{AndroidAccessSource, NullActionHandler, raise_if_enabled},
@@ -25,10 +19,7 @@ use super::{
render::{AndroidRedrawHandle, AndroidRenderer}, render::{AndroidRedrawHandle, AndroidRenderer},
}; };
/// The android-view analogue of `default::DefaultUiState`. `renderer` is an /// Android host state. The renderer follows the `SurfaceView` lifecycle.
/// `Option` because a `SurfaceView`'s surface does not outlive backgrounding
/// the way a winit `Window` does -- `surfaceDestroyed`/`surfaceCreated` can
/// happen any number of times over the life of one `IrisViewPeer`.
/// How many frames after each `surface_changed` `render()` logs a full /// How many frames after each `surface_changed` `render()` logs a full
/// diagnostic line for -- see the log site's own comment. /// diagnostic line for -- see the log site's own comment.
const DIAGNOSTIC_FRAMES: u64 = 10; const DIAGNOSTIC_FRAMES: u64 = 10;
@@ -39,24 +30,13 @@ pub struct AndroidUiState {
pub focus: Option<WeakWidget<TextEdit>>, pub focus: Option<WeakWidget<TextEdit>>,
pub cursor: CursorState, pub cursor: CursorState,
pub last_click: Instant, pub last_click: Instant,
/// The IME preedit's previous length, in `char`s -- the same /// Previous IME preedit length, in characters.
/// re-send-the-whole-composition bookkeeping `default::DefaultUiState`
/// keeps for winit's `Ime::Preedit`, since android-view's
/// `setComposingText` has the identical shape (see `android/ime.rs`).
pub compose_len: usize, pub compose_len: usize,
/// Set by `attr::FocusHost::focus_gained` when a `TextEdit` is focused; /// Deferred until the input callback regains access to JNI.
/// consumed by the touch handler after the sensor pass finishes, since
/// showing the keyboard is a JNI call and `focus_gained` runs deep
/// inside the platform-agnostic sensor dispatch with no `CallbackCtx`
/// in reach.
pub pending_show_keyboard: bool, pub pending_show_keyboard: bool,
/// A URL a tapped link asked the platform to open, for the same /// Also deferred until a JNI callback is available.
/// reason `pending_show_keyboard` is a flag rather than a call --
/// see `android/platform.rs`.
pub pending_open_url: Option<String>, pub pending_open_url: Option<String>,
/// Window insets, filled in from outside the normal `ViewPeer` callback /// Filled by the native insets callback registered in `android/insets.rs`.
/// path -- see `android/insets.rs` for why they need a registry of
/// their own.
shared: Rc<RefCell<Shared>>, shared: Rc<RefCell<Shared>>,
pub access_adapter: AccessAdapter, pub access_adapter: AccessAdapter,
pub access: AccessTree, pub access: AccessTree,
@@ -132,13 +112,7 @@ pub trait AndroidAppState: HasAndroidUiState {
fn on_insets_changed(&mut self, rsc: &mut AndroidRsc<Self>, insets: WindowInsets) {} fn on_insets_changed(&mut self, rsc: &mut AndroidRsc<Self>, insets: WindowInsets) {}
} }
/// `insets::Insets` as `f32`, for the widget-facing callback above -- a /// Widget-facing insets in physical pixels, decoupled from JNI's integer shape.
/// distinct type from `insets::Insets` so a caller of `on_insets_changed`
/// is not coupled to that module's own (`i32`, JNI-shaped) representation.
/// Both are physical pixels; this used to divide by `content_scale` into a
/// separate *logical* unit (hence the old name, `LogicalInsets`), back when
/// the rest of layout was logical too -- see `AndroidUiState::content_scale`'s
/// field comment for why that stopgap is gone.
#[derive(Clone, Copy, Default, Debug, PartialEq)] #[derive(Clone, Copy, Default, Debug, PartialEq)]
pub struct WindowInsets { pub struct WindowInsets {
pub left: f32, pub left: f32,
@@ -166,66 +140,7 @@ impl WindowInsets {
} }
} }
pub struct AndroidRsc<State: 'static> { pub type AndroidRsc<State> = AppRsc<State>;
pub ui: Ui,
pub events: EventManager<Self>,
pub tasks: Tasks<Self>,
pub state: WidgetState,
_state: PhantomData<State>,
}
impl<State> AndroidRsc<State> {
pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> {
self.state.add(id.id(), data)
}
}
impl<State> UiRsc for AndroidRsc<State> {
fn ui(&self) -> &Ui {
&self.ui
}
fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui
}
fn on_draw(&mut self, active: &ActiveData) {
self.events.draw(active);
}
fn on_undraw(&mut self, active: &ActiveData) {
self.events.undraw(active);
}
fn on_remove(&mut self, id: WidgetId) {
self.events.remove(id);
self.state.remove(id);
}
}
impl<State: 'static> HasState for AndroidRsc<State> {
type State = State;
}
impl<State: 'static> HasEvents for AndroidRsc<State> {
fn events(&self) -> &EventManager<Self> {
&self.events
}
fn events_mut(&mut self) -> &mut EventManager<Self> {
&mut self.events
}
}
impl<State: 'static> HasTasks for AndroidRsc<State> {
fn tasks_mut(&mut self) -> &mut Tasks<Self> {
&mut self.tasks
}
}
impl<State: 'static> HasWidgetState for AndroidRsc<State> {
fn widget_state(&self) -> &WidgetState {
&self.state
}
fn widget_state_mut(&mut self) -> &mut WidgetState {
&mut self.state
}
}
/// The `ViewPeer` android-view dispatches every callback to. One per /// The `ViewPeer` android-view dispatches every callback to. One per
/// `RustView` instance; `new_peer` (below) builds it and hands the id to /// `RustView` instance; `new_peer` (below) builds it and hands the id to
@@ -234,30 +149,10 @@ pub struct IrisViewPeer<State: AndroidAppState> {
pub(super) rsc: AndroidRsc<State>, pub(super) rsc: AndroidRsc<State>,
pub(super) state: State, pub(super) state: State,
task_recv: TaskMsgReceiver<AndroidRsc<State>>, task_recv: TaskMsgReceiver<AndroidRsc<State>>,
/// The one ruler this view dates everything on: touch samples in /// Converts input and Choreographer timestamps onto one monotonic clock.
/// `on_touch_event` and the `Choreographer` frame time in `do_frame`.
/// Anchored by whichever of the two arrives first and never
/// re-anchored after, which is what lets a fling be advanced on the
/// same clock the gesture that launched it was measured on. Its path
/// out is the peer's own drop: it holds nothing but three numbers and
/// is meaningless to any other view.
device_clock: Option<DeviceClock>, device_clock: Option<DeviceClock>,
} }
impl<State: 'static, I: RscIdx<AndroidRsc<State>>> std::ops::Index<I> for AndroidRsc<State> {
type Output = I::Output;
fn index(&self, index: I) -> &Self::Output {
index.get(self)
}
}
impl<State: 'static, I: RscIdx<AndroidRsc<State>>> std::ops::IndexMut<I> for AndroidRsc<State> {
fn index_mut(&mut self, index: I) -> &mut Self::Output {
index.get_mut(self)
}
}
impl<State: AndroidAppState> IrisViewPeer<State> { impl<State: AndroidAppState> IrisViewPeer<State> {
fn drain_tasks(&mut self) { fn drain_tasks(&mut self) {
while let Ok(update) = self.task_recv.try_recv() { while let Ok(update) = self.task_recv.try_recv() {
@@ -298,7 +193,7 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
/// Common tail for every callback that might have changed the cursor, /// Common tail for every callback that might have changed the cursor,
/// the text focus, or the widget tree: run the sensors that touch /// the text focus, or the widget tree: run the sensors that touch
/// input feeds, then ask for a frame if the result needs drawing. /// input feeds, then ask for a frame if the result needs drawing.
/// Mirrors `default::DefaultApp::window_event`'s tail, split across /// Mirrors `desktop::DesktopApp::window_event`'s tail, split across
/// android-view's several entry points instead of winit's one. /// android-view's several entry points instead of winit's one.
pub(super) fn after_input(&mut self, ctx: &mut CallbackCtx) { pub(super) fn after_input(&mut self, ctx: &mut CallbackCtx) {
self.run_input_frame(ctx); self.run_input_frame(ctx);
@@ -322,6 +217,42 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
.get_or_insert_with(|| DeviceClock::anchored(Instant::now(), event_time, oldest)) .get_or_insert_with(|| DeviceClock::anchored(Instant::now(), event_time, oldest))
} }
fn generic_motion(&mut self, ctx: &mut CallbackCtx, event: &MotionEvent<'_>) -> 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 { fn window_size(&self) -> Vec2 {
let ui_state = self.state.android_state(); let ui_state = self.state.android_state();
match &ui_state.renderer { match &ui_state.renderer {
@@ -514,27 +445,14 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
) -> bool { ) -> bool {
self.drain_tasks(); self.drain_tasks();
let action = event.action_masked(&mut ctx.env); let action = event.action_masked(&mut ctx.env);
// Device (physical) pixels, same space layout now uses throughout // MotionEvent and layout both use physical pixels.
// -- see `AndroidUiState::content_scale`'s field comment.
let x = event.x(&mut ctx.env); let x = event.x(&mut ctx.env);
let y = event.y(&mut ctx.env); let y = event.y(&mut ctx.env);
// The event's own clock, converted through the view's one anchor // Use the event clock so batched movement keeps its real timing.
// -- taken on whichever of a touch or a frame callback came first.
// Android reports sample times in the `SystemClock.uptimeMillis()`
// base, which is the same `CLOCK_MONOTONIC` an `Instant` reads, so
// a single `(Instant, nanos)` pair converts every later sample
// exactly. Anchoring **once** rather than per event is what keeps
// the times ordered, and anchoring on the first event's *oldest*
// sample rather than on its own time is what keeps that event's
// batch from collapsing onto one instant -- `sense::DeviceClock`'s
// doc has both, and owns the arithmetic so it can be unit-tested
// off a device (`sense_tests.rs`). See `CursorState::time`.
let event_time = event.event_time_nanos(&mut ctx.env); let event_time = event.event_time_nanos(&mut ctx.env);
let history = event.history_size(&mut ctx.env); let history = event.history_size(&mut ctx.env);
let mut clock = match self.device_clock { let mut clock = match self.device_clock {
Some(clock) => clock, Some(clock) => clock,
// Only the call that anchors needs the batch's oldest sample,
// so the JNI read for it stays off the per-event path.
None => { None => {
let oldest = if history > 0 { let oldest = if history > 0 {
event.historical_event_time_nanos(&mut ctx.env, 0) event.historical_event_time_nanos(&mut ctx.env, 0)
@@ -544,24 +462,12 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
self.device_clock(event_time, oldest) self.device_clock(event_time, oldest)
} }
}; };
// `iris::input`'s own doc (`sense::log_input_event`): collected // Avoid allocating trace history when input tracing is disabled.
// only when tracing is on, since this is otherwise a `Vec` per
// `MotionEvent` for a line nobody is reading -- the JNI reads
// themselves (`historical_axis`/`historical_event_time_nanos`
// below) already happen unconditionally, for the replay this
// function does regardless of tracing.
let trace_input = crate::diagnostics::trace_enabled(); let trace_input = crate::diagnostics::trace_enabled();
let mut historical_ms: Vec<(u64, f32, f32)> = Vec::new(); let mut historical_ms: Vec<(u64, f32, f32)> = Vec::new();
if matches!(action, MotionAction::Move) { if matches!(action, MotionAction::Move) {
// Android documents the historical samples as oldest first and // Android orders history oldest-first; `sample` checks monotonicity.
// the event's own sample as the newest of the batch; everything
// downstream (`VelocityTracker`, `DragArbiter`'s long-press
// clock) assumes it, so say so here rather than at each reader.
// `DeviceClock::sample` is what asserts it, and it carries the
// last sample seen *across* events, so the first sample of
// every event is checked against the previous event's last one
// rather than against the anchor.
for pos in 0..history { for pos in 0..history {
let hx = event.historical_axis(&mut ctx.env, Axis::X, 0, pos); 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 hy = event.historical_axis(&mut ctx.env, Axis::Y, 0, pos);
@@ -616,6 +522,38 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
true 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 ui = self.state.android_state_mut();
if let Some(events) = ui
.access_adapter
.on_hover_event(&mut NullActionHandler, action, x, y)
{
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>( fn on_focus_changed<'local>(
&mut self, &mut self,
ctx: &mut CallbackCtx<'local>, ctx: &mut CallbackCtx<'local>,
@@ -646,32 +584,10 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
height: i32, height: i32,
) { ) {
self.drain_tasks(); self.drain_tasks();
// The layout engine's own notion of the canvas size is separate // The layout canvas and wgpu surface are separate and both use physical pixels.
// from the wgpu surface's -- winit's backend sets it from
// `WindowEvent::Resized`, and there is no equivalent automatic
// trigger here, so this is the one place android-view's surface
// size has to be told to `UiRenderState` too. Missing this drew
// nothing but the clear colour: the widget tree laid out against
// whatever size `UiRenderState::new` starts at instead of the
// surface's real one.
//
// **Physical pixels, matching `AndroidRenderer`'s own
// `size()`/`resize()`/`new()`** -- `AndroidUiState::content_scale`'s
// field comment. This call sets `UiRenderState::output_size`, which
// every `rel`/`rest` length resolves against and every `abs`
// pixel-region compares to directly; a `dp(56)` height now folds
// in the density at `Len::apply_rest` time instead of this call
// dividing the whole window into a separate logical space, which
// is what used to make every `abs`-unit size (a fixed `.height(56)`
// in particular) mean something different from a `rest`-based one.
self.rsc.ui.resize((width as f32, height as f32)); self.rsc.ui.resize((width as f32, height as f32));
// `AndroidRenderer::resize` only reconfigures the wgpu surface and // Resizing preserves GPU resources; recreating a destroyed surface does not.
// rewrites the window uniform -- device, atlas, buffers and bind
// groups are untouched, so the glyph cache's coordinates stay
// valid. A genuinely new surface (after `surface_destroyed`, e.g.
// backgrounding) still goes through `AndroidRenderer::new` below,
// since `renderer` is `None` in that case.
let already_live = self.state.android_state().renderer.is_some(); let already_live = self.state.android_state().renderer.is_some();
log::info!( log::info!(
"iris surface: surface_changed {width}x{height} already_live={already_live} \ "iris surface: surface_changed {width}x{height} already_live={already_live} \
@@ -883,14 +799,7 @@ pub fn new_peer<'local, State: AndroidAppState>(
let vm = env.get_java_vm().unwrap(); let vm = env.get_java_vm().unwrap();
let global_view = env.new_global_ref(&view.0).unwrap(); let global_view = env.new_global_ref(&view.0).unwrap();
let redraw: Arc<dyn RequestRedraw> = Arc::new(AndroidRedrawHandle::new(vm, global_view)); let redraw: Arc<dyn RequestRedraw> = Arc::new(AndroidRedrawHandle::new(vm, global_view));
let (tasks, task_recv) = Tasks::init(redraw); let (mut rsc, task_recv) = AppRsc::new(redraw);
let mut rsc = AndroidRsc {
ui: Default::default(),
events: Default::default(),
tasks,
state: Default::default(),
_state: PhantomData,
};
rsc.ui.set_density(content_scale); rsc.ui.set_density(content_scale);
let shared = Rc::new(RefCell::new(Shared::default())); let shared = Rc::new(RefCell::new(Shared::default()));
let ui_state = AndroidUiState::new(shared.clone(), content_scale); let ui_state = AndroidUiState::new(shared.clone(), content_scale);
File renamed without changes.
File renamed without changes.
+5 -5
View File
@@ -1,21 +1,21 @@
use crate::prelude::*; use crate::prelude::*;
use winit::dpi::{PhysicalPosition, PhysicalSize}; use winit::dpi::{PhysicalPosition, PhysicalSize};
impl<T: HasDefaultUiState> FocusHost for T { impl<T: HasDesktopUiState> FocusHost for T {
fn recent_click(&mut self) -> bool { fn recent_click(&mut self) -> bool {
crate::attr::recent_click(&mut self.default_state_mut().last_click) crate::attr::recent_click(&mut self.desktop_state_mut().last_click)
} }
fn set_focus(&mut self, id: Option<WeakWidget<TextEdit>>) { fn set_focus(&mut self, id: Option<WeakWidget<TextEdit>>) {
self.default_state_mut().focus = id; self.desktop_state_mut().focus = id;
} }
fn is_focused(&self, id: WeakWidget<TextEdit>) -> bool { fn is_focused(&self, id: WeakWidget<TextEdit>) -> bool {
self.default_state().focus == Some(id) self.desktop_state().focus == Some(id)
} }
fn focus_gained(&mut self, region: Option<PixelRegion>) { fn focus_gained(&mut self, region: Option<PixelRegion>) {
let state = self.default_state_mut(); let state = self.desktop_state_mut();
let Some(region) = region else { return }; let Some(region) = region else { return };
state.window.set_ime_allowed(true); state.window.set_ime_allowed(true);
state.window.set_ime_cursor_area( state.window.set_ime_cursor_area(
@@ -19,7 +19,7 @@ pub struct Input {
impl Input { impl Input {
/// winit's pointer coordinates are physical pixels, which is the /// winit's pointer coordinates are physical pixels, which is the
/// space the whole tree is laid out and hit-tested in -- see /// space the whole tree is laid out and hit-tested in -- see
/// `default::content_scale`. Nothing is converted here; `dp(...)` /// `desktop::content_scale`. Nothing is converted here; `dp(...)`
/// resolves against the density at layout time instead. /// resolves against the density at layout time instead.
pub fn event(&mut self, event: &WindowEvent) -> bool { pub fn event(&mut self, event: &WindowEvent) -> bool {
match event { match event {
@@ -79,10 +79,10 @@ impl Input {
} }
} }
impl DefaultUiState { impl DesktopUiState {
/// Physical pixels, matching `WindowEvent::Resized` (what /// Physical pixels, matching `WindowEvent::Resized` (what
/// `UiRenderState::resize` is given) and the swapchain -- see /// `UiRenderState::resize` is given) and the swapchain -- see
/// `default::content_scale`. /// `desktop::content_scale`.
pub fn window_size(&self) -> Vec2 { pub fn window_size(&self) -> Vec2 {
let size = self.renderer.window().inner_size(); let size = self.renderer.window().inner_size();
Vec2::new(size.width as f32, size.height as f32) Vec2::new(size.width as f32, size.height as f32)
File renamed without changes.
+31 -156
View File
@@ -1,10 +1,6 @@
use crate::prelude::*; use crate::prelude::*;
use arboard::Clipboard; use arboard::Clipboard;
use std::{ use std::{marker::Sized, sync::Arc, time::Instant};
marker::{PhantomData, Sized},
sync::Arc,
time::Instant,
};
use winit::{ use winit::{
event::{Ime, WindowEvent}, event::{Ime, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy}, event_loop::{ActiveEventLoop, EventLoopProxy},
@@ -26,19 +22,8 @@ pub use render::*;
pub type Proxy<Event> = EventLoopProxy<Event>; pub type Proxy<Event> = EventLoopProxy<Event>;
/// The desktop's `content_scale`: physical pixels per dp, the same /// Physical pixels per dp. Layout and input stay in physical pixels; only
/// quantity Android reads from `DisplayMetrics.density` and feeds to /// `dp(...)` resolves through this scale.
/// `UiRenderState::set_density` (`android::view::AndroidUiState::
/// content_scale`'s field comment). Everything in this backend is
/// physical pixels -- the window size, the pointer, the widget tree --
/// and `dp(...)` is what resolves against this at layout time, exactly
/// as on the phone. That is a correction from an earlier version that
/// divided winit's coordinates into a separate "logical" space instead:
/// it left `UiRenderState::resize` (physical, from `WindowEvent::
/// Resized`) and the window uniform (logical) disagreeing on any
/// display whose scale factor is not 1.0, and it rasterised glyphs at
/// one resolution to display them at another -- the blur the phone's own
/// stopgap produced before `dp` existed.
pub fn content_scale(window: &Window) -> f32 { pub fn content_scale(window: &Window) -> f32 {
match std::env::var("IRIS_SCALE") { match std::env::var("IRIS_SCALE") {
Err(_) => window.scale_factor() as f32, Err(_) => window.scale_factor() as f32,
@@ -52,7 +37,7 @@ pub fn content_scale(window: &Window) -> f32 {
} }
} }
pub struct DefaultUiState { pub struct DesktopUiState {
pub root: Option<StrongWidget>, pub root: Option<StrongWidget>,
pub renderer: UiRenderer, pub renderer: UiRenderer,
pub input: Input, pub input: Input,
@@ -65,13 +50,13 @@ pub struct DefaultUiState {
pub access: AccessTree, pub access: AccessTree,
} }
impl<State: 'static> HasRoot<DefaultRsc<State>> for DefaultUiState { impl<State: 'static> HasRoot<DesktopRsc<State>> for DesktopUiState {
fn set_root(&mut self, rsc: &mut DefaultRsc<State>, root: StrongWidget) { fn set_root(&mut self, rsc: &mut DesktopRsc<State>, root: StrongWidget) {
self.root = Some(crate::overlay::default_overlay_root(rsc, root)); self.root = Some(crate::overlay::default_overlay_root(rsc, root));
} }
} }
impl DefaultUiState { impl DesktopUiState {
pub fn new(window: impl Into<Arc<Window>>, access_adapter: accesskit_winit::Adapter) -> Self { pub fn new(window: impl Into<Arc<Window>>, access_adapter: accesskit_winit::Adapter) -> Self {
let window = window.into(); let window = window.into();
Self { Self {
@@ -89,114 +74,35 @@ impl DefaultUiState {
} }
} }
pub trait HasDefaultUiState: Sized + 'static { pub trait HasDesktopUiState: Sized + 'static {
fn default_state(&self) -> &DefaultUiState; fn desktop_state(&self) -> &DesktopUiState;
fn default_state_mut(&mut self) -> &mut DefaultUiState; fn desktop_state_mut(&mut self) -> &mut DesktopUiState;
} }
pub trait DefaultAppState: HasDefaultUiState { pub trait DesktopAppState: HasDesktopUiState {
type Event = (); type Event = ();
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self::Event>) fn new(ui_state: DesktopUiState, rsc: &mut DesktopRsc<Self>, proxy: Proxy<Self::Event>)
-> Self; -> Self;
#[allow(unused_variables)] #[allow(unused_variables)]
fn event(&mut self, event: Self::Event, rsc: &mut DefaultRsc<Self>) {} fn event(&mut self, event: Self::Event, rsc: &mut DesktopRsc<Self>) {}
#[allow(unused_variables)] #[allow(unused_variables)]
fn exit(&mut self, rsc: &mut DefaultRsc<Self>) {} fn exit(&mut self, rsc: &mut DesktopRsc<Self>) {}
#[allow(unused_variables)] #[allow(unused_variables)]
fn window_event(&mut self, event: WindowEvent, rsc: &mut DefaultRsc<Self>) {} fn window_event(&mut self, event: WindowEvent, rsc: &mut DesktopRsc<Self>) {}
fn window_attributes() -> WindowAttributes { fn window_attributes() -> WindowAttributes {
Default::default() Default::default()
} }
} }
pub struct DefaultRsc<State: 'static> { pub type DesktopRsc<State> = AppRsc<State>;
pub ui: Ui,
pub events: EventManager<Self>,
pub tasks: Tasks<Self>,
pub state: WidgetState,
_state: PhantomData<State>,
}
impl<State> DefaultRsc<State> { pub struct DesktopApp<State: DesktopAppState> {
fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Self>) { rsc: DesktopRsc<State>,
let (tasks, recv) = Tasks::init(window);
(
Self {
ui: Default::default(),
events: Default::default(),
tasks,
state: Default::default(),
_state: Default::default(),
},
recv,
)
}
pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> {
self.state.add(id.id(), data)
}
}
impl<State> UiRsc for DefaultRsc<State> {
fn ui(&self) -> &Ui {
&self.ui
}
fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui
}
fn on_draw(&mut self, active: &ActiveData) {
self.events.draw(active);
}
fn on_undraw(&mut self, active: &ActiveData) {
self.events.undraw(active);
}
fn on_remove(&mut self, id: WidgetId) {
self.events.remove(id);
self.state.remove(id);
}
}
impl<State: 'static> HasState for DefaultRsc<State> {
type State = State;
}
impl<State: 'static> HasEvents for DefaultRsc<State> {
fn events(&self) -> &EventManager<Self> {
&self.events
}
fn events_mut(&mut self) -> &mut EventManager<Self> {
&mut self.events
}
}
impl<State: 'static> HasTasks for DefaultRsc<State> {
fn tasks_mut(&mut self) -> &mut Tasks<Self> {
&mut self.tasks
}
}
impl<State: 'static> HasWidgetState for DefaultRsc<State> {
fn widget_state(&self) -> &WidgetState {
&self.state
}
fn widget_state_mut(&mut self) -> &mut WidgetState {
&mut self.state
}
}
pub struct DefaultApp<State: DefaultAppState> {
rsc: DefaultRsc<State>,
state: State, state: State,
task_recv: TaskMsgReceiver<DefaultRsc<State>>, task_recv: TaskMsgReceiver<DesktopRsc<State>>,
} }
impl<State: DefaultAppState> AppState for DefaultApp<State> { impl<State: DesktopAppState> AppState for DesktopApp<State> {
type Event = State::Event; type Event = State::Event;
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self { fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
@@ -211,16 +117,12 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
NullDeactivationHandler, NullDeactivationHandler,
); );
window.set_visible(true); window.set_visible(true);
let default_state = DefaultUiState::new(window, access_adapter); let desktop_state = DesktopUiState::new(window, access_adapter);
let (mut rsc, task_recv) = DefaultRsc::init(default_state.window.clone()); let (mut rsc, task_recv) = AppRsc::new(desktop_state.window.clone());
// Both copies of the density, set before the first widget is // Set before building widgets so the first text shape uses the right density.
// built so text shapes at the right size on the opening frame -- let scale = content_scale(desktop_state.window.as_ref());
// the same pair `android::view::new_peer` sets from
// `content_scale`. See `iris_core::TextData::density` for why the
// shaper keeps its own.
let scale = content_scale(default_state.window.as_ref());
rsc.ui.set_density(scale); rsc.ui.set_density(scale);
let state = State::new(default_state, &mut rsc, proxy); let state = State::new(desktop_state, &mut rsc, proxy);
Self { Self {
rsc, rsc,
state, state,
@@ -243,7 +145,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
update(state, rsc); update(state, rsc);
} }
let ui_state = state.default_state_mut(); let ui_state = state.desktop_state_mut();
// Required by `accesskit_winit` on every window event, not just the // Required by `accesskit_winit` on every window event, not just the
// ones this backend otherwise cares about -- some platform adapters // ones this backend otherwise cares about -- some platform adapters
// rely on it to notice activation (a screen reader turning on). // rely on it to notice activation (a screen reader turning on).
@@ -257,14 +159,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
ui_state.focus = None; ui_state.focus = None;
} }
if input_changed { if input_changed {
// The winit half of `iris::input` (`sense::log_input_event`'s // Winit delivers one sample at a time, so there is no history batch.
// own doc): no batching here, so `historical` is always empty
// -- winit hands one `WindowEvent` per pointer sample, unlike
// Android's `MotionEvent`. The action is read back off the
// buttons `Input::event` just updated, the same test
// `GestureOutcome`'s callers already use to tell a press from a
// release. Computed only when tracing is on, same reasoning as
// `log_input_event` itself gating on it.
if crate::diagnostics::trace_enabled() { if crate::diagnostics::trace_enabled() {
let action = if cursor_state.buttons.left.is_start() { let action = if cursor_state.buttons.left.is_start() {
"down" "down"
@@ -292,7 +187,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
.get() .get()
.run_sensors(rsc, state, cursor_state, window_size); .run_sensors(rsc, state, cursor_state, window_size);
} }
let ui_state = state.default_state_mut(); let ui_state = state.desktop_state_mut();
if old != ui_state.focus if old != ui_state.focus
&& let Some(old) = old && let Some(old) = old
{ {
@@ -301,16 +196,10 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
match &event { match &event {
WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => { WindowEvent::RedrawRequested => {
// Before the draw, so this frame shows this instant's // Advance animations before drawing and keep requesting frames while active.
// position (`UiData::tick_animations`' own doc), and the
// window is asked for another frame while anything is
// still moving -- the winit half of what
// `IrisViewPeer::render`'s `post_frame_callback` does on
// Android. Nothing else in iris moves without an input
// event.
let frame_start = std::time::Instant::now(); let frame_start = std::time::Instant::now();
let animating = rsc.ui_mut().tick_animations(frame_start); let animating = rsc.ui_mut().tick_animations(frame_start);
let ui_state = state.default_state_mut(); let ui_state = state.desktop_state_mut();
if animating { if animating {
ui_state.window.request_redraw(); ui_state.window.request_redraw();
} }
@@ -415,7 +304,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
_ => (), _ => (),
} }
state.window_event(event, rsc); state.window_event(event, rsc);
let ui_state = self.state.default_state_mut(); let ui_state = self.state.desktop_state_mut();
let render_state = rsc.ui.render_state(); let render_state = rsc.ui.render_state();
if render_state if render_state
.get() .get()
@@ -430,17 +319,3 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
self.state.exit(&mut self.rsc); self.state.exit(&mut self.rsc);
} }
} }
impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::Index<I> for DefaultRsc<State> {
type Output = I::Output;
fn index(&self, index: I) -> &Self::Output {
index.get(self)
}
}
impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::IndexMut<I> for DefaultRsc<State> {
fn index_mut(&mut self, index: I) -> &mut Self::Output {
index.get_mut(self)
}
}
@@ -1,7 +1,7 @@
use crate::platform::OpenUrl; use crate::platform::OpenUrl;
use crate::prelude::HasDefaultUiState; use crate::prelude::HasDesktopUiState;
impl<T: HasDefaultUiState> OpenUrl for T { impl<T: HasDesktopUiState> OpenUrl for T {
fn open_url(&mut self, url: &str) { fn open_url(&mut self, url: &str) {
let (program, first): (&str, &[&str]) = if cfg!(target_os = "macos") { let (program, first): (&str, &[&str]) = if cfg!(target_os = "macos") {
("open", &[]) ("open", &[])
@@ -213,7 +213,7 @@ impl UiRenderer {
// that function's doc comment). // that function's doc comment).
// Physical size, the same units the swapchain, `WindowEvent:: // Physical size, the same units the swapchain, `WindowEvent::
// Resized`, the pointer and the widget tree all use -- see // Resized`, the pointer and the widget tree all use -- see
// `default::content_scale` for why this backend stopped dividing // `desktop::content_scale` for why this backend stopped dividing
// into a separate logical space, and what disagreed while it did. // into a separate logical space, and what disagreed while it did.
let physical_size = Vec2::new(size.width as f32, size.height as f32); let physical_size = Vec2::new(size.width as f32, size.height as f32);
let ui = UiRenderNode::new(&device, &queue, formats.view, physical_size) let ui = UiRenderNode::new(&device, &queue, formats.view, physical_size)
+1 -1
View File
@@ -15,7 +15,7 @@ pub fn trace_enabled() -> bool {
/// One `iris::frame` line, called once per frame from each backend's own /// One `iris::frame` line, called once per frame from each backend's own
/// frame function -- `android::view::IrisViewPeer::render`, /// frame function -- `android::view::IrisViewPeer::render`,
/// `default::DefaultApp::window_event`'s `RedrawRequested` arm, and /// `desktop::DesktopApp::window_event`'s `RedrawRequested` arm, and
/// `harness::Harness::frame` -- after the draw (or, on the harness, where a /// `harness::Harness::frame` -- after the draw (or, on the harness, where a
/// draw would be; `draw` is `Duration::ZERO` there since nothing is /// draw would be; `draw` is `Duration::ZERO` there since nothing is
/// actually submitted to a GPU). /// actually submitted to a GPU).
+2 -79
View File
@@ -1,5 +1,4 @@
use crate::prelude::*; use crate::prelude::*;
use std::marker::PhantomData;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@@ -164,76 +163,7 @@ impl OpenUrl for HarnessState {
} }
} }
/// The harness's `Rsc` -- identical in substance to `DefaultRsc`/ pub type HarnessRsc = AppRsc<HarnessState>;
/// `AndroidRsc` minus the windowing, for the same reason those two are
/// separate types (`AndroidRsc`'s own doc).
pub struct HarnessRsc {
pub ui: Ui,
pub events: EventManager<Self>,
pub tasks: Tasks<Self>,
pub state: WidgetState,
_state: PhantomData<HarnessState>,
}
impl UiRsc for HarnessRsc {
fn ui(&self) -> &Ui {
&self.ui
}
fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui
}
fn on_draw(&mut self, active: &ActiveData) {
self.events.draw(active);
}
fn on_undraw(&mut self, active: &ActiveData) {
self.events.undraw(active);
}
fn on_remove(&mut self, id: WidgetId) {
self.events.remove(id);
self.state.remove(id);
}
}
impl HasState for HarnessRsc {
type State = HarnessState;
}
impl HasEvents for HarnessRsc {
fn events(&self) -> &EventManager<Self> {
&self.events
}
fn events_mut(&mut self) -> &mut EventManager<Self> {
&mut self.events
}
}
impl HasTasks for HarnessRsc {
fn tasks_mut(&mut self) -> &mut Tasks<Self> {
&mut self.tasks
}
}
impl HasWidgetState for HarnessRsc {
fn widget_state(&self) -> &WidgetState {
&self.state
}
fn widget_state_mut(&mut self) -> &mut WidgetState {
&mut self.state
}
}
impl<I: RscIdx<HarnessRsc>> std::ops::Index<I> for HarnessRsc {
type Output = I::Output;
fn index(&self, index: I) -> &Self::Output {
index.get(self)
}
}
impl<I: RscIdx<HarnessRsc>> std::ops::IndexMut<I> for HarnessRsc {
fn index_mut(&mut self, index: I) -> &mut Self::Output {
index.get_mut(self)
}
}
/// A screen running with no window: the widget tree, the frame loop and /// A screen running with no window: the widget tree, the frame loop and
/// the pointer, all advanced by the caller. See the module doc. /// the pointer, all advanced by the caller. See the module doc.
@@ -255,14 +185,7 @@ impl Harness {
/// `PHONE_SCALE`. /// `PHONE_SCALE`.
pub fn new(size: Vec2, density: f32) -> Self { pub fn new(size: Vec2, density: f32) -> Self {
let redraws = Arc::new(RedrawCounter::default()); let redraws = Arc::new(RedrawCounter::default());
let (tasks, task_recv) = Tasks::init(redraws.clone()); let (mut rsc, task_recv) = AppRsc::new(redraws.clone());
let mut rsc = HarnessRsc {
ui: Ui::default(),
events: EventManager::default(),
tasks,
state: WidgetState::default(),
_state: PhantomData,
};
rsc.ui.set_density(density); rsc.ui.set_density(density);
rsc.ui.resize(size); rsc.ui.resize(size);
Self { Self {
+15 -5
View File
@@ -115,7 +115,7 @@ fn a_widget_retains_its_entry_layer_not_its_child_cursor() {
rsc.ui.widgets.get_mut(&outer_weak).unwrap().x = None; rsc.ui.widgets.get_mut(&outer_weak).unwrap().x = None;
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
assert_eq!( assert_eq!(
render.take_counters().0, render.take_counters().draws,
1, 1,
"redrawing the parent should retain the unchanged Stack subtree" "redrawing the parent should retain the unchanged Stack subtree"
); );
@@ -250,7 +250,12 @@ fn a_child_coordinate_offset_moves_only_the_child_subtree() {
rsc.ui.widgets.get_mut(&parent_weak).unwrap().offset.y = 35.0; rsc.ui.widgets.get_mut(&parent_weak).unwrap().offset.y = 35.0;
rsc.ui.widgets.get_mut(&outer_weak).unwrap().x = None; rsc.ui.widgets.get_mut(&outer_weak).unwrap().x = None;
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
let (draws, rewrites, moves, _shapes) = render.take_counters(); let RenderCounters {
draws,
region_rewrites: rewrites,
moves,
..
} = render.take_counters();
let parent_after = render.window_region(&parent_weak, &rsc).unwrap(); let parent_after = render.window_region(&parent_weak, &rsc).unwrap();
let child_after = render.window_region(&child_weak, &rsc).unwrap(); let child_after = render.window_region(&child_weak, &rsc).unwrap();
@@ -283,7 +288,7 @@ fn a_hinted_rest_draws_once_and_only_moves_the_fixed_child_after_it() {
let mut render = UiRenderState::new(); let mut render = UiRenderState::new();
render.resize((200.0, 300.0)); render.resize((200.0, 300.0));
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
let (draws, _rewrites, moves, _shapes) = render.take_counters(); let RenderCounters { draws, moves, .. } = render.take_counters();
assert_eq!(draws, 5); assert_eq!(draws, 5);
assert_eq!(moves, 1); assert_eq!(moves, 1);
@@ -328,7 +333,12 @@ fn an_unchanged_frame_draws_and_rewrites_nothing() {
render.take_counters(); // discard the first, real draws render.take_counters(); // discard the first, real draws
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
let (draws, rewrites, moves, _shapes) = render.take_counters(); let RenderCounters {
draws,
region_rewrites: rewrites,
moves,
..
} = render.take_counters();
assert_eq!((draws, rewrites, moves), (0, 0, 0)); assert_eq!((draws, rewrites, moves), (0, 0, 0));
} }
@@ -346,7 +356,7 @@ fn scrolling_moves_in_o1_without_a_redraw() {
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(-40.0); rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(-40.0);
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
let (draws, _rewrites, moves, _shapes) = render.take_counters(); let RenderCounters { draws, moves, .. } = render.take_counters();
assert_eq!(draws, 1, "only Scroll itself should redraw"); assert_eq!(draws, 1, "only Scroll itself should redraw");
assert_eq!(moves, 1, "the scrolled subtree should move in one write"); assert_eq!(moves, 1, "the scrolled subtree should move in one write");
+5 -3
View File
@@ -1,6 +1,6 @@
#![feature(unboxed_closures)] #![feature(unboxed_closures)]
#![feature(fn_traits)] #![feature(fn_traits)]
// Only `default::DefaultAppState::Event`'s default uses this; unused (and // Only `desktop::DesktopAppState::Event`'s default uses this; unused (and
// warned about) on the android target, which has no such default. // warned about) on the android target, which has no such default.
#![cfg_attr(not(target_os = "android"), feature(associated_type_defaults))] #![cfg_attr(not(target_os = "android"), feature(associated_type_defaults))]
#![feature(unsize)] #![feature(unsize)]
@@ -10,7 +10,7 @@
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
pub mod android; pub mod android;
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
pub mod default; pub mod desktop;
pub mod attr; pub mod attr;
pub mod diagnostics; pub mod diagnostics;
@@ -18,6 +18,7 @@ pub mod event;
pub mod harness; pub mod harness;
pub mod overlay; pub mod overlay;
pub mod platform; pub mod platform;
pub mod runtime;
pub mod sense; pub mod sense;
pub mod state; pub mod state;
pub mod task; pub mod task;
@@ -38,7 +39,7 @@ pub mod prelude {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
pub use android::*; pub use android::*;
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
pub use default::*; pub use desktop::*;
pub use attr::*; pub use attr::*;
pub use event::*; pub use event::*;
@@ -46,6 +47,7 @@ pub mod prelude {
pub use iris_macro::*; pub use iris_macro::*;
pub use overlay::*; pub use overlay::*;
pub use platform::*; pub use platform::*;
pub use runtime::*;
pub use sense::*; pub use sense::*;
pub use state::*; pub use state::*;
pub use task::*; pub use task::*;
+98
View File
@@ -0,0 +1,98 @@
use crate::prelude::*;
use std::sync::Arc;
/// Resources shared by every Iris host.
pub struct AppRsc<State: 'static> {
pub ui: Ui,
pub events: EventManager<Self>,
pub tasks: Tasks<Self>,
pub state: WidgetState,
_state: std::marker::PhantomData<State>,
}
impl<State> AppRsc<State> {
pub(crate) fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>) {
let (tasks, receiver) = Tasks::init(redraw);
(
Self {
ui: Ui::default(),
events: EventManager::default(),
tasks,
state: WidgetState::default(),
_state: std::marker::PhantomData,
},
receiver,
)
}
pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> {
self.state.add(id.id(), data)
}
}
impl<State> UiRsc for AppRsc<State> {
fn ui(&self) -> &Ui {
&self.ui
}
fn ui_mut(&mut self) -> &mut Ui {
&mut self.ui
}
fn on_draw(&mut self, active: &ActiveData) {
self.events.draw(active);
}
fn on_undraw(&mut self, active: &ActiveData) {
self.events.undraw(active);
}
fn on_remove(&mut self, id: WidgetId) {
self.events.remove(id);
self.state.remove(id);
}
}
impl<State> HasState for AppRsc<State> {
type State = State;
}
impl<State> HasEvents for AppRsc<State> {
fn events(&self) -> &EventManager<Self> {
&self.events
}
fn events_mut(&mut self) -> &mut EventManager<Self> {
&mut self.events
}
}
impl<State> HasTasks for AppRsc<State> {
fn tasks_mut(&mut self) -> &mut Tasks<Self> {
&mut self.tasks
}
}
impl<State> HasWidgetState for AppRsc<State> {
fn widget_state(&self) -> &WidgetState {
&self.state
}
fn widget_state_mut(&mut self) -> &mut WidgetState {
&mut self.state
}
}
impl<State, I: RscIdx<AppRsc<State>>> std::ops::Index<I> for AppRsc<State> {
type Output = I::Output;
fn index(&self, index: I) -> &Self::Output {
index.get(self)
}
}
impl<State, I: RscIdx<AppRsc<State>>> std::ops::IndexMut<I> for AppRsc<State> {
fn index_mut(&mut self, index: I) -> &mut Self::Output {
index.get_mut(self)
}
}
+3 -2
View File
@@ -1155,7 +1155,7 @@ mod tests {
.push_front(LazyItem::new(key, w)); .push_front(LazyItem::new(key, w));
} }
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
let (draws, _rewrites, _moves, _shapes) = render.take_counters(); let draws = render.take_counters().draws;
let extents_after = rsc.ui.widgets.get(&list_weak).unwrap().extents.clone(); let extents_after = rsc.ui.widgets.get(&list_weak).unwrap().extents.clone();
for key in [11u64, 12] { for key in [11u64, 12] {
@@ -1269,7 +1269,8 @@ mod tests {
rsc.ui.widgets.get_mut(&list_weak).unwrap().scroll(1.0); rsc.ui.widgets.get_mut(&list_weak).unwrap().scroll(1.0);
render.update(&root, &mut rsc); render.update(&root, &mut rsc);
let (draws, _rewrites, moves, _shapes) = render.take_counters(); let counters = render.take_counters();
let (draws, moves) = (counters.draws, counters.moves);
assert_eq!(draws, 1, "n={n}: only the list should really draw"); assert_eq!(draws, 1, "n={n}: only the list should really draw");
assert_eq!(moves, 1, "n={n}: the whole retained run should move once"); assert_eq!(moves, 1, "n={n}: the whole retained run should move once");