diff --git a/benches/message_list.rs b/benches/message_list.rs index b3cff62..5898519 100644 --- a/benches/message_list.rs +++ b/benches/message_list.rs @@ -68,7 +68,12 @@ fn bench_first_frame(n: usize) { let start = Instant::now(); render.update(&root, &mut rsc); let elapsed = start.elapsed(); - let (draws, rewrites, moves, _shapes) = render.take_counters(); + let RenderCounters { + draws, + region_rewrites: rewrites, + moves, + .. + } = render.take_counters(); report( &format!("(a) first frame, N={n}"), elapsed, @@ -97,7 +102,12 @@ fn bench_scroll(n: usize, ticks: usize) { let start = Instant::now(); render.update(&root, &mut rsc); 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_rewrites += rewrites; total_moves += moves; @@ -155,7 +165,12 @@ fn bench_input_grows(n: usize, lines: usize) { let start = Instant::now(); render.update(&root, &mut rsc); 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_rewrites += rewrites; total_moves += moves; @@ -200,7 +215,12 @@ fn bench_insert_above_anchor(n: usize, inserts: usize) { let start = Instant::now(); render.update(&root, &mut rsc); 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_rewrites += rewrites; total_moves += moves; @@ -270,7 +290,12 @@ fn bench_expand_holds_edge(n: usize, growths: usize) { let start = Instant::now(); render.update(&root, &mut rsc); 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_rewrites += rewrites; total_moves += moves; @@ -314,7 +339,12 @@ fn bench_redraw_big_text(chars: usize, redraws: usize) { render.update(&root, &mut rsc); total += start.elapsed(); } - let (draws, rewrites, moves, _shapes) = render.take_counters(); + let RenderCounters { + draws, + region_rewrites: rewrites, + moves, + .. + } = render.take_counters(); report( &format!("(g) redraw one {chars}-glyph text, {redraws}x (totals)"), total, diff --git a/core/src/ui/access.rs b/core/src/ui/access.rs index c742d0e..d8c8306 100644 --- a/core/src/ui/access.rs +++ b/core/src/ui/access.rs @@ -32,7 +32,7 @@ fn entry_node(entry: &Entry) -> Node { /// Owns the last tree pushed out, so `update` can tell "nothing /// accessibility-relevant changed" from "something did" without asking /// 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. #[derive(Default)] pub struct AccessTree { diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 6bb0a53..928b21e 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -18,6 +18,14 @@ pub enum RedrawKind { 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 active: HashMap, pub primitives: Primitives, @@ -115,13 +123,13 @@ impl UiRenderState { } } - pub fn take_counters(&mut self) -> (u64, u64, u64, u64) { - ( - std::mem::take(&mut self.draw_count), - std::mem::take(&mut self.region_mut_count), - std::mem::take(&mut self.mov_count), - std::mem::take(&mut self.shape_count), - ) + pub fn take_counters(&mut self) -> RenderCounters { + RenderCounters { + draws: std::mem::take(&mut self.draw_count), + region_rewrites: std::mem::take(&mut self.region_mut_count), + moves: std::mem::take(&mut self.mov_count), + shapes: std::mem::take(&mut self.shape_count), + } } pub(super) fn note_move(&mut self) { diff --git a/examples/bench_images.rs b/examples/bench_images.rs index 71d63a0..db4b0b5 100644 --- a/examples/bench_images.rs +++ b/examples/bench_images.rs @@ -4,24 +4,24 @@ const ROWS: usize = 1000; const SETTLE_FRAMES: usize = 4; const FRAMES: usize = 6; -#[derive(DefaultUiState)] +#[derive(DesktopUiState)] struct State { - ui_state: DefaultUiState, + ui_state: DesktopUiState, span: WeakWidget, frame: usize, appended: bool, } -impl DefaultAppState for State { +impl DesktopAppState for State { fn new( - mut ui_state: DefaultUiState, - rsc: &mut DefaultRsc, + mut ui_state: DesktopUiState, + rsc: &mut DesktopRsc, _: Proxy, ) -> Self { let mut span = Span::empty(Dir::DOWN); for _ in 0..ROWS { let img = image::DynamicImage::new_rgba8(32, 32); - let widget = image::>(img)(rsc); + let widget = image::>(img)(rsc); let widget = rsc.ui.widgets.add_strong(widget); span.push(widget.any()); } @@ -40,7 +40,7 @@ impl DefaultAppState for State { } } - fn window_event(&mut self, event: winit::event::WindowEvent, rsc: &mut DefaultRsc) { + fn window_event(&mut self, event: winit::event::WindowEvent, rsc: &mut DesktopRsc) { if !matches!(event, winit::event::WindowEvent::RedrawRequested) { return; } @@ -53,7 +53,7 @@ impl DefaultAppState for State { if self.frame == SETTLE_FRAMES && !self.appended { self.appended = true; let img = image::DynamicImage::new_rgba8(32, 32); - let widget = image::>(img)(rsc); + let widget = image::>(img)(rsc); let widget = rsc.ui.widgets.add_strong(widget); rsc.ui .widgets @@ -71,5 +71,5 @@ impl DefaultAppState for State { } fn main() { - DefaultApp::::run(); + DesktopApp::::run(); } diff --git a/examples/message_list.rs b/examples/message_list.rs index c8da116..73712c1 100644 --- a/examples/message_list.rs +++ b/examples/message_list.rs @@ -2,12 +2,12 @@ use iris::prelude::*; use winit::{dpi::LogicalSize, window::WindowAttributes}; fn main() { - DefaultApp::::run(); + DesktopApp::::run(); } -#[derive(DefaultUiState)] +#[derive(DesktopUiState)] struct State { - ui_state: DefaultUiState, + ui_state: DesktopUiState, } const ROWS: usize = 800; @@ -58,14 +58,14 @@ fn build_row(rsc: &mut Rsc, i: usize) -> StrongWidget { } } -impl DefaultAppState for State { +impl DesktopAppState for State { fn window_attributes() -> WindowAttributes { WindowAttributes::default().with_inner_size(LogicalSize::new(420.0, 900.0)) } fn new( - mut ui_state: DefaultUiState, - rsc: &mut DefaultRsc, + mut ui_state: DesktopUiState, + rsc: &mut DesktopRsc, _: Proxy, ) -> Self { let mut list = LazySpan::new(Dir::DOWN, Pin::End); diff --git a/examples/minimal.rs b/examples/minimal.rs index 4484baf..50b7807 100644 --- a/examples/minimal.rs +++ b/examples/minimal.rs @@ -1,18 +1,18 @@ use iris::prelude::*; fn main() { - DefaultApp::::run(); + DesktopApp::::run(); } -#[derive(DefaultUiState)] +#[derive(DesktopUiState)] struct State { - ui_state: DefaultUiState, + ui_state: DesktopUiState, } -impl DefaultAppState for State { +impl DesktopAppState for State { fn new( - mut ui_state: DefaultUiState, - rsc: &mut DefaultRsc, + mut ui_state: DesktopUiState, + rsc: &mut DesktopRsc, _: Proxy, ) -> Self { rect(PaintId::RED).set_root(rsc, &mut ui_state); diff --git a/examples/tabs/main.rs b/examples/tabs/main.rs index d4d4ec9..0732297 100644 --- a/examples/tabs/main.rs +++ b/examples/tabs/main.rs @@ -2,19 +2,19 @@ use iris::prelude::*; use winit::event::WindowEvent; fn main() { - DefaultApp::::run(); + DesktopApp::::run(); } -#[derive(DefaultUiState)] +#[derive(DesktopUiState)] pub struct Client { - ui_state: DefaultUiState, + ui_state: DesktopUiState, info: WeakWidget, } -impl DefaultAppState for Client { +impl DesktopAppState for Client { fn new( - mut ui_state: DefaultUiState, - rsc: &mut DefaultRsc, + mut ui_state: DesktopUiState, + rsc: &mut DesktopRsc, _: Proxy, ) -> Self { 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) { + fn window_event(&mut self, _: WindowEvent, rsc: &mut DesktopRsc) { let render_state = rsc.ui.render_state(); let new = format!( "widgets: {}\nactive: {}\nviews: {}", diff --git a/examples/task.rs b/examples/task.rs index b3aca61..7947832 100644 --- a/examples/task.rs +++ b/examples/task.rs @@ -2,18 +2,18 @@ use iris::prelude::*; use std::time::Duration; fn main() { - DefaultApp::::run(); + DesktopApp::::run(); } -#[derive(DefaultUiState)] +#[derive(DesktopUiState)] struct State { - ui_state: DefaultUiState, + ui_state: DesktopUiState, } -impl DefaultAppState for State { +impl DesktopAppState for State { fn new( - mut ui_state: DefaultUiState, - rsc: &mut DefaultRsc, + mut ui_state: DesktopUiState, + rsc: &mut DesktopRsc, _: Proxy, ) -> Self { let rect = rect(PaintId::RED).add(rsc); diff --git a/examples/view.rs b/examples/view.rs index cbe5ea4..7a4fbc5 100644 --- a/examples/view.rs +++ b/examples/view.rs @@ -1,15 +1,15 @@ use iris::prelude::*; fn main() { - DefaultApp::::run(); + DesktopApp::::run(); } -#[derive(DefaultUiState)] +#[derive(DesktopUiState)] struct State { - ui_state: DefaultUiState, + ui_state: DesktopUiState, } -type Rsc = DefaultRsc; +type Rsc = DesktopRsc; #[derive(Clone, Copy, WidgetView)] struct Test { @@ -35,10 +35,10 @@ impl Test { } } -impl DefaultAppState for State { +impl DesktopAppState for State { fn new( - mut ui_state: DefaultUiState, - rsc: &mut DefaultRsc, + mut ui_state: DesktopUiState, + rsc: &mut DesktopRsc, _: Proxy, ) -> Self { let test = Test::new(rsc); diff --git a/macro/src/lib.rs b/macro/src/lib.rs index d48908c..bdc335e 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -101,8 +101,8 @@ pub fn widget_trait(input: TokenStream) -> TokenStream { .into() } -#[proc_macro_derive(DefaultUiState, attributes(default_ui_state))] -pub fn derive_default_ui_state(input: TokenStream) -> TokenStream { +#[proc_macro_derive(DesktopUiState, attributes(desktop_ui_state))] +pub fn derive_desktop_ui_state(input: TokenStream) -> TokenStream { let mut output = proc_macro2::TokenStream::new(); 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 { if !found_attr && let Type::Path(path) = &field.ty - && path.path.is_ident("DefaultUiState") + && path.path.is_ident("DesktopUiState") { state_field = Some(field); } let Some(attr) = field .attrs .iter() - .find(|a| a.path().is_ident("default_ui_state")) + .find(|a| a.path().is_ident("desktop_ui_state")) else { continue; }; @@ -127,7 +127,7 @@ pub fn derive_default_ui_state(input: TokenStream) -> TokenStream { output.extend( Error::new( attr.span(), - "cannot have more than one default_ui_state attribute", + "cannot have more than one desktop_ui_state attribute", ) .into_compile_error(), ); @@ -138,18 +138,18 @@ pub fn derive_default_ui_state(input: TokenStream) -> TokenStream { } let Some(field) = state_field else { 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(); }; let sname = &state.ident; let fname = field.ident.as_ref().unwrap(); output.extend(quote! { - impl iris::default::HasDefaultUiState for #sname { - fn default_state(&self) -> &iris::default::DefaultUiState { + impl iris::desktop::HasDesktopUiState for #sname { + fn desktop_state(&self) -> &iris::desktop::DesktopUiState { &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 } } diff --git a/run-headless.sh b/run-headless.sh index 59d8e99..750c19e 100755 --- a/run-headless.sh +++ b/run-headless.sh @@ -18,7 +18,7 @@ # `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 # 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 # differs is only the renderer. Without it the output stays desktop- # shaped, which is what every other example wants. diff --git a/src/android/render.rs b/src/android/render.rs index f1c4e62..f6c20a9 100644 --- a/src/android/render.rs +++ b/src/android/render.rs @@ -176,7 +176,7 @@ impl AndroidRenderer { .filter(|part| !part.is_empty()) .collect::>() .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 // backend alone (logged by `view.rs` when a renderer is built) does // not separate the cases that matter. In this checkout's emulator diff --git a/src/android/view.rs b/src/android/view.rs index 9373576..efdbfe3 100644 --- a/src/android/view.rs +++ b/src/android/view.rs @@ -11,13 +11,7 @@ use android_view::{ }, ndk::event::{Axis, Keycode, MotionAction}, }; -use std::{ - cell::RefCell, - marker::{PhantomData, Sized}, - rc::Rc, - sync::Arc, - time::Instant, -}; +use std::{cell::RefCell, marker::Sized, rc::Rc, sync::Arc, time::Instant}; use super::{ access::{AndroidAccessSource, NullActionHandler, raise_if_enabled}, @@ -25,10 +19,7 @@ use super::{ render::{AndroidRedrawHandle, AndroidRenderer}, }; -/// The android-view analogue of `default::DefaultUiState`. `renderer` is an -/// `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`. +/// 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; @@ -39,24 +30,13 @@ pub struct AndroidUiState { pub focus: Option>, pub cursor: CursorState, pub last_click: Instant, - /// The IME preedit's previous length, in `char`s -- the same - /// 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`). + /// Previous IME preedit length, in characters. pub compose_len: usize, - /// Set by `attr::FocusHost::focus_gained` when a `TextEdit` is focused; - /// 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. + /// Deferred until the input callback regains access to JNI. pub pending_show_keyboard: bool, - /// A URL a tapped link asked the platform to open, for the same - /// reason `pending_show_keyboard` is a flag rather than a call -- - /// see `android/platform.rs`. + /// Also deferred until a JNI callback is available. pub pending_open_url: Option, - /// Window insets, filled in from outside the normal `ViewPeer` callback - /// path -- see `android/insets.rs` for why they need a registry of - /// their own. + /// Filled by the native insets callback registered in `android/insets.rs`. shared: Rc>, pub access_adapter: AccessAdapter, pub access: AccessTree, @@ -132,13 +112,7 @@ pub trait AndroidAppState: HasAndroidUiState { fn on_insets_changed(&mut self, rsc: &mut AndroidRsc, insets: WindowInsets) {} } -/// `insets::Insets` as `f32`, for the widget-facing callback above -- a -/// 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. +/// Widget-facing insets in physical pixels, decoupled from JNI's integer shape. #[derive(Clone, Copy, Default, Debug, PartialEq)] pub struct WindowInsets { pub left: f32, @@ -166,66 +140,7 @@ impl WindowInsets { } } -pub struct AndroidRsc { - pub ui: Ui, - pub events: EventManager, - pub tasks: Tasks, - pub state: WidgetState, - _state: PhantomData, -} - -impl AndroidRsc { - pub fn create_state(&mut self, id: impl IdLike, data: T) -> WeakState { - self.state.add(id.id(), data) - } -} - -impl UiRsc for AndroidRsc { - 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 AndroidRsc { - type State = State; -} - -impl HasEvents for AndroidRsc { - fn events(&self) -> &EventManager { - &self.events - } - fn events_mut(&mut self) -> &mut EventManager { - &mut self.events - } -} - -impl HasTasks for AndroidRsc { - fn tasks_mut(&mut self) -> &mut Tasks { - &mut self.tasks - } -} - -impl HasWidgetState for AndroidRsc { - fn widget_state(&self) -> &WidgetState { - &self.state - } - fn widget_state_mut(&mut self) -> &mut WidgetState { - &mut self.state - } -} +pub type AndroidRsc = AppRsc; /// The `ViewPeer` android-view dispatches every callback to. One per /// `RustView` instance; `new_peer` (below) builds it and hands the id to @@ -234,30 +149,10 @@ pub struct IrisViewPeer { pub(super) rsc: AndroidRsc, pub(super) state: State, task_recv: TaskMsgReceiver>, - /// The one ruler this view dates everything on: touch samples in - /// `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. + /// Converts input and Choreographer timestamps onto one monotonic clock. device_clock: Option, } -impl>> std::ops::Index for AndroidRsc { - type Output = I::Output; - - fn index(&self, index: I) -> &Self::Output { - index.get(self) - } -} - -impl>> std::ops::IndexMut for AndroidRsc { - fn index_mut(&mut self, index: I) -> &mut Self::Output { - index.get_mut(self) - } -} - impl IrisViewPeer { fn drain_tasks(&mut self) { while let Ok(update) = self.task_recv.try_recv() { @@ -298,7 +193,7 @@ impl IrisViewPeer { /// 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 `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. pub(super) fn after_input(&mut self, ctx: &mut CallbackCtx) { self.run_input_frame(ctx); @@ -322,6 +217,42 @@ impl IrisViewPeer { .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 { let ui_state = self.state.android_state(); match &ui_state.renderer { @@ -514,27 +445,14 @@ impl ViewPeer for IrisViewPeer { ) -> bool { self.drain_tasks(); let action = event.action_masked(&mut ctx.env); - // Device (physical) pixels, same space layout now uses throughout - // -- see `AndroidUiState::content_scale`'s field comment. + // MotionEvent and layout both use physical pixels. let x = event.x(&mut ctx.env); let y = event.y(&mut ctx.env); - // The event's own clock, converted through the view's one anchor - // -- 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`. + // 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, - // Only the call that anchors needs the batch's oldest sample, - // so the JNI read for it stays off the per-event path. None => { let oldest = if history > 0 { event.historical_event_time_nanos(&mut ctx.env, 0) @@ -544,24 +462,12 @@ impl ViewPeer for IrisViewPeer { self.device_clock(event_time, oldest) } }; - // `iris::input`'s own doc (`sense::log_input_event`): collected - // 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. + // 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 documents the historical samples as oldest first and - // 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. + // 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); @@ -616,6 +522,38 @@ impl ViewPeer for IrisViewPeer { 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>( &mut self, ctx: &mut CallbackCtx<'local>, @@ -646,32 +584,10 @@ impl ViewPeer for IrisViewPeer { height: i32, ) { self.drain_tasks(); - // The layout engine's own notion of the canvas size is separate - // 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. + // The layout canvas and wgpu surface are separate and both use physical pixels. self.rsc.ui.resize((width as f32, height as f32)); - // `AndroidRenderer::resize` only reconfigures the wgpu surface and - // 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. + // 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} \ @@ -883,14 +799,7 @@ 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 = Arc::new(AndroidRedrawHandle::new(vm, global_view)); - let (tasks, task_recv) = Tasks::init(redraw); - let mut rsc = AndroidRsc { - ui: Default::default(), - events: Default::default(), - tasks, - state: Default::default(), - _state: PhantomData, - }; + let (mut rsc, task_recv) = AppRsc::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); diff --git a/src/default/access.rs b/src/desktop/access.rs similarity index 100% rename from src/default/access.rs rename to src/desktop/access.rs diff --git a/src/default/app.rs b/src/desktop/app.rs similarity index 100% rename from src/default/app.rs rename to src/desktop/app.rs diff --git a/src/default/attr.rs b/src/desktop/attr.rs similarity index 71% rename from src/default/attr.rs rename to src/desktop/attr.rs index 5ecfa1c..74b33c0 100644 --- a/src/default/attr.rs +++ b/src/desktop/attr.rs @@ -1,21 +1,21 @@ use crate::prelude::*; use winit::dpi::{PhysicalPosition, PhysicalSize}; -impl FocusHost for T { +impl FocusHost for T { 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>) { - self.default_state_mut().focus = id; + self.desktop_state_mut().focus = id; } fn is_focused(&self, id: WeakWidget) -> bool { - self.default_state().focus == Some(id) + self.desktop_state().focus == Some(id) } fn focus_gained(&mut self, region: Option) { - let state = self.default_state_mut(); + let state = self.desktop_state_mut(); let Some(region) = region else { return }; state.window.set_ime_allowed(true); state.window.set_ime_cursor_area( diff --git a/src/default/input.rs b/src/desktop/input.rs similarity index 96% rename from src/default/input.rs rename to src/desktop/input.rs index 45d2297..6dbbc66 100644 --- a/src/default/input.rs +++ b/src/desktop/input.rs @@ -19,7 +19,7 @@ pub struct Input { impl Input { /// winit's pointer coordinates are physical pixels, which is the /// 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. pub fn event(&mut self, event: &WindowEvent) -> bool { match event { @@ -79,10 +79,10 @@ impl Input { } } -impl DefaultUiState { +impl DesktopUiState { /// Physical pixels, matching `WindowEvent::Resized` (what /// `UiRenderState::resize` is given) and the swapchain -- see - /// `default::content_scale`. + /// `desktop::content_scale`. pub fn window_size(&self) -> Vec2 { let size = self.renderer.window().inner_size(); Vec2::new(size.width as f32, size.height as f32) diff --git a/src/default/logging.rs b/src/desktop/logging.rs similarity index 100% rename from src/default/logging.rs rename to src/desktop/logging.rs diff --git a/src/default/mod.rs b/src/desktop/mod.rs similarity index 63% rename from src/default/mod.rs rename to src/desktop/mod.rs index 765b10e..62b2808 100644 --- a/src/default/mod.rs +++ b/src/desktop/mod.rs @@ -1,10 +1,6 @@ use crate::prelude::*; use arboard::Clipboard; -use std::{ - marker::{PhantomData, Sized}, - sync::Arc, - time::Instant, -}; +use std::{marker::Sized, sync::Arc, time::Instant}; use winit::{ event::{Ime, WindowEvent}, event_loop::{ActiveEventLoop, EventLoopProxy}, @@ -26,19 +22,8 @@ pub use render::*; pub type Proxy = EventLoopProxy; -/// The desktop's `content_scale`: physical pixels per dp, the same -/// quantity Android reads from `DisplayMetrics.density` and feeds to -/// `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. +/// Physical pixels per dp. Layout and input stay in physical pixels; only +/// `dp(...)` resolves through this scale. pub fn content_scale(window: &Window) -> f32 { match std::env::var("IRIS_SCALE") { 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, pub renderer: UiRenderer, pub input: Input, @@ -65,13 +50,13 @@ pub struct DefaultUiState { pub access: AccessTree, } -impl HasRoot> for DefaultUiState { - fn set_root(&mut self, rsc: &mut DefaultRsc, root: StrongWidget) { +impl HasRoot> for DesktopUiState { + fn set_root(&mut self, rsc: &mut DesktopRsc, root: StrongWidget) { self.root = Some(crate::overlay::default_overlay_root(rsc, root)); } } -impl DefaultUiState { +impl DesktopUiState { pub fn new(window: impl Into>, access_adapter: accesskit_winit::Adapter) -> Self { let window = window.into(); Self { @@ -89,114 +74,35 @@ impl DefaultUiState { } } -pub trait HasDefaultUiState: Sized + 'static { - fn default_state(&self) -> &DefaultUiState; - fn default_state_mut(&mut self) -> &mut DefaultUiState; +pub trait HasDesktopUiState: Sized + 'static { + fn desktop_state(&self) -> &DesktopUiState; + fn desktop_state_mut(&mut self) -> &mut DesktopUiState; } -pub trait DefaultAppState: HasDefaultUiState { +pub trait DesktopAppState: HasDesktopUiState { type Event = (); - fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc, proxy: Proxy) + fn new(ui_state: DesktopUiState, rsc: &mut DesktopRsc, proxy: Proxy) -> Self; #[allow(unused_variables)] - fn event(&mut self, event: Self::Event, rsc: &mut DefaultRsc) {} + fn event(&mut self, event: Self::Event, rsc: &mut DesktopRsc) {} #[allow(unused_variables)] - fn exit(&mut self, rsc: &mut DefaultRsc) {} + fn exit(&mut self, rsc: &mut DesktopRsc) {} #[allow(unused_variables)] - fn window_event(&mut self, event: WindowEvent, rsc: &mut DefaultRsc) {} + fn window_event(&mut self, event: WindowEvent, rsc: &mut DesktopRsc) {} fn window_attributes() -> WindowAttributes { Default::default() } } -pub struct DefaultRsc { - pub ui: Ui, - pub events: EventManager, - pub tasks: Tasks, - pub state: WidgetState, - _state: PhantomData, -} +pub type DesktopRsc = AppRsc; -impl DefaultRsc { - fn init(window: Arc) -> (Self, TaskMsgReceiver) { - 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(&mut self, id: impl IdLike, data: T) -> WeakState { - self.state.add(id.id(), data) - } -} - -impl UiRsc for DefaultRsc { - 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 DefaultRsc { - type State = State; -} - -impl HasEvents for DefaultRsc { - fn events(&self) -> &EventManager { - &self.events - } - - fn events_mut(&mut self) -> &mut EventManager { - &mut self.events - } -} - -impl HasTasks for DefaultRsc { - fn tasks_mut(&mut self) -> &mut Tasks { - &mut self.tasks - } -} - -impl HasWidgetState for DefaultRsc { - fn widget_state(&self) -> &WidgetState { - &self.state - } - - fn widget_state_mut(&mut self) -> &mut WidgetState { - &mut self.state - } -} - -pub struct DefaultApp { - rsc: DefaultRsc, +pub struct DesktopApp { + rsc: DesktopRsc, state: State, - task_recv: TaskMsgReceiver>, + task_recv: TaskMsgReceiver>, } -impl AppState for DefaultApp { +impl AppState for DesktopApp { type Event = State::Event; fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy) -> Self { @@ -211,16 +117,12 @@ impl AppState for DefaultApp { NullDeactivationHandler, ); window.set_visible(true); - let default_state = DefaultUiState::new(window, access_adapter); - let (mut rsc, task_recv) = DefaultRsc::init(default_state.window.clone()); - // Both copies of the density, set before the first widget is - // built so text shapes at the right size on the opening frame -- - // 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()); + let desktop_state = DesktopUiState::new(window, access_adapter); + let (mut rsc, task_recv) = AppRsc::new(desktop_state.window.clone()); + // Set before building widgets so the first text shape uses the right density. + let scale = content_scale(desktop_state.window.as_ref()); rsc.ui.set_density(scale); - let state = State::new(default_state, &mut rsc, proxy); + let state = State::new(desktop_state, &mut rsc, proxy); Self { rsc, state, @@ -243,7 +145,7 @@ impl AppState for DefaultApp { 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 // ones this backend otherwise cares about -- some platform adapters // rely on it to notice activation (a screen reader turning on). @@ -257,14 +159,7 @@ impl AppState for DefaultApp { ui_state.focus = None; } if input_changed { - // The winit half of `iris::input` (`sense::log_input_event`'s - // 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. + // Winit delivers one sample at a time, so there is no history batch. if crate::diagnostics::trace_enabled() { let action = if cursor_state.buttons.left.is_start() { "down" @@ -292,7 +187,7 @@ impl AppState for DefaultApp { .get() .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 && let Some(old) = old { @@ -301,16 +196,10 @@ impl AppState for DefaultApp { match &event { WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::RedrawRequested => { - // Before the draw, so this frame shows this instant's - // 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. + // Advance animations before drawing and keep requesting frames while active. let frame_start = std::time::Instant::now(); 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 { ui_state.window.request_redraw(); } @@ -415,7 +304,7 @@ impl AppState for DefaultApp { _ => (), } 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(); if render_state .get() @@ -430,17 +319,3 @@ impl AppState for DefaultApp { self.state.exit(&mut self.rsc); } } - -impl>> std::ops::Index for DefaultRsc { - type Output = I::Output; - - fn index(&self, index: I) -> &Self::Output { - index.get(self) - } -} - -impl>> std::ops::IndexMut for DefaultRsc { - fn index_mut(&mut self, index: I) -> &mut Self::Output { - index.get_mut(self) - } -} diff --git a/src/default/platform.rs b/src/desktop/platform.rs similarity index 87% rename from src/default/platform.rs rename to src/desktop/platform.rs index 3ed1281..993f349 100644 --- a/src/default/platform.rs +++ b/src/desktop/platform.rs @@ -1,7 +1,7 @@ use crate::platform::OpenUrl; -use crate::prelude::HasDefaultUiState; +use crate::prelude::HasDesktopUiState; -impl OpenUrl for T { +impl OpenUrl for T { fn open_url(&mut self, url: &str) { let (program, first): (&str, &[&str]) = if cfg!(target_os = "macos") { ("open", &[]) diff --git a/src/default/render.rs b/src/desktop/render.rs similarity index 99% rename from src/default/render.rs rename to src/desktop/render.rs index 4fa3103..ad83302 100644 --- a/src/default/render.rs +++ b/src/desktop/render.rs @@ -213,7 +213,7 @@ impl UiRenderer { // that function's doc comment). // Physical size, the same units the swapchain, `WindowEvent:: // 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. let physical_size = Vec2::new(size.width as f32, size.height as f32); let ui = UiRenderNode::new(&device, &queue, formats.view, physical_size) diff --git a/src/diagnostics.rs b/src/diagnostics.rs index 44fd8e7..ea57a2c 100644 --- a/src/diagnostics.rs +++ b/src/diagnostics.rs @@ -15,7 +15,7 @@ pub fn trace_enabled() -> bool { /// One `iris::frame` line, called once per frame from each backend's own /// 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 /// draw would be; `draw` is `Duration::ZERO` there since nothing is /// actually submitted to a GPU). diff --git a/src/harness.rs b/src/harness.rs index 56f4ab1..a044c80 100644 --- a/src/harness.rs +++ b/src/harness.rs @@ -1,5 +1,4 @@ use crate::prelude::*; -use std::marker::PhantomData; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::{Duration, Instant}; @@ -164,76 +163,7 @@ impl OpenUrl for HarnessState { } } -/// The harness's `Rsc` -- identical in substance to `DefaultRsc`/ -/// `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, - pub tasks: Tasks, - pub state: WidgetState, - _state: PhantomData, -} - -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.events - } - fn events_mut(&mut self) -> &mut EventManager { - &mut self.events - } -} - -impl HasTasks for HarnessRsc { - fn tasks_mut(&mut self) -> &mut Tasks { - &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> std::ops::Index for HarnessRsc { - type Output = I::Output; - fn index(&self, index: I) -> &Self::Output { - index.get(self) - } -} - -impl> std::ops::IndexMut for HarnessRsc { - fn index_mut(&mut self, index: I) -> &mut Self::Output { - index.get_mut(self) - } -} +pub type HarnessRsc = AppRsc; /// A screen running with no window: the widget tree, the frame loop and /// the pointer, all advanced by the caller. See the module doc. @@ -255,14 +185,7 @@ impl Harness { /// `PHONE_SCALE`. pub fn new(size: Vec2, density: f32) -> Self { let redraws = Arc::new(RedrawCounter::default()); - let (tasks, task_recv) = Tasks::init(redraws.clone()); - let mut rsc = HarnessRsc { - ui: Ui::default(), - events: EventManager::default(), - tasks, - state: WidgetState::default(), - _state: PhantomData, - }; + let (mut rsc, task_recv) = AppRsc::new(redraws.clone()); rsc.ui.set_density(density); rsc.ui.resize(size); Self { diff --git a/src/layout_tests.rs b/src/layout_tests.rs index 04e02d7..7427494 100644 --- a/src/layout_tests.rs +++ b/src/layout_tests.rs @@ -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; render.update(&root, &mut rsc); assert_eq!( - render.take_counters().0, + render.take_counters().draws, 1, "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(&outer_weak).unwrap().x = None; 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 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(); render.resize((200.0, 300.0)); 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!(moves, 1); @@ -328,7 +333,12 @@ fn an_unchanged_frame_draws_and_rewrites_nothing() { render.take_counters(); // discard the first, real draws 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)); } @@ -346,7 +356,7 @@ fn scrolling_moves_in_o1_without_a_redraw() { rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(-40.0); 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!(moves, 1, "the scrolled subtree should move in one write"); diff --git a/src/lib.rs b/src/lib.rs index b7dd1fd..5144d22 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ #![feature(unboxed_closures)] #![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. #![cfg_attr(not(target_os = "android"), feature(associated_type_defaults))] #![feature(unsize)] @@ -10,7 +10,7 @@ #[cfg(target_os = "android")] pub mod android; #[cfg(not(target_os = "android"))] -pub mod default; +pub mod desktop; pub mod attr; pub mod diagnostics; @@ -18,6 +18,7 @@ pub mod event; pub mod harness; pub mod overlay; pub mod platform; +pub mod runtime; pub mod sense; pub mod state; pub mod task; @@ -38,7 +39,7 @@ pub mod prelude { #[cfg(target_os = "android")] pub use android::*; #[cfg(not(target_os = "android"))] - pub use default::*; + pub use desktop::*; pub use attr::*; pub use event::*; @@ -46,6 +47,7 @@ pub mod prelude { pub use iris_macro::*; pub use overlay::*; pub use platform::*; + pub use runtime::*; pub use sense::*; pub use state::*; pub use task::*; diff --git a/src/runtime.rs b/src/runtime.rs new file mode 100644 index 0000000..b3a2c79 --- /dev/null +++ b/src/runtime.rs @@ -0,0 +1,98 @@ +use crate::prelude::*; +use std::sync::Arc; + +/// Resources shared by every Iris host. +pub struct AppRsc { + pub ui: Ui, + pub events: EventManager, + pub tasks: Tasks, + pub state: WidgetState, + _state: std::marker::PhantomData, +} + +impl AppRsc { + pub(crate) fn new(redraw: Arc) -> (Self, TaskMsgReceiver) { + 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(&mut self, id: impl IdLike, data: T) -> WeakState { + self.state.add(id.id(), data) + } +} + +impl UiRsc for AppRsc { + 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 AppRsc { + type State = State; +} + +impl HasEvents for AppRsc { + fn events(&self) -> &EventManager { + &self.events + } + + fn events_mut(&mut self) -> &mut EventManager { + &mut self.events + } +} + +impl HasTasks for AppRsc { + fn tasks_mut(&mut self) -> &mut Tasks { + &mut self.tasks + } +} + +impl HasWidgetState for AppRsc { + fn widget_state(&self) -> &WidgetState { + &self.state + } + + fn widget_state_mut(&mut self) -> &mut WidgetState { + &mut self.state + } +} + +impl>> std::ops::Index for AppRsc { + type Output = I::Output; + + fn index(&self, index: I) -> &Self::Output { + index.get(self) + } +} + +impl>> std::ops::IndexMut for AppRsc { + fn index_mut(&mut self, index: I) -> &mut Self::Output { + index.get_mut(self) + } +} diff --git a/src/widget/position/lazy_span.rs b/src/widget/position/lazy_span.rs index bedaed8..7897edb 100644 --- a/src/widget/position/lazy_span.rs +++ b/src/widget/position/lazy_span.rs @@ -1155,7 +1155,7 @@ mod tests { .push_front(LazyItem::new(key, w)); } 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(); for key in [11u64, 12] { @@ -1269,7 +1269,8 @@ mod tests { rsc.ui.widgets.get_mut(&list_weak).unwrap().scroll(1.0); 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!(moves, 1, "n={n}: the whole retained run should move once");