diff --git a/src/default/sense.rs b/src/default/sense.rs index 63f417d..a4fcaae 100644 --- a/src/default/sense.rs +++ b/src/default/sense.rs @@ -39,6 +39,19 @@ 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 { + return true; + } + should_run(self, cursor, hover).is_some_and(|sense| sense.is_momentary()) + } +} + impl CursorSense { pub fn click() -> Self { Self::PressStart(CursorButton::Left) @@ -53,17 +66,10 @@ impl CursorSense { matches!(self, CursorSense::Pressing(CursorButton::Left)) } - /// Takes what this sense answers to out of `cursor`, so a widget below - /// does not also get it. Hovering takes nothing: it goes to the topmost - /// widget in shape, which is not a question about the input. - fn take(&self, cursor: &mut CursorState) { - match self { - Self::PressStart(button) | Self::Pressing(button) | Self::PressEnd(button) => { - *cursor.buttons.select_mut(button) = ActivationState::Off - } - Self::Scroll => cursor.scroll_delta = Vec2::ZERO, - Self::HoverStart | Self::Hovering | Self::HoverEnd => {} - } + /// 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) } } @@ -91,14 +97,6 @@ impl CursorButtons { } } - pub fn select_mut(&mut self, button: &CursorButton) -> &mut ActivationState { - match button { - CursorButton::Left => &mut self.left, - CursorButton::Right => &mut self.right, - CursorButton::Middle => &mut self.middle, - } - } - pub fn end_frame(&mut self) { self.left.end_frame(); self.middle.end_frame(); @@ -117,6 +115,11 @@ 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()) + } + pub fn end_frame(&mut self) { self.buttons.end_frame(); self.scroll_delta = Vec2::ZERO; @@ -185,31 +188,20 @@ 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); - // Narrowed as it descends: a widget takes what it answers to, and - // what is left is what the layers below see. - let mut cursor = cursor; - let mut hovered = false; + let momentary = cursor.has_momentary_input(); for layer in self.layers.indices().rev() { - let mut below = cursor.clone(); - let mut hovered_here = false; + let mut consumed = false; for (id, sensor) in active.get_mut(&layer).into_flat_iter() { let shape = self.active.get(id).unwrap().region; let region = shape.to_px(window_size); - let over = cursor.exists && region.contains(cursor.pos); - // Hover goes to the topmost widget in shape and no further. - sensor.hover.update(over && !hovered); - // `is_off` would be wrong here: it counts `End`, which is - // the one frame a hover-end handler has to run on. - if !over && sensor.hover == ActivationState::Off { + let in_shape = cursor.exists && region.contains(cursor.pos); + sensor.hover.update(in_shape); + if sensor.hover == ActivationState::Off { continue; } - 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 { + // 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, @@ -217,14 +209,12 @@ impl SensorUi for UiRenderState { ..Default::default() }, }; - - for senses in rsc.events_mut().get_type::().registered(*id) { - for sense in senses.iter() { - if matches(sense, &cursor, sensor.hover) { - sense.take(&mut below); - } - } - } + consumed = consumed + || rsc + .events_mut() + .get_type::() + .registered(*id) + .any(|senses| senses.consumes(&cursor, sensor.hover, momentary)); let data = CursorData { pos: cursor.pos - region.top_left, @@ -239,9 +229,7 @@ impl SensorUi for UiRenderState { }; rsc.run_event::(*id, data, state); } - hovered |= hovered_here; - cursor = below; - if hovered && !is_momentary(&cursor) { + if consumed { break; } } @@ -254,27 +242,20 @@ pub fn should_run( cursor: &CursorState, hover: ActivationState, ) -> Option { - senses - .iter() - .find(|sense| matches(sense, cursor, hover)) - .copied() -} - -fn matches(sense: &CursorSense, cursor: &CursorState, hover: ActivationState) -> bool { - match sense { - CursorSense::PressStart(button) => cursor.buttons.select(button).is_start(), - CursorSense::Pressing(button) => cursor.buttons.select(button).is_on(), - CursorSense::PressEnd(button) => cursor.buttons.select(button).is_end(), - CursorSense::HoverStart => hover.is_start(), - CursorSense::Hovering => hover.is_on(), - CursorSense::HoverEnd => hover.is_end(), - CursorSense::Scroll => cursor.scroll_delta != Vec2::ZERO, + for sense in senses.iter() { + if match sense { + CursorSense::PressStart(button) => cursor.buttons.select(button).is_start(), + CursorSense::Pressing(button) => cursor.buttons.select(button).is_on(), + CursorSense::PressEnd(button) => cursor.buttons.select(button).is_end(), + CursorSense::HoverStart => hover.is_start(), + CursorSense::Hovering => hover.is_on(), + CursorSense::HoverEnd => hover.is_end(), + CursorSense::Scroll => cursor.scroll_delta != Vec2::ZERO, + } { + return Some(*sense); + } } -} - -/// Whether anything is happening to the cursor beyond where it rests. -fn is_momentary(cursor: &CursorState) -> bool { - cursor.scroll_delta != Vec2::ZERO || cursor.buttons.iter().any(|(_, state)| !state.is_off()) + None } impl ActivationState { diff --git a/tests/pointer_routing.rs b/tests/pointer_routing.rs index e9e79e5..6c28cae 100644 --- a/tests/pointer_routing.rs +++ b/tests/pointer_routing.rs @@ -1,4 +1,4 @@ -//! Input across layers: what a widget takes, what passes through it, and +//! Input across layers: what stops at a layer, what passes through it, and //! where hovering stops. These drive `run_sensors` directly, which needs no //! GPU and no window. @@ -167,27 +167,6 @@ fn a_scroll_passes_through_every_widget_that_does_not_want_it() { 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();