From e53ce585e61260c4292da1ebb663291aa60c7eba Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Sun, 13 Sep 2026 20:43:38 -0400 Subject: [PATCH] Say position-only, and stop falsifying the cursor `is_momentary` becomes `position_only` on both the sense and the cursor, inverted so it reads as what it tests. A widget the cursor has left was being handed a blanked cursor so its press senses would not match. `should_run` now skips non-position senses when the pointer is not inside, which is the same rule without lying about the input: the widget still gets the real cursor with its hover ending. `consumes` loses its `momentary` argument, since the cursor answers that itself. --- src/default/sense.rs | 57 +++++++++++++++++++--------------------- tests/pointer_routing.rs | 24 ++++++++--------- 2 files changed, 39 insertions(+), 42 deletions(-) diff --git a/src/default/sense.rs b/src/default/sense.rs index a4fcaae..b81bec6 100644 --- a/src/default/sense.rs +++ b/src/default/sense.rs @@ -41,14 +41,15 @@ impl Event for CursorSenses { impl CursorSenses { /// Whether a widget with these senses stops the input reaching the layer - /// below it. Where the cursor rests stops at the top layer that is under - /// it; something happening to the cursor stops only at a widget that - /// answers to that, so a click-only child does not swallow a scroll. - fn consumes(&self, cursor: &CursorState, hover: ActivationState, momentary: bool) -> bool { - if !momentary { + /// below it. A cursor that is only somewhere stops at the first layer it + /// is over. A cursor doing something -- a press, a scroll -- stops only + /// where something answers to it, so a button drawn over a list does not + /// swallow the list's scrolling. + fn consumes(&self, cursor: &CursorState, hover: ActivationState) -> bool { + if cursor.position_only() { return true; } - should_run(self, cursor, hover).is_some_and(|sense| sense.is_momentary()) + should_run(self, cursor, hover).is_some_and(|sense| !sense.position_only()) } } @@ -66,10 +67,10 @@ impl CursorSense { matches!(self, CursorSense::Pressing(CursorButton::Left)) } - /// Whether this sense is about something happening to the cursor, rather - /// than about where it rests. - fn is_momentary(&self) -> bool { - !matches!(self, Self::HoverStart | Self::Hovering | Self::HoverEnd) + /// False if the sense is a button or a scroll, true if it is only about + /// where the cursor is. + fn position_only(&self) -> bool { + matches!(self, Self::HoverStart | Self::Hovering | Self::HoverEnd) } } @@ -115,9 +116,10 @@ impl CursorButtons { } impl CursorState { - /// Whether anything is happening to the cursor beyond where it rests. - pub fn has_momentary_input(&self) -> bool { - self.scroll_delta != Vec2::ZERO || self.buttons.iter().any(|(_, state)| !state.is_off()) + /// True if the cursor is only reporting where it is: no button and no + /// scroll this frame. + pub fn position_only(&self) -> bool { + self.scroll_delta == Vec2::ZERO && self.buttons.iter().all(|(_, state)| state.is_off()) } pub fn end_frame(&mut self) { @@ -188,7 +190,6 @@ impl SensorUi for UiRenderState { // state like thing, but local to render state, and is passed to UiRsc events so you can // update it there? let mut active = std::mem::take(&mut rsc.events_mut().get_type::().active); - let momentary = cursor.has_momentary_input(); for layer in self.layers.indices().rev() { let mut consumed = false; for (id, sensor) in active.get_mut(&layer).into_flat_iter() { @@ -199,22 +200,13 @@ impl SensorUi for UiRenderState { if sensor.hover == ActivationState::Off { continue; } - // A widget the cursor has left still hears its hover ending, - // but a press or a scroll landing elsewhere is not its input. - let cursor = match in_shape { - true => cursor.clone(), - false => CursorState { - pos: cursor.pos, - exists: cursor.exists, - ..Default::default() - }, - }; - consumed = consumed - || rsc - .events_mut() - .get_type::() - .registered(*id) - .any(|senses| senses.consumes(&cursor, sensor.hover, momentary)); + consumed |= rsc + .events_mut() + .get_type::() + .registered(*id) + .any(|senses| senses.consumes(&cursor, sensor.hover)); + + let cursor = cursor.clone(); let data = CursorData { pos: cursor.pos - region.top_left, @@ -243,6 +235,11 @@ pub fn should_run( hover: ActivationState, ) -> Option { for sense in senses.iter() { + // A widget the cursor is no longer inside senses only its position: + // the press that ended its hover landed on something else. + if !hover.is_on() && !sense.position_only() { + continue; + } if match sense { CursorSense::PressStart(button) => cursor.buttons.select(button).is_start(), CursorSense::Pressing(button) => cursor.buttons.select(button).is_on(), diff --git a/tests/pointer_routing.rs b/tests/pointer_routing.rs index 6c28cae..ee38517 100644 --- a/tests/pointer_routing.rs +++ b/tests/pointer_routing.rs @@ -126,8 +126,8 @@ fn full(ui: &mut Ui) -> StrongWidget { #[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 ui = &mut Ui::new(); + let (bottom, middle, top) = (full(ui), full(ui), full(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); @@ -147,8 +147,8 @@ fn hover_stops_at_the_topmost_widget() { #[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 ui = &mut Ui::new(); + let (list, button, overlay) = (full(ui), full(ui), full(ui)); let scrolled = ui.listen(&list, CursorSense::Scroll); let clicked = ui.listen(&button, CursorSense::click()); let overlay_clicked = ui.listen(&overlay, CursorSense::click()); @@ -169,8 +169,8 @@ fn a_scroll_passes_through_every_widget_that_does_not_want_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 ui = &mut Ui::new(); + let (below, above) = (full(ui), full(ui)); let below_clicked = ui.listen(&below, CursorSense::click()); let above_clicked = ui.listen(&above, CursorSense::click()); ui.stack(vec![below.any(), above.any()]); @@ -185,11 +185,11 @@ fn only_the_topmost_listener_takes_a_press() { #[test] fn a_press_beside_the_button_reaches_the_layer_below() { - let mut ui = Ui::new(); - let list = full(&mut ui); + let ui = &mut Ui::new(); + let list = full(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 button = full(ui); + let gap = full(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 { @@ -218,8 +218,8 @@ fn a_press_beside_the_button_reaches_the_layer_below() { #[test] fn leaving_a_widget_still_ends_its_hover() { - let mut ui = Ui::new(); - let widget = full(&mut ui); + let ui = &mut Ui::new(); + let widget = full(ui); let hover = ui.listen(&widget, CursorSense::HoverStart | CursorSense::HoverEnd); ui.stack(vec![widget.any()]);