Consume by layer, not by widget
Replaces the taking mechanism with `CursorSenses::consumes`, which decides only whether a layer stops the input reaching the layer below. Nothing is removed from the cursor, and senses on one layer no longer block each other: every sensor the pointer is inside runs. Where the cursor rests stops at the top layer under it. Something happening to the cursor stops only at a widget that answers to it, so a click-only child does not swallow a scroll -- which is what `main` gets wrong, where any hovered sensor blocks the layer below. A widget the cursor has left still hears its hover ending, but is handed no press or scroll: that input landed somewhere else. This is a hit test rather than a consumption rule, and without it a press beside a button fires the button it just left. `a_click_and_a_scroll_in_one_frame_go_to_different_widgets` goes with the per-kind taking it tested. Of the five that remain, two fail on `main`.
This commit is contained in:
1 parent
3ab9c922fd
commit
f3fd9417d4
2 files changed
+43
-83
No files matched your search
+42
-61
@@ -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 {
|
impl CursorSense {
|
||||||
pub fn click() -> Self {
|
pub fn click() -> Self {
|
||||||
Self::PressStart(CursorButton::Left)
|
Self::PressStart(CursorButton::Left)
|
||||||
@@ -53,17 +66,10 @@ impl CursorSense {
|
|||||||
matches!(self, CursorSense::Pressing(CursorButton::Left))
|
matches!(self, CursorSense::Pressing(CursorButton::Left))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Takes what this sense answers to out of `cursor`, so a widget below
|
/// Whether this sense is about something happening to the cursor, rather
|
||||||
/// does not also get it. Hovering takes nothing: it goes to the topmost
|
/// than about where it rests.
|
||||||
/// widget in shape, which is not a question about the input.
|
fn is_momentary(&self) -> bool {
|
||||||
fn take(&self, cursor: &mut CursorState) {
|
!matches!(self, Self::HoverStart | Self::Hovering | Self::HoverEnd)
|
||||||
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 => {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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) {
|
pub fn end_frame(&mut self) {
|
||||||
self.left.end_frame();
|
self.left.end_frame();
|
||||||
self.middle.end_frame();
|
self.middle.end_frame();
|
||||||
@@ -117,6 +115,11 @@ impl CursorButtons {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CursorState {
|
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) {
|
pub fn end_frame(&mut self) {
|
||||||
self.buttons.end_frame();
|
self.buttons.end_frame();
|
||||||
self.scroll_delta = Vec2::ZERO;
|
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
|
// state like thing, but local to render state, and is passed to UiRsc events so you can
|
||||||
// update it there?
|
// update it there?
|
||||||
let mut active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active);
|
let mut active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active);
|
||||||
// Narrowed as it descends: a widget takes what it answers to, and
|
let momentary = cursor.has_momentary_input();
|
||||||
// what is left is what the layers below see.
|
|
||||||
let mut cursor = cursor;
|
|
||||||
let mut hovered = false;
|
|
||||||
for layer in self.layers.indices().rev() {
|
for layer in self.layers.indices().rev() {
|
||||||
let mut below = cursor.clone();
|
let mut consumed = false;
|
||||||
let mut hovered_here = false;
|
|
||||||
for (id, sensor) in active.get_mut(&layer).into_flat_iter() {
|
for (id, sensor) in active.get_mut(&layer).into_flat_iter() {
|
||||||
let shape = self.active.get(id).unwrap().region;
|
let shape = self.active.get(id).unwrap().region;
|
||||||
let region = shape.to_px(window_size);
|
let region = shape.to_px(window_size);
|
||||||
let over = cursor.exists && region.contains(cursor.pos);
|
let in_shape = cursor.exists && region.contains(cursor.pos);
|
||||||
// Hover goes to the topmost widget in shape and no further.
|
sensor.hover.update(in_shape);
|
||||||
sensor.hover.update(over && !hovered);
|
if sensor.hover == ActivationState::Off {
|
||||||
// `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 {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
hovered_here |= over;
|
// A widget the cursor has left still hears its hover ending,
|
||||||
|
// but a press or a scroll landing elsewhere is not its input.
|
||||||
// Momentary input belongs to whatever the cursor is on. A
|
let cursor = match in_shape {
|
||||||
// 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(),
|
true => cursor.clone(),
|
||||||
false => CursorState {
|
false => CursorState {
|
||||||
pos: cursor.pos,
|
pos: cursor.pos,
|
||||||
@@ -217,14 +209,12 @@ impl SensorUi for UiRenderState {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
consumed = consumed
|
||||||
for senses in rsc.events_mut().get_type::<CursorSense>().registered(*id) {
|
|| rsc
|
||||||
for sense in senses.iter() {
|
.events_mut()
|
||||||
if matches(sense, &cursor, sensor.hover) {
|
.get_type::<CursorSense>()
|
||||||
sense.take(&mut below);
|
.registered(*id)
|
||||||
}
|
.any(|senses| senses.consumes(&cursor, sensor.hover, momentary));
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = CursorData {
|
let data = CursorData {
|
||||||
pos: cursor.pos - region.top_left,
|
pos: cursor.pos - region.top_left,
|
||||||
@@ -239,9 +229,7 @@ impl SensorUi for UiRenderState {
|
|||||||
};
|
};
|
||||||
rsc.run_event::<CursorSense>(*id, data, state);
|
rsc.run_event::<CursorSense>(*id, data, state);
|
||||||
}
|
}
|
||||||
hovered |= hovered_here;
|
if consumed {
|
||||||
cursor = below;
|
|
||||||
if hovered && !is_momentary(&cursor) {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,14 +242,8 @@ pub fn should_run(
|
|||||||
cursor: &CursorState,
|
cursor: &CursorState,
|
||||||
hover: ActivationState,
|
hover: ActivationState,
|
||||||
) -> Option<CursorSense> {
|
) -> Option<CursorSense> {
|
||||||
senses
|
for sense in senses.iter() {
|
||||||
.iter()
|
if match sense {
|
||||||
.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::PressStart(button) => cursor.buttons.select(button).is_start(),
|
||||||
CursorSense::Pressing(button) => cursor.buttons.select(button).is_on(),
|
CursorSense::Pressing(button) => cursor.buttons.select(button).is_on(),
|
||||||
CursorSense::PressEnd(button) => cursor.buttons.select(button).is_end(),
|
CursorSense::PressEnd(button) => cursor.buttons.select(button).is_end(),
|
||||||
@@ -269,12 +251,11 @@ fn matches(sense: &CursorSense, cursor: &CursorState, hover: ActivationState) ->
|
|||||||
CursorSense::Hovering => hover.is_on(),
|
CursorSense::Hovering => hover.is_on(),
|
||||||
CursorSense::HoverEnd => hover.is_end(),
|
CursorSense::HoverEnd => hover.is_end(),
|
||||||
CursorSense::Scroll => cursor.scroll_delta != Vec2::ZERO,
|
CursorSense::Scroll => cursor.scroll_delta != Vec2::ZERO,
|
||||||
|
} {
|
||||||
|
return Some(*sense);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
None
|
||||||
/// 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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActivationState {
|
impl ActivationState {
|
||||||
|
|||||||
@@ -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
|
//! where hovering stops. These drive `run_sensors` directly, which needs no
|
||||||
//! GPU and no window.
|
//! 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(), []);
|
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]
|
#[test]
|
||||||
fn only_the_topmost_listener_takes_a_press() {
|
fn only_the_topmost_listener_takes_a_press() {
|
||||||
let mut ui = Ui::new();
|
let mut ui = Ui::new();
|
||||||
|
|||||||
Reference in new issue
Block a user