Post task updates to the loop instead of waking it

`WakeTaskQueue` becomes `TaskQueue`, which carries the update itself.
Delivery and waking are then one act: the winit host sends it through the
`EventLoopProxy` as a `DefaultEvent::Update`, so there is no channel
beside the loop and nothing has to claim a redraw is needed in order to be
looked at. `Window::request_redraw` is gone from this path; `event`
applies the update and then asks the tree whether anything became dirty,
which is the same question `window_event` already ended with -- now
`schedule_redraw`, called from both.

The loop's message type is `DefaultEvent<State>`, so `Proxy` becomes a
wrapper that takes the application's own `Event` and requires it to be
`Send`, since it now crosses to the task thread by that route.

The harness supplies a channel-backed queue, which is what lets a test
hold updates until it asks for them.

Tests split by subject -- layout, pointer, scroll, tasks -- with the
region helper in `tests/common`.
This commit is contained in:
iris committed 2026-09-13 21:04:29 -04:00
1 parent e97aba30e0
commit 3a74a04a5b
13 files changed
+266 -302

No files matched your search

+64 -48
View File
@@ -25,7 +25,37 @@ pub use sense::*;
pub use state::*;
pub use task::*;
pub type Proxy<Event> = EventLoopProxy<Event>;
/// Sends an application's own events to its event loop. It wraps the proxy
/// rather than being one because task updates travel the same way: what an
/// application sends is its `Event`, not the loop's whole message type.
pub struct Proxy<State: DefaultAppState>(EventLoopProxy<DefaultEvent<State>>);
impl<State: DefaultAppState> Clone for Proxy<State> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<State: DefaultAppState> Proxy<State> {
pub fn send_event(&self, event: State::Event) {
let _ = self.0.send_event(DefaultEvent::User(event));
}
}
/// What the event loop carries: the application's own events, and the
/// updates tasks send back to the ui thread.
pub enum DefaultEvent<State: DefaultAppState> {
User(State::Event),
Update(Box<dyn TaskUpdate<DefaultRsc<State>>>),
}
struct ProxyQueue<State: DefaultAppState>(EventLoopProxy<DefaultEvent<State>>);
impl<State: DefaultAppState> TaskQueue<DefaultRsc<State>> for ProxyQueue<State> {
fn send(&self, update: Box<dyn TaskUpdate<DefaultRsc<State>>>) {
let _ = self.0.send_event(DefaultEvent::Update(update));
}
}
pub struct DefaultUiState {
pub root: Option<StrongWidget>,
@@ -60,21 +90,14 @@ impl DefaultUiState {
}
}
impl WakeTaskQueue for Window {
fn wake(&self) {
self.request_redraw();
}
}
pub trait HasDefaultUiState: Sized + 'static {
fn default_state(&self) -> &DefaultUiState;
fn default_state_mut(&mut self) -> &mut DefaultUiState;
}
pub trait DefaultAppState: HasDefaultUiState {
type Event = ();
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self::Event>)
-> Self;
type Event: Send = ();
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self>) -> Self;
#[allow(unused_variables)]
fn event(
&mut self,
@@ -107,18 +130,14 @@ pub struct DefaultRsc<State: 'static> {
}
impl<State> DefaultRsc<State> {
pub fn init(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Self>) {
let (tasks, recv) = Tasks::init(wake);
(
Self {
ui: Default::default(),
events: Default::default(),
tasks,
state: Default::default(),
_state: Default::default(),
},
recv,
)
pub fn init(queue: Arc<dyn TaskQueue<Self>>) -> Self {
Self {
ui: Default::default(),
events: Default::default(),
tasks: Tasks::init(queue),
state: Default::default(),
_state: Default::default(),
}
}
pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> {
@@ -183,43 +202,34 @@ pub struct DefaultApp<State: DefaultAppState> {
rsc: DefaultRsc<State>,
render: UiRenderState,
state: State,
task_recv: TaskMsgReceiver<DefaultRsc<State>>,
}
impl<State: DefaultAppState> AppState for DefaultApp<State> {
type Event = State::Event;
type Event = DefaultEvent<State>;
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
let window = event_loop
.create_window(State::window_attributes())
.unwrap();
let default_state = DefaultUiState::new(window);
let (mut rsc, task_recv) = DefaultRsc::init(default_state.window.clone());
let state = State::new(default_state, &mut rsc, proxy);
let mut rsc = DefaultRsc::init(Arc::new(ProxyQueue(proxy.clone())));
let state = State::new(default_state, &mut rsc, Proxy(proxy));
let render = UiRenderState::new();
Self {
rsc,
state,
render,
task_recv,
}
Self { rsc, state, render }
}
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
self.state.event(event, &mut self.rsc, &mut self.render);
match event {
DefaultEvent::User(event) => self.state.event(event, &mut self.rsc, &mut self.render),
DefaultEvent::Update(update) => update(&mut self.state, &mut self.rsc),
}
// An update is not a reason to draw; whether it made anything dirty
// is. That is why a task posts here rather than asking for a redraw.
self.schedule_redraw();
}
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
let Self {
rsc,
render,
state,
task_recv,
} = self;
for update in task_recv.try_iter() {
update(state, rsc);
}
let Self { rsc, render, state } = self;
let ui_state = state.default_state_mut();
let input_changed = ui_state.input.event(&event);
@@ -299,11 +309,8 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
_ => (),
}
state.window_event(event, rsc, render);
let ui_state = self.state.default_state_mut();
if render.needs_redraw(&ui_state.root, rsc.widgets()) {
ui_state.renderer.window().request_redraw();
}
ui_state.input.end_frame();
self.schedule_redraw();
self.state.default_state_mut().input.end_frame();
}
fn exit(&mut self) {
@@ -311,6 +318,15 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
}
}
impl<State: DefaultAppState> DefaultApp<State> {
fn schedule_redraw(&mut self) {
let ui_state = self.state.default_state_mut();
if self.render.needs_redraw(&ui_state.root, self.rsc.widgets()) {
ui_state.renderer.window().request_redraw();
}
}
}
pub trait RscIdx<Rsc> {
type Output;
fn get(self, rsc: &Rsc) -> &Self::Output;
+16 -87
View File
@@ -1,11 +1,5 @@
use iris_core::HasState;
use std::{
pin::Pin,
sync::{
Arc,
mpsc::{Receiver as SyncReceiver, Sender as SyncSender, channel as sync_channel},
},
};
use std::{pin::Pin, sync::Arc};
use tokio::{
runtime::Runtime,
sync::mpsc::{
@@ -14,71 +8,51 @@ use tokio::{
},
};
/// Wakes the host so it applies queued task updates. A task reaches the
/// application only through [`TaskCtx::update`], so this is all the queue
/// needs of a platform.
pub trait WakeTaskQueue: Send + Sync + 'static {
fn wake(&self);
}
pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>;
pub type TaskMsgReceiver<Rsc> = SyncReceiver<Box<dyn TaskUpdate<Rsc>>>;
pub trait TaskUpdate<Rsc: HasState>: FnOnce(&mut Rsc::State, &mut Rsc) + Send {}
impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc> for F {}
/// Hands an update from a task to the thread that owns the ui. Delivery and
/// waking are one act: a host posts the update as a message its loop already
/// carries -- winit's `EventLoopProxy`, Android's looper -- so nothing has to
/// wake the loop separately, or claim a redraw to be looked at.
pub trait TaskQueue<Rsc: HasState>: Send + Sync + 'static {
fn send(&self, update: Box<dyn TaskUpdate<Rsc>>);
}
pub struct Tasks<Rsc: HasState> {
start: AsyncSender<BoxTask>,
wake: Arc<dyn WakeTaskQueue>,
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
queue: Arc<dyn TaskQueue<Rsc>>,
}
pub struct TaskCtx<Rsc: HasState> {
send: TaskMsgSender<Rsc>,
wake: Arc<dyn WakeTaskQueue>,
queue: Arc<dyn TaskQueue<Rsc>>,
}
impl<Rsc: HasState> TaskCtx<Rsc> {
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
if self.send.send(Box::new(f)).is_ok() {
self.wake.wake();
}
}
}
impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
fn new(send: TaskMsgSender<Rsc>, wake: Arc<dyn WakeTaskQueue>) -> Self {
Self { send, wake }
self.queue.send(Box::new(f));
}
}
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
impl<Rsc: HasState> Tasks<Rsc> {
pub fn init(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Rsc>) {
pub fn init(queue: Arc<dyn TaskQueue<Rsc>>) -> Self {
let (start, start_recv) = async_channel();
let (msgs, msgs_recv) = sync_channel();
std::thread::spawn(|| {
let rt = Runtime::new().unwrap();
rt.block_on(listen(start_recv))
});
(
Self {
start,
msg_send: msgs,
wake,
},
msgs_recv,
)
Self { start, queue }
}
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
where
F::CallOnceFuture: Send,
{
let send = self.msg_send.clone();
let wake = self.wake.clone();
let queue = self.queue.clone();
let _ = self.start.send(Box::pin(async move {
task(TaskCtx::new(send, wake)).await;
task(TaskCtx { queue }).await;
}));
}
}
@@ -88,48 +62,3 @@ async fn listen(mut recv: AsyncReceiver<BoxTask>) {
tokio::spawn(task);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{sync::mpsc::sync_channel, time::Duration};
struct TestRsc;
impl HasState for TestRsc {
type State = usize;
}
/// Signals rather than counts, so the test waits for a wake instead of
/// racing the task thread to sample it.
struct WakeSignal(std::sync::mpsc::SyncSender<()>);
impl WakeTaskQueue for WakeSignal {
fn wake(&self) {
let _ = self.0.send(());
}
}
#[test]
fn every_update_wakes_the_host() {
let (woken, wakes) = sync_channel(8);
let (mut tasks, updates) = Tasks::<TestRsc>::init(Arc::new(WakeSignal(woken)));
tasks.spawn(async move |mut ctx| {
ctx.update(|state: &mut usize, _| *state += 1);
ctx.update(|state: &mut usize, _| *state += 2);
});
let second = Duration::from_secs(1);
let (mut state, mut rsc) = (0, TestRsc);
for _ in 0..2 {
wakes.recv_timeout(second).expect("no wake for an update");
updates.recv_timeout(second).unwrap()(&mut state, &mut rsc);
}
assert_eq!(state, 3);
assert!(
wakes.recv_timeout(Duration::from_millis(100)).is_err(),
"woken with nothing to apply"
);
}
}
+20 -7
View File
@@ -4,13 +4,22 @@
//! It does not draw. A claim about pixels still needs a real surface.
use crate::prelude::*;
use std::{sync::Arc, time::Duration};
use std::{
sync::{
Arc,
mpsc::{Receiver, SyncSender, sync_channel},
},
time::Duration,
};
/// The harness drains the update queue itself, so there is no loop to wake.
struct NoWake;
/// There is no loop here to post to, so updates queue until the test asks
/// for them.
struct Queue(SyncSender<Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>>);
impl WakeTaskQueue for NoWake {
fn wake(&self) {}
impl TaskQueue<DefaultRsc<HarnessState>> for Queue {
fn send(&self, update: Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>) {
let _ = self.0.send(update);
}
}
#[derive(Default)]
@@ -28,14 +37,18 @@ pub struct Harness {
pub rsc: DefaultRsc<HarnessState>,
pub render: UiRenderState,
pub state: HarnessState,
updates: TaskMsgReceiver<DefaultRsc<HarnessState>>,
updates: Receiver<Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>>,
cursor: CursorState,
}
impl Harness {
/// `size` is the output in physical pixels.
pub fn new(size: impl Into<Vec2>) -> Self {
let (rsc, updates) = DefaultRsc::init(Arc::new(NoWake));
// A `TaskQueue` must be `Sync`, which `mpsc::Sender` is not; the
// bound that comes with `SyncSender` is far past anything a test
// leaves unread.
let (send, updates) = sync_channel(1024);
let rsc = DefaultRsc::init(Arc::new(Queue(send)));
let mut render = UiRenderState::new();
render.resize(size);
Self {