//! 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>>>); impl TaskQueue> for Queue { fn send(&self, update: Box>>) { let _ = self.0.send(update); } } /// `assert_eq!` for where a frame put a widget, written as its two corners. #[macro_export] macro_rules! assert_corners { ($harness:expr, $id:expr, ($x0:expr, $y0:expr), ($x1:expr, $y1:expr)) => { assert_eq!( $harness.region(&$id).expect("widget drew nothing"), $crate::core::PixelRegion { top_left: $crate::core::PxVec2::new( $crate::core::Px::from_f32($x0 as f32), $crate::core::Px::from_f32($y0 as f32), ), bot_right: $crate::core::PxVec2::new( $crate::core::Px::from_f32($x1 as f32), $crate::core::Px::from_f32($y1 as f32), ), } ); }; } pub use crate::assert_corners; /// One replayed pointer sample, cut down to what a window delivers: where the /// pointer is, and whether the button changed. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum TouchAction { Down, Move, Up, } impl TouchAction { fn parse(word: &str) -> Option { match word { "down" => Some(Self::Down), "move" => Some(Self::Move), "up" => Some(Self::Up), _ => None, } } } #[derive(Clone, Copy, Debug)] pub struct TouchSample { pub t_ms: u64, pub action: TouchAction, pub pos: Vec2, } /// A recorded gesture, in the output's own pixels: ` down|move|up ` /// a line, `#` to end of line ignored. /// /// One parser for both ways of replaying a recording -- into a harness, and /// into a real window -- so the two cannot read the same file differently. pub struct TouchScript { pub samples: Vec, } impl TouchScript { pub fn parse(text: &str) -> Result { let mut samples: Vec = Vec::new(); for (i, line) in text.lines().enumerate() { let line = line.split('#').next().unwrap_or("").trim(); if line.is_empty() { continue; } let at = |what: &str| format!("touch script line {}: {what}: {line:?}", i + 1); let mut words = line.split_whitespace(); let (Some(t), Some(action), Some(x), Some(y), None) = ( words.next(), words.next(), words.next(), words.next(), words.next(), ) else { return Err(at("expected `t_ms action x y`")); }; let t_ms: u64 = t.parse().map_err(|_| at("t_ms is not a whole number"))?; let action = TouchAction::parse(action).ok_or_else(|| at("action is not down/move/up"))?; let x: f32 = x.parse().map_err(|_| at("x is not a number"))?; let y: f32 = y.parse().map_err(|_| at("y is not a number"))?; if let Some(last) = samples.last() && t_ms < last.t_ms { return Err(at("samples must be in time order")); } samples.push(TouchSample { t_ms, action, pos: Vec2::new(x, y), }); } Ok(Self { samples }) } } #[derive(Default)] pub struct HarnessState { pub root: Option, } impl HasRoot for HarnessState { fn set_root(&mut self, root: StrongWidget) { self.root = Some(root); } } pub struct Harness { pub rsc: DefaultRsc, pub render: UiRenderState, pub state: HarnessState, updates: Receiver>>>, cursor: CursorState, } impl Harness { /// `size` is the output in physical pixels. pub fn new(size: impl Into) -> 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().to_f32() } pub fn resize(&mut self, size: impl Into) { self.render.resize(size); } /// Changes a length rule after the fact, the way `.width()` sets one. pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into) { self.rsc .widgets_mut() .set_size_rule(id, axis, SizeRule::Exact(len.into())); } /// Sets the root and lays it out, so a pointer event has something to hit. pub fn set_root(&mut self, widget: impl WidgetLike, 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 { self.render.window_region(id) } pub fn move_to(&mut self, pos: impl Into) { 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 the cursor was last /// moved to -- nowhere, until it has been moved. pub fn scroll(&mut self, delta: impl Into) { self.cursor.scroll_delta = delta.into(); self.sense(); } pub fn click(&mut self, pos: impl Into) { self.move_to(pos); self.press(CursorButton::Left); self.release(CursorButton::Left); } /// Drives a recorded gesture through the harness. pub fn replay(&mut self, script: &TouchScript) { for sample in &script.samples { match sample.action { TouchAction::Down => { self.move_to(sample.pos); self.press(CursorButton::Left); } TouchAction::Move => self.move_to(sample.pos), TouchAction::Up => { self.move_to(sample.pos); 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 layout of the last frame, which is what a /// window delivers input against too. fn sense(&mut self) { let cursor = self.cursor.clone(); self.render .run_sensors(&mut self.rsc, &mut self.state, cursor); self.cursor.end_frame(); } } #[cfg(test)] mod tests { use super::*; fn parse(text: &str) -> Result, String> { Ok(TouchScript::parse(text)? .samples .iter() .map(|s| (s.t_ms, s.action, s.pos.x, s.pos.y)) .collect()) } #[test] fn a_recording_is_time_action_and_a_point() { assert_eq!( parse("0 down 10 20\n16 move 10.5 24\n32 up 10.5 24").unwrap(), [ (0, TouchAction::Down, 10.0, 20.0), (16, TouchAction::Move, 10.5, 24.0), (32, TouchAction::Up, 10.5, 24.0), ] ); } #[test] fn blank_lines_and_comments_are_not_samples() { assert_eq!( parse("# a flick\n\n 0 down 1 2 # the finger lands\n\n").unwrap(), [(0, TouchAction::Down, 1.0, 2.0)] ); } #[test] fn a_recording_that_goes_backwards_is_rejected() { // Replay waits out the gap between samples, so time running backwards // would silently become no wait at all. let err = parse("16 down 1 2\n0 up 1 2").unwrap_err(); assert!(err.contains("time order"), "{err}"); } #[test] fn a_line_that_is_not_a_sample_says_which_line() { let err = parse("0 down 1 2\n16 wiggle 1 2").unwrap_err(); assert!(err.contains("line 2"), "{err}"); assert!(err.contains("down/move/up"), "{err}"); } }