Run a ui without a window, and test one
`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 moves from "the task ended" to "an update was sent", which is when there is something for the host to apply: a task that keeps running after sending one no longer holds its update until it finishes, and a task that sends none no longer asks for a frame it does not need. `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` does each of those; none of them could be written before, since the only entry point to layout was a window. It does not draw. A claim about pixels still needs a real surface.
This commit is contained in:
1 parent
00d2230b84
commit
d5efdd2b97
6 files changed
+370
-12
No files matched your search
+8
-2
@@ -64,6 +64,12 @@ 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;
|
||||
@@ -105,8 +111,8 @@ pub struct DefaultRsc<State: 'static> {
|
||||
}
|
||||
|
||||
impl<State> DefaultRsc<State> {
|
||||
fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Self>) {
|
||||
let (tasks, recv) = Tasks::init(window);
|
||||
pub fn init(wake: Arc<dyn WakeTaskQueue>) -> (Self, TaskMsgReceiver<Self>) {
|
||||
let (tasks, recv) = Tasks::init(wake);
|
||||
(
|
||||
Self {
|
||||
ui: Default::default(),
|
||||
|
||||
+63
-10
@@ -13,7 +13,13 @@ use tokio::{
|
||||
unbounded_channel as async_channel,
|
||||
},
|
||||
};
|
||||
use winit::window::Window;
|
||||
|
||||
/// 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>>>;
|
||||
@@ -23,29 +29,32 @@ impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc>
|
||||
|
||||
pub struct Tasks<Rsc: HasState> {
|
||||
start: AsyncSender<BoxTask>,
|
||||
window: Arc<Window>,
|
||||
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> 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(window: Arc<Window>) -> (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(|| {
|
||||
@@ -56,7 +65,7 @@ impl<Rsc: HasState> Tasks<Rsc> {
|
||||
Self {
|
||||
start,
|
||||
msg_send: msgs,
|
||||
window,
|
||||
wake,
|
||||
},
|
||||
msgs_recv,
|
||||
)
|
||||
@@ -67,10 +76,9 @@ impl<Rsc: HasState> Tasks<Rsc> {
|
||||
F::CallOnceFuture: Send,
|
||||
{
|
||||
let send = self.msg_send.clone();
|
||||
let window = self.window.clone();
|
||||
let wake = self.wake.clone();
|
||||
let _ = self.start.send(Box::pin(async move {
|
||||
task(TaskCtx::new(send)).await;
|
||||
window.request_redraw();
|
||||
task(TaskCtx::new(send, wake)).await;
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -80,3 +88,48 @@ 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
//! A ui with no window: build a tree, run frames, move a pointer, and read
|
||||
//! back where widgets landed.
|
||||
//!
|
||||
//! It does not draw. A claim about pixels still needs a real surface.
|
||||
|
||||
use crate::prelude::*;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
/// The harness drains the update queue itself, so there is no loop to wake.
|
||||
struct NoWake;
|
||||
|
||||
impl WakeTaskQueue for NoWake {
|
||||
fn wake(&self) {}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HarnessState {
|
||||
pub root: Option<StrongWidget>,
|
||||
}
|
||||
|
||||
impl HasRoot for HarnessState {
|
||||
fn set_root(&mut self, root: StrongWidget) {
|
||||
self.root = Some(root);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Harness {
|
||||
pub rsc: DefaultRsc<HarnessState>,
|
||||
pub render: UiRenderState,
|
||||
pub state: HarnessState,
|
||||
updates: TaskMsgReceiver<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));
|
||||
let mut render = UiRenderState::new();
|
||||
render.resize(size);
|
||||
Self {
|
||||
rsc,
|
||||
render,
|
||||
state: HarnessState::default(),
|
||||
updates,
|
||||
cursor: CursorState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Vec2 {
|
||||
self.render.output_size()
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||
self.render.resize(size);
|
||||
}
|
||||
|
||||
/// Sets the root and lays it out, so a pointer event has something to hit.
|
||||
pub fn set_root<T>(&mut self, widget: impl WidgetLike<DefaultRsc<HarnessState>, T>) {
|
||||
widget.set_root(&mut self.rsc, &mut self.state);
|
||||
self.frame();
|
||||
}
|
||||
|
||||
pub fn needs_redraw(&self) -> bool {
|
||||
self.render
|
||||
.needs_redraw(&self.state.root, self.rsc.widgets())
|
||||
}
|
||||
|
||||
pub fn apply_updates(&mut self) -> usize {
|
||||
let mut applied = 0;
|
||||
while let Ok(update) = self.updates.try_recv() {
|
||||
update(&mut self.state, &mut self.rsc);
|
||||
applied += 1;
|
||||
}
|
||||
applied
|
||||
}
|
||||
|
||||
/// Waits for a task's first update, then applies everything waiting.
|
||||
/// False if none arrived in time.
|
||||
#[must_use]
|
||||
pub fn await_update(&mut self, timeout: Duration) -> bool {
|
||||
let Ok(update) = self.updates.recv_timeout(timeout) else {
|
||||
return false;
|
||||
};
|
||||
update(&mut self.state, &mut self.rsc);
|
||||
self.apply_updates();
|
||||
true
|
||||
}
|
||||
|
||||
/// Lays the tree out and builds its primitives.
|
||||
pub fn frame(&mut self) {
|
||||
self.apply_updates();
|
||||
self.render.update(&self.state.root, &mut self.rsc);
|
||||
}
|
||||
|
||||
/// Where the last frame put a widget, or `None` if it drew nothing.
|
||||
pub fn region(&self, id: &impl IdLike) -> Option<PixelRegion> {
|
||||
self.render.window_region(id)
|
||||
}
|
||||
|
||||
pub fn move_to(&mut self, pos: impl Into<Vec2>) {
|
||||
self.cursor.pos = pos.into();
|
||||
self.cursor.exists = true;
|
||||
self.sense();
|
||||
}
|
||||
|
||||
pub fn leave(&mut self) {
|
||||
self.cursor.exists = false;
|
||||
self.sense();
|
||||
}
|
||||
|
||||
pub fn press(&mut self, button: CursorButton) {
|
||||
self.button(button).update(true);
|
||||
self.sense();
|
||||
}
|
||||
|
||||
pub fn release(&mut self, button: CursorButton) {
|
||||
self.button(button).update(false);
|
||||
self.sense();
|
||||
}
|
||||
|
||||
/// A wheel carries no position, so this goes wherever
|
||||
/// [`move_to`](Self::move_to) last put the cursor -- nowhere, until it has
|
||||
/// been called.
|
||||
pub fn scroll(&mut self, delta: impl Into<Vec2>) {
|
||||
self.cursor.scroll_delta = delta.into();
|
||||
self.sense();
|
||||
}
|
||||
|
||||
pub fn click(&mut self, pos: impl Into<Vec2>) {
|
||||
self.move_to(pos);
|
||||
self.press(CursorButton::Left);
|
||||
self.release(CursorButton::Left);
|
||||
}
|
||||
|
||||
fn button(&mut self, button: CursorButton) -> &mut ActivationState {
|
||||
let buttons = &mut self.cursor.buttons;
|
||||
match button {
|
||||
CursorButton::Left => &mut buttons.left,
|
||||
CursorButton::Middle => &mut buttons.middle,
|
||||
CursorButton::Right => &mut buttons.right,
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatches against the last [`frame`](Self::frame)'s layout, which is
|
||||
/// what a window delivers input against too.
|
||||
fn sense(&mut self) {
|
||||
let cursor = self.cursor.clone();
|
||||
let size = self.render.output_size();
|
||||
self.render
|
||||
.run_sensors(&mut self.rsc, &mut self.state, cursor, size);
|
||||
self.cursor.end_frame();
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
pub mod default;
|
||||
pub mod event;
|
||||
pub mod harness;
|
||||
pub mod widget;
|
||||
|
||||
pub use iris_core as core;
|
||||
|
||||
Reference in new issue
Block a user