`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`.
168 lines
4.8 KiB
Rust
168 lines
4.8 KiB
Rust
//! 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,
|
|
mpsc::{Receiver, SyncSender, sync_channel},
|
|
},
|
|
time::Duration,
|
|
};
|
|
|
|
/// 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 TaskQueue<DefaultRsc<HarnessState>> for Queue {
|
|
fn send(&self, update: Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>) {
|
|
let _ = self.0.send(update);
|
|
}
|
|
}
|
|
|
|
#[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: 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 {
|
|
// 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 {
|
|
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();
|
|
}
|
|
}
|