Small, and disjoint from #12 — this touches `task.rs`, `harness.rs` and `render_state.rs`, none of which #12 goes near. `Tasks` held an `Arc<Window>` only to call `request_redraw` when a task finished, which made the task queue, and so `DefaultRsc`, impossible to build without a window. It now takes an `Arc<dyn WakeTaskQueue>`, and `Window` implements it. Waking also moves from *the task ended* to *an update was sent*, which is when there is actually something for the host to apply. A task that keeps running after sending one no longer holds it until it finishes, and a task that sends none no longer asks for a frame nothing needs. `iris::harness` is what that buys. `UiRenderState` already does layout, hit testing and primitive building with no surface, so a test can build a tree, run frames, move a pointer and read back where widgets landed. `tests/harness.rs` covers span layout, resize relayout, press routing, hover start and end, wheel scrolling with its clamp, and a task update reaching the tree. None of them could be written before, since the only way into layout was a window. It does not draw. A claim about pixels still needs a real surface — I checked this one against the rig rather than asserting it: `examples/task` under headless sway, centre pixel `ff0000` before the click and `0000ff` after, so the windowed path still applies task updates under the new wake. The only core change is `UiRenderState::output_size()`, so that a host reading back the size it set does not have to keep a second copy. --------- Co-authored-by: iris <2+iris@noreply.localhost> Reviewed-on: iris/iris#15 Reviewed-by: iris <2+iris@noreply.localhost> Co-authored-by: AIris <4+iris-ai@noreply.localhost>
65 lines
1.9 KiB
Rust
65 lines
1.9 KiB
Rust
use iris_core::HasState;
|
|
use std::{pin::Pin, sync::Arc};
|
|
use tokio::{
|
|
runtime::Runtime,
|
|
sync::mpsc::{
|
|
UnboundedReceiver as AsyncReceiver, UnboundedSender as AsyncSender,
|
|
unbounded_channel as async_channel,
|
|
},
|
|
};
|
|
|
|
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, 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>,
|
|
queue: Arc<dyn TaskQueue<Rsc>>,
|
|
}
|
|
|
|
pub struct TaskCtx<Rsc: HasState> {
|
|
queue: Arc<dyn TaskQueue<Rsc>>,
|
|
}
|
|
|
|
impl<Rsc: HasState> TaskCtx<Rsc> {
|
|
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
|
|
self.queue.send(Box::new(f));
|
|
}
|
|
}
|
|
|
|
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
|
|
|
|
impl<Rsc: HasState> Tasks<Rsc> {
|
|
pub fn init(queue: Arc<dyn TaskQueue<Rsc>>) -> Self {
|
|
let (start, start_recv) = async_channel();
|
|
std::thread::spawn(|| {
|
|
let rt = Runtime::new().unwrap();
|
|
rt.block_on(listen(start_recv))
|
|
});
|
|
Self { start, queue }
|
|
}
|
|
|
|
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
|
|
where
|
|
F::CallOnceFuture: Send,
|
|
{
|
|
let queue = self.queue.clone();
|
|
let _ = self.start.send(Box::pin(async move {
|
|
task(TaskCtx { queue }).await;
|
|
}));
|
|
}
|
|
}
|
|
|
|
async fn listen(mut recv: AsyncReceiver<BoxTask>) {
|
|
while let Some(task) = recv.recv().await {
|
|
tokio::spawn(task);
|
|
}
|
|
}
|