Use one standard Iris resource bundle

This commit is contained in:
iris committed 2026-09-11 02:09:17 -04:00
1 parent d8bb1699a8
commit 213a0debb7
24 files changed
+177 -186

No files matched your search

+1 -5
View File
@@ -52,11 +52,7 @@ impl DesktopAppState for Client {
)) ))
} }
fn new( fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
mut ui_state: DesktopUiState,
rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let screen = match ai_app::ui::fixture::open(rsc, &mut ui_state) { let screen = match ai_app::ui::fixture::open(rsc, &mut ui_state) {
Ok(opened) => { Ok(opened) => {
if let Some(message) = message_argv() { if let Some(message) = message_argv() {
+1 -5
View File
@@ -158,11 +158,7 @@ fn fold_event(items: Vec<Item>, seq: u64) -> Vec<Item> {
"; ";
impl DesktopAppState for Client { impl DesktopAppState for Client {
fn new( fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
mut ui_state: DesktopUiState,
rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let mut screen = ai_app::ui::build(rsc, &mut ui_state, synthetic_rows()); let mut screen = ai_app::ui::build(rsc, &mut ui_state, synthetic_rows());
screen.push_row( screen.push_row(
rsc, rsc,
+7 -19
View File
@@ -3,7 +3,7 @@ use crate::client::transcript_fold::{TranscriptItem, fold_event};
use crate::ui::{self, TranscriptScreen}; use crate::ui::{self, TranscriptScreen};
use android_view::jni::{JavaVM, objects::GlobalRef}; use android_view::jni::{JavaVM, objects::GlobalRef};
use event_model::SeqEvent; use event_model::SeqEvent;
use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState}; use iris::android::{AndroidAppState, AndroidUiState};
use iris::prelude::*; use iris::prelude::*;
use std::{ use std::{
fs, mem, fs, mem,
@@ -42,6 +42,7 @@ const POLL_MS: u64 = 16;
const REPORT_MAX_HEIGHT_DP: f32 = 260.0; const REPORT_MAX_HEIGHT_DP: f32 = 260.0;
#[derive(AndroidUiState)]
pub struct BenchClient { pub struct BenchClient {
ui_state: AndroidUiState, ui_state: AndroidUiState,
content: WeakWidget<WidgetPtr>, content: WeakWidget<WidgetPtr>,
@@ -65,15 +66,6 @@ struct ImeState {
hidden_events: u32, 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: HasEvents>(rsc: &mut Rsc, message: &str) -> StrongWidget { fn placeholder<Rsc: HasEvents>(rsc: &mut Rsc, message: &str) -> StrongWidget {
wtext(message.to_string()) wtext(message.to_string())
.color(PaintId::WHITE) .color(PaintId::WHITE)
@@ -121,7 +113,7 @@ fn battery_line(samples: &[i32]) -> String {
} }
impl AndroidAppState for BenchClient { impl AndroidAppState for BenchClient {
fn new(mut ui_state: AndroidUiState, rsc: &mut AndroidRsc<Self>) -> Self { fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self {
let content = WidgetPtr::new().add(rsc); let content = WidgetPtr::new().add(rsc);
let loading = placeholder(rsc, "Loading fixture..."); let loading = placeholder(rsc, "Loading fixture...");
content(rsc).set(loading); content(rsc).set(loading);
@@ -195,19 +187,15 @@ impl AndroidAppState for BenchClient {
client client
} }
fn platform_ready(&mut self, _rsc: &mut AndroidRsc<Self>, vm: JavaVM, view: GlobalRef) { fn platform_ready(&mut self, _rsc: &mut StdRsc<Self>, vm: JavaVM, view: GlobalRef) {
self.platform = Some(Arc::new(PlatformHandle::new(vm, view))); self.platform = Some(Arc::new(PlatformHandle::new(vm, view)));
} }
fn back_pressed(&mut self, _rsc: &mut AndroidRsc<Self>) -> bool { fn back_pressed(&mut self, _rsc: &mut StdRsc<Self>) -> bool {
false false
} }
fn on_insets_changed( fn on_insets_changed(&mut self, rsc: &mut StdRsc<Self>, insets: iris::android::WindowInsets) {
&mut self,
rsc: &mut AndroidRsc<Self>,
insets: iris::android::WindowInsets,
) {
if insets.top != self.last_top_pad { if insets.top != self.last_top_pad {
self.last_top_pad = insets.top; self.last_top_pad = insets.top;
let controls = bench_controls(rsc, insets.top); let controls = bench_controls(rsc, insets.top);
@@ -255,7 +243,7 @@ impl AndroidAppState for BenchClient {
const KEYBOARD_DIAGNOSTICS_DELAY_MS: u64 = 500; const KEYBOARD_DIAGNOSTICS_DELAY_MS: u64 = 500;
type Rsc = AndroidRsc<BenchClient>; type Rsc = StdRsc<BenchClient>;
/// What a report says about the `iris::input`/`iris::frame` trace, from /// 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 /// the flag read at the start of what is being reported and again at the
+3 -1
View File
@@ -4,7 +4,9 @@
use android_view::jni::JNIEnv; use android_view::jni::JNIEnv;
use android_view::jni::objects::{JClass, JObject, JString}; use android_view::jni::objects::{JClass, JObject, JString};
use android_view::jni::sys::{jlong, jobjectArray}; 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 /// 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 /// has no ring to lay out, and an ungated constant is a warning in that
+11 -17
View File
@@ -8,12 +8,15 @@ use android_view::{
register_view_class, register_view_class,
}; };
#[cfg(not(feature = "transcript-screen"))] #[cfg(not(feature = "transcript-screen"))]
use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState}; use iris::{
use log::LevelFilter; android::{AndroidAppState, AndroidUiState},
use std::{ macros::AndroidUiState,
ffi::c_void, rsc::StdRsc,
path::{Path, PathBuf},
}; };
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 /// 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 /// 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"; const VIEW_CLASS: &str = "dev/iris/android/demo/IrisView";
#[cfg(not(feature = "transcript-screen"))] #[cfg(not(feature = "transcript-screen"))]
#[derive(AndroidUiState)]
pub struct Client { pub struct Client {
ui_state: AndroidUiState, 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"))] #[cfg(not(feature = "transcript-screen"))]
impl AndroidAppState for Client { impl AndroidAppState for Client {
fn new(mut ui_state: AndroidUiState, rsc: &mut AndroidRsc<Self>) -> Self { fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self {
// `widgets.info` is the winit example's frame-debug readout, kept // `widgets.info` is the winit example's frame-debug readout, kept
// current from `DesktopAppState::window_event` -- android-view has // current from `DesktopAppState::window_event` -- android-view has
// no per-frame hook to drive the equivalent from here yet, so it // no per-frame hook to drive the equivalent from here yet, so it
@@ -60,7 +54,7 @@ impl AndroidAppState for Client {
Self { ui_state } Self { ui_state }
} }
fn back_pressed(&mut self, _rsc: &mut AndroidRsc<Self>) -> bool { fn back_pressed(&mut self, _rsc: &mut StdRsc<Self>) -> bool {
false false
} }
} }
+11 -19
View File
@@ -5,7 +5,7 @@ use crate::client::transcript_fold::{
}; };
use crate::ui::{self, TranscriptScreen}; use crate::ui::{self, TranscriptScreen};
use event_model::SeqEvent; use event_model::SeqEvent;
use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState}; use iris::android::{AndroidAppState, AndroidUiState};
use iris::prelude::*; use iris::prelude::*;
use std::{ use std::{
sync::{ sync::{
@@ -15,6 +15,7 @@ use std::{
thread, thread,
}; };
#[derive(AndroidUiState)]
pub struct TranscriptClient { pub struct TranscriptClient {
ui_state: AndroidUiState, ui_state: AndroidUiState,
/// The screen's own content -- everything under the fixed /// The screen's own content -- everything under the fixed
@@ -29,15 +30,6 @@ pub struct TranscriptClient {
generation: Arc<AtomicU64>, generation: Arc<AtomicU64>,
} }
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<UreqTransport, String> { fn build_transport() -> Result<UreqTransport, String> {
crate::android::enrollment::transport() crate::android::enrollment::transport()
} }
@@ -51,8 +43,8 @@ fn placeholder<Rsc: HasEvents>(rsc: &mut Rsc, message: &str) -> StrongWidget {
.any() .any()
} }
fn frame_report_controls(rsc: &mut AndroidRsc<TranscriptClient>) -> WeakWidget { fn frame_report_controls(rsc: &mut StdRsc<TranscriptClient>) -> WeakWidget {
type Rsc = AndroidRsc<TranscriptClient>; type Rsc = StdRsc<TranscriptClient>;
let report_rect = rect(Srgba8::rgb(50, 50, 60)) let report_rect = rect(Srgba8::rgb(50, 50, 60))
.on( .on(
CursorSense::click(), CursorSense::click(),
@@ -98,7 +90,7 @@ fn frame_report_controls(rsc: &mut AndroidRsc<TranscriptClient>) -> WeakWidget {
} }
impl AndroidAppState for TranscriptClient { impl AndroidAppState for TranscriptClient {
fn new(mut ui_state: AndroidUiState, rsc: &mut AndroidRsc<Self>) -> Self { fn new(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self {
let content = WidgetPtr::new().add(rsc); let content = WidgetPtr::new().add(rsc);
let loading = placeholder(rsc, "Loading sessions..."); let loading = placeholder(rsc, "Loading sessions...");
content(rsc).set(loading); content(rsc).set(loading);
@@ -121,7 +113,7 @@ impl AndroidAppState for TranscriptClient {
client client
} }
fn back_pressed(&mut self, _rsc: &mut AndroidRsc<Self>) -> bool { fn back_pressed(&mut self, _rsc: &mut StdRsc<Self>) -> bool {
// No screen stack of its own -- same "let the activity finish" // No screen stack of its own -- same "let the activity finish"
// answer `iris-android-app`'s tabs `Client` already gives. // answer `iris-android-app`'s tabs `Client` already gives.
false false
@@ -129,13 +121,13 @@ impl AndroidAppState for TranscriptClient {
} }
impl TranscriptClient { impl TranscriptClient {
fn show_message(&mut self, rsc: &mut AndroidRsc<Self>, message: &str) { fn show_message(&mut self, rsc: &mut StdRsc<Self>, message: &str) {
let widget = placeholder(rsc, message); let widget = placeholder(rsc, message);
(self.content)(rsc).set(widget); (self.content)(rsc).set(widget);
self.screen = None; self.screen = None;
} }
fn spawn_fetch_sessions(&mut self, rsc: &mut AndroidRsc<Self>) { fn spawn_fetch_sessions(&mut self, rsc: &mut StdRsc<Self>) {
let redraw = rsc.tasks.redraw_handle(); let redraw = rsc.tasks.redraw_handle();
let my_generation = self.generation.load(Ordering::SeqCst); let my_generation = self.generation.load(Ordering::SeqCst);
let generation = self.generation.clone(); let generation = self.generation.clone();
@@ -164,7 +156,7 @@ impl TranscriptClient {
}); });
} }
fn select_session(&mut self, rsc: &mut AndroidRsc<Self>, session_id: String) { fn select_session(&mut self, rsc: &mut StdRsc<Self>, session_id: String) {
let my_generation = self.generation.fetch_add(1, Ordering::SeqCst) + 1; let my_generation = self.generation.fetch_add(1, Ordering::SeqCst) + 1;
self.items.clear(); self.items.clear();
self.session_id = Some(session_id.clone()); self.session_id = Some(session_id.clone());
@@ -251,7 +243,7 @@ impl TranscriptClient {
}); });
} }
fn rebuild_transcript(&mut self, rsc: &mut AndroidRsc<Self>) { fn rebuild_transcript(&mut self, rsc: &mut StdRsc<Self>) {
let in_progress = self let in_progress = self
.screen .screen
.as_ref() .as_ref()
@@ -279,7 +271,7 @@ impl TranscriptClient {
self.screen = Some(screen); self.screen = Some(screen);
} }
fn apply_event(&mut self, rsc: &mut AndroidRsc<Self>, event: &SeqEvent) { fn apply_event(&mut self, rsc: &mut StdRsc<Self>, event: &SeqEvent) {
let old_items = self.items.clone(); let old_items = self.items.clone();
self.items = fold_event(&self.items, event); self.items = fold_event(&self.items, event);
match self.screen.as_mut() { match self.screen.as_mut() {
+9 -17
View File
@@ -58,11 +58,7 @@ struct Client {
impl DesktopAppState for Client { impl DesktopAppState for Client {
type Event = AppEvent; type Event = AppEvent;
fn new( fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, proxy: Proxy<AppEvent>) -> Self {
mut ui_state: DesktopUiState,
rsc: &mut DesktopRsc<Self>,
proxy: Proxy<AppEvent>,
) -> Self {
let (server, ca_pem) = super::startup::load_startup_config().unwrap_or_else(|e| { let (server, ca_pem) = super::startup::load_startup_config().unwrap_or_else(|e| {
eprintln!("desktop-app: {e}"); eprintln!("desktop-app: {e}");
process::exit(2); process::exit(2);
@@ -107,7 +103,7 @@ impl DesktopAppState for Client {
client client
} }
fn event(&mut self, event: AppEvent, rsc: &mut DesktopRsc<Self>) { fn event(&mut self, event: AppEvent, rsc: &mut StdRsc<Self>) {
match event { match event {
AppEvent::Sessions(Ok(sessions)) => { AppEvent::Sessions(Ok(sessions)) => {
self.sessions = sessions; self.sessions = sessions;
@@ -177,7 +173,7 @@ impl Client {
&& self.generation.load(Ordering::SeqCst) == generation && self.generation.load(Ordering::SeqCst) == generation
} }
fn show_message(&mut self, rsc: &mut DesktopRsc<Self>, message: &str) { fn show_message(&mut self, rsc: &mut StdRsc<Self>, message: &str) {
let widget = placeholder(rsc, message); let widget = placeholder(rsc, message);
(self.transcript_ptr)(rsc).set(widget); (self.transcript_ptr)(rsc).set(widget);
} }
@@ -191,7 +187,7 @@ impl Client {
}); });
} }
fn rebuild_list(&mut self, rsc: &mut DesktopRsc<Self>) { fn rebuild_list(&mut self, rsc: &mut StdRsc<Self>) {
let list = Span::empty(Dir::DOWN).gap(2).add(rsc); let list = Span::empty(Dir::DOWN).gap(2).add(rsc);
for session in &self.sessions { for session in &self.sessions {
let selected = self.selected.as_deref() == Some(session.id.as_str()); let selected = self.selected.as_deref() == Some(session.id.as_str());
@@ -205,7 +201,7 @@ impl Client {
(self.list_ptr)(rsc).set(tree); (self.list_ptr)(rsc).set(tree);
} }
fn select_session(&mut self, rsc: &mut DesktopRsc<Self>, session_id: String) { fn select_session(&mut self, rsc: &mut StdRsc<Self>, session_id: String) {
let generation = self.generation.fetch_add(1, Ordering::SeqCst) + 1; let generation = self.generation.fetch_add(1, Ordering::SeqCst) + 1;
self.selected = Some(session_id.clone()); self.selected = Some(session_id.clone());
self.items.clear(); self.items.clear();
@@ -270,7 +266,7 @@ impl Client {
}); });
} }
fn rebuild_transcript(&mut self, rsc: &mut DesktopRsc<Self>) { fn rebuild_transcript(&mut self, rsc: &mut StdRsc<Self>) {
let in_progress = self let in_progress = self
.screen .screen
.as_ref() .as_ref()
@@ -299,11 +295,7 @@ impl Client {
} }
} }
fn session_row( fn session_row(rsc: &mut StdRsc<Client>, session: &SessionSummary, selected: bool) -> StrongWidget {
rsc: &mut DesktopRsc<Client>,
session: &SessionSummary,
selected: bool,
) -> StrongWidget {
let bg = if selected { let bg = if selected {
Srgba8::rgb(58, 90, 138) Srgba8::rgb(58, 90, 138)
} else { } else {
@@ -319,7 +311,7 @@ fn session_row(
.background(rect(bg)) .background(rect(bg))
.on( .on(
CursorSense::click(), CursorSense::click(),
move |ctx, rsc: &mut DesktopRsc<Client>| { move |ctx, rsc: &mut StdRsc<Client>| {
ctx.state.select_session(rsc, id.clone()); ctx.state.select_session(rsc, id.clone());
}, },
) )
@@ -327,7 +319,7 @@ fn session_row(
.any() .any()
} }
fn placeholder(rsc: &mut DesktopRsc<Client>, message: &str) -> StrongWidget { fn placeholder(rsc: &mut StdRsc<Client>, message: &str) -> StrongWidget {
wtext(message.to_string()) wtext(message.to_string())
.color(PaintId::WHITE) .color(PaintId::WHITE)
.wrap(true) .wrap(true)
+5 -3
View File
@@ -13,10 +13,12 @@ open work and measured constraints that are expensive to rediscover.
Platform services are exposed to shared UI through small traits. Platform services are exposed to shared UI through small traits.
- `iris/` is only a UI framework. Session, transcript, server, setup, and - `iris/` is only a UI framework. Session, transcript, server, setup, and
product styling concepts never belong there. 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 attributes, events, overlays, pointer sensing/input, tasks, and per-widget
state. `widget/` is reserved for concrete retained widgets. Public paths state. It is the standard bundle used by Iris's hosts, not a requirement;
such as `iris::sense` remain aliases for readability. 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, - Phone and desktop layouts may differ. Their component implementations,
colors, spacing, text styles, gestures, folding, and network behavior do colors, spacing, text styles, gestures, folding, and network behavior do
not. not.
+4 -8
View File
@@ -13,15 +13,11 @@ struct State {
} }
impl DesktopAppState for State { impl DesktopAppState for State {
fn new( fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
mut ui_state: DesktopUiState,
rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>,
) -> 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::<DesktopRsc<Self>>(img)(rsc); let widget = image::<StdRsc<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 +36,7 @@ impl DesktopAppState for State {
} }
} }
fn window_event(&mut self, event: winit::event::WindowEvent, rsc: &mut DesktopRsc<Self>) { fn window_event(&mut self, event: winit::event::WindowEvent, rsc: &mut StdRsc<Self>) {
if !matches!(event, winit::event::WindowEvent::RedrawRequested) { if !matches!(event, winit::event::WindowEvent::RedrawRequested) {
return; return;
} }
@@ -53,7 +49,7 @@ impl DesktopAppState 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::<DesktopRsc<Self>>(img)(rsc); let widget = image::<StdRsc<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
+1 -5
View File
@@ -63,11 +63,7 @@ impl DesktopAppState for State {
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: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
mut ui_state: DesktopUiState,
rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let mut list = LazySpan::new(Dir::DOWN, Pin::End); let mut list = LazySpan::new(Dir::DOWN, Pin::End);
for i in 0..ROWS { for i in 0..ROWS {
let row = build_row(rsc, i); let row = build_row(rsc, i);
+1 -5
View File
@@ -10,11 +10,7 @@ struct State {
} }
impl DesktopAppState for State { impl DesktopAppState for State {
fn new( fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
mut ui_state: DesktopUiState,
rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
rect(PaintId::RED).set_root(rsc, &mut ui_state); rect(PaintId::RED).set_root(rsc, &mut ui_state);
Self { ui_state } Self { ui_state }
} }
+2 -6
View File
@@ -12,11 +12,7 @@ pub struct Client {
} }
impl DesktopAppState for Client { impl DesktopAppState for Client {
fn new( fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
mut ui_state: DesktopUiState,
rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let widgets = tabs_ui::build(rsc, &mut ui_state); let widgets = tabs_ui::build(rsc, &mut ui_state);
Self { Self {
ui_state, ui_state,
@@ -24,7 +20,7 @@ impl DesktopAppState for Client {
} }
} }
fn window_event(&mut self, _: WindowEvent, rsc: &mut DesktopRsc<Self>) { fn window_event(&mut self, _: WindowEvent, rsc: &mut StdRsc<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: {}",
+1 -5
View File
@@ -11,11 +11,7 @@ struct State {
} }
impl DesktopAppState for State { impl DesktopAppState for State {
fn new( fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
mut ui_state: DesktopUiState,
rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let rect = rect(PaintId::RED).add(rsc); let rect = rect(PaintId::RED).add(rsc);
rect.task_on(CursorSense::click(), async move |mut ctx| { rect.task_on(CursorSense::click(), async move |mut ctx| {
tokio::time::sleep(Duration::from_secs(1)).await; tokio::time::sleep(Duration::from_secs(1)).await;
+2 -6
View File
@@ -9,7 +9,7 @@ struct State {
ui_state: DesktopUiState, ui_state: DesktopUiState,
} }
type Rsc = DesktopRsc<State>; type Rsc = StdRsc<State>;
#[derive(Clone, Copy, WidgetView)] #[derive(Clone, Copy, WidgetView)]
struct Test { struct Test {
@@ -36,11 +36,7 @@ impl Test {
} }
impl DesktopAppState for State { impl DesktopAppState for State {
fn new( fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
mut ui_state: DesktopUiState,
rsc: &mut DesktopRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
let test = Test::new(rsc); let test = Test::new(rsc);
test.on(CursorSense::click(), move |_, rsc| { test.on(CursorSense::click(), move |_, rsc| {
+69 -14
View File
@@ -103,31 +103,72 @@ pub fn widget_trait(input: TokenStream) -> TokenStream {
#[proc_macro_derive(DesktopUiState, attributes(desktop_ui_state))] #[proc_macro_derive(DesktopUiState, attributes(desktop_ui_state))]
pub fn derive_desktop_ui_state(input: TokenStream) -> TokenStream { pub fn derive_desktop_ui_state(input: TokenStream) -> TokenStream {
let mut output = proc_macro2::TokenStream::new();
let state: ItemStruct = parse_macro_input!(input); 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 found_attr = false;
let mut state_field = None; let mut state_field = None;
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("DesktopUiState") && path.path.is_ident(state_type)
{ {
state_field = Some(field); state_field = Some(field);
} }
let Some(attr) = field let Some(attr) = field.attrs.iter().find(|a| a.path().is_ident(field_attr)) else {
.attrs
.iter()
.find(|a| a.path().is_ident("desktop_ui_state"))
else {
continue; continue;
}; };
if found_attr { if found_attr {
output.extend( output.extend(
Error::new( Error::new(
attr.span(), attr.span(),
"cannot have more than one desktop_ui_state attribute", format!("cannot have more than one {field_attr} attribute"),
) )
.into_compile_error(), .into_compile_error(),
); );
@@ -138,18 +179,32 @@ pub fn derive_desktop_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 DesktopUiState field found").into_compile_error(), Error::new(state.ident.span(), format!("no {state_type} 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 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! { output.extend(quote! {
impl iris::desktop::HasDesktopUiState for #sname { impl #impl_generics iris::#module::#state_trait for #sname #type_generics #where_clause {
fn desktop_state(&self) -> &iris::desktop::DesktopUiState { fn #get(&self) -> &iris::#module::#state_type {
&self.#fname &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 &mut self.#fname
} }
} }
+2 -2
View File
@@ -1,7 +1,7 @@
use crate::prelude::*; use crate::prelude::*;
use android_view::{jni::JNIEnv, ndk::event::Keycode}; 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 /// Hardware/synthesized key handling for the field that currently has
/// focus. Most typing on Android goes through the IME's `InputConnection` /// 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` /// the arrow keys on a physical keyboard) plus whatever `unicode_char`
/// reports for a plain key press. Returns whether anything used the event. /// reports for a plain key press. Returns whether anything used the event.
pub(super) fn on_key<'local, State: AndroidAppState>( pub(super) fn on_key<'local, State: AndroidAppState>(
rsc: &mut AndroidRsc<State>, rsc: &mut StdRsc<State>,
state: &mut State, state: &mut State,
env: &mut JNIEnv<'local>, env: &mut JNIEnv<'local>,
key_code: Keycode, key_code: Keycode,
+1 -2
View File
@@ -10,8 +10,7 @@ mod view;
pub use insets::Insets; pub use insets::Insets;
pub use render::AndroidRenderer; pub use render::AndroidRenderer;
pub use view::{ pub use view::{
AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState, IrisViewPeer, WindowInsets, AndroidAppState, AndroidUiState, HasAndroidUiState, IrisViewPeer, WindowInsets, new_peer,
new_peer,
}; };
/// Registers the extra native methods this backend needs beyond what /// Registers the extra native methods this backend needs beyond what
+9 -11
View File
@@ -89,8 +89,8 @@ impl AndroidUiState {
} }
} }
impl<State: 'static> HasRoot<AndroidRsc<State>> for AndroidUiState { impl<State: 'static> HasRoot<StdRsc<State>> for AndroidUiState {
fn set_root(&mut self, rsc: &mut AndroidRsc<State>, root: StrongWidget) { fn set_root(&mut self, rsc: &mut StdRsc<State>, root: StrongWidget) {
self.root = Some(crate::overlay::default_overlay_root(rsc, root)); self.root = Some(crate::overlay::default_overlay_root(rsc, root));
} }
} }
@@ -101,15 +101,15 @@ pub trait HasAndroidUiState: Sized + 'static {
} }
pub trait AndroidAppState: HasAndroidUiState { pub trait AndroidAppState: HasAndroidUiState {
fn new(ui_state: AndroidUiState, rsc: &mut AndroidRsc<Self>) -> Self; fn new(ui_state: AndroidUiState, rsc: &mut StdRsc<Self>) -> Self;
#[allow(unused_variables)] #[allow(unused_variables)]
fn back_pressed(&mut self, rsc: &mut AndroidRsc<Self>) -> bool { fn back_pressed(&mut self, rsc: &mut StdRsc<Self>) -> bool {
false false
} }
#[allow(unused_variables)] #[allow(unused_variables)]
fn platform_ready(&mut self, rsc: &mut AndroidRsc<Self>, vm: JavaVM, view: GlobalRef) {} fn platform_ready(&mut self, rsc: &mut StdRsc<Self>, vm: JavaVM, view: GlobalRef) {}
#[allow(unused_variables)] #[allow(unused_variables)]
fn on_insets_changed(&mut self, rsc: &mut AndroidRsc<Self>, insets: WindowInsets) {} fn on_insets_changed(&mut self, rsc: &mut StdRsc<Self>, insets: WindowInsets) {}
} }
/// Widget-facing insets in physical pixels, decoupled from JNI's integer shape. /// Widget-facing insets in physical pixels, decoupled from JNI's integer shape.
@@ -140,15 +140,13 @@ impl WindowInsets {
} }
} }
pub type AndroidRsc<State> = AppRsc<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
/// Java the same way android-view's own demo does. /// Java the same way android-view's own demo does.
pub struct IrisViewPeer<State: AndroidAppState> { pub struct IrisViewPeer<State: AndroidAppState> {
pub(super) rsc: AndroidRsc<State>, pub(super) rsc: StdRsc<State>,
pub(super) state: State, pub(super) state: State,
task_recv: TaskMsgReceiver<AndroidRsc<State>>, task_recv: TaskMsgReceiver<StdRsc<State>>,
/// Converts input and Choreographer timestamps onto one monotonic clock. /// Converts input and Choreographer timestamps onto one monotonic clock.
device_clock: Option<DeviceClock>, device_clock: Option<DeviceClock>,
} }
@@ -812,7 +810,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 (mut rsc, task_recv) = AppRsc::new(redraw); let (mut rsc, task_recv) = StdRsc::new(redraw);
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);
+9 -12
View File
@@ -50,8 +50,8 @@ pub struct DesktopUiState {
pub access: AccessTree, pub access: AccessTree,
} }
impl<State: 'static> HasRoot<DesktopRsc<State>> for DesktopUiState { impl<State: 'static> HasRoot<StdRsc<State>> for DesktopUiState {
fn set_root(&mut self, rsc: &mut DesktopRsc<State>, root: StrongWidget) { fn set_root(&mut self, rsc: &mut StdRsc<State>, root: StrongWidget) {
self.root = Some(crate::overlay::default_overlay_root(rsc, root)); self.root = Some(crate::overlay::default_overlay_root(rsc, root));
} }
} }
@@ -81,25 +81,22 @@ pub trait HasDesktopUiState: Sized + 'static {
pub trait DesktopAppState: HasDesktopUiState { pub trait DesktopAppState: HasDesktopUiState {
type Event = (); type Event = ();
fn new(ui_state: DesktopUiState, rsc: &mut DesktopRsc<Self>, proxy: Proxy<Self::Event>) fn new(ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, proxy: Proxy<Self::Event>) -> Self;
-> Self;
#[allow(unused_variables)] #[allow(unused_variables)]
fn event(&mut self, event: Self::Event, rsc: &mut DesktopRsc<Self>) {} fn event(&mut self, event: Self::Event, rsc: &mut StdRsc<Self>) {}
#[allow(unused_variables)] #[allow(unused_variables)]
fn exit(&mut self, rsc: &mut DesktopRsc<Self>) {} fn exit(&mut self, rsc: &mut StdRsc<Self>) {}
#[allow(unused_variables)] #[allow(unused_variables)]
fn window_event(&mut self, event: WindowEvent, rsc: &mut DesktopRsc<Self>) {} fn window_event(&mut self, event: WindowEvent, rsc: &mut StdRsc<Self>) {}
fn window_attributes() -> WindowAttributes { fn window_attributes() -> WindowAttributes {
Default::default() Default::default()
} }
} }
pub type DesktopRsc<State> = AppRsc<State>;
pub struct DesktopApp<State: DesktopAppState> { pub struct DesktopApp<State: DesktopAppState> {
rsc: DesktopRsc<State>, rsc: StdRsc<State>,
state: State, state: State,
task_recv: TaskMsgReceiver<DesktopRsc<State>>, task_recv: TaskMsgReceiver<StdRsc<State>>,
} }
impl<State: DesktopAppState> AppState for DesktopApp<State> { impl<State: DesktopAppState> AppState for DesktopApp<State> {
@@ -118,7 +115,7 @@ impl<State: DesktopAppState> AppState for DesktopApp<State> {
); );
window.set_visible(true); window.set_visible(true);
let desktop_state = DesktopUiState::new(window, access_adapter); 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. // Set before building widgets so the first text shape uses the right density.
let scale = content_scale(desktop_state.window.as_ref()); let scale = content_scale(desktop_state.window.as_ref());
rsc.ui.set_density(scale); rsc.ui.set_density(scale);
+5 -7
View File
@@ -134,8 +134,8 @@ impl HarnessState {
} }
} }
impl HasRoot<HarnessRsc> for HarnessState { impl HasRoot<StdRsc<HarnessState>> for HarnessState {
fn set_root(&mut self, rsc: &mut HarnessRsc, root: StrongWidget) { fn set_root(&mut self, rsc: &mut StdRsc<HarnessState>, root: StrongWidget) {
self.root = Some(crate::overlay::default_overlay_root(rsc, root)); self.root = Some(crate::overlay::default_overlay_root(rsc, root));
} }
} }
@@ -163,14 +163,12 @@ impl OpenUrl for HarnessState {
} }
} }
pub type HarnessRsc = AppRsc<HarnessState>;
/// 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.
pub struct Harness { pub struct Harness {
pub rsc: HarnessRsc, pub rsc: StdRsc<HarnessState>,
pub state: HarnessState, pub state: HarnessState,
task_recv: TaskMsgReceiver<HarnessRsc>, task_recv: TaskMsgReceiver<StdRsc<HarnessState>>,
redraws: Arc<RedrawCounter>, redraws: Arc<RedrawCounter>,
cursor: CursorState, cursor: CursorState,
base: Instant, base: Instant,
@@ -185,7 +183,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 (mut rsc, task_recv) = AppRsc::new(redraws.clone()); let (mut rsc, task_recv) = StdRsc::new(redraws.clone());
rsc.ui.set_density(density); rsc.ui.set_density(density);
rsc.ui.resize(size); rsc.ui.resize(size);
Self { Self {
+14 -11
View File
@@ -1,4 +1,4 @@
//! Host-independent resources carried by [`AppRsc`]. //! Host-independent resources carried by [`StdRsc`].
pub mod attr; pub mod attr;
pub mod event; pub mod event;
@@ -20,8 +20,11 @@ pub use task::*;
use crate::prelude::*; use crate::prelude::*;
use std::sync::Arc; use std::sync::Arc;
/// Resources shared by every Iris host. /// Iris's standard resource bundle.
pub struct AppRsc<State: 'static> { ///
/// 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<State: 'static> {
pub ui: Ui, pub ui: Ui,
pub events: EventManager<Self>, pub events: EventManager<Self>,
pub tasks: Tasks<Self>, pub tasks: Tasks<Self>,
@@ -29,7 +32,7 @@ pub struct AppRsc<State: 'static> {
_state: std::marker::PhantomData<State>, _state: std::marker::PhantomData<State>,
} }
impl<State> AppRsc<State> { impl<State> StdRsc<State> {
pub(crate) fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>) { pub(crate) fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>) {
let (tasks, receiver) = Tasks::init(redraw); let (tasks, receiver) = Tasks::init(redraw);
( (
@@ -49,7 +52,7 @@ impl<State> AppRsc<State> {
} }
} }
impl<State> UiRsc for AppRsc<State> { impl<State> UiRsc for StdRsc<State> {
fn ui(&self) -> &Ui { fn ui(&self) -> &Ui {
&self.ui &self.ui
} }
@@ -72,11 +75,11 @@ impl<State> UiRsc for AppRsc<State> {
} }
} }
impl<State> HasState for AppRsc<State> { impl<State> HasState for StdRsc<State> {
type State = State; type State = State;
} }
impl<State> HasEvents for AppRsc<State> { impl<State> HasEvents for StdRsc<State> {
fn events(&self) -> &EventManager<Self> { fn events(&self) -> &EventManager<Self> {
&self.events &self.events
} }
@@ -86,13 +89,13 @@ impl<State> HasEvents for AppRsc<State> {
} }
} }
impl<State> HasTasks for AppRsc<State> { impl<State> HasTasks for StdRsc<State> {
fn tasks_mut(&mut self) -> &mut Tasks<Self> { fn tasks_mut(&mut self) -> &mut Tasks<Self> {
&mut self.tasks &mut self.tasks
} }
} }
impl<State> HasWidgetState for AppRsc<State> { impl<State> HasWidgetState for StdRsc<State> {
fn widget_state(&self) -> &WidgetState { fn widget_state(&self) -> &WidgetState {
&self.state &self.state
} }
@@ -102,7 +105,7 @@ impl<State> HasWidgetState for AppRsc<State> {
} }
} }
impl<State, I: RscIdx<AppRsc<State>>> std::ops::Index<I> for AppRsc<State> { impl<State, I: RscIdx<StdRsc<State>>> std::ops::Index<I> for StdRsc<State> {
type Output = I::Output; type Output = I::Output;
fn index(&self, index: I) -> &Self::Output { fn index(&self, index: I) -> &Self::Output {
@@ -110,7 +113,7 @@ impl<State, I: RscIdx<AppRsc<State>>> std::ops::Index<I> for AppRsc<State> {
} }
} }
impl<State, I: RscIdx<AppRsc<State>>> std::ops::IndexMut<I> for AppRsc<State> { impl<State, I: RscIdx<StdRsc<State>>> std::ops::IndexMut<I> for StdRsc<State> {
fn index_mut(&mut self, index: I) -> &mut Self::Output { fn index_mut(&mut self, index: I) -> &mut Self::Output {
index.get_mut(self) index.get_mut(self)
} }
+3 -3
View File
@@ -525,13 +525,13 @@ fn open_stackable_at<Rsc: HasEvents>(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::harness::{Harness, HarnessRsc, TouchAction}; use crate::harness::{Harness, HarnessState, TouchAction};
use std::{cell::Cell, rc::Rc}; use std::{cell::Cell, rc::Rc};
struct EscapeCounter(Rc<Cell<usize>>); struct EscapeCounter(Rc<Cell<usize>>);
impl Controller<HarnessRsc> for EscapeCounter { impl Controller<StdRsc<HarnessState>> for EscapeCounter {
fn command(&mut self, command: Command, _rsc: &mut HarnessRsc) -> CommandResult { fn command(&mut self, command: Command, _rsc: &mut StdRsc<HarnessState>) -> CommandResult {
if command == Command::Escape { if command == Command::Escape {
self.0.set(self.0.get() + 1); self.0.set(self.0.get() + 1);
CommandResult::Used CommandResult::Used
+1 -1
View File
@@ -78,7 +78,7 @@ impl<'a, T: 'static> FnOnce<(&'a mut WidgetState,)> for WeakState<T> {
/// What `Rsc[weak_handle]` indexes through -- one impl per kind of handle /// 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` /// (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. /// 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. /// implementation over every possible resource type would conflict.
pub trait RscIdx<Rsc> { pub trait RscIdx<Rsc> {
type Output; type Output;
+5 -2
View File
@@ -1,6 +1,9 @@
#![recursion_limit = "256"] #![recursion_limit = "256"]
use iris::{harness::Harness, prelude::*}; use iris::{
harness::{Harness, HarnessState},
prelude::*,
};
use pollster::FutureExt; use pollster::FutureExt;
use std::sync::OnceLock; use std::sync::OnceLock;
use wgpu::TextureFormat; use wgpu::TextureFormat;
@@ -25,7 +28,7 @@ fn solid_paints_and_images_round_trip_through_an_srgb_target() {
1, 1,
image::Rgba([IMAGE.r, IMAGE.g, IMAGE.b, IMAGE.a]), image::Rgba([IMAGE.r, IMAGE.g, IMAGE.b, IMAGE.a]),
)); ));
let bitmap = image::<iris::harness::HarnessRsc>(bitmap)(&mut harness.rsc); let bitmap = image::<StdRsc<HarnessState>>(bitmap)(&mut harness.rsc);
let root = (rect(solid.clone()).sized((1, 1)), bitmap) let root = (rect(solid.clone()).sized((1, 1)), bitmap)
.span(Dir::RIGHT) .span(Dir::RIGHT)
.add_strong(&mut harness.rsc) .add_strong(&mut harness.rsc)