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