diff --git a/src/default/sense.rs b/src/default/sense.rs index 937b93a..63f417d 100644 --- a/src/default/sense.rs +++ b/src/default/sense.rs @@ -4,14 +4,14 @@ use std::{ rc::Rc, }; -#[derive(Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum CursorButton { Left, Right, Middle, } -#[derive(Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum CursorSense { PressStart(CursorButton), Pressing(CursorButton), @@ -205,6 +205,19 @@ impl SensorUi for UiRenderState { } hovered_here |= over; + // Momentary input belongs to whatever the cursor is on. A + // widget it has just left still hears its hover ending, but + // a press landing elsewhere is neither its press to receive + // nor its press to take. + let cursor = match over { + true => cursor.clone(), + false => CursorState { + pos: cursor.pos, + exists: cursor.exists, + ..Default::default() + }, + }; + for senses in rsc.events_mut().get_type::().registered(*id) { for sense in senses.iter() { if matches(sense, &cursor, sensor.hover) { @@ -213,8 +226,6 @@ impl SensorUi for UiRenderState { } } - let cursor = cursor.clone(); - let data = CursorData { pos: cursor.pos - region.top_left, size: region.bot_right - region.top_left, diff --git a/tests/pointer_routing.rs b/tests/pointer_routing.rs index 7a978a2..e9e79e5 100644 --- a/tests/pointer_routing.rs +++ b/tests/pointer_routing.rs @@ -1,9 +1,9 @@ -//! A widget takes only what it answers to: a button over a list takes the -//! click and leaves the scroll. These drive `run_sensors` directly, which -//! needs no GPU and no window. +//! Input across layers: what a widget takes, what passes through it, and +//! where hovering stops. These drive `run_sensors` directly, which needs no +//! GPU and no window. use iris::prelude::*; -use std::{cell::Cell, rc::Rc}; +use std::{cell::RefCell, rc::Rc}; struct SenseRsc { ui: UiData, @@ -41,140 +41,215 @@ impl HasEvents for SenseRsc { } } -fn cursor_at(pos: Vec2) -> CursorState { - CursorState { - pos, - exists: true, - buttons: Default::default(), - scroll_delta: Vec2::ZERO, +const WINDOW: f32 = 100.0; + +/// Every sense that has fired on one widget since it was last read. +#[derive(Default, Clone)] +struct Fired(Rc>>); + +impl Fired { + fn take(&self) -> Vec { + std::mem::take(&mut self.0.borrow_mut()) } } -/// A button covering a list, on the layer above it: the list scrolls, the -/// button clicks, and the returned flags say which fired. -fn button_over_list() -> (UiRenderState, SenseRsc, Rc>, Rc>) { - let mut rsc = SenseRsc { - ui: UiData::default(), - events: EventManager::default(), - }; +struct Ui { + rsc: SenseRsc, + render: UiRenderState, + state: (), +} - // Both cover the whole window: the button "sitting over" the list. - let list = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE)); - let list_weak = list.weak(); - let button = rsc.ui.widgets.add_strong(Rect::new(UiColor::RED)); - let button_weak = button.weak(); - - let scrolled = Rc::new(Cell::new(false)); - let clicked = Rc::new(Cell::new(false)); - { - let scrolled = scrolled.clone(); - rsc.register_event(list_weak, CursorSense::Scroll, move |_ctx, _rsc| { - scrolled.set(true); - }); - } - { - let clicked = clicked.clone(); - rsc.register_event(button_weak, CursorSense::click(), move |_ctx, _rsc| { - clicked.set(true); - }); +impl Ui { + fn new() -> Self { + Self { + rsc: SenseRsc { + ui: UiData::default(), + events: EventManager::default(), + }, + render: UiRenderState::new(), + state: (), + } } - // A Stack draws each child on its own layer, in order. - let root = rsc - .ui - .widgets - .add_strong(Stack { - children: vec![list.any(), button.any()], - size: StackSize::default(), - }) - .any(); + fn listen( + &mut self, + widget: &StrongWidget, + senses: impl Into, + ) -> Fired { + let fired = Fired::default(); + let sink = fired.clone(); + self.rsc + .register_event(widget.weak(), senses.into(), move |ctx, _rsc| { + sink.0.borrow_mut().push(ctx.data.sense) + }); + fired + } - let mut render = UiRenderState::new(); - render.resize((100.0, 100.0)); - render.update(&root, &mut rsc); + /// Stacks the widgets bottom first, each on its own layer, and lays them + /// out in a square window. + fn stack(&mut self, children: Vec) { + let root = self + .rsc + .ui + .widgets + .add_strong(Stack { + children, + size: StackSize::default(), + }) + .any(); + self.render.resize((WINDOW, WINDOW)); + self.render.update(&root, &mut self.rsc); + } - (render, rsc, scrolled, clicked) + fn cursor(&mut self, at: (f32, f32)) -> CursorState { + CursorState { + pos: at.into(), + exists: true, + buttons: Default::default(), + scroll_delta: Vec2::ZERO, + } + } + + fn run(&mut self, cursor: CursorState) { + self.render.run_sensors( + &mut self.rsc, + &mut self.state, + cursor, + (WINDOW, WINDOW).into(), + ); + } +} + +fn full(ui: &mut Ui) -> StrongWidget { + rect(UiColor::WHITE).add_strong(&mut ui.rsc) } #[test] -fn a_button_over_a_list_scrolls_the_list_and_still_clicks() { - let (render, mut rsc, scrolled, clicked) = button_over_list(); - let mut state = (); +fn hover_stops_at_the_topmost_widget() { + let mut ui = Ui::new(); + let (bottom, middle, top) = (full(&mut ui), full(&mut ui), full(&mut ui)); + let bottom_hover = ui.listen(&bottom, CursorSense::HoverStart); + let middle_hover = ui.listen(&middle, CursorSense::HoverStart); + let top_hover = ui.listen(&top, CursorSense::HoverStart); + ui.stack(vec![bottom.any(), middle.any(), top.any()]); - let mut scroll = cursor_at((50.0, 50.0).into()); - scroll.scroll_delta = (0.0, 10.0).into(); - render.run_sensors(&mut rsc, &mut state, scroll, (100.0, 100.0).into()); + let cursor = ui.cursor((50.0, 50.0)); + ui.run(cursor); - assert!( - scrolled.get(), - "a scroll over the button must still reach the list underneath it" - ); - assert!( - !clicked.get(), - "a scroll is not a click; the button must not have fired" + assert_eq!(top_hover.take(), [CursorSense::HoverStart]); + assert_eq!( + middle_hover.take(), + [], + "hover is not shared with a layer below" ); + assert_eq!(bottom_hover.take(), []); +} - let mut click = cursor_at((50.0, 50.0).into()); - click.buttons.left = ActivationState::Start; - render.run_sensors(&mut rsc, &mut state, click, (100.0, 100.0).into()); +#[test] +fn a_scroll_passes_through_every_widget_that_does_not_want_it() { + let mut ui = Ui::new(); + let (list, button, overlay) = (full(&mut ui), full(&mut ui), full(&mut ui)); + let scrolled = ui.listen(&list, CursorSense::Scroll); + let clicked = ui.listen(&button, CursorSense::click()); + let overlay_clicked = ui.listen(&overlay, CursorSense::click()); + ui.stack(vec![list.any(), button.any(), overlay.any()]); - assert!( - clicked.get(), - "the button on top must still receive an actual click" + let mut cursor = ui.cursor((50.0, 50.0)); + cursor.scroll_delta = (0.0, 10.0).into(); + ui.run(cursor); + + assert_eq!( + scrolled.take(), + [CursorSense::Scroll], + "two layers of click-only widgets do not stop a scroll" ); + assert_eq!(clicked.take(), []); + assert_eq!(overlay_clicked.take(), []); } #[test] fn a_click_and_a_scroll_in_one_frame_go_to_different_widgets() { - let (render, mut rsc, scrolled, clicked) = button_over_list(); - let mut state = (); + let mut ui = Ui::new(); + let (list, button) = (full(&mut ui), full(&mut ui)); + let scrolled = ui.listen(&list, CursorSense::Scroll); + let clicked = ui.listen(&button, CursorSense::click()); + ui.stack(vec![list.any(), button.any()]); - let mut both = cursor_at((50.0, 50.0).into()); - both.scroll_delta = (0.0, 10.0).into(); - both.buttons.left = ActivationState::Start; - render.run_sensors(&mut rsc, &mut state, both, (100.0, 100.0).into()); + let mut cursor = ui.cursor((50.0, 50.0)); + cursor.scroll_delta = (0.0, 10.0).into(); + cursor.buttons.left = ActivationState::Start; + ui.run(cursor); - assert!( - clicked.get(), - "the button takes the click it registered for" - ); - assert!( - scrolled.get(), + assert_eq!(clicked.take(), [CursorSense::click()]); + assert_eq!( + scrolled.take(), + [CursorSense::Scroll], "taking the click must not take the scroll with it" ); } #[test] -fn leaving_a_widget_still_ends_its_hover() { - let mut rsc = SenseRsc { - ui: UiData::default(), - events: EventManager::default(), - }; - let widget = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE)); - let ended = Rc::new(Cell::new(false)); - { - let ended = ended.clone(); - rsc.register_event(widget.weak(), CursorSense::HoverEnd, move |_ctx, _rsc| { - ended.set(true); - }); - } +fn only_the_topmost_listener_takes_a_press() { + let mut ui = Ui::new(); + let (below, above) = (full(&mut ui), full(&mut ui)); + let below_clicked = ui.listen(&below, CursorSense::click()); + let above_clicked = ui.listen(&above, CursorSense::click()); + ui.stack(vec![below.any(), above.any()]); - let root = widget.any(); - let mut render = UiRenderState::new(); - render.resize((100.0, 100.0)); - render.update(&root, &mut rsc); + let mut cursor = ui.cursor((50.0, 50.0)); + cursor.buttons.left = ActivationState::Start; + ui.run(cursor); - let mut state = (); - render.run_sensors( - &mut rsc, - &mut state, - cursor_at((50.0, 50.0).into()), - (100.0, 100.0).into(), + assert_eq!(above_clicked.take(), [CursorSense::click()]); + assert_eq!(below_clicked.take(), [], "one press goes to one widget"); +} + +#[test] +fn a_press_beside_the_button_reaches_the_layer_below() { + let mut ui = Ui::new(); + let list = full(&mut ui); + // The row above the list covers it, but only its left half is the button. + let button = full(&mut ui); + let gap = full(&mut ui); + let list_clicked = ui.listen(&list, CursorSense::click()); + let button_clicked = ui.listen(&button, CursorSense::click()); + let row = ui.rsc.ui.widgets.add_strong(Span { + children: vec![button.any(), gap.any()], + dir: Dir::RIGHT, + gap: 0.0, + }); + ui.stack(vec![list.any(), row.any()]); + + let mut on_button = ui.cursor((20.0, 50.0)); + on_button.buttons.left = ActivationState::Start; + ui.run(on_button); + assert_eq!(button_clicked.take(), [CursorSense::click()]); + assert_eq!(list_clicked.take(), []); + + let mut beside_it = ui.cursor((80.0, 50.0)); + beside_it.buttons.left = ActivationState::Start; + ui.run(beside_it); + assert_eq!(button_clicked.take(), [], "the cursor is not on the button"); + assert_eq!( + list_clicked.take(), + [CursorSense::click()], + "a press beside the button belongs to what is under it" ); - assert!(!ended.get(), "the cursor is still on it"); +} + +#[test] +fn leaving_a_widget_still_ends_its_hover() { + let mut ui = Ui::new(); + let widget = full(&mut ui); + let hover = ui.listen(&widget, CursorSense::HoverStart | CursorSense::HoverEnd); + ui.stack(vec![widget.any()]); + + let cursor = ui.cursor((50.0, 50.0)); + ui.run(cursor); + assert_eq!(hover.take(), [CursorSense::HoverStart]); - let mut gone = cursor_at((50.0, 50.0).into()); + let mut gone = ui.cursor((50.0, 50.0)); gone.exists = false; - render.run_sensors(&mut rsc, &mut state, gone, (100.0, 100.0).into()); - assert!(ended.get(), "leaving a widget ends its hover"); + ui.run(gone); + assert_eq!(hover.take(), [CursorSense::HoverEnd]); }