From 213a0debb7ea07e0bdfd16c42bced87c2a4648bf Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Fri, 11 Sep 2026 02:09:17 -0400 Subject: [PATCH] Use one standard Iris resource bundle --- app/examples/phone.rs | 6 +- app/examples/transcript.rs | 6 +- app/src/android/bench_client.rs | 26 +++------ app/src/android/devlog.rs | 4 +- app/src/android/mod.rs | 28 ++++------ app/src/android/transcript_client.rs | 30 ++++------ app/src/desktop/app.rs | 26 +++------ docs/RUST.md | 8 ++- iris/examples/bench_images.rs | 12 ++-- iris/examples/message_list.rs | 6 +- iris/examples/minimal.rs | 6 +- iris/examples/tabs/main.rs | 8 +-- iris/examples/task.rs | 6 +- iris/examples/view.rs | 8 +-- iris/macro/src/lib.rs | 83 +++++++++++++++++++++++----- iris/src/android/input.rs | 4 +- iris/src/android/mod.rs | 3 +- iris/src/android/view.rs | 20 +++---- iris/src/desktop/mod.rs | 21 +++---- iris/src/harness.rs | 12 ++-- iris/src/rsc/mod.rs | 25 +++++---- iris/src/rsc/overlay.rs | 6 +- iris/src/rsc/state.rs | 2 +- iris/tests/color_space.rs | 7 ++- 24 files changed, 177 insertions(+), 186 deletions(-) diff --git a/app/examples/phone.rs b/app/examples/phone.rs index fd855df..a13f91a 100644 --- a/app/examples/phone.rs +++ b/app/examples/phone.rs @@ -52,11 +52,7 @@ impl DesktopAppState for Client { )) } - fn new( - mut ui_state: DesktopUiState, - rsc: &mut DesktopRsc, - _: Proxy, - ) -> Self { + fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc, _: Proxy) -> Self { let screen = match ai_app::ui::fixture::open(rsc, &mut ui_state) { Ok(opened) => { if let Some(message) = message_argv() { diff --git a/app/examples/transcript.rs b/app/examples/transcript.rs index 17672df..91801ae 100644 --- a/app/examples/transcript.rs +++ b/app/examples/transcript.rs @@ -158,11 +158,7 @@ fn fold_event(items: Vec, seq: u64) -> Vec { "; impl DesktopAppState for Client { - fn new( - mut ui_state: DesktopUiState, - rsc: &mut DesktopRsc, - _: Proxy, - ) -> Self { + fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc, _: Proxy) -> Self { let mut screen = ai_app::ui::build(rsc, &mut ui_state, synthetic_rows()); screen.push_row( rsc, diff --git a/app/src/android/bench_client.rs b/app/src/android/bench_client.rs index cd4f607..3d96dfe 100644 --- a/app/src/android/bench_client.rs +++ b/app/src/android/bench_client.rs @@ -3,7 +3,7 @@ use crate::client::transcript_fold::{TranscriptItem, fold_event}; use crate::ui::{self, TranscriptScreen}; use android_view::jni::{JavaVM, objects::GlobalRef}; use event_model::SeqEvent; -use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState}; +use iris::android::{AndroidAppState, AndroidUiState}; use iris::prelude::*; use std::{ fs, mem, @@ -42,6 +42,7 @@ const POLL_MS: u64 = 16; const REPORT_MAX_HEIGHT_DP: f32 = 260.0; +#[derive(AndroidUiState)] pub struct BenchClient { ui_state: AndroidUiState, content: WeakWidget, @@ -65,15 +66,6 @@ struct ImeState { hidden_events: u32, } -impl HasAndroidUiState for BenchClient { - fn android_state(&self) -> &AndroidUiState { - &self.ui_state - } - fn android_state_mut(&mut self) -> &mut AndroidUiState { - &mut self.ui_state - } -} - fn placeholder(rsc: &mut Rsc, message: &str) -> StrongWidget { wtext(message.to_string()) .color(PaintId::WHITE) @@ -121,7 +113,7 @@ fn battery_line(samples: &[i32]) -> String { } impl AndroidAppState for BenchClient { - fn new(mut ui_state: AndroidUiState, rsc: &mut AndroidRsc) -> Self { + fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc) -> Self { let content = WidgetPtr::new().add(rsc); let loading = placeholder(rsc, "Loading fixture..."); content(rsc).set(loading); @@ -195,19 +187,15 @@ impl AndroidAppState for BenchClient { client } - fn platform_ready(&mut self, _rsc: &mut AndroidRsc, vm: JavaVM, view: GlobalRef) { + fn platform_ready(&mut self, _rsc: &mut StdRsc, vm: JavaVM, view: GlobalRef) { self.platform = Some(Arc::new(PlatformHandle::new(vm, view))); } - fn back_pressed(&mut self, _rsc: &mut AndroidRsc) -> bool { + fn back_pressed(&mut self, _rsc: &mut StdRsc) -> bool { false } - fn on_insets_changed( - &mut self, - rsc: &mut AndroidRsc, - insets: iris::android::WindowInsets, - ) { + fn on_insets_changed(&mut self, rsc: &mut StdRsc, insets: iris::android::WindowInsets) { if insets.top != self.last_top_pad { self.last_top_pad = insets.top; let controls = bench_controls(rsc, insets.top); @@ -255,7 +243,7 @@ impl AndroidAppState for BenchClient { const KEYBOARD_DIAGNOSTICS_DELAY_MS: u64 = 500; -type Rsc = AndroidRsc; +type Rsc = StdRsc; /// What a report says about the `iris::input`/`iris::frame` trace, from /// the flag read at the start of what is being reported and again at the diff --git a/app/src/android/devlog.rs b/app/src/android/devlog.rs index 6e71631..420e817 100644 --- a/app/src/android/devlog.rs +++ b/app/src/android/devlog.rs @@ -4,7 +4,9 @@ use android_view::jni::JNIEnv; use android_view::jni::objects::{JClass, JObject, JString}; use android_view::jni::sys::{jlong, jobjectArray}; -use std::{path::Path, ptr, sync::OnceLock}; +#[cfg(feature = "transcript-screen")] +use std::path::Path; +use std::{ptr, sync::OnceLock}; /// Gated with its one reader: the tabs demo links no `client-core` and so /// has no ring to lay out, and an ungated constant is a warning in that diff --git a/app/src/android/mod.rs b/app/src/android/mod.rs index af29da9..d570a0b 100644 --- a/app/src/android/mod.rs +++ b/app/src/android/mod.rs @@ -8,12 +8,15 @@ use android_view::{ register_view_class, }; #[cfg(not(feature = "transcript-screen"))] -use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState}; -use log::LevelFilter; -use std::{ - ffi::c_void, - path::{Path, PathBuf}, +use iris::{ + android::{AndroidAppState, AndroidUiState}, + macros::AndroidUiState, + rsc::StdRsc, }; +use log::LevelFilter; +use std::ffi::c_void; +#[cfg(feature = "transcript-screen")] +use std::path::{Path, PathBuf}; /// The app's own log ring and its upload -- only where `client-core` is /// linked, which is every build that has a server to send to. The plain @@ -35,23 +38,14 @@ mod transcript_client; const VIEW_CLASS: &str = "dev/iris/android/demo/IrisView"; #[cfg(not(feature = "transcript-screen"))] +#[derive(AndroidUiState)] pub struct Client { ui_state: AndroidUiState, } -#[cfg(not(feature = "transcript-screen"))] -impl HasAndroidUiState for Client { - fn android_state(&self) -> &AndroidUiState { - &self.ui_state - } - fn android_state_mut(&mut self) -> &mut AndroidUiState { - &mut self.ui_state - } -} - #[cfg(not(feature = "transcript-screen"))] impl AndroidAppState for Client { - fn new(mut ui_state: AndroidUiState, rsc: &mut AndroidRsc) -> Self { + fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc) -> Self { // `widgets.info` is the winit example's frame-debug readout, kept // current from `DesktopAppState::window_event` -- android-view has // no per-frame hook to drive the equivalent from here yet, so it @@ -60,7 +54,7 @@ impl AndroidAppState for Client { Self { ui_state } } - fn back_pressed(&mut self, _rsc: &mut AndroidRsc) -> bool { + fn back_pressed(&mut self, _rsc: &mut StdRsc) -> bool { false } } diff --git a/app/src/android/transcript_client.rs b/app/src/android/transcript_client.rs index b866545..fe95797 100644 --- a/app/src/android/transcript_client.rs +++ b/app/src/android/transcript_client.rs @@ -5,7 +5,7 @@ use crate::client::transcript_fold::{ }; use crate::ui::{self, TranscriptScreen}; use event_model::SeqEvent; -use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState}; +use iris::android::{AndroidAppState, AndroidUiState}; use iris::prelude::*; use std::{ sync::{ @@ -15,6 +15,7 @@ use std::{ thread, }; +#[derive(AndroidUiState)] pub struct TranscriptClient { ui_state: AndroidUiState, /// The screen's own content -- everything under the fixed @@ -29,15 +30,6 @@ pub struct TranscriptClient { generation: Arc, } -impl HasAndroidUiState for TranscriptClient { - fn android_state(&self) -> &AndroidUiState { - &self.ui_state - } - fn android_state_mut(&mut self) -> &mut AndroidUiState { - &mut self.ui_state - } -} - fn build_transport() -> Result { crate::android::enrollment::transport() } @@ -51,8 +43,8 @@ fn placeholder(rsc: &mut Rsc, message: &str) -> StrongWidget { .any() } -fn frame_report_controls(rsc: &mut AndroidRsc) -> WeakWidget { - type Rsc = AndroidRsc; +fn frame_report_controls(rsc: &mut StdRsc) -> WeakWidget { + type Rsc = StdRsc; let report_rect = rect(Srgba8::rgb(50, 50, 60)) .on( CursorSense::click(), @@ -98,7 +90,7 @@ fn frame_report_controls(rsc: &mut AndroidRsc) -> WeakWidget { } impl AndroidAppState for TranscriptClient { - fn new(mut ui_state: AndroidUiState, rsc: &mut AndroidRsc) -> Self { + fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc) -> Self { let content = WidgetPtr::new().add(rsc); let loading = placeholder(rsc, "Loading sessions..."); content(rsc).set(loading); @@ -121,7 +113,7 @@ impl AndroidAppState for TranscriptClient { client } - fn back_pressed(&mut self, _rsc: &mut AndroidRsc) -> bool { + fn back_pressed(&mut self, _rsc: &mut StdRsc) -> bool { // No screen stack of its own -- same "let the activity finish" // answer `iris-android-app`'s tabs `Client` already gives. false @@ -129,13 +121,13 @@ impl AndroidAppState for TranscriptClient { } impl TranscriptClient { - fn show_message(&mut self, rsc: &mut AndroidRsc, message: &str) { + fn show_message(&mut self, rsc: &mut StdRsc, message: &str) { let widget = placeholder(rsc, message); (self.content)(rsc).set(widget); self.screen = None; } - fn spawn_fetch_sessions(&mut self, rsc: &mut AndroidRsc) { + fn spawn_fetch_sessions(&mut self, rsc: &mut StdRsc) { let redraw = rsc.tasks.redraw_handle(); let my_generation = self.generation.load(Ordering::SeqCst); let generation = self.generation.clone(); @@ -164,7 +156,7 @@ impl TranscriptClient { }); } - fn select_session(&mut self, rsc: &mut AndroidRsc, session_id: String) { + fn select_session(&mut self, rsc: &mut StdRsc, session_id: String) { let my_generation = self.generation.fetch_add(1, Ordering::SeqCst) + 1; self.items.clear(); self.session_id = Some(session_id.clone()); @@ -251,7 +243,7 @@ impl TranscriptClient { }); } - fn rebuild_transcript(&mut self, rsc: &mut AndroidRsc) { + fn rebuild_transcript(&mut self, rsc: &mut StdRsc) { let in_progress = self .screen .as_ref() @@ -279,7 +271,7 @@ impl TranscriptClient { self.screen = Some(screen); } - fn apply_event(&mut self, rsc: &mut AndroidRsc, event: &SeqEvent) { + fn apply_event(&mut self, rsc: &mut StdRsc, event: &SeqEvent) { let old_items = self.items.clone(); self.items = fold_event(&self.items, event); match self.screen.as_mut() { diff --git a/app/src/desktop/app.rs b/app/src/desktop/app.rs index 018df05..721c3ef 100644 --- a/app/src/desktop/app.rs +++ b/app/src/desktop/app.rs @@ -58,11 +58,7 @@ struct Client { impl DesktopAppState for Client { type Event = AppEvent; - fn new( - mut ui_state: DesktopUiState, - rsc: &mut DesktopRsc, - proxy: Proxy, - ) -> Self { + fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc, proxy: Proxy) -> Self { let (server, ca_pem) = super::startup::load_startup_config().unwrap_or_else(|e| { eprintln!("desktop-app: {e}"); process::exit(2); @@ -107,7 +103,7 @@ impl DesktopAppState for Client { client } - fn event(&mut self, event: AppEvent, rsc: &mut DesktopRsc) { + fn event(&mut self, event: AppEvent, rsc: &mut StdRsc) { match event { AppEvent::Sessions(Ok(sessions)) => { self.sessions = sessions; @@ -177,7 +173,7 @@ impl Client { && self.generation.load(Ordering::SeqCst) == generation } - fn show_message(&mut self, rsc: &mut DesktopRsc, message: &str) { + fn show_message(&mut self, rsc: &mut StdRsc, message: &str) { let widget = placeholder(rsc, message); (self.transcript_ptr)(rsc).set(widget); } @@ -191,7 +187,7 @@ impl Client { }); } - fn rebuild_list(&mut self, rsc: &mut DesktopRsc) { + fn rebuild_list(&mut self, rsc: &mut StdRsc) { let list = Span::empty(Dir::DOWN).gap(2).add(rsc); for session in &self.sessions { let selected = self.selected.as_deref() == Some(session.id.as_str()); @@ -205,7 +201,7 @@ impl Client { (self.list_ptr)(rsc).set(tree); } - fn select_session(&mut self, rsc: &mut DesktopRsc, session_id: String) { + fn select_session(&mut self, rsc: &mut StdRsc, session_id: String) { let generation = self.generation.fetch_add(1, Ordering::SeqCst) + 1; self.selected = Some(session_id.clone()); self.items.clear(); @@ -270,7 +266,7 @@ impl Client { }); } - fn rebuild_transcript(&mut self, rsc: &mut DesktopRsc) { + fn rebuild_transcript(&mut self, rsc: &mut StdRsc) { let in_progress = self .screen .as_ref() @@ -299,11 +295,7 @@ impl Client { } } -fn session_row( - rsc: &mut DesktopRsc, - session: &SessionSummary, - selected: bool, -) -> StrongWidget { +fn session_row(rsc: &mut StdRsc, session: &SessionSummary, selected: bool) -> StrongWidget { let bg = if selected { Srgba8::rgb(58, 90, 138) } else { @@ -319,7 +311,7 @@ fn session_row( .background(rect(bg)) .on( CursorSense::click(), - move |ctx, rsc: &mut DesktopRsc| { + move |ctx, rsc: &mut StdRsc| { ctx.state.select_session(rsc, id.clone()); }, ) @@ -327,7 +319,7 @@ fn session_row( .any() } -fn placeholder(rsc: &mut DesktopRsc, message: &str) -> StrongWidget { +fn placeholder(rsc: &mut StdRsc, message: &str) -> StrongWidget { wtext(message.to_string()) .color(PaintId::WHITE) .wrap(true) diff --git a/docs/RUST.md b/docs/RUST.md index dc6fc3a..200a43f 100644 --- a/docs/RUST.md +++ b/docs/RUST.md @@ -13,10 +13,12 @@ open work and measured constraints that are expensive to rediscover. Platform services are exposed to shared UI through small traits. - `iris/` is only a UI framework. Session, transcript, server, setup, and product styling concepts never belong there. -- `iris/src/rsc` groups resources carried by or installed through `AppRsc`: +- `iris/src/rsc` groups resources carried by or installed through `StdRsc`: attributes, events, overlays, pointer sensing/input, tasks, and per-widget - state. `widget/` is reserved for concrete retained widgets. Public paths - such as `iris::sense` remain aliases for readability. + state. It is the standard bundle used by Iris's hosts, not a requirement; + shared widgets depend on its narrow resource traits so a custom bundle can + implement only what it needs. `widget/` is reserved for concrete retained + widgets. Public paths such as `iris::sense` remain aliases for readability. - Phone and desktop layouts may differ. Their component implementations, colors, spacing, text styles, gestures, folding, and network behavior do not. diff --git a/iris/examples/bench_images.rs b/iris/examples/bench_images.rs index db4b0b5..4754fe2 100644 --- a/iris/examples/bench_images.rs +++ b/iris/examples/bench_images.rs @@ -13,15 +13,11 @@ struct State { } impl DesktopAppState for State { - fn new( - mut ui_state: DesktopUiState, - rsc: &mut DesktopRsc, - _: Proxy, - ) -> Self { + fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc, _: 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 +36,7 @@ impl DesktopAppState for State { } } - fn window_event(&mut self, event: winit::event::WindowEvent, rsc: &mut DesktopRsc) { + fn window_event(&mut self, event: winit::event::WindowEvent, rsc: &mut StdRsc) { if !matches!(event, winit::event::WindowEvent::RedrawRequested) { return; } @@ -53,7 +49,7 @@ impl DesktopAppState 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 diff --git a/iris/examples/message_list.rs b/iris/examples/message_list.rs index 73712c1..0a6470b 100644 --- a/iris/examples/message_list.rs +++ b/iris/examples/message_list.rs @@ -63,11 +63,7 @@ impl DesktopAppState for State { WindowAttributes::default().with_inner_size(LogicalSize::new(420.0, 900.0)) } - fn new( - mut ui_state: DesktopUiState, - rsc: &mut DesktopRsc, - _: Proxy, - ) -> Self { + fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc, _: Proxy) -> Self { let mut list = LazySpan::new(Dir::DOWN, Pin::End); for i in 0..ROWS { let row = build_row(rsc, i); diff --git a/iris/examples/minimal.rs b/iris/examples/minimal.rs index 50b7807..579ae5a 100644 --- a/iris/examples/minimal.rs +++ b/iris/examples/minimal.rs @@ -10,11 +10,7 @@ struct State { } impl DesktopAppState for State { - fn new( - mut ui_state: DesktopUiState, - rsc: &mut DesktopRsc, - _: Proxy, - ) -> Self { + fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc, _: Proxy) -> Self { rect(PaintId::RED).set_root(rsc, &mut ui_state); Self { ui_state } } diff --git a/iris/examples/tabs/main.rs b/iris/examples/tabs/main.rs index 0732297..151168c 100644 --- a/iris/examples/tabs/main.rs +++ b/iris/examples/tabs/main.rs @@ -12,11 +12,7 @@ pub struct Client { } impl DesktopAppState for Client { - fn new( - mut ui_state: DesktopUiState, - rsc: &mut DesktopRsc, - _: Proxy, - ) -> Self { + fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc, _: Proxy) -> Self { let widgets = tabs_ui::build(rsc, &mut ui_state); Self { ui_state, @@ -24,7 +20,7 @@ impl DesktopAppState for Client { } } - fn window_event(&mut self, _: WindowEvent, rsc: &mut DesktopRsc) { + fn window_event(&mut self, _: WindowEvent, rsc: &mut StdRsc) { let render_state = rsc.ui.render_state(); let new = format!( "widgets: {}\nactive: {}\nviews: {}", diff --git a/iris/examples/task.rs b/iris/examples/task.rs index 7947832..f0d0a3c 100644 --- a/iris/examples/task.rs +++ b/iris/examples/task.rs @@ -11,11 +11,7 @@ struct State { } impl DesktopAppState for State { - fn new( - mut ui_state: DesktopUiState, - rsc: &mut DesktopRsc, - _: Proxy, - ) -> Self { + fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc, _: Proxy) -> Self { let rect = rect(PaintId::RED).add(rsc); rect.task_on(CursorSense::click(), async move |mut ctx| { tokio::time::sleep(Duration::from_secs(1)).await; diff --git a/iris/examples/view.rs b/iris/examples/view.rs index 7a4fbc5..396344c 100644 --- a/iris/examples/view.rs +++ b/iris/examples/view.rs @@ -9,7 +9,7 @@ struct State { ui_state: DesktopUiState, } -type Rsc = DesktopRsc; +type Rsc = StdRsc; #[derive(Clone, Copy, WidgetView)] struct Test { @@ -36,11 +36,7 @@ impl Test { } impl DesktopAppState for State { - fn new( - mut ui_state: DesktopUiState, - rsc: &mut DesktopRsc, - _: Proxy, - ) -> Self { + fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc, _: Proxy) -> Self { let test = Test::new(rsc); test.on(CursorSense::click(), move |_, rsc| { diff --git a/iris/macro/src/lib.rs b/iris/macro/src/lib.rs index bdc335e..07f8d17 100644 --- a/iris/macro/src/lib.rs +++ b/iris/macro/src/lib.rs @@ -103,31 +103,72 @@ pub fn widget_trait(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); + derive_ui_state( + state, + UiStateDerive { + module: "desktop", + state_type: "DesktopUiState", + state_trait: "HasDesktopUiState", + field_attr: "desktop_ui_state", + get: "desktop_state", + get_mut: "desktop_state_mut", + }, + ) +} +#[proc_macro_derive(AndroidUiState, attributes(android_ui_state))] +pub fn derive_android_ui_state(input: TokenStream) -> TokenStream { + let state: ItemStruct = parse_macro_input!(input); + derive_ui_state( + state, + UiStateDerive { + module: "android", + state_type: "AndroidUiState", + state_trait: "HasAndroidUiState", + field_attr: "android_ui_state", + get: "android_state", + get_mut: "android_state_mut", + }, + ) +} + +struct UiStateDerive { + module: &'static str, + state_type: &'static str, + state_trait: &'static str, + field_attr: &'static str, + get: &'static str, + get_mut: &'static str, +} + +fn derive_ui_state(state: ItemStruct, names: UiStateDerive) -> TokenStream { + let UiStateDerive { + module, + state_type, + state_trait, + field_attr, + get, + get_mut, + } = names; + let mut output = proc_macro2::TokenStream::new(); let mut found_attr = false; let mut state_field = None; for field in &state.fields { if !found_attr && let Type::Path(path) = &field.ty - && path.path.is_ident("DesktopUiState") + && path.path.is_ident(state_type) { state_field = Some(field); } - let Some(attr) = field - .attrs - .iter() - .find(|a| a.path().is_ident("desktop_ui_state")) - else { + let Some(attr) = field.attrs.iter().find(|a| a.path().is_ident(field_attr)) else { continue; }; if found_attr { output.extend( Error::new( attr.span(), - "cannot have more than one desktop_ui_state attribute", + format!("cannot have more than one {field_attr} attribute"), ) .into_compile_error(), ); @@ -138,18 +179,32 @@ pub fn derive_desktop_ui_state(input: TokenStream) -> TokenStream { } let Some(field) = state_field else { output.extend( - Error::new(state.ident.span(), "no DesktopUiState field found").into_compile_error(), + Error::new(state.ident.span(), format!("no {state_type} field found")) + .into_compile_error(), ); return output.into(); }; let sname = &state.ident; - let fname = field.ident.as_ref().unwrap(); + let Some(fname) = field.ident.as_ref() else { + return Error::new( + field.span(), + format!("the {state_type} field must be named"), + ) + .into_compile_error() + .into(); + }; + let module = Ident::new(module, sname.span()); + let state_type = Ident::new(state_type, sname.span()); + let state_trait = Ident::new(state_trait, sname.span()); + let get = Ident::new(get, sname.span()); + let get_mut = Ident::new(get_mut, sname.span()); + let (impl_generics, type_generics, where_clause) = state.generics.split_for_impl(); output.extend(quote! { - impl iris::desktop::HasDesktopUiState for #sname { - fn desktop_state(&self) -> &iris::desktop::DesktopUiState { + impl #impl_generics iris::#module::#state_trait for #sname #type_generics #where_clause { + fn #get(&self) -> &iris::#module::#state_type { &self.#fname } - fn desktop_state_mut(&mut self) -> &mut iris::desktop::DesktopUiState { + fn #get_mut(&mut self) -> &mut iris::#module::#state_type { &mut self.#fname } } diff --git a/iris/src/android/input.rs b/iris/src/android/input.rs index 963675e..fd507bf 100644 --- a/iris/src/android/input.rs +++ b/iris/src/android/input.rs @@ -1,7 +1,7 @@ use crate::prelude::*; use android_view::{jni::JNIEnv, ndk::event::Keycode}; -use super::view::{AndroidAppState, AndroidRsc}; +use super::view::AndroidAppState; /// Hardware/synthesized key handling for the field that currently has /// focus. Most typing on Android goes through the IME's `InputConnection` @@ -10,7 +10,7 @@ use super::view::{AndroidAppState, AndroidRsc}; /// the arrow keys on a physical keyboard) plus whatever `unicode_char` /// reports for a plain key press. Returns whether anything used the event. pub(super) fn on_key<'local, State: AndroidAppState>( - rsc: &mut AndroidRsc, + rsc: &mut StdRsc, state: &mut State, env: &mut JNIEnv<'local>, key_code: Keycode, diff --git a/iris/src/android/mod.rs b/iris/src/android/mod.rs index 10965fa..d37c7e7 100644 --- a/iris/src/android/mod.rs +++ b/iris/src/android/mod.rs @@ -10,8 +10,7 @@ mod view; pub use insets::Insets; pub use render::AndroidRenderer; pub use view::{ - AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState, IrisViewPeer, WindowInsets, - new_peer, + AndroidAppState, AndroidUiState, HasAndroidUiState, IrisViewPeer, WindowInsets, new_peer, }; /// Registers the extra native methods this backend needs beyond what diff --git a/iris/src/android/view.rs b/iris/src/android/view.rs index dc65117..7235a68 100644 --- a/iris/src/android/view.rs +++ b/iris/src/android/view.rs @@ -89,8 +89,8 @@ impl AndroidUiState { } } -impl HasRoot> for AndroidUiState { - fn set_root(&mut self, rsc: &mut AndroidRsc, root: StrongWidget) { +impl HasRoot> for AndroidUiState { + fn set_root(&mut self, rsc: &mut StdRsc, root: StrongWidget) { self.root = Some(crate::overlay::default_overlay_root(rsc, root)); } } @@ -101,15 +101,15 @@ pub trait HasAndroidUiState: Sized + 'static { } pub trait AndroidAppState: HasAndroidUiState { - fn new(ui_state: AndroidUiState, rsc: &mut AndroidRsc) -> Self; + fn new(ui_state: AndroidUiState, rsc: &mut StdRsc) -> Self; #[allow(unused_variables)] - fn back_pressed(&mut self, rsc: &mut AndroidRsc) -> bool { + fn back_pressed(&mut self, rsc: &mut StdRsc) -> bool { false } #[allow(unused_variables)] - fn platform_ready(&mut self, rsc: &mut AndroidRsc, vm: JavaVM, view: GlobalRef) {} + fn platform_ready(&mut self, rsc: &mut StdRsc, vm: JavaVM, view: GlobalRef) {} #[allow(unused_variables)] - fn on_insets_changed(&mut self, rsc: &mut AndroidRsc, insets: WindowInsets) {} + fn on_insets_changed(&mut self, rsc: &mut StdRsc, insets: WindowInsets) {} } /// Widget-facing insets in physical pixels, decoupled from JNI's integer shape. @@ -140,15 +140,13 @@ impl WindowInsets { } } -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 /// Java the same way android-view's own demo does. pub struct IrisViewPeer { - pub(super) rsc: AndroidRsc, + pub(super) rsc: StdRsc, pub(super) state: State, - task_recv: TaskMsgReceiver>, + task_recv: TaskMsgReceiver>, /// Converts input and Choreographer timestamps onto one monotonic clock. device_clock: Option, } @@ -812,7 +810,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 (mut rsc, task_recv) = AppRsc::new(redraw); + let (mut rsc, task_recv) = StdRsc::new(redraw); rsc.ui.set_density(content_scale); let shared = Rc::new(RefCell::new(Shared::default())); let ui_state = AndroidUiState::new(shared.clone(), content_scale); diff --git a/iris/src/desktop/mod.rs b/iris/src/desktop/mod.rs index 62b2808..de6b239 100644 --- a/iris/src/desktop/mod.rs +++ b/iris/src/desktop/mod.rs @@ -50,8 +50,8 @@ pub struct DesktopUiState { pub access: AccessTree, } -impl HasRoot> for DesktopUiState { - fn set_root(&mut self, rsc: &mut DesktopRsc, root: StrongWidget) { +impl HasRoot> for DesktopUiState { + fn set_root(&mut self, rsc: &mut StdRsc, root: StrongWidget) { self.root = Some(crate::overlay::default_overlay_root(rsc, root)); } } @@ -81,25 +81,22 @@ pub trait HasDesktopUiState: Sized + 'static { pub trait DesktopAppState: HasDesktopUiState { type Event = (); - fn new(ui_state: DesktopUiState, rsc: &mut DesktopRsc, proxy: Proxy) - -> Self; + fn new(ui_state: DesktopUiState, rsc: &mut StdRsc, proxy: Proxy) -> Self; #[allow(unused_variables)] - fn event(&mut self, event: Self::Event, rsc: &mut DesktopRsc) {} + fn event(&mut self, event: Self::Event, rsc: &mut StdRsc) {} #[allow(unused_variables)] - fn exit(&mut self, rsc: &mut DesktopRsc) {} + fn exit(&mut self, rsc: &mut StdRsc) {} #[allow(unused_variables)] - fn window_event(&mut self, event: WindowEvent, rsc: &mut DesktopRsc) {} + fn window_event(&mut self, event: WindowEvent, rsc: &mut StdRsc) {} fn window_attributes() -> WindowAttributes { Default::default() } } -pub type DesktopRsc = AppRsc; - pub struct DesktopApp { - rsc: DesktopRsc, + rsc: StdRsc, state: State, - task_recv: TaskMsgReceiver>, + task_recv: TaskMsgReceiver>, } impl AppState for DesktopApp { @@ -118,7 +115,7 @@ impl AppState for DesktopApp { ); window.set_visible(true); let desktop_state = DesktopUiState::new(window, access_adapter); - let (mut rsc, task_recv) = AppRsc::new(desktop_state.window.clone()); + let (mut rsc, task_recv) = StdRsc::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); diff --git a/iris/src/harness.rs b/iris/src/harness.rs index a044c80..a4f2937 100644 --- a/iris/src/harness.rs +++ b/iris/src/harness.rs @@ -134,8 +134,8 @@ impl HarnessState { } } -impl HasRoot for HarnessState { - fn set_root(&mut self, rsc: &mut HarnessRsc, root: StrongWidget) { +impl HasRoot> for HarnessState { + fn set_root(&mut self, rsc: &mut StdRsc, root: StrongWidget) { self.root = Some(crate::overlay::default_overlay_root(rsc, root)); } } @@ -163,14 +163,12 @@ impl OpenUrl for HarnessState { } } -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. pub struct Harness { - pub rsc: HarnessRsc, + pub rsc: StdRsc, pub state: HarnessState, - task_recv: TaskMsgReceiver, + task_recv: TaskMsgReceiver>, redraws: Arc, cursor: CursorState, base: Instant, @@ -185,7 +183,7 @@ impl Harness { /// `PHONE_SCALE`. pub fn new(size: Vec2, density: f32) -> Self { let redraws = Arc::new(RedrawCounter::default()); - let (mut rsc, task_recv) = AppRsc::new(redraws.clone()); + let (mut rsc, task_recv) = StdRsc::new(redraws.clone()); rsc.ui.set_density(density); rsc.ui.resize(size); Self { diff --git a/iris/src/rsc/mod.rs b/iris/src/rsc/mod.rs index 72c83b8..f6442ce 100644 --- a/iris/src/rsc/mod.rs +++ b/iris/src/rsc/mod.rs @@ -1,4 +1,4 @@ -//! Host-independent resources carried by [`AppRsc`]. +//! Host-independent resources carried by [`StdRsc`]. pub mod attr; pub mod event; @@ -20,8 +20,11 @@ pub use task::*; use crate::prelude::*; use std::sync::Arc; -/// Resources shared by every Iris host. -pub struct AppRsc { +/// Iris's standard resource bundle. +/// +/// Built-in hosts use this bundle, but widgets should depend on the narrow +/// resource traits they need so applications can supply a different bundle. +pub struct StdRsc { pub ui: Ui, pub events: EventManager, pub tasks: Tasks, @@ -29,7 +32,7 @@ pub struct AppRsc { _state: std::marker::PhantomData, } -impl AppRsc { +impl StdRsc { pub(crate) fn new(redraw: Arc) -> (Self, TaskMsgReceiver) { let (tasks, receiver) = Tasks::init(redraw); ( @@ -49,7 +52,7 @@ impl AppRsc { } } -impl UiRsc for AppRsc { +impl UiRsc for StdRsc { fn ui(&self) -> &Ui { &self.ui } @@ -72,11 +75,11 @@ impl UiRsc for AppRsc { } } -impl HasState for AppRsc { +impl HasState for StdRsc { type State = State; } -impl HasEvents for AppRsc { +impl HasEvents for StdRsc { fn events(&self) -> &EventManager { &self.events } @@ -86,13 +89,13 @@ impl HasEvents for AppRsc { } } -impl HasTasks for AppRsc { +impl HasTasks for StdRsc { fn tasks_mut(&mut self) -> &mut Tasks { &mut self.tasks } } -impl HasWidgetState for AppRsc { +impl HasWidgetState for StdRsc { fn widget_state(&self) -> &WidgetState { &self.state } @@ -102,7 +105,7 @@ impl HasWidgetState for AppRsc { } } -impl>> std::ops::Index for AppRsc { +impl>> std::ops::Index for StdRsc { type Output = I::Output; fn index(&self, index: I) -> &Self::Output { @@ -110,7 +113,7 @@ impl>> std::ops::Index for AppRsc { } } -impl>> std::ops::IndexMut for AppRsc { +impl>> std::ops::IndexMut for StdRsc { fn index_mut(&mut self, index: I) -> &mut Self::Output { index.get_mut(self) } diff --git a/iris/src/rsc/overlay.rs b/iris/src/rsc/overlay.rs index 8bc5b77..13bae42 100644 --- a/iris/src/rsc/overlay.rs +++ b/iris/src/rsc/overlay.rs @@ -525,13 +525,13 @@ fn open_stackable_at( #[cfg(test)] mod tests { use super::*; - use crate::harness::{Harness, HarnessRsc, TouchAction}; + use crate::harness::{Harness, HarnessState, TouchAction}; use std::{cell::Cell, rc::Rc}; struct EscapeCounter(Rc>); - impl Controller for EscapeCounter { - fn command(&mut self, command: Command, _rsc: &mut HarnessRsc) -> CommandResult { + impl Controller> for EscapeCounter { + fn command(&mut self, command: Command, _rsc: &mut StdRsc) -> CommandResult { if command == Command::Escape { self.0.set(self.0.get() + 1); CommandResult::Used diff --git a/iris/src/rsc/state.rs b/iris/src/rsc/state.rs index d166663..95139a4 100644 --- a/iris/src/rsc/state.rs +++ b/iris/src/rsc/state.rs @@ -78,7 +78,7 @@ impl<'a, T: 'static> FnOnce<(&'a mut WidgetState,)> for WeakState { /// What `Rsc[weak_handle]` indexes through -- one impl per kind of handle /// (a widget, a piece of per-widget state), shared by both backends' `Rsc` /// types since indexing a widget tree has nothing to do with windowing. -/// `AppRsc` supplies the shared `Index`/`IndexMut` implementation; a blanket +/// `StdRsc` supplies the shared `Index`/`IndexMut` implementation; a blanket /// implementation over every possible resource type would conflict. pub trait RscIdx { type Output; diff --git a/iris/tests/color_space.rs b/iris/tests/color_space.rs index ff2b8a7..5869ee5 100644 --- a/iris/tests/color_space.rs +++ b/iris/tests/color_space.rs @@ -1,6 +1,9 @@ #![recursion_limit = "256"] -use iris::{harness::Harness, prelude::*}; +use iris::{ + harness::{Harness, HarnessState}, + prelude::*, +}; use pollster::FutureExt; use std::sync::OnceLock; use wgpu::TextureFormat; @@ -25,7 +28,7 @@ fn solid_paints_and_images_round_trip_through_an_srgb_target() { 1, image::Rgba([IMAGE.r, IMAGE.g, IMAGE.b, IMAGE.a]), )); - let bitmap = image::(bitmap)(&mut harness.rsc); + let bitmap = image::>(bitmap)(&mut harness.rsc); let root = (rect(solid.clone()).sized((1, 1)), bitmap) .span(Dir::RIGHT) .add_strong(&mut harness.rsc)