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
+409 -377

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) {
Ok(opened) => {
if let Some(message) = message_argv() {
@@ -60,7 +60,6 @@ impl DesktopAppState for Client {
}
if let Some(text) = typed_argv() {
let field = opened.screen.composer.field;
let redraw = rsc.tasks.redraw_handle();
rsc.spawn_task(async move |mut ctx| {
for ch in text.chars() {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
@@ -73,7 +72,6 @@ impl DesktopAppState for Client {
}
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 {
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());
screen.push_row(
rsc,
+17 -54
View File
@@ -235,13 +235,11 @@ impl AndroidAppState for BenchClient {
if ime_visible && !self.keyboard_was_visible {
self.keyboard_was_visible = true;
let redraw = rsc.tasks.redraw_handle();
rsc.spawn_task(async move |mut ctx| {
tokio::time::sleep(Duration::from_millis(KEYBOARD_DIAGNOSTICS_DELAY_MS)).await;
ctx.update(|state: &mut BenchClient, rsc| {
state.capture_keyboard_diagnostics(rsc);
});
redraw.request_redraw();
});
} else if !ime_visible {
self.keyboard_was_visible = false;
@@ -485,7 +483,6 @@ impl BenchClient {
self.android_state_mut().frame_report.reset();
self.report_display.edit(rsc).set("Running benchmark...");
let redraw = rsc.tasks.redraw_handle();
let platform = self.platform.clone();
let stream_tail = self.stream_tail.clone();
let ime_state = self.ime_state.clone();
@@ -518,9 +515,9 @@ impl BenchClient {
})
});
let travel = run_fling_phase(&mut ctx, &redraw).await;
let (sent, total) = run_stream_phase(&mut ctx, &redraw, stream_tail).await;
run_type_phase(&mut ctx, &redraw, &platform).await;
let travel = run_fling_phase(&mut ctx).await;
let (sent, total) = run_stream_phase(&mut ctx, stream_tail).await;
run_type_phase(&mut ctx, &platform).await;
let keyboard = run_keyboard_phase(&mut ctx, &platform, &ime_state).await;
sampler_done.store(true, Ordering::Relaxed);
@@ -626,32 +623,18 @@ impl BenchClient {
state.report_display.edit(rsc).set(&report);
state.last_report = Some(report);
});
redraw.request_redraw();
});
}
}
/// Runs `f` against the real `BenchClient`/`Rsc` on the main thread (the
/// 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
/// to hand a value back, since the closure only actually runs once the
/// next frame callback drains `IrisViewPeer`'s task channel
/// (`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.
/// its result to the caller's async task. `ctx.update` wakes the UI thread,
/// whose task callback drains Iris's update queue before checking whether the
/// retained widget tree needs another frame.
/// Polls rather than assuming one `POLL_MS` sleep is enough, since a
/// slow device's frame callback can lag further than that.
async fn read_from_state<T, F>(
ctx: &mut iris::task::TaskCtx<Rsc>,
redraw: &Arc<dyn RequestRedraw>,
f: F,
) -> T
async fn read_from_state<T, F>(ctx: &mut iris::task::TaskCtx<Rsc>, f: F) -> T
where
T: Send + 'static,
F: FnOnce(&mut BenchClient, &mut Rsc) -> T + Send + 'static,
@@ -660,7 +643,6 @@ where
ctx.update(move |state: &mut BenchClient, rsc| {
let _ = tx.send(f(state, rsc));
});
redraw.request_redraw();
loop {
if let Ok(value) = rx.try_recv() {
return value;
@@ -669,10 +651,7 @@ where
}
}
async fn run_fling_phase(
ctx: &mut iris::task::TaskCtx<Rsc>,
redraw: &Arc<dyn RequestRedraw>,
) -> String {
async fn run_fling_phase(ctx: &mut iris::task::TaskCtx<Rsc>) -> String {
ctx.update(|state: &mut BenchClient, _rsc| {
state.android_state_mut().frame_report.mark_phase("fling");
});
@@ -681,11 +660,10 @@ async fn run_fling_phase(
(screen.list)(rsc).jump_to_end();
}
});
redraw.request_redraw();
// Lets the next frame's `repair_anchor` resolve `jump_to_end`'s
// `anchor = None` into a real slot before `start` is read.
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 {
ctx.update(|state: &mut BenchClient, rsc| {
@@ -694,11 +672,10 @@ async fn run_fling_phase(
animate_scroll(screen.list, rsc);
}
});
redraw.request_redraw();
wait_for_fling_settle(ctx, redraw).await;
wait_for_fling_settle(ctx).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 {
ctx.update(|state: &mut BenchClient, rsc| {
@@ -707,20 +684,16 @@ async fn run_fling_phase(
animate_scroll(screen.list, rsc);
}
});
redraw.request_redraw();
wait_for_fling_settle(ctx, redraw).await;
wait_for_fling_settle(ctx).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")
}
async fn read_anchor_position(
ctx: &mut iris::task::TaskCtx<Rsc>,
redraw: &Arc<dyn RequestRedraw>,
) -> String {
read_from_state(ctx, redraw, |state, rsc| match &state.screen {
async fn read_anchor_position(ctx: &mut iris::task::TaskCtx<Rsc>) -> String {
read_from_state(ctx, |state, rsc| match &state.screen {
Some(screen) => (screen.list)(rsc).anchor_position_display(),
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);
}
async fn wait_for_fling_settle(
ctx: &mut iris::task::TaskCtx<Rsc>,
redraw: &Arc<dyn RequestRedraw>,
) {
async fn wait_for_fling_settle(ctx: &mut iris::task::TaskCtx<Rsc>) {
let cap = Duration::from_millis(FLING_SETTLE_CAP_MS);
let started = Instant::now();
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(),
None => false,
})
@@ -753,7 +723,6 @@ async fn wait_for_fling_settle(
async fn run_stream_phase(
ctx: &mut iris::task::TaskCtx<Rsc>,
redraw: &Arc<dyn RequestRedraw>,
stream_tail: Vec<SeqEvent>,
) -> (usize, usize) {
ctx.update(|state: &mut BenchClient, _rsc| {
@@ -764,7 +733,6 @@ async fn run_stream_phase(
(screen.list)(rsc).jump_to_end();
}
});
redraw.request_redraw();
let total = (STREAM_EVENTS_PER_SEC * STREAM_SECONDS) as usize;
let mut sent = 0usize;
@@ -777,7 +745,6 @@ async fn run_stream_phase(
None => state.rebuild_transcript(rsc),
}
});
redraw.request_redraw();
sent += 1;
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(
ctx: &mut iris::task::TaskCtx<Rsc>,
redraw: &Arc<dyn RequestRedraw>,
platform: &Option<Arc<PlatformHandle>>,
) {
ctx.update(|state: &mut BenchClient, _rsc| {
@@ -799,7 +765,6 @@ async fn run_type_phase(
state.set_focus(Some(screen.composer.field));
}
});
redraw.request_redraw();
if let Some(p) = platform {
p.show_ime();
}
@@ -814,7 +779,6 @@ async fn run_type_phase(
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(200)).await;
@@ -826,7 +790,6 @@ async fn run_type_phase(
screen.composer.field.edit(rsc).set(&text);
}
});
redraw.request_redraw();
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>) {
let redraw = rsc.tasks.redraw_handle();
let my_generation = self.generation.load(Ordering::SeqCst);
let generation = self.generation.clone();
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.show_message(rsc, "Loading transcript...");
let redraw = rsc.tasks.redraw_handle();
let live_generation = self.generation.clone();
rsc.spawn_task(async move |mut ctx| {
let transports =
@@ -180,7 +177,6 @@ impl TranscriptClient {
state.show_message(rsc, &message);
}
});
redraw.request_redraw();
return;
}
};
@@ -214,8 +210,6 @@ impl TranscriptClient {
}
});
}
redraw.request_redraw();
if live_generation.load(Ordering::SeqCst) != my_generation {
return;
}
@@ -239,7 +233,6 @@ impl TranscriptClient {
}
state.apply_event(rsc, &event);
});
redraw.request_redraw();
true
}
},
+45 -26
View File
@@ -45,7 +45,7 @@ struct Client {
ui_state: DesktopUiState,
api: Arc<ApiClient<UreqTransport>>,
stream_transport: Arc<UreqTransport>,
proxy: Proxy<AppEvent>,
updates: TaskCtx<StdRsc<Self>>,
sessions: Vec<SessionSummary>,
selected: Option<String>,
items: Vec<TranscriptItem>,
@@ -56,9 +56,7 @@ struct Client {
}
impl DesktopAppState for Client {
type Event = AppEvent;
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, proxy: Proxy<AppEvent>) -> Self {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>) -> Self {
let (server, ca_pem) = super::startup::load_startup_config().unwrap_or_else(|e| {
eprintln!("desktop-app: {e}");
process::exit(2);
@@ -90,7 +88,7 @@ impl DesktopAppState for Client {
ui_state,
api,
stream_transport,
proxy,
updates: rsc.tasks.context(),
sessions: Vec::new(),
selected: None,
items: Vec::new(),
@@ -102,8 +100,10 @@ impl DesktopAppState for Client {
client.spawn_fetch_sessions();
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 {
AppEvent::Sessions(Ok(sessions)) => {
self.sessions = sessions;
@@ -163,11 +163,8 @@ impl DesktopAppState for Client {
eprintln!("desktop-app: couldn't send: {message}");
}
}
self.ui_state.window.request_redraw();
}
}
impl Client {
fn current(&self, session_id: &str, generation: u64) -> bool {
self.selected.as_deref() == Some(session_id)
&& self.generation.load(Ordering::SeqCst) == generation
@@ -180,10 +177,12 @@ impl Client {
fn spawn_fetch_sessions(&self) {
let api = self.api.clone();
let proxy = self.proxy.clone();
let mut updates = self.updates.clone();
thread::spawn(move || {
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 stream_transport = self.stream_transport.clone();
let proxy = self.proxy.clone();
let mut updates = self.updates.clone();
let live_generation = self.generation.clone();
thread::spawn(move || {
let page: Result<Vec<serde_json::Value>, String> = api
@@ -223,10 +222,16 @@ impl Client {
.and_then(|values| raw_seq(values.last()?))
.unwrap_or(0);
let result = page.and_then(|values| fold_page(&values));
let _ = proxy.send_event(AppEvent::TranscriptLoaded {
session_id: session_id.clone(),
generation,
result,
let loaded_session_id = session_id.clone();
updates.update(move |state: &mut Client, rsc| {
state.apply_event(
AppEvent::TranscriptLoaded {
session_id: loaded_session_id,
generation,
result,
},
rsc,
);
});
let stop = || live_generation.load(Ordering::SeqCst) != generation;
@@ -240,28 +245,42 @@ impl Client {
if stop() {
return false;
}
let _ = proxy.send_event(AppEvent::StreamEvent {
session_id: session_id.clone(),
generation,
event,
let event_session_id = session_id.clone();
updates.update(move |state: &mut Client, rsc| {
state.apply_event(
AppEvent::StreamEvent {
session_id: event_session_id,
generation,
event,
},
rsc,
);
});
true
}
});
let _ = proxy.send_event(AppEvent::StreamEnded {
session_id,
generation,
message: outcome.err().map(|e| e.to_string()),
updates.update(move |state: &mut Client, rsc| {
state.apply_event(
AppEvent::StreamEnded {
session_id,
generation,
message: outcome.err().map(|e| e.to_string()),
},
rsc,
);
});
});
}
fn send_message(&mut self, session_id: String, text: String) {
let api = self.api.clone();
let proxy = self.proxy.clone();
let mut updates = self.updates.clone();
thread::spawn(move || {
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
state/resources; Iris supplies the JNI host and APK packager.**
`#[iris::app_init]` marks the factory Android calls when it creates the Iris
view. The macro target-gates the factory and generates the single exported
`#[iris::app_init]` marks the initializer Android calls when it creates the Iris
view. The macro target-gates the initializer and generates the single exported
`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
host requirement; a custom bundle works by implementing the narrow
`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
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
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.
## One convention for a delta
+2 -2
View File
@@ -28,8 +28,8 @@ pub struct Resources {
}
impl AndroidResources<State> for Resources {
fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>) {
let (tasks, receiver) = Tasks::init(redraw);
fn new(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Self>) {
let (tasks, receiver) = Tasks::init(wake);
(
Self {
ui: Ui::default(),
+5 -13
View File
@@ -2,19 +2,11 @@ use iris::prelude::*;
#[path = "lib.rs"]
mod app;
use app::*;
#[derive(AndroidUiState)]
struct State {
ui_state: AndroidUiState,
}
impl AndroidAppState for State {
type Resources = StdRsc<Self>;
}
#[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State {
let _ = build(rsc, &mut ui_state);
State { ui_state }
fn create(
ui_state: &mut AndroidUiState,
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"]
mod app;
use app::*;
const SETTLE_FRAMES: usize = 4;
const FRAMES: usize = 6;
@@ -17,8 +16,8 @@ struct State {
}
impl DesktopAppState for State {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
let span = build(rsc, &mut ui_state);
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>) -> Self {
let span = app::build(rsc, &mut ui_state);
Self {
ui_state,
span,
+5 -13
View File
@@ -2,19 +2,11 @@ use iris::prelude::*;
#[path = "lib.rs"]
mod app;
use app::*;
#[derive(AndroidUiState)]
struct State {
ui_state: AndroidUiState,
}
impl AndroidAppState for State {
type Resources = StdRsc<Self>;
}
#[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State {
build(rsc, &mut ui_state);
State { ui_state }
fn create(
ui_state: &mut AndroidUiState,
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"]
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() {
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"]
mod app;
use app::*;
#[derive(AndroidUiState)]
struct State {
ui_state: AndroidUiState,
}
impl AndroidAppState for State {
type Resources = StdRsc<Self>;
}
#[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State {
build(rsc, &mut ui_state);
State { ui_state }
fn create(
ui_state: &mut AndroidUiState,
rsc: &mut StdRsc<AndroidUiState>,
) {
app::build(rsc, ui_state);
}
+1 -14
View File
@@ -2,20 +2,7 @@ use iris::prelude::*;
#[path = "lib.rs"]
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() {
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"]
mod app;
use app::*;
#[derive(AndroidUiState)]
struct Client {
@@ -19,14 +18,14 @@ impl AndroidAppState for Client {
.renderer
.as_ref()
.map_or(0, |renderer| renderer.ui.view_count());
update_info(rsc, self.info, views);
app::update_info(rsc, self.info, views);
}
}
#[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Client>) -> Client {
let widgets = build(rsc, &mut ui_state);
update_info(rsc, widgets.info, 0);
let widgets = app::build(rsc, &mut ui_state);
app::update_info(rsc, widgets.info, 0);
Client {
ui_state,
info: widgets.info,
+4 -5
View File
@@ -3,7 +3,6 @@ use winit::event::WindowEvent;
#[path = "lib.rs"]
mod app;
use app::*;
#[derive(DesktopUiState)]
struct Client {
@@ -12,9 +11,9 @@ struct Client {
}
impl DesktopAppState for Client {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
let widgets = build(rsc, &mut ui_state);
update_info(rsc, widgets.info, 0);
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>) -> Self {
let widgets = app::build(rsc, &mut ui_state);
app::update_info(rsc, widgets.info, 0);
Self {
ui_state,
info: widgets.info,
@@ -22,7 +21,7 @@ impl DesktopAppState for Client {
}
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"]
mod app;
use app::*;
#[derive(AndroidUiState)]
struct State {
ui_state: AndroidUiState,
}
impl AndroidAppState for State {
type Resources = StdRsc<Self>;
}
#[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State {
build(rsc, &mut ui_state);
State { ui_state }
fn create(
ui_state: &mut AndroidUiState,
rsc: &mut StdRsc<AndroidUiState>,
) {
app::build(rsc, ui_state);
}
+1 -14
View File
@@ -2,20 +2,7 @@ use iris::prelude::*;
#[path = "lib.rs"]
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() {
DesktopApp::<State>::run();
DesktopApp::<DesktopUiState>::run_with(|ui_state, rsc| app::build(rsc, ui_state));
}
+13 -12
View File
@@ -6,16 +6,17 @@ where
Rsc::State: FocusHost,
{
let rect = rect(PaintId::RED).add(rsc);
rect.task_on(CursorSense::click(), async move |mut ctx| {
iris::task::sleep(Duration::from_secs(1)).await;
ctx.update(move |_, rsc| {
let rect = rect(rsc);
if rect.is_paint(&PaintId::RED) {
rect.set_paint(PaintId::BLUE);
} else {
rect.set_paint(PaintId::RED);
}
});
})
.set_root(rsc, ui_state);
rect.label("Toggle color")
.task_on(CursorSense::click(), async move |mut ctx| {
iris::task::sleep(Duration::from_secs(1)).await;
ctx.update(move |_, rsc| {
let rect = rect(rsc);
if rect.is_paint(&PaintId::RED) {
rect.set_paint(PaintId::BLUE);
} else {
rect.set_paint(PaintId::RED);
}
});
})
.set_root(rsc, ui_state);
}
+5 -13
View File
@@ -2,19 +2,11 @@ use iris::prelude::*;
#[path = "lib.rs"]
mod app;
use app::*;
#[derive(AndroidUiState)]
struct State {
ui_state: AndroidUiState,
}
impl AndroidAppState for State {
type Resources = StdRsc<Self>;
}
#[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State {
build(rsc, &mut ui_state);
State { ui_state }
fn create(
ui_state: &mut AndroidUiState,
rsc: &mut StdRsc<AndroidUiState>,
) {
app::build(rsc, ui_state);
}
+1 -14
View File
@@ -2,20 +2,7 @@ use iris::prelude::*;
#[path = "lib.rs"]
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() {
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,
};
/// 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
/// exported `JNI_OnLoad` symbol and `android-view` requires a plain function
/// pointer monomorphized for the returned application state. The generated
/// linker and JNI glue is Android-gated; the annotated function therefore
/// does not need its own `cfg` attribute.
/// pointer monomorphized for the application state. A function returning a
/// custom state remains its factory; a function with no return value receives
/// `&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]
pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream {
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 name = &function.sig.ident;
let ReturnType::Type(_, state) = &function.sig.output else {
return Error::new(
function.sig.output.span(),
"an app_init function must return its application state",
)
.into_compile_error()
.into();
let (state, direct_initializer): (Type, bool) = match &function.sig.output {
ReturnType::Default => (parse_quote!(::iris::android::AndroidUiState), true),
ReturnType::Type(_, state) => ((**state).clone(), false),
};
if function.sig.inputs.len() != 2
|| function
@@ -46,7 +44,7 @@ pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream {
{
return Error::new(
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();
@@ -64,6 +62,25 @@ pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream {
.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! {
#[cfg(target_os = "android")]
#function
@@ -72,12 +89,14 @@ pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream {
mod __iris_android_app {
use super::*;
#factory
extern "system" fn new_view_peer<'local>(
env: ::iris::android::__private::JNIEnv<'local>,
view: ::iris::android::__private::View<'local>,
context: ::iris::android::__private::Context<'local>,
) -> ::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)]
+33 -16
View File
@@ -25,31 +25,48 @@ application-id = "com.example.myapp"
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
loader glue. The returned state chooses its resources through
`AndroidAppState::Resources`; `StdRsc` is the standard bundle, not a
requirement.
loader glue. An application with no state beyond the UI state receives
`AndroidUiState` directly by mutable reference:
```rust
use iris::prelude::*;
#[derive(AndroidUiState)]
struct State {
ui_state: AndroidUiState,
}
impl AndroidAppState for State {
type Resources = StdRsc<Self>;
}
#[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State {
rect(PaintId::RED).set_root(rsc, &mut ui_state);
State { ui_state }
fn create(
ui_state: &mut AndroidUiState,
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
application's directory:
+8 -8
View File
@@ -1,4 +1,4 @@
use crate::task::RequestRedraw;
use crate::task::WakeTaskQueue;
use android_view::{
View,
jni::{JavaVM, objects::GlobalRef},
@@ -429,23 +429,23 @@ impl AndroidRenderer {
}
}
/// `Tasks`' redraw handle on Android: a background task finishes on the
/// tokio thread `Tasks::init` spawned, which is not attached to the JVM, so
/// asking for a frame means attaching first. The global ref is what
/// `Tasks`' UI-thread wake on Android. Updates can be submitted from the
/// tokio runtime or an application-owned thread that is not attached to the
/// JVM, so posting the callback means attaching first. The global ref is what
/// survives past the JNI call that handed the `View` to us.
pub struct AndroidRedrawHandle {
pub struct AndroidTaskWake {
vm: JavaVM,
view: GlobalRef,
}
impl AndroidRedrawHandle {
impl AndroidTaskWake {
pub fn new(vm: JavaVM, view: GlobalRef) -> Self {
Self { vm, view }
}
}
impl RequestRedraw for AndroidRedrawHandle {
fn request_redraw(&self) {
impl WakeTaskQueue for AndroidTaskWake {
fn wake(&self) {
let Ok(mut env) = self.vm.attach_current_thread() else {
return;
};
+31 -19
View File
@@ -1,5 +1,5 @@
use crate::prelude::*;
use crate::task::RequestRedraw;
use crate::task::WakeTaskQueue;
use accesskit_android::Adapter as AccessAdapter;
use android_view::{
AccessibilityNodeInfo, AccessibilityNodeProvider, Bundle, CallbackCtx, Context,
@@ -16,7 +16,7 @@ use std::{cell::RefCell, marker::Sized, rc::Rc, sync::Arc, time::Instant};
use super::{
access::{AndroidAccessSource, NullActionHandler, raise_if_enabled},
insets::{Insets, Shared},
render::{AndroidRedrawHandle, AndroidRenderer},
render::{AndroidRenderer, AndroidTaskWake},
};
/// 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;
}
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`.
///
/// [`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) {}
}
impl AndroidAppState for AndroidUiState {
type Resources = StdRsc<Self>;
}
/// Resources the Android host needs to draw and dispatch application events.
///
/// This deliberately names capabilities rather than storage. Custom bundles
@@ -128,12 +142,12 @@ pub trait AndroidResources<State>:
where
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> {
fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>) {
StdRsc::new(redraw)
fn new(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Self>) {
StdRsc::new(wake)
}
}
@@ -223,8 +237,12 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
self.update_ime_selection(ctx);
let ui_state = self.state.android_state_mut();
ui_state.cursor.end_frame();
self.state.android_state_mut().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();
if render_state
.get()
@@ -720,18 +738,12 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
self.render(ctx, now);
}
/// Where `AndroidRedrawHandle::request_redraw` (`android/render.rs`)
/// actually lands: `View.postDelayed`'s Runnable resolves to this, on
/// the UI thread, which is what makes it safe to call from a background
/// 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.
/// Where `AndroidTaskWake::wake` lands on the UI thread. Applying an
/// update and drawing it are deliberately separate: retained widget
/// invalidation decides whether this wake needs a frame.
fn delayed_callback(&mut self, ctx: &mut CallbackCtx) {
self.drain_tasks();
self.render(ctx, Instant::now());
self.request_frame_if_needed(ctx);
}
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}");
let vm = env.get_java_vm().unwrap();
let global_view = env.new_global_ref(&view.0).unwrap();
let redraw: Arc<dyn RequestRedraw> = Arc::new(AndroidRedrawHandle::new(vm, global_view));
let (mut rsc, task_recv) = State::Resources::new(redraw);
let wake: Arc<dyn WakeTaskQueue> = Arc::new(AndroidTaskWake::new(vm, global_view));
let (mut rsc, task_recv) = State::Resources::new(wake);
rsc.ui_mut().set_density(content_scale);
let shared = Rc::new(RefCell::new(Shared::default()));
let ui_state = AndroidUiState::new(shared.clone(), content_scale);
+18 -19
View File
@@ -5,41 +5,40 @@ use winit::{
window::WindowId,
};
pub trait AppState {
type Event: 'static;
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self;
pub trait AppState: 'static {
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 run()
where
Self: Sized,
{
App::<Self>::run();
}
}
pub struct App<State: AppState> {
state: Option<State>,
proxy: EventLoopProxy<State::Event>,
proxy: EventLoopProxy<()>,
init: Option<Box<Init<State>>>,
}
impl<State: AppState> App<State> {
pub fn run() {
type Init<State> = dyn FnOnce(&ActiveEventLoop, EventLoopProxy<()>) -> State;
impl<State: AppState + 'static> App<State> {
pub fn run_with(init: impl FnOnce(&ActiveEventLoop, EventLoopProxy<()>) -> State + 'static) {
super::logging::install(log::LevelFilter::Info);
let event_loop = EventLoop::with_user_event().build().unwrap();
let proxy = event_loop.create_proxy();
event_loop
.run_app(&mut App::<State> { state: None, proxy })
.run_app(&mut App::<State> {
state: None,
proxy,
init: Some(Box::new(init)),
})
.unwrap();
}
}
impl<State: AppState> ApplicationHandler<State::Event> for App<State> {
impl<State: AppState> ApplicationHandler<()> for App<State> {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
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);
}
}
@@ -49,9 +48,9 @@ impl<State: AppState> ApplicationHandler<State::Event> for App<State> {
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();
state.event(event, event_loop);
state.tasks_ready();
}
fn exiting(&mut self, _: &ActiveEventLoop) {
+76 -14
View File
@@ -16,11 +16,15 @@ mod platform;
mod render;
pub use access::*;
pub use app::*;
use app::{App, AppState};
pub use input::*;
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
/// `dp(...)` resolves through this scale.
@@ -79,11 +83,18 @@ pub trait HasDesktopUiState: Sized + 'static {
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 {
type Event = ();
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>) {}
fn new(ui_state: DesktopUiState, rsc: &mut StdRsc<Self>) -> Self;
#[allow(unused_variables)]
fn exit(&mut self, rsc: &mut StdRsc<Self>) {}
#[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> {
rsc: StdRsc<State>,
state: State,
task_recv: TaskMsgReceiver<StdRsc<State>>,
}
impl<State: DesktopAppState> AppState for DesktopApp<State> {
type Event = State::Event;
impl<State: DesktopAppState> DesktopApp<State> {
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
.create_window(State::window_attributes().with_visible(false))
.create_window(attributes.with_visible(false))
.unwrap();
let access_adapter = accesskit_winit::Adapter::with_direct_handlers(
event_loop,
@@ -115,20 +141,56 @@ impl<State: DesktopAppState> AppState for DesktopApp<State> {
);
window.set_visible(true);
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.
let scale = content_scale(desktop_state.window.as_ref());
rsc.ui.set_density(scale);
let state = State::new(desktop_state, &mut rsc, proxy);
let state = init(desktop_state, &mut rsc);
Self {
rsc,
state,
task_recv,
}
}
}
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
self.state.event(event, &mut self.rsc);
impl DesktopApp<DesktopUiState> {
/// 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) {
-7
View File
@@ -1,4 +1,3 @@
use crate::task::RequestRedraw;
use iris_core::{FrameParts, LinearRgba, Ui, UiRenderNode, util::Vec2};
use pollster::FutureExt;
use std::sync::Arc;
@@ -8,12 +7,6 @@ use winit::{dpi::PhysicalSize, window::Window};
pub const CLEAR_COLOR: LinearRgba = LinearRgba::BLACK;
impl RequestRedraw for Window {
fn request_redraw(&self) {
Window::request_redraw(self);
}
}
pub struct UiRenderer {
window: Arc<Window>,
surface: Surface<'static>,
+2 -2
View File
@@ -103,8 +103,8 @@ impl RedrawCounter {
}
}
impl RequestRedraw for RedrawCounter {
fn request_redraw(&self) {
impl WakeTaskQueue for RedrawCounter {
fn wake(&self) {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
-3
View File
@@ -1,8 +1,5 @@
#![feature(unboxed_closures)]
#![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(option_into_flat_iter)]
#![feature(async_fn_traits)]
+2 -2
View File
@@ -33,8 +33,8 @@ pub struct StdRsc<State: 'static> {
}
impl<State> StdRsc<State> {
pub(crate) fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>) {
let (tasks, receiver) = Tasks::init(redraw);
pub(crate) fn new(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Self>) {
let (tasks, receiver) = Tasks::init(wake);
(
Self {
ui: Ui::default(),
+72 -28
View File
@@ -19,15 +19,13 @@ pub async fn sleep(duration: std::time::Duration) {
tokio::time::sleep(duration).await;
}
/// What a completed task nudges when it wants its result drawn. Shared
/// between backends rather than typed as `winit::window::Window` directly:
/// android-view has no `Window` at all, and the redraw request there is a
/// JNI call (`View::post_frame_callback`) rather than a method call on a
/// value this crate owns. Each backend supplies its own implementation --
/// `desktop/render.rs` for winit, `android/render.rs` for android-view --
/// and this module never needs to know which one it is holding.
pub trait RequestRedraw: Send + Sync + 'static {
fn request_redraw(&self);
/// Wakes the platform UI thread so it can apply queued task updates.
///
/// Waking does not itself mean drawing. Once the updates have run, the host
/// asks the retained UI tree whether anything visible became dirty and only
/// then schedules a frame.
pub trait WakeTaskQueue: Send + Sync + 'static {
fn wake(&self);
}
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> {
start: AsyncSender<BoxTask>,
redraw: Arc<dyn RequestRedraw>,
wake: Arc<dyn WakeTaskQueue>,
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
}
pub struct TaskCtx<Rsc: HasState> {
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> {
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> {
fn new(send: TaskMsgSender<Rsc>) -> Self {
Self { send }
fn new(send: TaskMsgSender<Rsc>, wake: Arc<dyn WakeTaskQueue>) -> Self {
Self { send, wake }
}
}
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
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 (msgs, msgs_recv) = sync_channel();
std::thread::spawn(|| {
@@ -71,22 +81,16 @@ impl<Rsc: HasState> Tasks<Rsc> {
Self {
start,
msg_send: msgs,
redraw,
wake,
},
msgs_recv,
)
}
/// The same redraw handle `spawn`'s wrapper calls once, after a whole
/// task's future completes -- exposed so a caller running its own
/// longer-lived loop *inside* a spawned task (a live SSE follow, here)
/// can ask for a frame after each `TaskCtx::update`, not just at the
/// 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()
/// A cloneable, platform-neutral way for an application-owned thread to
/// submit work to the UI thread.
pub fn context(&self) -> TaskCtx<Rsc> {
TaskCtx::new(self.msg_send.clone(), self.wake.clone())
}
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,
{
let send = self.msg_send.clone();
let redraw = self.redraw.clone();
let wake = self.wake.clone();
let _ = self.start.send(Box::pin(async move {
task(TaskCtx::new(send)).await;
redraw.request_redraw();
task(TaskCtx::new(send, wake)).await;
}));
}
}
@@ -107,3 +110,44 @@ async fn listen(mut recv: AsyncReceiver<BoxTask>) {
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);
}
}