diff --git a/examples/minimal.rs b/examples/minimal.rs index e98ae5b..ca10f51 100644 --- a/examples/minimal.rs +++ b/examples/minimal.rs @@ -10,11 +10,7 @@ struct State { } impl DefaultAppState for State { - fn new( - mut ui_state: DefaultUiState, - rsc: &mut DefaultRsc, - _: Proxy, - ) -> Self { + fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc, _: Proxy) -> Self { rect(Color::RED).set_root(rsc, &mut ui_state); Self { ui_state } } diff --git a/examples/tabs/main.rs b/examples/tabs/main.rs index ba314c6..6bd93c4 100644 --- a/examples/tabs/main.rs +++ b/examples/tabs/main.rs @@ -15,11 +15,7 @@ pub struct Client { } impl DefaultAppState for Client { - fn new( - mut ui_state: DefaultUiState, - rsc: &mut DefaultRsc, - _: Proxy, - ) -> Self { + fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc, _: Proxy) -> Self { let rrect = rect(Color::WHITE).radius(20); let pad_test = ( rrect.color(Color::BLUE), diff --git a/examples/task.rs b/examples/task.rs index 63e4d43..12ba8dc 100644 --- a/examples/task.rs +++ b/examples/task.rs @@ -11,11 +11,7 @@ struct State { } impl DefaultAppState for State { - fn new( - mut ui_state: DefaultUiState, - rsc: &mut DefaultRsc, - _: Proxy, - ) -> Self { + fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc, _: Proxy) -> Self { let rect = rect(Color::RED).add(rsc); rect.task_on(CursorSense::click(), async move |mut ctx| { tokio::time::sleep(Duration::from_secs(1)).await; diff --git a/examples/view.rs b/examples/view.rs index b8e06cf..2fb4995 100644 --- a/examples/view.rs +++ b/examples/view.rs @@ -36,11 +36,7 @@ impl Test { } impl DefaultAppState for State { - fn new( - mut ui_state: DefaultUiState, - rsc: &mut DefaultRsc, - _: Proxy, - ) -> Self { + fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc, _: Proxy) -> Self { let test = Test::new(rsc); test.on(CursorSense::click(), move |_, rsc| { diff --git a/src/default/mod.rs b/src/default/mod.rs index e242441..0587fbc 100644 --- a/src/default/mod.rs +++ b/src/default/mod.rs @@ -25,7 +25,37 @@ pub use sense::*; pub use state::*; pub use task::*; -pub type Proxy = EventLoopProxy; +/// 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(EventLoopProxy>); + +impl Clone for Proxy { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl Proxy { + 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 { + User(State::Event), + Update(Box>>), +} + +struct ProxyQueue(EventLoopProxy>); + +impl TaskQueue> for ProxyQueue { + fn send(&self, update: Box>>) { + let _ = self.0.send_event(DefaultEvent::Update(update)); + } +} pub struct DefaultUiState { pub root: Option, @@ -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, proxy: Proxy) - -> Self; + type Event: Send = (); + fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc, proxy: Proxy) -> Self; #[allow(unused_variables)] fn event( &mut self, @@ -107,18 +130,14 @@ pub struct DefaultRsc { } impl DefaultRsc { - pub fn init(wake: Arc) -> (Self, TaskMsgReceiver) { - 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>) -> Self { + Self { + ui: Default::default(), + events: Default::default(), + tasks: Tasks::init(queue), + state: Default::default(), + _state: Default::default(), + } } pub fn create_state(&mut self, id: impl IdLike, data: T) -> WeakState { @@ -183,43 +202,34 @@ pub struct DefaultApp { rsc: DefaultRsc, render: UiRenderState, state: State, - task_recv: TaskMsgReceiver>, } impl AppState for DefaultApp { - type Event = State::Event; + type Event = DefaultEvent; fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy) -> 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 AppState for DefaultApp { _ => (), } 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 AppState for DefaultApp { } } +impl DefaultApp { + 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 { type Output; fn get(self, rsc: &Rsc) -> &Self::Output; diff --git a/src/default/task.rs b/src/default/task.rs index 80588b9..4fd655c 100644 --- a/src/default/task.rs +++ b/src/default/task.rs @@ -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 = SyncSender>>; -pub type TaskMsgReceiver = SyncReceiver>>; - pub trait TaskUpdate: FnOnce(&mut Rsc::State, &mut Rsc) + Send {} impl TaskUpdate 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: Send + Sync + 'static { + fn send(&self, update: Box>); +} + pub struct Tasks { start: AsyncSender, - wake: Arc, - msg_send: SyncSender>>, + queue: Arc>, } pub struct TaskCtx { - send: TaskMsgSender, - wake: Arc, + queue: Arc>, } impl TaskCtx { pub fn update(&mut self, f: impl TaskUpdate + 'static) { - if self.send.send(Box::new(f)).is_ok() { - self.wake.wake(); - } - } -} -impl TaskCtx { - fn new(send: TaskMsgSender, wake: Arc) -> Self { - Self { send, wake } + self.queue.send(Box::new(f)); } } type BoxTask = Pin + Send>>; impl Tasks { - pub fn init(wake: Arc) -> (Self, TaskMsgReceiver) { + pub fn init(queue: Arc>) -> 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) + '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) { 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::::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" - ); - } -} diff --git a/src/harness.rs b/src/harness.rs index 1a268ff..de43df1 100644 --- a/src/harness.rs +++ b/src/harness.rs @@ -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>>>); -impl WakeTaskQueue for NoWake { - fn wake(&self) {} +impl TaskQueue> for Queue { + fn send(&self, update: Box>>) { + let _ = self.0.send(update); + } } #[derive(Default)] @@ -28,14 +37,18 @@ pub struct Harness { pub rsc: DefaultRsc, pub render: UiRenderState, pub state: HarnessState, - updates: TaskMsgReceiver>, + updates: Receiver>>>, cursor: CursorState, } impl Harness { /// `size` is the output in physical pixels. pub fn new(size: impl Into) -> 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 { diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..3ceaf79 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,13 @@ +use iris::harness::Harness; +use iris::prelude::*; + +/// `PixelRegion` neither compares nor prints. +pub fn corners(h: &Harness, id: &impl IdLike) -> (f32, f32, f32, f32) { + let region = h.region(id).expect("widget drew nothing"); + ( + region.top_left.x, + region.top_left.y, + region.bot_right.x, + region.bot_right.y, + ) +} diff --git a/tests/harness.rs b/tests/harness.rs deleted file mode 100644 index 52262c8..0000000 --- a/tests/harness.rs +++ /dev/null @@ -1,140 +0,0 @@ -//! Layout, hit testing and task updates, driven without a window. - -use std::{cell::RefCell, rc::Rc, time::Duration}; - -use iris::harness::Harness; -use iris::prelude::*; - -/// `PixelRegion` neither compares nor prints. -fn corners(h: &Harness, id: &impl IdLike) -> (f32, f32, f32, f32) { - let region = h.region(id).expect("widget drew nothing"); - ( - region.top_left.x, - region.top_left.y, - region.bot_right.x, - region.bot_right.y, - ) -} - -/// A fixed 100 wide, and the rest of the 400 to its neighbour. -fn two_rects(h: &mut Harness) -> (WidgetId, WidgetId) { - let left = rect(Color::RED).width(100).add(&mut h.rsc); - let right = rect(Color::BLUE).add(&mut h.rsc); - h.set_root((left, right).span(Dir::RIGHT)); - (left.id(), right.id()) -} - -#[test] -fn a_span_gives_each_child_the_width_it_asked_for() { - let mut h = Harness::new((400, 200)); - let (left, right) = two_rects(&mut h); - - assert_eq!(corners(&h, &left), (0.0, 0.0, 100.0, 200.0)); - assert_eq!(corners(&h, &right), (100.0, 0.0, 400.0, 200.0)); -} - -#[test] -fn resizing_relays_out_against_the_new_output() { - let mut h = Harness::new((400, 200)); - let (left, right) = two_rects(&mut h); - - h.resize((800, 100)); - assert!(h.needs_redraw()); - h.frame(); - - assert_eq!(corners(&h, &left), (0.0, 0.0, 100.0, 100.0)); - assert_eq!(corners(&h, &right), (100.0, 0.0, 800.0, 100.0)); -} - -#[test] -fn a_press_reaches_only_the_widget_under_the_cursor() { - let mut h = Harness::new((400, 200)); - let clicks = Rc::new(RefCell::new(Vec::new())); - - let (on_left, on_right) = (clicks.clone(), clicks.clone()); - let left = rect(Color::RED) - .width(100) - .on(CursorSense::click(), move |_, _| { - on_left.borrow_mut().push("left") - }) - .add(&mut h.rsc); - let right = rect(Color::BLUE) - .on(CursorSense::click(), move |_, _| { - on_right.borrow_mut().push("right") - }) - .add(&mut h.rsc); - h.set_root((left, right).span(Dir::RIGHT)); - - h.click((50, 100)); - assert_eq!(*clicks.borrow(), ["left"]); - - h.click((300, 100)); - assert_eq!(*clicks.borrow(), ["left", "right"]); -} - -#[test] -fn hover_ends_when_the_cursor_leaves_the_window() { - let mut h = Harness::new((400, 200)); - let hovered = Rc::new(RefCell::new(0)); - let ended = Rc::new(RefCell::new(0)); - - let (h_count, e_count) = (hovered.clone(), ended.clone()); - let widget = rect(Color::RED) - .on(CursorSense::HoverStart, move |_, _| { - *h_count.borrow_mut() += 1 - }) - .on(CursorSense::HoverEnd, move |_, _| { - *e_count.borrow_mut() += 1 - }) - .add(&mut h.rsc); - h.set_root(widget); - - h.move_to((200, 100)); - assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 0)); - - // A second sample inside the same widget is not a second hover. - h.move_to((210, 100)); - assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 0)); - - h.leave(); - assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 1)); -} - -#[test] -fn a_task_update_reaches_the_tree() { - let mut h = Harness::new((400, 200)); - let widget = rect(Color::RED).add(&mut h.rsc); - h.set_root(widget.task_on(CursorSense::click(), async move |mut ctx| { - ctx.update(move |_, rsc| widget(rsc).color = Color::BLUE); - })); - - h.click((200, 100)); - - assert!( - h.await_update(Duration::from_secs(5)), - "the task sent no update" - ); - assert_eq!(h.rsc[widget].color, Color::BLUE); -} - -#[test] -fn a_wheel_scrolls_the_content_and_stops_at_its_end() { - let mut h = Harness::new((400, 200)); - // Twice the window's height, so there is 200 to scroll. - let top = rect(Color::RED).height(200).add(&mut h.rsc); - let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc); - h.set_root((top, bottom).span(Dir::DOWN).scrollable()); - h.move_to((200, 100)); - - // `Scroll` starts snapped to the end. - assert_eq!(corners(&h, &top).1, -200.0); - - // The handler scales a wheel line by 50. - h.scroll((0, 1)); - h.frame(); - assert_eq!(corners(&h, &top).1, -150.0); - - h.scroll((0, 10)); - h.frame(); - assert_eq!(corners(&h, &top).1, 0.0); -} diff --git a/tests/layout.rs b/tests/layout.rs new file mode 100644 index 0000000..a9ee482 --- /dev/null +++ b/tests/layout.rs @@ -0,0 +1,37 @@ +//! Where a frame puts things, with no window to put them in. + +mod common; + +use common::corners; +use iris::harness::Harness; +use iris::prelude::*; + +/// A fixed 100 wide, and the rest of the 400 to its neighbour. +fn two_rects(h: &mut Harness) -> (WidgetId, WidgetId) { + let left = rect(Color::RED).width(100).add(&mut h.rsc); + let right = rect(Color::BLUE).add(&mut h.rsc); + h.set_root((left, right).span(Dir::RIGHT)); + (left.id(), right.id()) +} + +#[test] +fn a_span_gives_each_child_the_width_it_asked_for() { + let mut h = Harness::new((400, 200)); + let (left, right) = two_rects(&mut h); + + assert_eq!(corners(&h, &left), (0.0, 0.0, 100.0, 200.0)); + assert_eq!(corners(&h, &right), (100.0, 0.0, 400.0, 200.0)); +} + +#[test] +fn resizing_relays_out_against_the_new_output() { + let mut h = Harness::new((400, 200)); + let (left, right) = two_rects(&mut h); + + h.resize((800, 100)); + assert!(h.needs_redraw()); + h.frame(); + + assert_eq!(corners(&h, &left), (0.0, 0.0, 100.0, 100.0)); + assert_eq!(corners(&h, &right), (100.0, 0.0, 800.0, 100.0)); +} diff --git a/tests/pointer.rs b/tests/pointer.rs new file mode 100644 index 0000000..bcc17d4 --- /dev/null +++ b/tests/pointer.rs @@ -0,0 +1,60 @@ +//! Which widget an input reaches. + +use std::{cell::RefCell, rc::Rc}; + +use iris::harness::Harness; +use iris::prelude::*; + +#[test] +fn a_press_reaches_only_the_widget_under_the_cursor() { + let mut h = Harness::new((400, 200)); + let clicks = Rc::new(RefCell::new(Vec::new())); + + let (on_left, on_right) = (clicks.clone(), clicks.clone()); + let left = rect(Color::RED) + .width(100) + .on(CursorSense::click(), move |_, _| { + on_left.borrow_mut().push("left") + }) + .add(&mut h.rsc); + let right = rect(Color::BLUE) + .on(CursorSense::click(), move |_, _| { + on_right.borrow_mut().push("right") + }) + .add(&mut h.rsc); + h.set_root((left, right).span(Dir::RIGHT)); + + h.click((50, 100)); + assert_eq!(*clicks.borrow(), ["left"]); + + h.click((300, 100)); + assert_eq!(*clicks.borrow(), ["left", "right"]); +} + +#[test] +fn hover_ends_when_the_cursor_leaves_the_window() { + let mut h = Harness::new((400, 200)); + let hovered = Rc::new(RefCell::new(0)); + let ended = Rc::new(RefCell::new(0)); + + let (h_count, e_count) = (hovered.clone(), ended.clone()); + let widget = rect(Color::RED) + .on(CursorSense::HoverStart, move |_, _| { + *h_count.borrow_mut() += 1 + }) + .on(CursorSense::HoverEnd, move |_, _| { + *e_count.borrow_mut() += 1 + }) + .add(&mut h.rsc); + h.set_root(widget); + + h.move_to((200, 100)); + assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 0)); + + // A second sample inside the same widget is not a second hover. + h.move_to((210, 100)); + assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 0)); + + h.leave(); + assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 1)); +} diff --git a/tests/scroll.rs b/tests/scroll.rs new file mode 100644 index 0000000..bdf6d0f --- /dev/null +++ b/tests/scroll.rs @@ -0,0 +1,29 @@ +//! Scrolling moves content and stops at its ends. + +mod common; + +use common::corners; +use iris::harness::Harness; +use iris::prelude::*; + +#[test] +fn a_wheel_scrolls_the_content_and_stops_at_its_end() { + let mut h = Harness::new((400, 200)); + // Twice the window's height, so there is 200 to scroll. + let top = rect(Color::RED).height(200).add(&mut h.rsc); + let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc); + h.set_root((top, bottom).span(Dir::DOWN).scrollable()); + h.move_to((200, 100)); + + // `Scroll` starts snapped to the end. + assert_eq!(corners(&h, &top).1, -200.0); + + // The handler scales a wheel line by 50. + h.scroll((0, 1)); + h.frame(); + assert_eq!(corners(&h, &top).1, -150.0); + + h.scroll((0, 10)); + h.frame(); + assert_eq!(corners(&h, &top).1, 0.0); +} diff --git a/tests/tasks.rs b/tests/tasks.rs new file mode 100644 index 0000000..b036f3e --- /dev/null +++ b/tests/tasks.rs @@ -0,0 +1,23 @@ +//! What a background task can change, and how it gets back to the ui. + +use std::time::Duration; + +use iris::harness::Harness; +use iris::prelude::*; + +#[test] +fn a_task_update_reaches_the_tree() { + let mut h = Harness::new((400, 200)); + let widget = rect(Color::RED).add(&mut h.rsc); + h.set_root(widget.task_on(CursorSense::click(), async move |mut ctx| { + ctx.update(move |_, rsc| widget(rsc).color = Color::BLUE); + })); + + h.click((200, 100)); + + assert!( + h.await_update(Duration::from_secs(5)), + "the task sent no update" + ); + assert_eq!(h.rsc[widget].color, Color::BLUE); +}