diff --git a/core/src/event/manager.rs b/core/src/event/manager.rs index 840fcb8..1a92557 100644 --- a/core/src/event/manager.rs +++ b/core/src/event/manager.rs @@ -135,6 +135,15 @@ impl TypeEventManager { )); } + /// The event lists this widget was registered with, without running + /// anything. Asking what a widget would match is a separate question + /// from dispatching to it: input routing needs the first to decide + /// whether a widget consumes an event, and `run_fn` can only answer + /// the second. + pub fn registered(&self, id: WidgetId) -> impl Iterator { + self.map.get(&id).into_iter().flatten().map(|(e, _)| e) + } + pub fn run_fn<'a>( &mut self, id: impl IdLike, diff --git a/src/default/sense.rs b/src/default/sense.rs index ee73ee9..37c9a4f 100644 --- a/src/default/sense.rs +++ b/src/default/sense.rs @@ -52,6 +52,18 @@ impl CursorSense { pub fn is_dragging(&self) -> bool { matches!(self, CursorSense::Pressing(CursorButton::Left)) } + + /// True for a sense that names something happening this frame (a + /// button transitioning, a scroll), as opposed to the ambient + /// `Hover*` family that is on for as long as the cursor rests there. + /// Only a momentary sense can consume an input -- see + /// `SensorUi::run_sensors`. + pub fn is_momentary(&self) -> bool { + !matches!( + self, + CursorSense::HoverStart | CursorSense::Hovering | CursorSense::HoverEnd + ) + } } #[derive(Default, Clone)] @@ -163,6 +175,11 @@ impl SensorUi for UiRenderState { // this would probably be done through a generic parameter that adds yet another rsc / // state like thing, but local to render state, and is passed to UiRsc events so you can // update it there? + // Whether anything momentary is happening this frame at all; see + // `consumed` below, which is only judged per kind when it is. + let momentary_active = + cursor.scroll_delta != Vec2::ZERO || cursor.buttons.iter().any(|(_, a)| !a.is_off()); + let mut active = std::mem::take(&mut rsc.events_mut().get_type::().active); for layer in self.layers.indices().rev() { let mut sensed = false; @@ -174,7 +191,29 @@ impl SensorUi for UiRenderState { if sensor.hover == ActivationState::Off { continue; } - sensed = true; + + // A widget in shape always still runs: a hover-only + // highlight must fire on the topmost thing under the + // cursor even while a scroll passes through it. What is + // judged per input kind is whether it *consumes* that + // input, stopping a lower layer from seeing it. With + // nothing momentary happening, being in shape is + // consumption and the topmost widget wins the hover; with + // a scroll or a press happening, only a widget that + // registered a matching momentary sense consumes it, so a + // button registered for `click()` alone cannot block a + // scroll meant for the list behind it. + let consumed = if momentary_active { + rsc.events_mut() + .get_type::() + .registered(*id) + .any(|senses| { + matches!(should_run(senses, &cursor, sensor.hover), Some(s) if s.is_momentary()) + }) + } else { + true + }; + sensed |= consumed; let cursor = cursor.clone(); diff --git a/tests/pointer_routing.rs b/tests/pointer_routing.rs new file mode 100644 index 0000000..5d31f6e --- /dev/null +++ b/tests/pointer_routing.rs @@ -0,0 +1,121 @@ +//! A widget that registered only `click()` used to block a `Scroll` meant +//! for whatever is behind it: `run_sensors` decided "consumed, stop looking +//! at lower layers" from mere hover rather than from anything actually +//! matching the input. These drive `run_sensors` directly, which needs no +//! GPU and no window. + +use iris::prelude::*; +use std::{cell::Cell, 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 + } +} + +fn cursor_at(pos: Vec2) -> CursorState { + CursorState { + pos, + exists: true, + buttons: Default::default(), + scroll_delta: Vec2::ZERO, + } +} + +#[test] +fn a_button_over_a_list_scrolls_the_list_and_still_clicks() { + let mut rsc = SenseRsc { + ui: UiData::default(), + events: EventManager::default(), + }; + + // 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); + }); + } + + // A Stack draws its children on separate layers in order, which is + // exactly the "one thing drawn over another" shape `run_sensors` + // walks top layer first. + let root = rsc + .ui + .widgets + .add_strong(Stack { + children: vec![list.any(), button.any()], + size: StackSize::default(), + }) + .any(); + + let mut render = UiRenderState::new(); + render.resize((100.0, 100.0)); + render.update(&root, &mut rsc); + + let mut state = (); + let mut scroll_cursor = cursor_at((50.0, 50.0).into()); + scroll_cursor.scroll_delta = (0.0, 10.0).into(); + render.run_sensors(&mut rsc, &mut state, scroll_cursor, (100.0, 100.0).into()); + + 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" + ); + + let mut click_cursor = cursor_at((50.0, 50.0).into()); + click_cursor.buttons.left = ActivationState::Start; + render.run_sensors(&mut rsc, &mut state, click_cursor, (100.0, 100.0).into()); + + assert!( + clicked.get(), + "the button on top must still receive an actual click" + ); +}