Simplify Iris app initialization and task updates

This commit is contained in:
iris committed 2026-09-11 12:28:33 -04:00
1 parent b5666cdef9
commit 166bac2a93
25 files changed
+319 -271

No files matched your search

+2 -2
View File
@@ -28,8 +28,8 @@ pub struct Resources {
} }
impl AndroidResources<State> for Resources { impl AndroidResources<State> for Resources {
fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>) { fn new(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Self>) {
let (tasks, receiver) = Tasks::init(redraw); let (tasks, receiver) = Tasks::init(wake);
( (
Self { Self {
ui: Ui::default(), ui: Ui::default(),
+5 -13
View File
@@ -2,19 +2,11 @@ use iris::prelude::*;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(AndroidUiState)]
struct State {
ui_state: AndroidUiState,
}
impl AndroidAppState for State {
type Resources = StdRsc<Self>;
}
#[iris::app_init] #[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State { fn create(
let _ = build(rsc, &mut ui_state); ui_state: &mut AndroidUiState,
State { ui_state } rsc: &mut StdRsc<AndroidUiState>,
) {
let _ = app::build(rsc, ui_state);
} }
+2 -3
View File
@@ -3,7 +3,6 @@ use winit::event::WindowEvent;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
const SETTLE_FRAMES: usize = 4; const SETTLE_FRAMES: usize = 4;
const FRAMES: usize = 6; const FRAMES: usize = 6;
@@ -17,8 +16,8 @@ struct State {
} }
impl DesktopAppState for State { impl DesktopAppState for State {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self { fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>) -> Self {
let span = build(rsc, &mut ui_state); let span = app::build(rsc, &mut ui_state);
Self { Self {
ui_state, ui_state,
span, span,
+5 -13
View File
@@ -2,19 +2,11 @@ use iris::prelude::*;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(AndroidUiState)]
struct State {
ui_state: AndroidUiState,
}
impl AndroidAppState for State {
type Resources = StdRsc<Self>;
}
#[iris::app_init] #[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State { fn create(
build(rsc, &mut ui_state); ui_state: &mut AndroidUiState,
State { ui_state } rsc: &mut StdRsc<AndroidUiState>,
) {
app::build(rsc, ui_state);
} }
+4 -18
View File
@@ -3,24 +3,10 @@ use winit::{dpi::LogicalSize, window::WindowAttributes};
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(DesktopUiState)]
struct State {
ui_state: DesktopUiState,
}
impl DesktopAppState for State {
fn window_attributes() -> WindowAttributes {
WindowAttributes::default().with_inner_size(LogicalSize::new(420.0, 900.0))
}
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
build(rsc, &mut ui_state);
Self { ui_state }
}
}
fn main() { fn main() {
DesktopApp::<State>::run(); let attributes = WindowAttributes::default().with_inner_size(LogicalSize::new(420.0, 900.0));
DesktopApp::<DesktopUiState>::run_with_attributes(attributes, |ui_state, rsc| {
app::build(rsc, ui_state)
});
} }
+5 -13
View File
@@ -2,19 +2,11 @@ use iris::prelude::*;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(AndroidUiState)]
struct State {
ui_state: AndroidUiState,
}
impl AndroidAppState for State {
type Resources = StdRsc<Self>;
}
#[iris::app_init] #[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State { fn create(
build(rsc, &mut ui_state); ui_state: &mut AndroidUiState,
State { ui_state } rsc: &mut StdRsc<AndroidUiState>,
) {
app::build(rsc, ui_state);
} }
+1 -14
View File
@@ -2,20 +2,7 @@ use iris::prelude::*;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(DesktopUiState)]
struct State {
ui_state: DesktopUiState,
}
impl DesktopAppState for State {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
build(rsc, &mut ui_state);
Self { ui_state }
}
}
fn main() { fn main() {
DesktopApp::<State>::run(); DesktopApp::<DesktopUiState>::run_with(|ui_state, rsc| app::build(rsc, ui_state));
} }
+3 -4
View File
@@ -2,7 +2,6 @@ use iris::prelude::*;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(AndroidUiState)] #[derive(AndroidUiState)]
struct Client { struct Client {
@@ -19,14 +18,14 @@ impl AndroidAppState for Client {
.renderer .renderer
.as_ref() .as_ref()
.map_or(0, |renderer| renderer.ui.view_count()); .map_or(0, |renderer| renderer.ui.view_count());
update_info(rsc, self.info, views); app::update_info(rsc, self.info, views);
} }
} }
#[iris::app_init] #[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Client>) -> Client { fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Client>) -> Client {
let widgets = build(rsc, &mut ui_state); let widgets = app::build(rsc, &mut ui_state);
update_info(rsc, widgets.info, 0); app::update_info(rsc, widgets.info, 0);
Client { Client {
ui_state, ui_state,
info: widgets.info, info: widgets.info,
+4 -5
View File
@@ -3,7 +3,6 @@ use winit::event::WindowEvent;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(DesktopUiState)] #[derive(DesktopUiState)]
struct Client { struct Client {
@@ -12,9 +11,9 @@ struct Client {
} }
impl DesktopAppState for Client { impl DesktopAppState for Client {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self { fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>) -> Self {
let widgets = build(rsc, &mut ui_state); let widgets = app::build(rsc, &mut ui_state);
update_info(rsc, widgets.info, 0); app::update_info(rsc, widgets.info, 0);
Self { Self {
ui_state, ui_state,
info: widgets.info, info: widgets.info,
@@ -22,7 +21,7 @@ impl DesktopAppState for Client {
} }
fn window_event(&mut self, _: WindowEvent, rsc: &mut StdRsc<Self>) { fn window_event(&mut self, _: WindowEvent, rsc: &mut StdRsc<Self>) {
update_info(rsc, self.info, self.ui_state.renderer.ui.view_count()); app::update_info(rsc, self.info, self.ui_state.renderer.ui.view_count());
} }
} }
+5 -13
View File
@@ -2,19 +2,11 @@ use iris::prelude::*;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(AndroidUiState)]
struct State {
ui_state: AndroidUiState,
}
impl AndroidAppState for State {
type Resources = StdRsc<Self>;
}
#[iris::app_init] #[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State { fn create(
build(rsc, &mut ui_state); ui_state: &mut AndroidUiState,
State { ui_state } rsc: &mut StdRsc<AndroidUiState>,
) {
app::build(rsc, ui_state);
} }
+1 -14
View File
@@ -2,20 +2,7 @@ use iris::prelude::*;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(DesktopUiState)]
struct State {
ui_state: DesktopUiState,
}
impl DesktopAppState for State {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
build(rsc, &mut ui_state);
Self { ui_state }
}
}
fn main() { fn main() {
DesktopApp::<State>::run(); DesktopApp::<DesktopUiState>::run_with(|ui_state, rsc| app::build(rsc, ui_state));
} }
+2 -1
View File
@@ -6,7 +6,8 @@ where
Rsc::State: FocusHost, Rsc::State: FocusHost,
{ {
let rect = rect(PaintId::RED).add(rsc); let rect = rect(PaintId::RED).add(rsc);
rect.task_on(CursorSense::click(), async move |mut ctx| { rect.label("Toggle color")
.task_on(CursorSense::click(), async move |mut ctx| {
iris::task::sleep(Duration::from_secs(1)).await; iris::task::sleep(Duration::from_secs(1)).await;
ctx.update(move |_, rsc| { ctx.update(move |_, rsc| {
let rect = rect(rsc); let rect = rect(rsc);
+5 -13
View File
@@ -2,19 +2,11 @@ use iris::prelude::*;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(AndroidUiState)]
struct State {
ui_state: AndroidUiState,
}
impl AndroidAppState for State {
type Resources = StdRsc<Self>;
}
#[iris::app_init] #[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State { fn create(
build(rsc, &mut ui_state); ui_state: &mut AndroidUiState,
State { ui_state } rsc: &mut StdRsc<AndroidUiState>,
) {
app::build(rsc, ui_state);
} }
+1 -14
View File
@@ -2,20 +2,7 @@ use iris::prelude::*;
#[path = "lib.rs"] #[path = "lib.rs"]
mod app; mod app;
use app::*;
#[derive(DesktopUiState)]
struct State {
ui_state: DesktopUiState,
}
impl DesktopAppState for State {
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, _: Proxy<Self::Event>) -> Self {
build(rsc, &mut ui_state);
Self { ui_state }
}
}
fn main() { fn main() {
DesktopApp::<State>::run(); DesktopApp::<DesktopUiState>::run_with(|ui_state, rsc| app::build(rsc, ui_state));
} }
+32 -13
View File
@@ -9,13 +9,15 @@ use syn::{
spanned::Spanned, spanned::Spanned,
}; };
/// Marks the factory called when Android creates an Iris view. /// Marks the initializer called when Android creates an Iris view.
/// ///
/// An attribute is necessary here because the Android loader requires one /// An attribute is necessary here because the Android loader requires one
/// exported `JNI_OnLoad` symbol and `android-view` requires a plain function /// exported `JNI_OnLoad` symbol and `android-view` requires a plain function
/// pointer monomorphized for the returned application state. The generated /// pointer monomorphized for the application state. A function returning a
/// linker and JNI glue is Android-gated; the annotated function therefore /// custom state remains its factory; a function with no return value receives
/// does not need its own `cfg` attribute. /// `&mut AndroidUiState` and uses that state directly. The generated linker and
/// JNI glue is Android-gated; the annotated function therefore does not need
/// its own `cfg` attribute.
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream { pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream {
if !args.is_empty() { if !args.is_empty() {
@@ -29,13 +31,9 @@ pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream {
let function = parse_macro_input!(item as ItemFn); let function = parse_macro_input!(item as ItemFn);
let name = &function.sig.ident; let name = &function.sig.ident;
let ReturnType::Type(_, state) = &function.sig.output else { let (state, direct_initializer): (Type, bool) = match &function.sig.output {
return Error::new( ReturnType::Default => (parse_quote!(::iris::android::AndroidUiState), true),
function.sig.output.span(), ReturnType::Type(_, state) => ((**state).clone(), false),
"an app_init function must return its application state",
)
.into_compile_error()
.into();
}; };
if function.sig.inputs.len() != 2 if function.sig.inputs.len() != 2
|| function || function
@@ -46,7 +44,7 @@ pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream {
{ {
return Error::new( return Error::new(
function.sig.inputs.span(), function.sig.inputs.span(),
"an app_init function takes AndroidUiState and &mut State::Resources", "an app_init function takes UI state and resources",
) )
.into_compile_error() .into_compile_error()
.into(); .into();
@@ -64,6 +62,25 @@ pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream {
.into(); .into();
} }
let factory = if direct_initializer {
quote! {
fn init(
mut ui_state: ::iris::android::AndroidUiState,
rsc: &mut <#state as ::iris::android::AndroidAppState>::Resources,
) -> #state {
super::#name(&mut ui_state, rsc);
ui_state
}
}
} else {
quote! {}
};
let create = if direct_initializer {
quote! { init }
} else {
quote! { super::#name }
};
quote! { quote! {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
#function #function
@@ -72,12 +89,14 @@ pub fn app_init(args: TokenStream, item: TokenStream) -> TokenStream {
mod __iris_android_app { mod __iris_android_app {
use super::*; use super::*;
#factory
extern "system" fn new_view_peer<'local>( extern "system" fn new_view_peer<'local>(
env: ::iris::android::__private::JNIEnv<'local>, env: ::iris::android::__private::JNIEnv<'local>,
view: ::iris::android::__private::View<'local>, view: ::iris::android::__private::View<'local>,
context: ::iris::android::__private::Context<'local>, context: ::iris::android::__private::Context<'local>,
) -> ::iris::android::__private::JLong { ) -> ::iris::android::__private::JLong {
::iris::android::new_peer::<#state>(env, view, context, super::#name) ::iris::android::new_peer::<#state>(env, view, context, #create)
} }
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
+33 -16
View File
@@ -25,31 +25,48 @@ application-id = "com.example.myapp"
label = "My app" label = "My app"
``` ```
`#[iris::app_init]` marks the factory called when Android creates the Iris `#[iris::app_init]` marks the initializer called when Android creates the Iris
view. The attribute supplies its own Android target gate and generates the JNI view. The attribute supplies its own Android target gate and generates the JNI
loader glue. The returned state chooses its resources through loader glue. An application with no state beyond the UI state receives
`AndroidAppState::Resources`; `StdRsc` is the standard bundle, not a `AndroidUiState` directly by mutable reference:
requirement.
```rust ```rust
use iris::prelude::*; use iris::prelude::*;
#[derive(AndroidUiState)]
struct State {
ui_state: AndroidUiState,
}
impl AndroidAppState for State {
type Resources = StdRsc<Self>;
}
#[iris::app_init] #[iris::app_init]
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<State>) -> State { fn create(
rect(PaintId::RED).set_root(rsc, &mut ui_state); ui_state: &mut AndroidUiState,
State { ui_state } rsc: &mut StdRsc<AndroidUiState>,
) {
rect(PaintId::RED).set_root(rsc, ui_state);
} }
``` ```
Desktop has the corresponding initializer form:
```rust
DesktopApp::<DesktopUiState>::run_with(|ui_state, rsc| {
rect(PaintId::RED).set_root(rsc, ui_state);
});
```
Background work updates either host through the same task context. An update
wakes the UI thread; Iris schedules a frame automatically if the closure made
the retained widget tree dirty:
```rust
rsc.spawn_task(async move |mut ctx| {
let text = load_text().await;
ctx.update(move |_, rsc| label.edit(rsc).set(&text));
});
```
Applications do not need a winit event proxy or an explicit redraw request.
Applications with additional fields use their own state type. Its
`AndroidAppState::Resources` associated type can also replace `StdRsc` with a
custom resource bundle.
Install the Cargo subcommand from a checkout, then invoke it from the Install the Cargo subcommand from a checkout, then invoke it from the
application's directory: application's directory:
+8 -8
View File
@@ -1,4 +1,4 @@
use crate::task::RequestRedraw; use crate::task::WakeTaskQueue;
use android_view::{ use android_view::{
View, View,
jni::{JavaVM, objects::GlobalRef}, jni::{JavaVM, objects::GlobalRef},
@@ -429,23 +429,23 @@ impl AndroidRenderer {
} }
} }
/// `Tasks`' redraw handle on Android: a background task finishes on the /// `Tasks`' UI-thread wake on Android. Updates can be submitted from the
/// tokio thread `Tasks::init` spawned, which is not attached to the JVM, so /// tokio runtime or an application-owned thread that is not attached to the
/// asking for a frame means attaching first. The global ref is what /// JVM, so posting the callback means attaching first. The global ref is what
/// survives past the JNI call that handed the `View` to us. /// survives past the JNI call that handed the `View` to us.
pub struct AndroidRedrawHandle { pub struct AndroidTaskWake {
vm: JavaVM, vm: JavaVM,
view: GlobalRef, view: GlobalRef,
} }
impl AndroidRedrawHandle { impl AndroidTaskWake {
pub fn new(vm: JavaVM, view: GlobalRef) -> Self { pub fn new(vm: JavaVM, view: GlobalRef) -> Self {
Self { vm, view } Self { vm, view }
} }
} }
impl RequestRedraw for AndroidRedrawHandle { impl WakeTaskQueue for AndroidTaskWake {
fn request_redraw(&self) { fn wake(&self) {
let Ok(mut env) = self.vm.attach_current_thread() else { let Ok(mut env) = self.vm.attach_current_thread() else {
return; return;
}; };
+31 -19
View File
@@ -1,5 +1,5 @@
use crate::prelude::*; use crate::prelude::*;
use crate::task::RequestRedraw; use crate::task::WakeTaskQueue;
use accesskit_android::Adapter as AccessAdapter; use accesskit_android::Adapter as AccessAdapter;
use android_view::{ use android_view::{
AccessibilityNodeInfo, AccessibilityNodeProvider, Bundle, CallbackCtx, Context, AccessibilityNodeInfo, AccessibilityNodeProvider, Bundle, CallbackCtx, Context,
@@ -16,7 +16,7 @@ use std::{cell::RefCell, marker::Sized, rc::Rc, sync::Arc, time::Instant};
use super::{ use super::{
access::{AndroidAccessSource, NullActionHandler, raise_if_enabled}, access::{AndroidAccessSource, NullActionHandler, raise_if_enabled},
insets::{Insets, Shared}, insets::{Insets, Shared},
render::{AndroidRedrawHandle, AndroidRenderer}, render::{AndroidRenderer, AndroidTaskWake},
}; };
/// Android host state. The renderer follows the `SurfaceView` lifecycle. /// Android host state. The renderer follows the `SurfaceView` lifecycle.
@@ -100,6 +100,16 @@ pub trait HasAndroidUiState: Sized + 'static {
fn android_state_mut(&mut self) -> &mut AndroidUiState; fn android_state_mut(&mut self) -> &mut AndroidUiState;
} }
impl HasAndroidUiState for AndroidUiState {
fn android_state(&self) -> &AndroidUiState {
self
}
fn android_state_mut(&mut self) -> &mut AndroidUiState {
self
}
}
/// Application state retained for the lifetime of one Android `View`. /// Application state retained for the lifetime of one Android `View`.
/// ///
/// [`StdRsc`] is the usual [`AndroidResources`] implementation, but the host only /// [`StdRsc`] is the usual [`AndroidResources`] implementation, but the host only
@@ -118,6 +128,10 @@ pub trait AndroidAppState: HasAndroidUiState {
fn on_insets_changed(&mut self, rsc: &mut Self::Resources, insets: WindowInsets) {} fn on_insets_changed(&mut self, rsc: &mut Self::Resources, insets: WindowInsets) {}
} }
impl AndroidAppState for AndroidUiState {
type Resources = StdRsc<Self>;
}
/// Resources the Android host needs to draw and dispatch application events. /// Resources the Android host needs to draw and dispatch application events.
/// ///
/// This deliberately names capabilities rather than storage. Custom bundles /// This deliberately names capabilities rather than storage. Custom bundles
@@ -128,12 +142,12 @@ pub trait AndroidResources<State>:
where where
State: 'static, State: 'static,
{ {
fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>); fn new(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Self>);
} }
impl<State: 'static> AndroidResources<State> for StdRsc<State> { impl<State: 'static> AndroidResources<State> for StdRsc<State> {
fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>) { fn new(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Self>) {
StdRsc::new(redraw) StdRsc::new(wake)
} }
} }
@@ -223,8 +237,12 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
self.update_ime_selection(ctx); self.update_ime_selection(ctx);
let ui_state = self.state.android_state_mut(); self.state.android_state_mut().cursor.end_frame();
ui_state.cursor.end_frame(); self.request_frame_if_needed(ctx);
}
fn request_frame_if_needed(&self, ctx: &mut CallbackCtx) {
let ui_state = self.state.android_state();
let render_state = self.rsc.ui().render_state(); let render_state = self.rsc.ui().render_state();
if render_state if render_state
.get() .get()
@@ -720,18 +738,12 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
self.render(ctx, now); self.render(ctx, now);
} }
/// Where `AndroidRedrawHandle::request_redraw` (`android/render.rs`) /// Where `AndroidTaskWake::wake` lands on the UI thread. Applying an
/// actually lands: `View.postDelayed`'s Runnable resolves to this, on /// update and drawing it are deliberately separate: retained widget
/// the UI thread, which is what makes it safe to call from a background /// invalidation decides whether this wake needs a frame.
/// task's own thread when `post_frame_callback`'s `Choreographer`
/// requirement (a `Looper` on the *calling* thread) is not. Same body
/// as `do_frame` -- draining tasks and rendering immediately is a
/// perfectly good answer to "a background fetch has new state," and
/// avoids a second frame-scheduling path to keep in sync with the real
/// one.
fn delayed_callback(&mut self, ctx: &mut CallbackCtx) { fn delayed_callback(&mut self, ctx: &mut CallbackCtx) {
self.drain_tasks(); self.drain_tasks();
self.render(ctx, Instant::now()); self.request_frame_if_needed(ctx);
} }
fn as_input_connection(&mut self) -> Option<&mut dyn InputConnection> { fn as_input_connection(&mut self) -> Option<&mut dyn InputConnection> {
@@ -835,8 +847,8 @@ pub fn new_peer<'local, State: AndroidAppState>(
log::info!("iris: new_peer content_scale={content_scale}"); log::info!("iris: new_peer content_scale={content_scale}");
let vm = env.get_java_vm().unwrap(); let vm = env.get_java_vm().unwrap();
let global_view = env.new_global_ref(&view.0).unwrap(); let global_view = env.new_global_ref(&view.0).unwrap();
let redraw: Arc<dyn RequestRedraw> = Arc::new(AndroidRedrawHandle::new(vm, global_view)); let wake: Arc<dyn WakeTaskQueue> = Arc::new(AndroidTaskWake::new(vm, global_view));
let (mut rsc, task_recv) = State::Resources::new(redraw); let (mut rsc, task_recv) = State::Resources::new(wake);
rsc.ui_mut().set_density(content_scale); rsc.ui_mut().set_density(content_scale);
let shared = Rc::new(RefCell::new(Shared::default())); let shared = Rc::new(RefCell::new(Shared::default()));
let ui_state = AndroidUiState::new(shared.clone(), content_scale); let ui_state = AndroidUiState::new(shared.clone(), content_scale);
+18 -19
View File
@@ -5,41 +5,40 @@ use winit::{
window::WindowId, window::WindowId,
}; };
pub trait AppState { pub trait AppState: 'static {
type Event: 'static;
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self;
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop); fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop);
fn event(&mut self, event: Self::Event, event_loop: &ActiveEventLoop); fn tasks_ready(&mut self);
fn exit(&mut self); fn exit(&mut self);
fn run()
where
Self: Sized,
{
App::<Self>::run();
}
} }
pub struct App<State: AppState> { pub struct App<State: AppState> {
state: Option<State>, state: Option<State>,
proxy: EventLoopProxy<State::Event>, proxy: EventLoopProxy<()>,
init: Option<Box<Init<State>>>,
} }
impl<State: AppState> App<State> { type Init<State> = dyn FnOnce(&ActiveEventLoop, EventLoopProxy<()>) -> State;
pub fn run() {
impl<State: AppState + 'static> App<State> {
pub fn run_with(init: impl FnOnce(&ActiveEventLoop, EventLoopProxy<()>) -> State + 'static) {
super::logging::install(log::LevelFilter::Info); super::logging::install(log::LevelFilter::Info);
let event_loop = EventLoop::with_user_event().build().unwrap(); let event_loop = EventLoop::with_user_event().build().unwrap();
let proxy = event_loop.create_proxy(); let proxy = event_loop.create_proxy();
event_loop event_loop
.run_app(&mut App::<State> { state: None, proxy }) .run_app(&mut App::<State> {
state: None,
proxy,
init: Some(Box::new(init)),
})
.unwrap(); .unwrap();
} }
} }
impl<State: AppState> ApplicationHandler<State::Event> for App<State> { impl<State: AppState> ApplicationHandler<()> for App<State> {
fn resumed(&mut self, event_loop: &ActiveEventLoop) { fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.state.is_none() { if self.state.is_none() {
let state = State::new(event_loop, self.proxy.clone()); let init = self.init.take().unwrap();
let state = init(event_loop, self.proxy.clone());
self.state = Some(state); self.state = Some(state);
} }
} }
@@ -49,9 +48,9 @@ impl<State: AppState> ApplicationHandler<State::Event> for App<State> {
state.window_event(event, event_loop); state.window_event(event, event_loop);
} }
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: State::Event) { fn user_event(&mut self, _: &ActiveEventLoop, (): ()) {
let state = self.state.as_mut().unwrap(); let state = self.state.as_mut().unwrap();
state.event(event, event_loop); state.tasks_ready();
} }
fn exiting(&mut self, _: &ActiveEventLoop) { fn exiting(&mut self, _: &ActiveEventLoop) {
+76 -14
View File
@@ -16,11 +16,15 @@ mod platform;
mod render; mod render;
pub use access::*; pub use access::*;
pub use app::*; use app::{App, AppState};
pub use input::*; pub use input::*;
pub use render::*; pub use render::*;
pub type Proxy<Event> = EventLoopProxy<Event>; impl WakeTaskQueue for EventLoopProxy<()> {
fn wake(&self) {
let _ = self.send_event(());
}
}
/// Physical pixels per dp. Layout and input stay in physical pixels; only /// Physical pixels per dp. Layout and input stay in physical pixels; only
/// `dp(...)` resolves through this scale. /// `dp(...)` resolves through this scale.
@@ -79,11 +83,18 @@ pub trait HasDesktopUiState: Sized + 'static {
fn desktop_state_mut(&mut self) -> &mut DesktopUiState; fn desktop_state_mut(&mut self) -> &mut DesktopUiState;
} }
impl HasDesktopUiState for DesktopUiState {
fn desktop_state(&self) -> &DesktopUiState {
self
}
fn desktop_state_mut(&mut self) -> &mut DesktopUiState {
self
}
}
pub trait DesktopAppState: HasDesktopUiState { pub trait DesktopAppState: HasDesktopUiState {
type Event = (); fn new(ui_state: DesktopUiState, rsc: &mut StdRsc<Self>) -> Self;
fn new(ui_state: DesktopUiState, rsc: &mut StdRsc<Self>, proxy: Proxy<Self::Event>) -> Self;
#[allow(unused_variables)]
fn event(&mut self, event: Self::Event, rsc: &mut StdRsc<Self>) {}
#[allow(unused_variables)] #[allow(unused_variables)]
fn exit(&mut self, rsc: &mut StdRsc<Self>) {} fn exit(&mut self, rsc: &mut StdRsc<Self>) {}
#[allow(unused_variables)] #[allow(unused_variables)]
@@ -93,18 +104,33 @@ pub trait DesktopAppState: HasDesktopUiState {
} }
} }
impl DesktopAppState for DesktopUiState {
fn new(ui_state: DesktopUiState, _: &mut StdRsc<Self>) -> Self {
ui_state
}
}
pub struct DesktopApp<State: DesktopAppState> { pub struct DesktopApp<State: DesktopAppState> {
rsc: StdRsc<State>, rsc: StdRsc<State>,
state: State, state: State,
task_recv: TaskMsgReceiver<StdRsc<State>>, task_recv: TaskMsgReceiver<StdRsc<State>>,
} }
impl<State: DesktopAppState> AppState for DesktopApp<State> { impl<State: DesktopAppState> DesktopApp<State> {
type Event = State::Event; pub fn run() {
App::<Self>::run_with(|event_loop, proxy| {
Self::new_with(event_loop, proxy, State::window_attributes(), State::new)
});
}
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self { fn new_with(
event_loop: &ActiveEventLoop,
proxy: EventLoopProxy<()>,
attributes: WindowAttributes,
init: impl FnOnce(DesktopUiState, &mut StdRsc<State>) -> State,
) -> Self {
let window = event_loop let window = event_loop
.create_window(State::window_attributes().with_visible(false)) .create_window(attributes.with_visible(false))
.unwrap(); .unwrap();
let access_adapter = accesskit_winit::Adapter::with_direct_handlers( let access_adapter = accesskit_winit::Adapter::with_direct_handlers(
event_loop, event_loop,
@@ -115,20 +141,56 @@ impl<State: DesktopAppState> AppState for DesktopApp<State> {
); );
window.set_visible(true); window.set_visible(true);
let desktop_state = DesktopUiState::new(window, access_adapter); let desktop_state = DesktopUiState::new(window, access_adapter);
let (mut rsc, task_recv) = StdRsc::new(desktop_state.window.clone()); let (mut rsc, task_recv) = StdRsc::new(Arc::new(proxy));
// Set before building widgets so the first text shape uses the right density. // Set before building widgets so the first text shape uses the right density.
let scale = content_scale(desktop_state.window.as_ref()); let scale = content_scale(desktop_state.window.as_ref());
rsc.ui.set_density(scale); rsc.ui.set_density(scale);
let state = State::new(desktop_state, &mut rsc, proxy); let state = init(desktop_state, &mut rsc);
Self { Self {
rsc, rsc,
state, state,
task_recv, task_recv,
} }
} }
}
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) { impl DesktopApp<DesktopUiState> {
self.state.event(event, &mut self.rsc); /// Runs an application whose only state is Iris's desktop UI state.
pub fn run_with(init: impl FnOnce(&mut DesktopUiState, &mut StdRsc<DesktopUiState>) + 'static) {
Self::run_with_attributes(DesktopUiState::window_attributes(), init);
}
pub fn run_with_attributes(
attributes: WindowAttributes,
init: impl FnOnce(&mut DesktopUiState, &mut StdRsc<DesktopUiState>) + 'static,
) {
App::<Self>::run_with(move |event_loop, proxy| {
Self::new_with(event_loop, proxy, attributes, move |mut ui_state, rsc| {
init(&mut ui_state, rsc);
ui_state
})
});
}
}
impl<State: DesktopAppState> AppState for DesktopApp<State> {
fn tasks_ready(&mut self) {
let Self {
rsc,
state,
task_recv,
} = self;
for update in task_recv.try_iter() {
update(state, rsc);
}
let ui_state = state.desktop_state();
let render_state = rsc.ui.render_state();
if render_state
.get()
.needs_redraw(&ui_state.root, rsc.widgets())
{
ui_state.renderer.window().request_redraw();
}
} }
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) { fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
-7
View File
@@ -1,4 +1,3 @@
use crate::task::RequestRedraw;
use iris_core::{FrameParts, LinearRgba, Ui, UiRenderNode, util::Vec2}; use iris_core::{FrameParts, LinearRgba, Ui, UiRenderNode, util::Vec2};
use pollster::FutureExt; use pollster::FutureExt;
use std::sync::Arc; use std::sync::Arc;
@@ -8,12 +7,6 @@ use winit::{dpi::PhysicalSize, window::Window};
pub const CLEAR_COLOR: LinearRgba = LinearRgba::BLACK; pub const CLEAR_COLOR: LinearRgba = LinearRgba::BLACK;
impl RequestRedraw for Window {
fn request_redraw(&self) {
Window::request_redraw(self);
}
}
pub struct UiRenderer { pub struct UiRenderer {
window: Arc<Window>, window: Arc<Window>,
surface: Surface<'static>, surface: Surface<'static>,
+2 -2
View File
@@ -103,8 +103,8 @@ impl RedrawCounter {
} }
} }
impl RequestRedraw for RedrawCounter { impl WakeTaskQueue for RedrawCounter {
fn request_redraw(&self) { fn wake(&self) {
self.0.fetch_add(1, Ordering::Relaxed); self.0.fetch_add(1, Ordering::Relaxed);
} }
} }
-3
View File
@@ -1,8 +1,5 @@
#![feature(unboxed_closures)] #![feature(unboxed_closures)]
#![feature(fn_traits)] #![feature(fn_traits)]
// Only `desktop::DesktopAppState::Event`'s default uses this; unused (and
// warned about) on the android target, which has no such default.
#![cfg_attr(not(target_os = "android"), feature(associated_type_defaults))]
#![feature(unsize)] #![feature(unsize)]
#![feature(option_into_flat_iter)] #![feature(option_into_flat_iter)]
#![feature(async_fn_traits)] #![feature(async_fn_traits)]
+2 -2
View File
@@ -33,8 +33,8 @@ pub struct StdRsc<State: 'static> {
} }
impl<State> StdRsc<State> { impl<State> StdRsc<State> {
pub(crate) fn new(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Self>) { pub(crate) fn new(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Self>) {
let (tasks, receiver) = Tasks::init(redraw); let (tasks, receiver) = Tasks::init(wake);
( (
Self { Self {
ui: Ui::default(), ui: Ui::default(),
+72 -28
View File
@@ -19,15 +19,13 @@ pub async fn sleep(duration: std::time::Duration) {
tokio::time::sleep(duration).await; tokio::time::sleep(duration).await;
} }
/// What a completed task nudges when it wants its result drawn. Shared /// Wakes the platform UI thread so it can apply queued task updates.
/// between backends rather than typed as `winit::window::Window` directly: ///
/// android-view has no `Window` at all, and the redraw request there is a /// Waking does not itself mean drawing. Once the updates have run, the host
/// JNI call (`View::post_frame_callback`) rather than a method call on a /// asks the retained UI tree whether anything visible became dirty and only
/// value this crate owns. Each backend supplies its own implementation -- /// then schedules a frame.
/// `desktop/render.rs` for winit, `android/render.rs` for android-view -- pub trait WakeTaskQueue: Send + Sync + 'static {
/// and this module never needs to know which one it is holding. fn wake(&self);
pub trait RequestRedraw: Send + Sync + 'static {
fn request_redraw(&self);
} }
pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>; pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>;
@@ -38,29 +36,41 @@ impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc>
pub struct Tasks<Rsc: HasState> { pub struct Tasks<Rsc: HasState> {
start: AsyncSender<BoxTask>, start: AsyncSender<BoxTask>,
redraw: Arc<dyn RequestRedraw>, wake: Arc<dyn WakeTaskQueue>,
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>, msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
} }
pub struct TaskCtx<Rsc: HasState> { pub struct TaskCtx<Rsc: HasState> {
send: TaskMsgSender<Rsc>, send: TaskMsgSender<Rsc>,
wake: Arc<dyn WakeTaskQueue>,
}
impl<Rsc: HasState> Clone for TaskCtx<Rsc> {
fn clone(&self) -> Self {
Self {
send: self.send.clone(),
wake: self.wake.clone(),
}
}
} }
impl<Rsc: HasState> TaskCtx<Rsc> { impl<Rsc: HasState> TaskCtx<Rsc> {
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) { pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
let _ = self.send.send(Box::new(f)); if self.send.send(Box::new(f)).is_ok() {
self.wake.wake();
}
} }
} }
impl<Rsc: HasState + 'static> TaskCtx<Rsc> { impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
fn new(send: TaskMsgSender<Rsc>) -> Self { fn new(send: TaskMsgSender<Rsc>, wake: Arc<dyn WakeTaskQueue>) -> Self {
Self { send } Self { send, wake }
} }
} }
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>; type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
impl<Rsc: HasState> Tasks<Rsc> { impl<Rsc: HasState> Tasks<Rsc> {
pub fn init(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Rsc>) { pub fn init(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Rsc>) {
let (start, start_recv) = async_channel(); let (start, start_recv) = async_channel();
let (msgs, msgs_recv) = sync_channel(); let (msgs, msgs_recv) = sync_channel();
std::thread::spawn(|| { std::thread::spawn(|| {
@@ -71,22 +81,16 @@ impl<Rsc: HasState> Tasks<Rsc> {
Self { Self {
start, start,
msg_send: msgs, msg_send: msgs,
redraw, wake,
}, },
msgs_recv, msgs_recv,
) )
} }
/// The same redraw handle `spawn`'s wrapper calls once, after a whole /// A cloneable, platform-neutral way for an application-owned thread to
/// task's future completes -- exposed so a caller running its own /// submit work to the UI thread.
/// longer-lived loop *inside* a spawned task (a live SSE follow, here) pub fn context(&self) -> TaskCtx<Rsc> {
/// can ask for a frame after each `TaskCtx::update`, not just at the TaskCtx::new(self.msg_send.clone(), self.wake.clone())
/// end. Without this a caller has no way to get a redraw mid-stream,
/// which is exactly the gap `iris/desktop-app`'s `app.rs` module doc
/// names for why it uses winit's `Proxy` instead of `Tasks` -- Android
/// has no `Proxy`, so this is what closes the same gap there.
pub fn redraw_handle(&self) -> Arc<dyn RequestRedraw> {
self.redraw.clone()
} }
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F) pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
@@ -94,10 +98,9 @@ impl<Rsc: HasState> Tasks<Rsc> {
F::CallOnceFuture: Send, F::CallOnceFuture: Send,
{ {
let send = self.msg_send.clone(); let send = self.msg_send.clone();
let redraw = self.redraw.clone(); let wake = self.wake.clone();
let _ = self.start.send(Box::pin(async move { let _ = self.start.send(Box::pin(async move {
task(TaskCtx::new(send)).await; task(TaskCtx::new(send, wake)).await;
redraw.request_redraw();
})); }));
} }
} }
@@ -107,3 +110,44 @@ async fn listen(mut recv: AsyncReceiver<BoxTask>) {
tokio::spawn(task); tokio::spawn(task);
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use std::{
sync::atomic::{AtomicUsize, Ordering},
time::Duration,
};
struct TestRsc;
impl HasState for TestRsc {
type State = usize;
}
#[derive(Default)]
struct WakeCounter(AtomicUsize);
impl WakeTaskQueue for WakeCounter {
fn wake(&self) {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
#[test]
fn every_update_wakes_the_ui_queue() {
let wakes = Arc::new(WakeCounter::default());
let (tasks, updates) = Tasks::<TestRsc>::init(wakes.clone());
let mut ctx = tasks.context();
ctx.update(|state: &mut usize, _| *state += 1);
ctx.update(|state: &mut usize, _| *state += 2);
assert_eq!(wakes.0.load(Ordering::Relaxed), 2);
let mut state = 0;
let mut rsc = TestRsc;
updates.recv_timeout(Duration::from_secs(1)).unwrap()(&mut state, &mut rsc);
updates.recv_timeout(Duration::from_secs(1)).unwrap()(&mut state, &mut rsc);
assert_eq!(state, 3);
}
}