Simplify Iris app initialization and task updates
This commit is contained in:
1 parent
8218e84b62
commit
ecf74055c7
32 files changed
+391
-359
No files matched your search
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
|
||||
+38
-19
@@ -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(),
|
||||
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(),
|
||||
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 {
|
||||
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
@@ -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
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -6,7 +6,8 @@ where
|
||||
Rsc::State: FocusHost,
|
||||
{
|
||||
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;
|
||||
ctx.update(move |_, rsc| {
|
||||
let rect = rect(rsc);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user