//! 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::RefCell, rc::Rc}; struct SenseRsc { ui: UiData, events: EventManager, } impl UiRsc for SenseRsc { fn ui(&self) -> &UiData { &self.ui } fn ui_mut(&mut self) -> &mut UiData { &mut self.ui } fn on_draw(&mut self, active: &ActiveData) { self.events.draw(active); } fn on_undraw(&mut self, active: &ActiveData) { self.events.undraw(active); } fn on_remove(&mut self, id: WidgetId) { self.events.remove(id); } } impl HasState for SenseRsc { type State = (); } impl HasEvents for SenseRsc { fn events(&self) -> &EventManager { &self.events } fn events_mut(&mut self) -> &mut EventManager { &mut self.events } } 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()) } } struct Ui { rsc: SenseRsc, render: UiRenderState, state: (), } impl Ui { fn new() -> Self { Self { rsc: SenseRsc { ui: UiData::default(), events: EventManager::default(), }, render: UiRenderState::new(), state: (), } } 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 } /// 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); } 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 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 cursor = ui.cursor((50.0, 50.0)); ui.run(cursor); 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(), []); } #[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()]); 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 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 cursor = ui.cursor((50.0, 50.0)); cursor.scroll_delta = (0.0, 10.0).into(); cursor.buttons.left = ActivationState::Start; ui.run(cursor); assert_eq!(clicked.take(), [CursorSense::click()]); assert_eq!( scrolled.take(), [CursorSense::Scroll], "taking the click must not take the scroll with it" ); } #[test] 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 mut cursor = ui.cursor((50.0, 50.0)); cursor.buttons.left = ActivationState::Start; ui.run(cursor); 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" ); } #[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 = ui.cursor((50.0, 50.0)); gone.exists = false; ui.run(gone); assert_eq!(hover.take(), [CursorSense::HoverEnd]); }