Simplify Iris app initialization and task updates

This commit is contained in:
iris committed 2026-09-11 12:28:33 -04:00
1 parent 8218e84b62
commit ecf74055c7
32 files changed
+391 -359

No files matched your search

+1 -3
View File
@@ -52,7 +52,7 @@ impl DesktopAppState for Client {
)) ))
} }
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self { fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>) -> 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() {
@@ -60,7 +60,6 @@ impl DesktopAppState for Client {
} }
if let Some(text) = typed_argv() { if let Some(text) = typed_argv() {
let field = opened.screen.composer.field; let field = opened.screen.composer.field;
let redraw = rsc.tasks.redraw_handle();
rsc.spawn_task(async move |mut ctx| { rsc.spawn_task(async move |mut ctx| {
for ch in text.chars() { for ch in text.chars() {
tokio::time::sleep(std::time::Duration::from_millis(100)).await; tokio::time::sleep(std::time::Duration::from_millis(100)).await;
@@ -73,7 +72,6 @@ impl DesktopAppState for Client {
} }
edit.insert(&ch.to_string()); edit.insert(&ch.to_string());
}); });
redraw.request_redraw();
} }
}); });
} }
+1 -1
View File
@@ -158,7 +158,7 @@ fn fold_event(items: Vec<Item>, seq: u64) -> Vec<Item> {
"; ";
impl DesktopAppState for Client { impl DesktopAppState for Client {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self { fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>) -> 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,
+17 -54
View File
@@ -235,13 +235,11 @@ impl AndroidAppState for BenchClient {
if ime_visible && !self.keyboard_was_visible { if ime_visible && !self.keyboard_was_visible {
self.keyboard_was_visible = true; self.keyboard_was_visible = true;
let redraw = rsc.tasks.redraw_handle();
rsc.spawn_task(async move |mut ctx| { rsc.spawn_task(async move |mut ctx| {
tokio::time::sleep(Duration::from_millis(KEYBOARD_DIAGNOSTICS_DELAY_MS)).await; tokio::time::sleep(Duration::from_millis(KEYBOARD_DIAGNOSTICS_DELAY_MS)).await;
ctx.update(|state: &mut BenchClient, rsc| { ctx.update(|state: &mut BenchClient, rsc| {
state.capture_keyboard_diagnostics(rsc); state.capture_keyboard_diagnostics(rsc);
}); });
redraw.request_redraw();
}); });
} else if !ime_visible { } else if !ime_visible {
self.keyboard_was_visible = false; self.keyboard_was_visible = false;
@@ -485,7 +483,6 @@ impl BenchClient {
self.android_state_mut().frame_report.reset(); self.android_state_mut().frame_report.reset();
self.report_display.edit(rsc).set("Running benchmark..."); self.report_display.edit(rsc).set("Running benchmark...");
let redraw = rsc.tasks.redraw_handle();
let platform = self.platform.clone(); let platform = self.platform.clone();
let stream_tail = self.stream_tail.clone(); let stream_tail = self.stream_tail.clone();
let ime_state = self.ime_state.clone(); let ime_state = self.ime_state.clone();
@@ -518,9 +515,9 @@ impl BenchClient {
}) })
}); });
let travel = run_fling_phase(&mut ctx, &redraw).await; let travel = run_fling_phase(&mut ctx).await;
let (sent, total) = run_stream_phase(&mut ctx, &redraw, stream_tail).await; let (sent, total) = run_stream_phase(&mut ctx, stream_tail).await;
run_type_phase(&mut ctx, &redraw, &platform).await; run_type_phase(&mut ctx, &platform).await;
let keyboard = run_keyboard_phase(&mut ctx, &platform, &ime_state).await; let keyboard = run_keyboard_phase(&mut ctx, &platform, &ime_state).await;
sampler_done.store(true, Ordering::Relaxed); sampler_done.store(true, Ordering::Relaxed);
@@ -626,32 +623,18 @@ impl BenchClient {
state.report_display.edit(rsc).set(&report); state.report_display.edit(rsc).set(&report);
state.last_report = Some(report); state.last_report = Some(report);
}); });
redraw.request_redraw();
}); });
} }
} }
/// Runs `f` against the real `BenchClient`/`Rsc` on the main thread (the /// Runs `f` against the real `BenchClient`/`Rsc` on the main thread (the
/// same `ctx.update` every other mutation here goes through) and returns /// same `ctx.update` every other mutation here goes through) and returns
/// its result to the caller's async task -- `ctx.update` alone has no way /// its result to the caller's async task. `ctx.update` wakes the UI thread,
/// to hand a value back, since the closure only actually runs once the /// whose task callback drains Iris's update queue before checking whether the
/// next frame callback drains `IrisViewPeer`'s task channel /// retained widget tree needs another frame.
/// (`drain_tasks`). **Must call `redraw.request_redraw()` itself, right
/// after enqueueing** -- `ctx.update` only ever pushes onto a channel;
/// nothing drains it until something schedules the frame callback that
/// calls `drain_tasks`, and a caller relying on some *earlier*,
/// already-in-flight `request_redraw()` to cover a *later* `ctx.update`
/// deadlocks the moment that earlier callback has already fired and
/// drained everything queued before this call existed. Cost a real hang
/// in this file's first version of the fling phase: every loop iteration
/// after the first sat forever with nothing scheduled to drain it.
/// Polls rather than assuming one `POLL_MS` sleep is enough, since a /// Polls rather than assuming one `POLL_MS` sleep is enough, since a
/// slow device's frame callback can lag further than that. /// slow device's frame callback can lag further than that.
async fn read_from_state<T, F>( async fn read_from_state<T, F>(ctx: &mut iris::task::TaskCtx<Rsc>, f: F) -> T
ctx: &mut iris::task::TaskCtx<Rsc>,
redraw: &Arc<dyn RequestRedraw>,
f: F,
) -> T
where where
T: Send + 'static, T: Send + 'static,
F: FnOnce(&mut BenchClient, &mut Rsc) -> T + Send + 'static, F: FnOnce(&mut BenchClient, &mut Rsc) -> T + Send + 'static,
@@ -660,7 +643,6 @@ where
ctx.update(move |state: &mut BenchClient, rsc| { ctx.update(move |state: &mut BenchClient, rsc| {
let _ = tx.send(f(state, rsc)); let _ = tx.send(f(state, rsc));
}); });
redraw.request_redraw();
loop { loop {
if let Ok(value) = rx.try_recv() { if let Ok(value) = rx.try_recv() {
return value; return value;
@@ -669,10 +651,7 @@ where
} }
} }
async fn run_fling_phase( async fn run_fling_phase(ctx: &mut iris::task::TaskCtx<Rsc>) -> String {
ctx: &mut iris::task::TaskCtx<Rsc>,
redraw: &Arc<dyn RequestRedraw>,
) -> String {
ctx.update(|state: &mut BenchClient, _rsc| { ctx.update(|state: &mut BenchClient, _rsc| {
state.android_state_mut().frame_report.mark_phase("fling"); state.android_state_mut().frame_report.mark_phase("fling");
}); });
@@ -681,11 +660,10 @@ async fn run_fling_phase(
(screen.list)(rsc).jump_to_end(); (screen.list)(rsc).jump_to_end();
} }
}); });
redraw.request_redraw();
// Lets the next frame's `repair_anchor` resolve `jump_to_end`'s // Lets the next frame's `repair_anchor` resolve `jump_to_end`'s
// `anchor = None` into a real slot before `start` is read. // `anchor = None` into a real slot before `start` is read.
tokio::time::sleep(Duration::from_millis(POLL_MS * 2)).await; tokio::time::sleep(Duration::from_millis(POLL_MS * 2)).await;
let start = read_anchor_position(ctx, redraw).await; let start = read_anchor_position(ctx).await;
for _ in 0..FLING_COUNT { for _ in 0..FLING_COUNT {
ctx.update(|state: &mut BenchClient, rsc| { ctx.update(|state: &mut BenchClient, rsc| {
@@ -694,11 +672,10 @@ async fn run_fling_phase(
animate_scroll(screen.list, rsc); animate_scroll(screen.list, rsc);
} }
}); });
redraw.request_redraw(); wait_for_fling_settle(ctx).await;
wait_for_fling_settle(ctx, redraw).await;
tokio::time::sleep(Duration::from_millis(FLING_PAUSE_MS)).await; tokio::time::sleep(Duration::from_millis(FLING_PAUSE_MS)).await;
} }
let outward = read_anchor_position(ctx, redraw).await; let outward = read_anchor_position(ctx).await;
for _ in 0..FLING_COUNT { for _ in 0..FLING_COUNT {
ctx.update(|state: &mut BenchClient, rsc| { ctx.update(|state: &mut BenchClient, rsc| {
@@ -707,20 +684,16 @@ async fn run_fling_phase(
animate_scroll(screen.list, rsc); animate_scroll(screen.list, rsc);
} }
}); });
redraw.request_redraw(); wait_for_fling_settle(ctx).await;
wait_for_fling_settle(ctx, redraw).await;
tokio::time::sleep(Duration::from_millis(FLING_PAUSE_MS)).await; tokio::time::sleep(Duration::from_millis(FLING_PAUSE_MS)).await;
} }
let end = read_anchor_position(ctx, redraw).await; let end = read_anchor_position(ctx).await;
format!("start={start} outward={outward} end={end} ticked=frame-loop") format!("start={start} outward={outward} end={end} ticked=frame-loop")
} }
async fn read_anchor_position( async fn read_anchor_position(ctx: &mut iris::task::TaskCtx<Rsc>) -> String {
ctx: &mut iris::task::TaskCtx<Rsc>, read_from_state(ctx, |state, rsc| match &state.screen {
redraw: &Arc<dyn RequestRedraw>,
) -> String {
read_from_state(ctx, redraw, |state, rsc| match &state.screen {
Some(screen) => (screen.list)(rsc).anchor_position_display(), Some(screen) => (screen.list)(rsc).anchor_position_display(),
None => "idx=none".to_string(), None => "idx=none".to_string(),
}) })
@@ -732,14 +705,11 @@ fn animate_scroll(scroll: iris::prelude::WeakWidget<iris::prelude::LazySpan>, rs
rsc.ui_mut().animate(id); rsc.ui_mut().animate(id);
} }
async fn wait_for_fling_settle( async fn wait_for_fling_settle(ctx: &mut iris::task::TaskCtx<Rsc>) {
ctx: &mut iris::task::TaskCtx<Rsc>,
redraw: &Arc<dyn RequestRedraw>,
) {
let cap = Duration::from_millis(FLING_SETTLE_CAP_MS); let cap = Duration::from_millis(FLING_SETTLE_CAP_MS);
let started = Instant::now(); let started = Instant::now();
while started.elapsed() < cap { while started.elapsed() < cap {
let still_scrolling = read_from_state(ctx, redraw, |state, rsc| match &state.screen { let still_scrolling = read_from_state(ctx, |state, rsc| match &state.screen {
Some(screen) => (screen.list)(rsc).is_scrolling(), Some(screen) => (screen.list)(rsc).is_scrolling(),
None => false, None => false,
}) })
@@ -753,7 +723,6 @@ async fn wait_for_fling_settle(
async fn run_stream_phase( async fn run_stream_phase(
ctx: &mut iris::task::TaskCtx<Rsc>, ctx: &mut iris::task::TaskCtx<Rsc>,
redraw: &Arc<dyn RequestRedraw>,
stream_tail: Vec<SeqEvent>, stream_tail: Vec<SeqEvent>,
) -> (usize, usize) { ) -> (usize, usize) {
ctx.update(|state: &mut BenchClient, _rsc| { ctx.update(|state: &mut BenchClient, _rsc| {
@@ -764,7 +733,6 @@ async fn run_stream_phase(
(screen.list)(rsc).jump_to_end(); (screen.list)(rsc).jump_to_end();
} }
}); });
redraw.request_redraw();
let total = (STREAM_EVENTS_PER_SEC * STREAM_SECONDS) as usize; let total = (STREAM_EVENTS_PER_SEC * STREAM_SECONDS) as usize;
let mut sent = 0usize; let mut sent = 0usize;
@@ -777,7 +745,6 @@ async fn run_stream_phase(
None => state.rebuild_transcript(rsc), None => state.rebuild_transcript(rsc),
} }
}); });
redraw.request_redraw();
sent += 1; sent += 1;
tokio::time::sleep(Duration::from_millis(1000 / STREAM_EVENTS_PER_SEC)).await; tokio::time::sleep(Duration::from_millis(1000 / STREAM_EVENTS_PER_SEC)).await;
} }
@@ -787,7 +754,6 @@ async fn run_stream_phase(
async fn run_type_phase( async fn run_type_phase(
ctx: &mut iris::task::TaskCtx<Rsc>, ctx: &mut iris::task::TaskCtx<Rsc>,
redraw: &Arc<dyn RequestRedraw>,
platform: &Option<Arc<PlatformHandle>>, platform: &Option<Arc<PlatformHandle>>,
) { ) {
ctx.update(|state: &mut BenchClient, _rsc| { ctx.update(|state: &mut BenchClient, _rsc| {
@@ -799,7 +765,6 @@ async fn run_type_phase(
state.set_focus(Some(screen.composer.field)); state.set_focus(Some(screen.composer.field));
} }
}); });
redraw.request_redraw();
if let Some(p) = platform { if let Some(p) = platform {
p.show_ime(); p.show_ime();
} }
@@ -814,7 +779,6 @@ async fn run_type_phase(
screen.composer.field.edit(rsc).set(&text); screen.composer.field.edit(rsc).set(&text);
} }
}); });
redraw.request_redraw();
tokio::time::sleep(Duration::from_millis(TYPE_CHAR_MS)).await; tokio::time::sleep(Duration::from_millis(TYPE_CHAR_MS)).await;
} }
tokio::time::sleep(Duration::from_millis(200)).await; tokio::time::sleep(Duration::from_millis(200)).await;
@@ -826,7 +790,6 @@ async fn run_type_phase(
screen.composer.field.edit(rsc).set(&text); screen.composer.field.edit(rsc).set(&text);
} }
}); });
redraw.request_redraw();
tokio::time::sleep(Duration::from_millis(TYPE_CHAR_MS)).await; tokio::time::sleep(Duration::from_millis(TYPE_CHAR_MS)).await;
} }
} }
-7
View File
@@ -132,7 +132,6 @@ impl TranscriptClient {
} }
fn spawn_fetch_sessions(&mut self, rsc: &mut StdRsc<Self>) { fn spawn_fetch_sessions(&mut self, rsc: &mut StdRsc<Self>) {
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();
rsc.spawn_task(async move |mut ctx| { rsc.spawn_task(async move |mut ctx| {
@@ -156,7 +155,6 @@ impl TranscriptClient {
} }
} }
}); });
redraw.request_redraw();
}); });
} }
@@ -166,7 +164,6 @@ impl TranscriptClient {
self.session_id = Some(session_id.clone()); self.session_id = Some(session_id.clone());
self.show_message(rsc, "Loading transcript..."); self.show_message(rsc, "Loading transcript...");
let redraw = rsc.tasks.redraw_handle();
let live_generation = self.generation.clone(); let live_generation = self.generation.clone();
rsc.spawn_task(async move |mut ctx| { rsc.spawn_task(async move |mut ctx| {
let transports = let transports =
@@ -180,7 +177,6 @@ impl TranscriptClient {
state.show_message(rsc, &message); state.show_message(rsc, &message);
} }
}); });
redraw.request_redraw();
return; return;
} }
}; };
@@ -214,8 +210,6 @@ impl TranscriptClient {
} }
}); });
} }
redraw.request_redraw();
if live_generation.load(Ordering::SeqCst) != my_generation { if live_generation.load(Ordering::SeqCst) != my_generation {
return; return;
} }
@@ -239,7 +233,6 @@ impl TranscriptClient {
} }
state.apply_event(rsc, &event); state.apply_event(rsc, &event);
}); });
redraw.request_redraw();
true true
} }
}, },
+38 -19
View File
@@ -45,7 +45,7 @@ struct Client {
ui_state: DesktopUiState, ui_state: DesktopUiState,
api: Arc<ApiClient<UreqTransport>>, api: Arc<ApiClient<UreqTransport>>,
stream_transport: Arc<UreqTransport>, stream_transport: Arc<UreqTransport>,
proxy: Proxy<AppEvent>, updates: TaskCtx<StdRsc<Self>>,
sessions: Vec<SessionSummary>, sessions: Vec<SessionSummary>,
selected: Option<String>, selected: Option<String>,
items: Vec<TranscriptItem>, items: Vec<TranscriptItem>,
@@ -56,9 +56,7 @@ struct Client {
} }
impl DesktopAppState for Client { impl DesktopAppState for Client {
type Event = AppEvent; fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>) -> Self {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<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);
@@ -90,7 +88,7 @@ impl DesktopAppState for Client {
ui_state, ui_state,
api, api,
stream_transport, stream_transport,
proxy, updates: rsc.tasks.context(),
sessions: Vec::new(), sessions: Vec::new(),
selected: None, selected: None,
items: Vec::new(), items: Vec::new(),
@@ -102,8 +100,10 @@ impl DesktopAppState for Client {
client.spawn_fetch_sessions(); client.spawn_fetch_sessions();
client client
} }
}
fn event(&mut self, event: AppEvent, rsc: &mut StdRsc<Self>) { impl Client {
fn apply_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;
@@ -163,11 +163,8 @@ impl DesktopAppState for Client {
eprintln!("desktop-app: couldn't send: {message}"); eprintln!("desktop-app: couldn't send: {message}");
} }
} }
self.ui_state.window.request_redraw();
} }
}
impl Client {
fn current(&self, session_id: &str, generation: u64) -> bool { fn current(&self, session_id: &str, generation: u64) -> bool {
self.selected.as_deref() == Some(session_id) self.selected.as_deref() == Some(session_id)
&& self.generation.load(Ordering::SeqCst) == generation && self.generation.load(Ordering::SeqCst) == generation
@@ -180,10 +177,12 @@ impl Client {
fn spawn_fetch_sessions(&self) { fn spawn_fetch_sessions(&self) {
let api = self.api.clone(); let api = self.api.clone();
let proxy = self.proxy.clone(); let mut updates = self.updates.clone();
thread::spawn(move || { thread::spawn(move || {
let result = api.fetch_sessions().map_err(|e| e.to_string()); let result = api.fetch_sessions().map_err(|e| e.to_string());
let _ = proxy.send_event(AppEvent::Sessions(result)); updates.update(move |state: &mut Client, rsc| {
state.apply_event(AppEvent::Sessions(result), rsc);
});
}); });
} }
@@ -211,7 +210,7 @@ impl Client {
let api = self.api.clone(); let api = self.api.clone();
let stream_transport = self.stream_transport.clone(); let stream_transport = self.stream_transport.clone();
let proxy = self.proxy.clone(); let mut updates = self.updates.clone();
let live_generation = self.generation.clone(); let live_generation = self.generation.clone();
thread::spawn(move || { thread::spawn(move || {
let page: Result<Vec<serde_json::Value>, String> = api let page: Result<Vec<serde_json::Value>, String> = api
@@ -223,10 +222,16 @@ impl Client {
.and_then(|values| raw_seq(values.last()?)) .and_then(|values| raw_seq(values.last()?))
.unwrap_or(0); .unwrap_or(0);
let result = page.and_then(|values| fold_page(&values)); let result = page.and_then(|values| fold_page(&values));
let _ = proxy.send_event(AppEvent::TranscriptLoaded { let loaded_session_id = session_id.clone();
session_id: session_id.clone(), updates.update(move |state: &mut Client, rsc| {
state.apply_event(
AppEvent::TranscriptLoaded {
session_id: loaded_session_id,
generation, generation,
result, result,
},
rsc,
);
}); });
let stop = || live_generation.load(Ordering::SeqCst) != generation; let stop = || live_generation.load(Ordering::SeqCst) != generation;
@@ -240,28 +245,42 @@ impl Client {
if stop() { if stop() {
return false; return false;
} }
let _ = proxy.send_event(AppEvent::StreamEvent { let event_session_id = session_id.clone();
session_id: session_id.clone(), updates.update(move |state: &mut Client, rsc| {
state.apply_event(
AppEvent::StreamEvent {
session_id: event_session_id,
generation, generation,
event, event,
},
rsc,
);
}); });
true true
} }
}); });
let _ = proxy.send_event(AppEvent::StreamEnded { updates.update(move |state: &mut Client, rsc| {
state.apply_event(
AppEvent::StreamEnded {
session_id, session_id,
generation, generation,
message: outcome.err().map(|e| e.to_string()), message: outcome.err().map(|e| e.to_string()),
},
rsc,
);
}); });
}); });
} }
fn send_message(&mut self, session_id: String, text: String) { fn send_message(&mut self, session_id: String, text: String) {
let api = self.api.clone(); let api = self.api.clone();
let proxy = self.proxy.clone(); let mut updates = self.updates.clone();
thread::spawn(move || { thread::spawn(move || {
if let Err(e) = api.send_message(&session_id, &text, &[]) { if let Err(e) = api.send_message(&session_id, &text, &[]) {
let _ = proxy.send_event(AppEvent::SendFailed(e.to_string())); let message = e.to_string();
updates.update(move |state: &mut Client, rsc| {
state.apply_event(AppEvent::SendFailed(message), rsc);
});
} }
}); });
} }
+14 -3
View File
@@ -794,13 +794,24 @@ ssh case are one implementation.
**An Iris Android application supplies an initialization function and its own **An Iris Android application supplies an initialization function and its own
state/resources; Iris supplies the JNI host and APK packager.** state/resources; Iris supplies the JNI host and APK packager.**
`#[iris::app_init]` marks the factory Android calls when it creates the Iris `#[iris::app_init]` marks the initializer Android calls when it creates the Iris
view. The macro target-gates the factory and generates the single exported view. The macro target-gates the initializer and generates the single exported
`JNI_OnLoad` plus the concrete `android-view` registration callback. The `JNI_OnLoad` plus the concrete `android-view` registration callback. The
function returns the application state, whose `AndroidAppState::Resources` simple form mutates `AndroidUiState` through a reference. An application with
custom state returns that state instead, and its `AndroidAppState::Resources`
associated type selects the resource bundle. `StdRsc` is the default, never a associated type selects the resource bundle. `StdRsc` is the default, never a
host requirement; a custom bundle works by implementing the narrow host requirement; a custom bundle works by implementing the narrow
`AndroidResources` capabilities. `AndroidResources` capabilities.
`AndroidUiState` and `DesktopUiState` implement their own host-state traits,
so an application with no additional fields uses them directly. Desktop takes
the widget-building initializer through `DesktopApp::run_with`; custom state
types retain the lifecycle trait hooks and derive their host-state accessors.
Background work crosses back to either UI thread through `TaskCtx::update`.
Each update wakes the host, Iris applies all queued closures, and the retained
tree's existing dirty-state check requests a frame only when a root, layout, or
widget changed. Winit's user-event proxy and Android's posted callback are
private implementations of that wake; applications do not define platform
event types or manually request redraws.
`cargo-iris` is an installable Cargo subcommand, rather than a script callers `cargo-iris` is an installable Cargo subcommand, rather than a script callers
must find inside an Iris checkout. `cargo iris apk` builds the Rust `cdylib` must find inside an Iris checkout. `cargo iris apk` builds the Rust `cdylib`
+1 -1
View File
@@ -27,7 +27,7 @@ Two widgets have one, and they differ only in how they spend a delta:
`.scrollable()` registers the same two senses against the controller it `.scrollable()` registers the same two senses against the controller it
already has. already has.
Do not give a widget its own fling, scroll amount, or `RequestRedraw` Do not give a widget its own fling, scroll amount, or platform wake
handle, and do not add scrolling methods to the general `Widget` trait. handle, and do not add scrolling methods to the general `Widget` trait.
## One convention for a delta ## One convention for a delta
+2 -2
View File
@@ -28,8 +28,8 @@ pub struct Resources {
} }
impl AndroidResources<State> for Resources { impl AndroidResources<State> for Resources {
fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>) { fn new(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Self>) {
let (tasks, receiver) = Tasks::init(redraw); let (tasks, receiver) = Tasks::init(wake);
( (
Self { Self {
ui: Ui::default(), ui: Ui::default(),
+5 -13
View File
@@ -2,19 +2,11 @@ use iris::prelude::*;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(AndroidUiState)]
struct State {
ui_state: AndroidUiState,
}
impl AndroidAppState for State {
type Resources = StdRsc<Self>;
}
#[iris::app_init] #[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State { fn create(
let _ = build(rsc, &mut ui_state); ui_state: &mut AndroidUiState,
State { ui_state } rsc: &mut StdRsc<AndroidUiState>,
) {
let _ = app::build(rsc, ui_state);
} }
+2 -3
View File
@@ -3,7 +3,6 @@ use winit::event::WindowEvent;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
const SETTLE_FRAMES: usize = 4; const SETTLE_FRAMES: usize = 4;
const FRAMES: usize = 6; const FRAMES: usize = 6;
@@ -17,8 +16,8 @@ struct State {
} }
impl DesktopAppState for State { impl DesktopAppState for State {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self { fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>) -> Self {
let span = build(rsc, &mut ui_state); let span = app::build(rsc, &mut ui_state);
Self { Self {
ui_state, ui_state,
span, span,
+5 -13
View File
@@ -2,19 +2,11 @@ use iris::prelude::*;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(AndroidUiState)]
struct State {
ui_state: AndroidUiState,
}
impl AndroidAppState for State {
type Resources = StdRsc<Self>;
}
#[iris::app_init] #[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State { fn create(
build(rsc, &mut ui_state); ui_state: &mut AndroidUiState,
State { ui_state } rsc: &mut StdRsc<AndroidUiState>,
) {
app::build(rsc, ui_state);
} }
+4 -18
View File
@@ -3,24 +3,10 @@ use winit::{dpi::LogicalSize, window::WindowAttributes};
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(DesktopUiState)]
struct State {
ui_state: DesktopUiState,
}
impl DesktopAppState for State {
fn window_attributes() -> WindowAttributes {
WindowAttributes::default().with_inner_size(LogicalSize::new(420.0, 900.0))
}
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
build(rsc, &mut ui_state);
Self { ui_state }
}
}
fn main() { fn main() {
DesktopApp::<State>::run(); let attributes = WindowAttributes::default().with_inner_size(LogicalSize::new(420.0, 900.0));
DesktopApp::<DesktopUiState>::run_with_attributes(attributes, |ui_state, rsc| {
app::build(rsc, ui_state)
});
} }
+5 -13
View File
@@ -2,19 +2,11 @@ use iris::prelude::*;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(AndroidUiState)]
struct State {
ui_state: AndroidUiState,
}
impl AndroidAppState for State {
type Resources = StdRsc<Self>;
}
#[iris::app_init] #[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State { fn create(
build(rsc, &mut ui_state); ui_state: &mut AndroidUiState,
State { ui_state } rsc: &mut StdRsc<AndroidUiState>,
) {
app::build(rsc, ui_state);
} }
+1 -14
View File
@@ -2,20 +2,7 @@ use iris::prelude::*;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(DesktopUiState)]
struct State {
ui_state: DesktopUiState,
}
impl DesktopAppState for State {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
build(rsc, &mut ui_state);
Self { ui_state }
}
}
fn main() { fn main() {
DesktopApp::<State>::run(); DesktopApp::<DesktopUiState>::run_with(|ui_state, rsc| app::build(rsc, ui_state));
} }
+3 -4
View File
@@ -2,7 +2,6 @@ use iris::prelude::*;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(AndroidUiState)] #[derive(AndroidUiState)]
struct Client { struct Client {
@@ -19,14 +18,14 @@ impl AndroidAppState for Client {
.renderer .renderer
.as_ref() .as_ref()
.map_or(0, |renderer| renderer.ui.view_count()); .map_or(0, |renderer| renderer.ui.view_count());
update_info(rsc, self.info, views); app::update_info(rsc, self.info, views);
} }
} }
#[iris::app_init] #[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Client>) -> Client { fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Client>) -> Client {
let widgets = build(rsc, &mut ui_state); let widgets = app::build(rsc, &mut ui_state);
update_info(rsc, widgets.info, 0); app::update_info(rsc, widgets.info, 0);
Client { Client {
ui_state, ui_state,
info: widgets.info, info: widgets.info,
+4 -5
View File
@@ -3,7 +3,6 @@ use winit::event::WindowEvent;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(DesktopUiState)] #[derive(DesktopUiState)]
struct Client { struct Client {
@@ -12,9 +11,9 @@ struct Client {
} }
impl DesktopAppState for Client { impl DesktopAppState for Client {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self { fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>) -> Self {
let widgets = build(rsc, &mut ui_state); let widgets = app::build(rsc, &mut ui_state);
update_info(rsc, widgets.info, 0); app::update_info(rsc, widgets.info, 0);
Self { Self {
ui_state, ui_state,
info: widgets.info, info: widgets.info,
@@ -22,7 +21,7 @@ impl DesktopAppState for Client {
} }
fn window_event(&mut self, _: WindowEvent, rsc: &mut StdRsc<Self>) { fn window_event(&mut self, _: WindowEvent, rsc: &mut StdRsc<Self>) {
update_info(rsc, self.info, self.ui_state.renderer.ui.view_count()); app::update_info(rsc, self.info, self.ui_state.renderer.ui.view_count());
} }
} }
+5 -13
View File
@@ -2,19 +2,11 @@ use iris::prelude::*;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(AndroidUiState)]
struct State {
ui_state: AndroidUiState,
}
impl AndroidAppState for State {
type Resources = StdRsc<Self>;
}
#[iris::app_init] #[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State { fn create(
build(rsc, &mut ui_state); ui_state: &mut AndroidUiState,
State { ui_state } rsc: &mut StdRsc<AndroidUiState>,
) {
app::build(rsc, ui_state);
} }
+1 -14
View File
@@ -2,20 +2,7 @@ use iris::prelude::*;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(DesktopUiState)]
struct State {
ui_state: DesktopUiState,
}
impl DesktopAppState for State {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
build(rsc, &mut ui_state);
Self { ui_state }
}
}
fn main() { fn main() {
DesktopApp::<State>::run(); DesktopApp::<DesktopUiState>::run_with(|ui_state, rsc| app::build(rsc, ui_state));
} }
+2 -1
View File
@@ -6,7 +6,8 @@ where
Rsc::State: FocusHost, Rsc::State: FocusHost,
{ {
let rect = rect(PaintId::RED).add(rsc); let rect = rect(PaintId::RED).add(rsc);
rect.task_on(CursorSense::click(), async move |mut ctx| { rect.label("Toggle color")
.task_on(CursorSense::click(), async move |mut ctx| {
iris::task::sleep(Duration::from_secs(1)).await; iris::task::sleep(Duration::from_secs(1)).await;
ctx.update(move |_, rsc| { ctx.update(move |_, rsc| {
let rect = rect(rsc); let rect = rect(rsc);
+5 -13
View File
@@ -2,19 +2,11 @@ use iris::prelude::*;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(AndroidUiState)]
struct State {
ui_state: AndroidUiState,
}
impl AndroidAppState for State {
type Resources = StdRsc<Self>;
}
#[iris::app_init] #[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State { fn create(
build(rsc, &mut ui_state); ui_state: &mut AndroidUiState,
State { ui_state } rsc: &mut StdRsc<AndroidUiState>,
) {
app::build(rsc, ui_state);
} }
+1 -14
View File
@@ -2,20 +2,7 @@ use iris::prelude::*;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(DesktopUiState)]
struct State {
ui_state: DesktopUiState,
}
impl DesktopAppState for State {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
build(rsc, &mut ui_state);
Self { ui_state }
}
}
fn main() { fn main() {
DesktopApp::<State>::run(); DesktopApp::<DesktopUiState>::run_with(|ui_state, rsc| app::build(rsc, ui_state));
} }
+32 -13
View File
@@ -9,13 +9,15 @@ use syn::{
spanned::Spanned, spanned::Spanned,
}; };
/// Marks the factory called when Android creates an Iris view. /// Marks the initializer called when Android creates an Iris view.
/// ///
/// An attribute is necessary here because the Android loader requires one /// An attribute is necessary here because the Android loader requires one
/// exported `JNI_OnLoad` symbol and `android-view` requires a plain function /// exported `JNI_OnLoad` symbol and `android-view` requires a plain function
/// pointer monomorphized for the returned application state. The generated /// pointer monomorphized for the application state. A function returning a
/// linker and JNI glue is Android-gated; the annotated function therefore /// custom state remains its factory; a function with no return value receives
/// does not need its own `cfg` attribute. /// `&mut AndroidUiState` and uses that state directly. The generated linker and
/// JNI glue is Android-gated; the annotated function therefore does not need
/// its own `cfg` attribute.
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream { pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream {
if !args.is_empty() { if !args.is_empty() {
@@ -29,13 +31,9 @@ pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream {
let function = parse_macro_input!(item as ItemFn); let function = parse_macro_input!(item as ItemFn);
let name = &function.sig.ident; let name = &function.sig.ident;
let ReturnType::Type(_, state) = &function.sig.output else { let (state, direct_initializer): (Type, bool) = match &function.sig.output {
return Error::new( ReturnType::Default => (parse_quote!(::iris::android::AndroidUiState), true),
function.sig.output.span(), ReturnType::Type(_, state) => ((**state).clone(), false),
"an app_init function must return its application state",
)
.into_compile_error()
.into();
}; };
if function.sig.inputs.len() != 2 if function.sig.inputs.len() != 2
|| function || function
@@ -46,7 +44,7 @@ pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream {
{ {
return Error::new( return Error::new(
function.sig.inputs.span(), function.sig.inputs.span(),
"an app_init function takes AndroidUiState and &mut State::Resources", "an app_init function takes UI state and resources",
) )
.into_compile_error() .into_compile_error()
.into(); .into();
@@ -64,6 +62,25 @@ pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream {
.into(); .into();
} }
let factory = if direct_initializer {
quote! {
fn init(
mut ui_state: ::iris::android::AndroidUiState,
rsc: &mut <#state as ::iris::android::AndroidAppState>::Resources,
) -> #state {
super::#name(&mut ui_state, rsc);
ui_state
}
}
} else {
quote! {}
};
let create = if direct_initializer {
quote! { init }
} else {
quote! { super::#name }
};
quote! { quote! {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
#function #function
@@ -72,12 +89,14 @@ pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream {
mod __iris_android_app { mod __iris_android_app {
use super::*; use super::*;
#factory
extern "system" fn new_view_peer<'local>( extern "system" fn new_view_peer<'local>(
env: ::iris::android::__private::JNIEnv<'local>, env: ::iris::android::__private::JNIEnv<'local>,
view: ::iris::android::__private::View<'local>, view: ::iris::android::__private::View<'local>,
context: ::iris::android::__private::Context<'local>, context: ::iris::android::__private::Context<'local>,
) -> ::iris::android::__private::JLong { ) -> ::iris::android::__private::JLong {
::iris::android::new_peer::<#state>(env, view, context, super::#name) ::iris::android::new_peer::<#state>(env, view, context, #create)
} }
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
+33 -16
View File
@@ -25,31 +25,48 @@ application-id = "com.example.myapp"
label = "My app" label = "My app"
``` ```
`#[iris::app_init]` marks the factory called when Android creates the Iris `#[iris::app_init]` marks the initializer called when Android creates the Iris
view. The attribute supplies its own Android target gate and generates the JNI view. The attribute supplies its own Android target gate and generates the JNI
loader glue. The returned state chooses its resources through loader glue. An application with no state beyond the UI state receives
`AndroidAppState::Resources`; `StdRsc` is the standard bundle, not a `AndroidUiState` directly by mutable reference:
requirement.
```rust ```rust
use iris::prelude::*; use iris::prelude::*;
#[derive(AndroidUiState)]
struct State {
ui_state: AndroidUiState,
}
impl AndroidAppState for State {
type Resources = StdRsc<Self>;
}
#[iris::app_init] #[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State { fn create(
rect(PaintId::RED).set_root(rsc, &mut ui_state); ui_state: &mut AndroidUiState,
State { ui_state } rsc: &mut StdRsc<AndroidUiState>,
) {
rect(PaintId::RED).set_root(rsc, ui_state);
} }
``` ```
Desktop has the corresponding initializer form:
```rust
DesktopApp::<DesktopUiState>::run_with(|ui_state, rsc| {
rect(PaintId::RED).set_root(rsc, ui_state);
});
```
Background work updates either host through the same task context. An update
wakes the UI thread; Iris schedules a frame automatically if the closure made
the retained widget tree dirty:
```rust
rsc.spawn_task(async move |mut ctx| {
let text = load_text().await;
ctx.update(move |_, rsc| label.edit(rsc).set(&text));
});
```
Applications do not need a winit event proxy or an explicit redraw request.
Applications with additional fields use their own state type. Its
`AndroidAppState::Resources` associated type can also replace `StdRsc` with a
custom resource bundle.
Install the Cargo subcommand from a checkout, then invoke it from the Install the Cargo subcommand from a checkout, then invoke it from the
application's directory: application's directory:
+8 -8
View File
@@ -1,4 +1,4 @@
use crate::task::RequestRedraw; use crate::task::WakeTaskQueue;
use android_view::{ use android_view::{
View, View,
jni::{JavaVM, objects::GlobalRef}, jni::{JavaVM, objects::GlobalRef},
@@ -429,23 +429,23 @@ impl AndroidRenderer {
} }
} }
/// `Tasks`' redraw handle on Android: a background task finishes on the /// `Tasks`' UI-thread wake on Android. Updates can be submitted from the
/// tokio thread `Tasks::init` spawned, which is not attached to the JVM, so /// tokio runtime or an application-owned thread that is not attached to the
/// asking for a frame means attaching first. The global ref is what /// JVM, so posting the callback means attaching first. The global ref is what
/// survives past the JNI call that handed the `View` to us. /// survives past the JNI call that handed the `View` to us.
pub struct AndroidRedrawHandle { pub struct AndroidTaskWake {
vm: JavaVM, vm: JavaVM,
view: GlobalRef, view: GlobalRef,
} }
impl AndroidRedrawHandle { impl AndroidTaskWake {
pub fn new(vm: JavaVM, view: GlobalRef) -> Self { pub fn new(vm: JavaVM, view: GlobalRef) -> Self {
Self { vm, view } Self { vm, view }
} }
} }
impl RequestRedraw for AndroidRedrawHandle { impl WakeTaskQueue for AndroidTaskWake {
fn request_redraw(&self) { fn wake(&self) {
let Ok(mut env) = self.vm.attach_current_thread() else { let Ok(mut env) = self.vm.attach_current_thread() else {
return; return;
}; };
+31 -19
View File
@@ -1,5 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
use crate::task::RequestRedraw; use crate::task::WakeTaskQueue;
use accesskit_android::Adapter as AccessAdapter; use accesskit_android::Adapter as AccessAdapter;
use android_view::{ use android_view::{
AccessibilityNodeInfo, AccessibilityNodeProvider, Bundle, CallbackCtx, Context, AccessibilityNodeInfo, AccessibilityNodeProvider, Bundle, CallbackCtx, Context,
@@ -16,7 +16,7 @@ use std::{cell::RefCell, marker::Sized, rc::Rc, sync::Arc, time::Instant};
use super::{ use super::{
access::{AndroidAccessSource, NullActionHandler, raise_if_enabled}, access::{AndroidAccessSource, NullActionHandler, raise_if_enabled},
insets::{Insets, Shared}, insets::{Insets, Shared},
render::{AndroidRedrawHandle, AndroidRenderer}, render::{AndroidRenderer, AndroidTaskWake},
}; };
/// Android host state. The renderer follows the `SurfaceView` lifecycle. /// Android host state. The renderer follows the `SurfaceView` lifecycle.
@@ -100,6 +100,16 @@ pub trait HasAndroidUiState: Sized + 'static {
fn android_state_mut(&mut self) -> &mut AndroidUiState; fn android_state_mut(&mut self) -> &mut AndroidUiState;
} }
impl HasAndroidUiState for AndroidUiState {
fn android_state(&self) -> &AndroidUiState {
self
}
fn android_state_mut(&mut self) -> &mut AndroidUiState {
self
}
}
/// Application state retained for the lifetime of one Android `View`. /// Application state retained for the lifetime of one Android `View`.
/// ///
/// [`StdRsc`] is the usual [`AndroidResources`] implementation, but the host only /// [`StdRsc`] is the usual [`AndroidResources`] implementation, but the host only
@@ -118,6 +128,10 @@ pub trait AndroidAppState: HasAndroidUiState {
fn on_insets_changed(&mut self, rsc: &mut Self::Resources, insets: WindowInsets) {} fn on_insets_changed(&mut self, rsc: &mut Self::Resources, insets: WindowInsets) {}
} }
impl AndroidAppState for AndroidUiState {
type Resources = StdRsc<Self>;
}
/// Resources the Android host needs to draw and dispatch application events. /// Resources the Android host needs to draw and dispatch application events.
/// ///
/// This deliberately names capabilities rather than storage. Custom bundles /// This deliberately names capabilities rather than storage. Custom bundles
@@ -128,12 +142,12 @@ pub trait AndroidResources<State>:
where where
State: 'static, State: 'static,
{ {
fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>); fn new(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Self>);
} }
impl<State: 'static> AndroidResources<State> for StdRsc<State> { impl<State: 'static> AndroidResources<State> for StdRsc<State> {
fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>) { fn new(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Self>) {
StdRsc::new(redraw) StdRsc::new(wake)
} }
} }
@@ -223,8 +237,12 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
self.update_ime_selection(ctx); self.update_ime_selection(ctx);
let ui_state = self.state.android_state_mut(); self.state.android_state_mut().cursor.end_frame();
ui_state.cursor.end_frame(); self.request_frame_if_needed(ctx);
}
fn request_frame_if_needed(&self, ctx: &mut CallbackCtx) {
let ui_state = self.state.android_state();
let render_state = self.rsc.ui().render_state(); let render_state = self.rsc.ui().render_state();
if render_state if render_state
.get() .get()
@@ -720,18 +738,12 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
self.render(ctx, now); self.render(ctx, now);
} }
/// Where `AndroidRedrawHandle::request_redraw` (`android/render.rs`) /// Where `AndroidTaskWake::wake` lands on the UI thread. Applying an
/// actually lands: `View.postDelayed`'s Runnable resolves to this, on /// update and drawing it are deliberately separate: retained widget
/// the UI thread, which is what makes it safe to call from a background /// invalidation decides whether this wake needs a frame.
/// task's own thread when `post_frame_callback`'s `Choreographer`
/// requirement (a `Looper` on the *calling* thread) is not. Same body
/// as `do_frame` -- draining tasks and rendering immediately is a
/// perfectly good answer to "a background fetch has new state," and
/// avoids a second frame-scheduling path to keep in sync with the real
/// one.
fn delayed_callback(&mut self, ctx: &mut CallbackCtx) { fn delayed_callback(&mut self, ctx: &mut CallbackCtx) {
self.drain_tasks(); self.drain_tasks();
self.render(ctx, Instant::now()); self.request_frame_if_needed(ctx);
} }
fn as_input_connection(&mut self) -> Option<&mut dyn InputConnection> { fn as_input_connection(&mut self) -> Option<&mut dyn InputConnection> {
@@ -835,8 +847,8 @@ pub fn new_peer<'local, State: AndroidAppState>(
log::info!("iris: new_peer content_scale={content_scale}"); log::info!("iris: new_peer content_scale={content_scale}");
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 wake: Arc<dyn WakeTaskQueue> = Arc::new(AndroidTaskWake::new(vm, global_view));
let (mut rsc, task_recv) = State::Resources::new(redraw); let (mut rsc, task_recv) = State::Resources::new(wake);
rsc.ui_mut().set_density(content_scale); rsc.ui_mut().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);
+18 -19
View File
@@ -5,41 +5,40 @@ use winit::{
window::WindowId, window::WindowId,
}; };
pub trait AppState { pub trait AppState: 'static {
type Event: 'static;
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self;
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop); fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop);
fn event(&mut self, event: Self::Event, event_loop: &ActiveEventLoop); fn tasks_ready(&mut self);
fn exit(&mut self); fn exit(&mut self);
fn run()
where
Self: Sized,
{
App::<Self>::run();
}
} }
pub struct App<State: AppState> { pub struct App<State: AppState> {
state: Option<State>, state: Option<State>,
proxy: EventLoopProxy<State::Event>, proxy: EventLoopProxy<()>,
init: Option<Box<Init<State>>>,
} }
impl<State: AppState> App<State> { type Init<State> = dyn FnOnce(&ActiveEventLoop, EventLoopProxy<()>) -> State;
pub fn run() {
impl<State: AppState + 'static> App<State> {
pub fn run_with(init: impl FnOnce(&ActiveEventLoop, EventLoopProxy<()>) -> State + 'static) {
super::logging::install(log::LevelFilter::Info); super::logging::install(log::LevelFilter::Info);
let event_loop = EventLoop::with_user_event().build().unwrap(); let event_loop = EventLoop::with_user_event().build().unwrap();
let proxy = event_loop.create_proxy(); let proxy = event_loop.create_proxy();
event_loop event_loop
.run_app(&mut App::<State> { state: None, proxy }) .run_app(&mut App::<State> {
state: None,
proxy,
init: Some(Box::new(init)),
})
.unwrap(); .unwrap();
} }
} }
impl<State: AppState> ApplicationHandler<State::Event> for App<State> { impl<State: AppState> ApplicationHandler<()> for App<State> {
fn resumed(&mut self, event_loop: &ActiveEventLoop) { fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.state.is_none() { if self.state.is_none() {
let state = State::new(event_loop, self.proxy.clone()); let init = self.init.take().unwrap();
let state = init(event_loop, self.proxy.clone());
self.state = Some(state); self.state = Some(state);
} }
} }
@@ -49,9 +48,9 @@ impl<State: AppState> ApplicationHandler<State::Event> for App<State> {
state.window_event(event, event_loop); state.window_event(event, event_loop);
} }
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: State::Event) { fn user_event(&mut self, _: &ActiveEventLoop, (): ()) {
let state = self.state.as_mut().unwrap(); let state = self.state.as_mut().unwrap();
state.event(event, event_loop); state.tasks_ready();
} }
fn exiting(&mut self, _: &ActiveEventLoop) { fn exiting(&mut self, _: &ActiveEventLoop) {
+76 -14
View File
@@ -16,11 +16,15 @@ mod platform;
mod render; mod render;
pub use access::*; pub use access::*;
pub use app::*; use app::{App, AppState};
pub use input::*; pub use input::*;
pub use render::*; pub use render::*;
pub type Proxy<Event> = EventLoopProxy<Event>; impl WakeTaskQueue for EventLoopProxy<()> {
fn wake(&self) {
let _ = self.send_event(());
}
}
/// Physical pixels per dp. Layout and input stay in physical pixels; only /// Physical pixels per dp. Layout and input stay in physical pixels; only
/// `dp(...)` resolves through this scale. /// `dp(...)` resolves through this scale.
@@ -79,11 +83,18 @@ pub trait HasDesktopUiState: Sized + 'static {
fn desktop_state_mut(&mut self) -> &mut DesktopUiState; fn desktop_state_mut(&mut self) -> &mut DesktopUiState;
} }
impl HasDesktopUiState for DesktopUiState {
fn desktop_state(&self) -> &DesktopUiState {
self
}
fn desktop_state_mut(&mut self) -> &mut DesktopUiState {
self
}
}
pub trait DesktopAppState: HasDesktopUiState { pub trait DesktopAppState: HasDesktopUiState {
type Event = (); fn new(ui_state: DesktopUiState, rsc: &mut StdRsc<Self>) -> Self;
fn new(ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, proxy: Proxy<Self::Event>) -> Self;
#[allow(unused_variables)]
fn event(&mut self, event: Self::Event, rsc: &mut StdRsc<Self>) {}
#[allow(unused_variables)] #[allow(unused_variables)]
fn exit(&mut self, rsc: &mut StdRsc<Self>) {} fn exit(&mut self, rsc: &mut StdRsc<Self>) {}
#[allow(unused_variables)] #[allow(unused_variables)]
@@ -93,18 +104,33 @@ pub trait DesktopAppState: HasDesktopUiState {
} }
} }
impl DesktopAppState for DesktopUiState {
fn new(ui_state: DesktopUiState, _: &mut StdRsc<Self>) -> Self {
ui_state
}
}
pub struct DesktopApp<State: DesktopAppState> { pub struct DesktopApp<State: DesktopAppState> {
rsc: StdRsc<State>, rsc: StdRsc<State>,
state: State, state: State,
task_recv: TaskMsgReceiver<StdRsc<State>>, task_recv: TaskMsgReceiver<StdRsc<State>>,
} }
impl<State: DesktopAppState> AppState for DesktopApp<State> { impl<State: DesktopAppState> DesktopApp<State> {
type Event = State::Event; pub fn run() {
App::<Self>::run_with(|event_loop, proxy| {
Self::new_with(event_loop, proxy, State::window_attributes(), State::new)
});
}
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self { fn new_with(
event_loop: &ActiveEventLoop,
proxy: EventLoopProxy<()>,
attributes: WindowAttributes,
init: impl FnOnce(DesktopUiState, &mut StdRsc<State>) -> State,
) -> Self {
let window = event_loop let window = event_loop
.create_window(State::window_attributes().with_visible(false)) .create_window(attributes.with_visible(false))
.unwrap(); .unwrap();
let access_adapter = accesskit_winit::Adapter::with_direct_handlers( let access_adapter = accesskit_winit::Adapter::with_direct_handlers(
event_loop, event_loop,
@@ -115,20 +141,56 @@ 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) = StdRsc::new(desktop_state.window.clone()); let (mut rsc, task_recv) = StdRsc::new(Arc::new(proxy));
// 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);
let state = State::new(desktop_state, &mut rsc, proxy); let state = init(desktop_state, &mut rsc);
Self { Self {
rsc, rsc,
state, state,
task_recv, task_recv,
} }
} }
}
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) { impl DesktopApp<DesktopUiState> {
self.state.event(event, &mut self.rsc); /// Runs an application whose only state is Iris's desktop UI state.
pub fn run_with(init: impl FnOnce(&mut DesktopUiState, &mut StdRsc<DesktopUiState>) + 'static) {
Self::run_with_attributes(DesktopUiState::window_attributes(), init);
}
pub fn run_with_attributes(
attributes: WindowAttributes,
init: impl FnOnce(&mut DesktopUiState, &mut StdRsc<DesktopUiState>) + 'static,
) {
App::<Self>::run_with(move |event_loop, proxy| {
Self::new_with(event_loop, proxy, attributes, move |mut ui_state, rsc| {
init(&mut ui_state, rsc);
ui_state
})
});
}
}
impl<State: DesktopAppState> AppState for DesktopApp<State> {
fn tasks_ready(&mut self) {
let Self {
rsc,
state,
task_recv,
} = self;
for update in task_recv.try_iter() {
update(state, rsc);
}
let ui_state = state.desktop_state();
let render_state = rsc.ui.render_state();
if render_state
.get()
.needs_redraw(&ui_state.root, rsc.widgets())
{
ui_state.renderer.window().request_redraw();
}
} }
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) { fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
-7
View File
@@ -1,4 +1,3 @@
use crate::task::RequestRedraw;
use iris_core::{FrameParts, LinearRgba, Ui, UiRenderNode, util::Vec2}; use iris_core::{FrameParts, LinearRgba, Ui, UiRenderNode, util::Vec2};
use pollster::FutureExt; use pollster::FutureExt;
use std::sync::Arc; use std::sync::Arc;
@@ -8,12 +7,6 @@ use winit::{dpi::PhysicalSize, window::Window};
pub const CLEAR_COLOR: LinearRgba = LinearRgba::BLACK; pub const CLEAR_COLOR: LinearRgba = LinearRgba::BLACK;
impl RequestRedraw for Window {
fn request_redraw(&self) {
Window::request_redraw(self);
}
}
pub struct UiRenderer { pub struct UiRenderer {
window: Arc<Window>, window: Arc<Window>,
surface: Surface<'static>, surface: Surface<'static>,
+2 -2
View File
@@ -103,8 +103,8 @@ impl RedrawCounter {
} }
} }
impl RequestRedraw for RedrawCounter { impl WakeTaskQueue for RedrawCounter {
fn request_redraw(&self) { fn wake(&self) {
self.0.fetch_add(1, Ordering::Relaxed); self.0.fetch_add(1, Ordering::Relaxed);
} }
} }
-3
View File
@@ -1,8 +1,5 @@
#![feature(unboxed_closures)] #![feature(unboxed_closures)]
#![feature(fn_traits)] #![feature(fn_traits)]
// 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)] #![feature(unsize)]
#![feature(option_into_flat_iter)] #![feature(option_into_flat_iter)]
#![feature(async_fn_traits)] #![feature(async_fn_traits)]
+2 -2
View File
@@ -33,8 +33,8 @@ pub struct StdRsc<State: 'static> {
} }
impl<State> StdRsc<State> { impl<State> StdRsc<State> {
pub(crate) fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>) { pub(crate) fn new(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Self>) {
let (tasks, receiver) = Tasks::init(redraw); let (tasks, receiver) = Tasks::init(wake);
( (
Self { Self {
ui: Ui::default(), ui: Ui::default(),
+72 -28
View File
@@ -19,15 +19,13 @@ pub async fn sleep(duration: std::time::Duration) {
tokio::time::sleep(duration).await; tokio::time::sleep(duration).await;
} }
/// What a completed task nudges when it wants its result drawn. Shared /// Wakes the platform UI thread so it can apply queued task updates.
/// between backends rather than typed as `winit::window::Window` directly: ///
/// android-view has no `Window` at all, and the redraw request there is a /// Waking does not itself mean drawing. Once the updates have run, the host
/// JNI call (`View::post_frame_callback`) rather than a method call on a /// asks the retained UI tree whether anything visible became dirty and only
/// value this crate owns. Each backend supplies its own implementation -- /// then schedules a frame.
/// `desktop/render.rs` for winit, `android/render.rs` for android-view -- pub trait WakeTaskQueue: Send + Sync + 'static {
/// and this module never needs to know which one it is holding. fn wake(&self);
pub trait RequestRedraw: Send + Sync + 'static {
fn request_redraw(&self);
} }
pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>; pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>;
@@ -38,29 +36,41 @@ impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc>
pub struct Tasks<Rsc: HasState> { pub struct Tasks<Rsc: HasState> {
start: AsyncSender<BoxTask>, start: AsyncSender<BoxTask>,
redraw: Arc<dyn RequestRedraw>, wake: Arc<dyn WakeTaskQueue>,
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>, msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
} }
pub struct TaskCtx<Rsc: HasState> { pub struct TaskCtx<Rsc: HasState> {
send: TaskMsgSender<Rsc>, send: TaskMsgSender<Rsc>,
wake: Arc<dyn WakeTaskQueue>,
}
impl<Rsc: HasState> Clone for TaskCtx<Rsc> {
fn clone(&self) -> Self {
Self {
send: self.send.clone(),
wake: self.wake.clone(),
}
}
} }
impl<Rsc: HasState> TaskCtx<Rsc> { impl<Rsc: HasState> TaskCtx<Rsc> {
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) { pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
let _ = self.send.send(Box::new(f)); if self.send.send(Box::new(f)).is_ok() {
self.wake.wake();
}
} }
} }
impl<Rsc: HasState + 'static> TaskCtx<Rsc> { impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
fn new(send: TaskMsgSender<Rsc>) -> Self { fn new(send: TaskMsgSender<Rsc>, wake: Arc<dyn WakeTaskQueue>) -> Self {
Self { send } Self { send, wake }
} }
} }
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>; type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
impl<Rsc: HasState> Tasks<Rsc> { impl<Rsc: HasState> Tasks<Rsc> {
pub fn init(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Rsc>) { pub fn init(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Rsc>) {
let (start, start_recv) = async_channel(); let (start, start_recv) = async_channel();
let (msgs, msgs_recv) = sync_channel(); let (msgs, msgs_recv) = sync_channel();
std::thread::spawn(|| { std::thread::spawn(|| {
@@ -71,22 +81,16 @@ impl<Rsc: HasState> Tasks<Rsc> {
Self { Self {
start, start,
msg_send: msgs, msg_send: msgs,
redraw, wake,
}, },
msgs_recv, msgs_recv,
) )
} }
/// The same redraw handle `spawn`'s wrapper calls once, after a whole /// A cloneable, platform-neutral way for an application-owned thread to
/// task's future completes -- exposed so a caller running its own /// submit work to the UI thread.
/// longer-lived loop *inside* a spawned task (a live SSE follow, here) pub fn context(&self) -> TaskCtx<Rsc> {
/// can ask for a frame after each `TaskCtx::update`, not just at the TaskCtx::new(self.msg_send.clone(), self.wake.clone())
/// end. Without this a caller has no way to get a redraw mid-stream,
/// which is exactly the gap `iris/desktop-app`'s `app.rs` module doc
/// names for why it uses winit's `Proxy` instead of `Tasks` -- Android
/// has no `Proxy`, so this is what closes the same gap there.
pub fn redraw_handle(&self) -> Arc<dyn RequestRedraw> {
self.redraw.clone()
} }
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F) pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
@@ -94,10 +98,9 @@ impl<Rsc: HasState> Tasks<Rsc> {
F::CallOnceFuture: Send, F::CallOnceFuture: Send,
{ {
let send = self.msg_send.clone(); let send = self.msg_send.clone();
let redraw = self.redraw.clone(); let wake = self.wake.clone();
let _ = self.start.send(Box::pin(async move { let _ = self.start.send(Box::pin(async move {
task(TaskCtx::new(send)).await; task(TaskCtx::new(send, wake)).await;
redraw.request_redraw();
})); }));
} }
} }
@@ -107,3 +110,44 @@ async fn listen(mut recv: AsyncReceiver<BoxTask>) {
tokio::spawn(task); tokio::spawn(task);
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use std::{
sync::atomic::{AtomicUsize, Ordering},
time::Duration,
};
struct TestRsc;
impl HasState for TestRsc {
type State = usize;
}
#[derive(Default)]
struct WakeCounter(AtomicUsize);
impl WakeTaskQueue for WakeCounter {
fn wake(&self) {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
#[test]
fn every_update_wakes_the_ui_queue() {
let wakes = Arc::new(WakeCounter::default());
let (tasks, updates) = Tasks::<TestRsc>::init(wakes.clone());
let mut ctx = tasks.context();
ctx.update(|state: &mut usize, _| *state += 1);
ctx.update(|state: &mut usize, _| *state += 2);
assert_eq!(wakes.0.load(Ordering::Relaxed), 2);
let mut state = 0;
let mut rsc = TestRsc;
updates.recv_timeout(Duration::from_secs(1)).unwrap()(&mut state, &mut rsc);
updates.recv_timeout(Duration::from_secs(1)).unwrap()(&mut state, &mut rsc);
assert_eq!(state, 3);
}
}