use super::*;
use crate::gesture::{GestureArena, GestureEvent};
impl WidgetTree {
pub(crate) fn ensure_gesture_arena(
node: &mut crate::arena::WidgetNode,
id: WidgetId,
gesture_owners: &mut std::collections::HashSet<WidgetId>,
) {
if node.handlers.gesture_arena.is_some() {
gesture_owners.insert(id);
return;
}
let has_tap = node.any_handler(|h| h.on_tap.is_some());
let has_double_tap = node.any_handler(|h| h.on_double_tap.is_some());
let has_triple_tap = node.any_handler(|h| h.on_triple_tap.is_some());
let has_drag = node.any_handler(|h| h.on_drag.is_some());
let has_long_press = node.any_handler(|h| h.on_long_press.is_some());
let has_swipe = node.any_handler(|h| h.on_swipe.is_some());
if !(has_tap || has_double_tap || has_triple_tap || has_drag || has_long_press || has_swipe)
{
return;
}
let tap_buttons = node
.handlers
.tap_buttons
.or(node.external_handlers.tap_buttons);
let double_tap_buttons = node
.handlers
.double_tap_buttons
.or(node.external_handlers.double_tap_buttons);
let triple_tap_buttons = node
.handlers
.triple_tap_buttons
.or(node.external_handlers.triple_tap_buttons);
let long_press_buttons = node
.handlers
.long_press_buttons
.or(node.external_handlers.long_press_buttons);
let mut arena = GestureArena::new();
if has_tap && !(has_double_tap || has_triple_tap) {
let mut rec = crate::gesture::TapRecognizer::new();
if let Some(mask) = tap_buttons {
rec = rec.accept_buttons(mask);
}
arena.add(rec);
}
if has_double_tap {
let mut rec = crate::gesture::DoubleTapRecognizer::new();
if let Some(mask) = double_tap_buttons {
rec = rec.accept_buttons(mask);
}
arena.add(rec);
}
if has_triple_tap {
let mut rec = crate::gesture::TripleTapRecognizer::new();
if let Some(mask) = triple_tap_buttons {
rec = rec.accept_buttons(mask);
}
arena.add(rec);
}
if has_drag {
arena.add(crate::gesture::DragRecognizer::new().threshold(5.0));
}
if has_long_press {
let mut rec = crate::gesture::LongPressRecognizer::new();
if let Some(mask) = long_press_buttons {
rec = rec.accept_buttons(mask);
}
arena.add(rec);
}
if has_swipe {
arena.add(crate::gesture::SwipeRecognizer::new());
}
node.handlers.gesture_arena = Some(arena);
gesture_owners.insert(id);
}
pub(crate) fn dispatch_recognized_gesture(
node: &mut crate::arena::WidgetNode,
gesture: GestureEvent,
ctx: &mut EventContext,
) {
use crate::gesture::{DragPhase, PinchPhase};
let saved_source = ctx
.current_source
.replace(crate::telemetry::IntentSource::Handler);
match gesture {
GestureEvent::Tap(event) => {
if let Some(h) = node.external_handlers.on_tap.as_mut() {
h(&event, ctx);
}
if let Some(h) = node.handlers.on_tap.as_mut() {
h(&event, ctx);
}
}
GestureEvent::DoubleTap(event) => {
if let Some(h) = node.external_handlers.on_double_tap.as_mut() {
h(&event, ctx);
}
if let Some(h) = node.handlers.on_double_tap.as_mut() {
h(&event, ctx);
}
}
GestureEvent::TripleTap(event) => {
if let Some(h) = node.external_handlers.on_triple_tap.as_mut() {
h(&event, ctx);
}
if let Some(h) = node.handlers.on_triple_tap.as_mut() {
h(&event, ctx);
}
}
GestureEvent::LongPress(event) => {
if let Some(h) = node.external_handlers.on_long_press.as_mut() {
h(&event, ctx);
}
if let Some(h) = node.handlers.on_long_press.as_mut() {
h(&event, ctx);
}
}
GestureEvent::DragStarted { position, button } => {
ctx.capture_pointer();
let phase = DragPhase::Started { position, button };
if let Some(h) = node.external_handlers.on_drag.as_mut() {
h(phase, ctx);
}
if let Some(h) = node.handlers.on_drag.as_mut() {
h(phase, ctx);
}
}
GestureEvent::DragMoved { position, delta } => {
let phase = DragPhase::Moved { position, delta };
if let Some(h) = node.external_handlers.on_drag.as_mut() {
h(phase, ctx);
}
if let Some(h) = node.handlers.on_drag.as_mut() {
h(phase, ctx);
}
}
GestureEvent::DragEnded { position } => {
let phase = DragPhase::Ended { position };
if let Some(h) = node.external_handlers.on_drag.as_mut() {
h(phase, ctx);
}
if let Some(h) = node.handlers.on_drag.as_mut() {
h(phase, ctx);
}
ctx.release_pointer();
}
GestureEvent::Swipe {
direction,
velocity,
} => {
if let Some(h) = node.external_handlers.on_swipe.as_mut() {
h(direction, velocity, ctx);
}
if let Some(h) = node.handlers.on_swipe.as_mut() {
h(direction, velocity, ctx);
}
}
GestureEvent::PinchStarted { center } => {
if let Some(h) = node.external_handlers.on_pinch.as_mut() {
h(PinchPhase::Started { center }, ctx);
}
if let Some(h) = node.handlers.on_pinch.as_mut() {
h(PinchPhase::Started { center }, ctx);
}
}
GestureEvent::PinchChanged {
center,
scale,
rotation,
} => {
let phase = PinchPhase::Changed {
center,
scale,
rotation,
};
if let Some(h) = node.external_handlers.on_pinch.as_mut() {
h(phase, ctx);
}
if let Some(h) = node.handlers.on_pinch.as_mut() {
h(phase, ctx);
}
}
GestureEvent::PinchEnded => {
if let Some(h) = node.external_handlers.on_pinch.as_mut() {
h(PinchPhase::Ended, ctx);
}
if let Some(h) = node.handlers.on_pinch.as_mut() {
h(PinchPhase::Ended, ctx);
}
}
}
ctx.current_source = saved_source;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_widgets::FillWidget;
use crate::widget_builder::WidgetBuilder;
#[test]
fn gesture_tap_recognized_on_click() {
use std::cell::Cell;
use std::rc::Rc;
let tapped = Rc::new(Cell::new(false));
let tapped_flag = tapped.clone();
let mut tree = WidgetTree::new();
let widget = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
tapped_flag.set(true);
}));
tree.layout(SizeProposal::exact(100.0, 50.0));
tree.click(widget);
assert!(tapped.get());
}
#[test]
fn gesture_drag_recognized_on_drag() {
use crate::gesture::DragPhase;
use std::cell::Cell;
use std::rc::Rc;
let drag_started = Rc::new(Cell::new(false));
let drag_ended = Rc::new(Cell::new(false));
let start_flag = drag_started.clone();
let end_flag = drag_ended.clone();
let mut tree = WidgetTree::new();
let _widget = tree.add(FillWidget::new().on_drag(move |phase, _ctx| match phase {
DragPhase::Started { .. } => start_flag.set(true),
DragPhase::Ended { .. } => end_flag.set(true),
_ => {}
}));
tree.layout(SizeProposal::exact(100.0, 50.0));
tree.drag(Point::new(50.0, 25.0), Point::new(80.0, 25.0));
assert!(drag_started.get());
assert!(drag_ended.get());
}
#[test]
fn gesture_handler_called_on_tap() {
use std::cell::Cell;
use std::rc::Rc;
let handler_called = Rc::new(Cell::new(false));
let handler_flag = handler_called.clone();
let mut tree = WidgetTree::new();
let widget = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
handler_flag.set(true);
}));
tree.layout(SizeProposal::exact(100.0, 50.0));
tree.click(widget);
assert!(handler_called.get());
}
#[test]
fn on_swipe_fires_from_platform_gesture_event() {
use crate::gesture::{GestureEvent, SwipeDirection};
use std::cell::Cell;
use std::rc::Rc;
let observed: Rc<Cell<Option<(SwipeDirection, i32)>>> = Rc::new(Cell::new(None));
let flag = observed.clone();
let mut tree = WidgetTree::new();
tree.add(
FillWidget::new().on_swipe(move |direction, velocity, _ctx| {
flag.set(Some((direction, velocity as i32)));
}),
);
tree.layout(SizeProposal::exact(100.0, 50.0));
tree.pointer_move(Point::new(50.0, 25.0));
tree.dispatch_event(WidgetEvent::Gesture {
gesture: GestureEvent::Swipe {
direction: SwipeDirection::Left,
velocity: 450.0,
},
});
let got = observed.get();
assert!(matches!(got, Some((SwipeDirection::Left, 450))));
}
#[test]
fn on_pinch_fires_from_platform_gesture_event() {
use crate::gesture::{GestureEvent, PinchPhase};
use std::cell::Cell;
use std::rc::Rc;
let started = Rc::new(Cell::new(false));
let scale_seen = Rc::new(Cell::new(0.0_f32));
let ended = Rc::new(Cell::new(false));
let started_flag = started.clone();
let scale_flag = scale_seen.clone();
let ended_flag = ended.clone();
let mut tree = WidgetTree::new();
tree.add(FillWidget::new().on_pinch(move |phase, _ctx| match phase {
PinchPhase::Started { .. } => started_flag.set(true),
PinchPhase::Changed { scale, .. } => scale_flag.set(scale),
PinchPhase::Ended => ended_flag.set(true),
}));
tree.layout(SizeProposal::exact(100.0, 50.0));
tree.pointer_move(Point::new(50.0, 25.0));
tree.dispatch_event(WidgetEvent::Gesture {
gesture: GestureEvent::PinchStarted {
center: Point::new(50.0, 25.0),
},
});
tree.dispatch_event(WidgetEvent::Gesture {
gesture: GestureEvent::PinchChanged {
center: Point::new(50.0, 25.0),
scale: 1.5,
rotation: 0.0,
},
});
tree.dispatch_event(WidgetEvent::Gesture {
gesture: GestureEvent::PinchEnded,
});
assert!(started.get());
assert!((scale_seen.get() - 1.5).abs() < 0.001);
assert!(ended.get());
}
#[test]
fn drag_auto_captures_pointer_until_ended() {
use crate::gesture::DragPhase;
use std::cell::Cell;
use std::rc::Rc;
let started = Rc::new(Cell::new(false));
let moved = Rc::new(Cell::new(0));
let ended = Rc::new(Cell::new(false));
let started_flag = started.clone();
let moved_flag = moved.clone();
let ended_flag = ended.clone();
let mut tree = WidgetTree::new();
let widget = tree.add(FillWidget::new().on_drag(move |phase, _ctx| match phase {
DragPhase::Started { .. } => started_flag.set(true),
DragPhase::Moved { .. } => moved_flag.set(moved_flag.get() + 1),
DragPhase::Ended { .. } => ended_flag.set(true),
}));
tree.layout(SizeProposal::exact(100.0, 50.0));
tree.dispatch_event(WidgetEvent::PointerDown {
position: Point::new(50.0, 25.0),
button: PointerButton::Primary,
modifiers: Modifiers::NONE,
});
tree.dispatch_event(WidgetEvent::PointerMove {
position: Point::new(70.0, 25.0),
});
assert!(started.get(), "DragStarted must fire");
tree.dispatch_event(WidgetEvent::PointerMove {
position: Point::new(500.0, 500.0),
});
assert!(
moved.get() >= 1,
"Move outside bounds must still reach drag handler"
);
tree.dispatch_event(WidgetEvent::PointerUp {
position: Point::new(500.0, 500.0),
button: PointerButton::Primary,
modifiers: Modifiers::NONE,
});
assert!(ended.get(), "DragEnded must fire on the original widget");
assert_eq!(
tree.pointer_captured_by, None,
"pointer capture must be released after DragEnded"
);
let _ = widget;
}
#[test]
fn on_long_press_fires_from_tick_gestures() {
use std::cell::Cell;
use std::rc::Rc;
use std::time::{Duration, Instant};
let pressed = Rc::new(Cell::new(false));
let pressed_flag = pressed.clone();
let mut tree = WidgetTree::new();
let widget = tree.add(FillWidget::new().on_long_press(move |_pos, _ctx| {
pressed_flag.set(true);
}));
tree.layout(SizeProposal::exact(100.0, 50.0));
let center = tree.bounds(widget).center();
tree.dispatch_event(WidgetEvent::PointerDown {
position: center,
button: PointerButton::Primary,
modifiers: Modifiers::NONE,
});
tree.tick_gestures(Instant::now());
assert!(!pressed.get());
tree.tick_gestures(Instant::now() + Duration::from_millis(600));
assert!(pressed.get());
assert!(tree.next_gesture_deadline().is_none());
}
#[test]
fn multiple_recognizers_on_same_widget() {
use crate::gesture::DragPhase;
use std::cell::Cell;
use std::rc::Rc;
let tapped = Rc::new(Cell::new(false));
let dragged = Rc::new(Cell::new(false));
let tapped_flag = tapped.clone();
let dragged_flag = dragged.clone();
let mut tree = WidgetTree::new();
let widget = tree.add(
FillWidget::new()
.on_tap(move |_pos, _ctx| {
tapped_flag.set(true);
})
.on_drag(move |phase, _ctx| {
if matches!(phase, DragPhase::Started { .. }) {
dragged_flag.set(true);
}
}),
);
tree.layout(SizeProposal::exact(100.0, 50.0));
tree.click(widget);
assert!(tapped.get());
assert!(!dragged.get());
tapped.set(false);
dragged.set(false);
tree.drag(Point::new(50.0, 25.0), Point::new(80.0, 25.0));
assert!(dragged.get());
}
#[test]
fn ancestor_drag_starts_through_descendant_tap_capture() {
use crate::gesture::DragPhase;
use crate::test_widgets::StackWidget;
use std::cell::Cell;
use std::rc::Rc;
let tapped = Rc::new(Cell::new(false));
let drag_started = Rc::new(Cell::new(false));
let t = tapped.clone();
let d = drag_started.clone();
let mut tree = WidgetTree::new();
let child = tree.add(FillWidget::new().on_tap(move |_p, _c| t.set(true)));
let _parent = tree.add(
StackWidget::new()
.add_child(child)
.on_drag(move |phase, _c| {
if matches!(phase, DragPhase::Started { .. }) {
d.set(true);
}
}),
);
tree.layout(SizeProposal::exact(100.0, 50.0));
tree.dispatch_event(WidgetEvent::PointerDown {
position: Point::new(50.0, 25.0),
button: PointerButton::Primary,
modifiers: Modifiers::NONE,
});
tree.dispatch_event(WidgetEvent::PointerUp {
position: Point::new(50.0, 25.0),
button: PointerButton::Primary,
modifiers: Modifiers::NONE,
});
assert!(
tapped.get(),
"a click on the child fires the descendant tap"
);
assert!(
!drag_started.get(),
"a click must not start the ancestor drag"
);
tapped.set(false);
drag_started.set(false);
tree.dispatch_event(WidgetEvent::PointerDown {
position: Point::new(50.0, 25.0),
button: PointerButton::Primary,
modifiers: Modifiers::NONE,
});
tree.dispatch_event(WidgetEvent::PointerMove {
position: Point::new(80.0, 25.0),
});
assert!(
drag_started.get(),
"dragging from the child must start the ancestor drag"
);
tree.dispatch_event(WidgetEvent::PointerUp {
position: Point::new(80.0, 25.0),
button: PointerButton::Primary,
modifiers: Modifiers::NONE,
});
assert!(!tapped.get(), "a drag must not fire the descendant tap");
}
#[test]
fn ancestor_drag_starts_through_deeply_nested_tap_capture() {
use crate::gesture::DragPhase;
use crate::test_widgets::StackWidget;
use std::cell::Cell;
use std::rc::Rc;
let drag_started = Rc::new(Cell::new(false));
let d = drag_started.clone();
let mut tree = WidgetTree::new();
let inner = tree.add(FillWidget::new());
let card = tree.add(
StackWidget::new()
.add_child(inner)
.on_tap(move |_p, _c| { }),
);
let _canvas = tree.add(
StackWidget::new()
.add_child(card)
.on_drag(move |phase, _c| {
if matches!(phase, DragPhase::Started { .. }) {
d.set(true);
}
}),
);
tree.layout(SizeProposal::exact(100.0, 50.0));
tree.dispatch_event(WidgetEvent::PointerDown {
position: Point::new(50.0, 25.0),
button: PointerButton::Primary,
modifiers: Modifiers::NONE,
});
tree.dispatch_event(WidgetEvent::PointerMove {
position: Point::new(80.0, 25.0),
});
assert!(
drag_started.get(),
"dragging a deeply-nested tappable child must start the ancestor drag"
);
}
#[test]
fn click_on_tap_child_then_hover_does_not_start_ancestor_drag() {
use crate::gesture::DragPhase;
use crate::test_widgets::StackWidget;
use std::cell::Cell;
use std::rc::Rc;
let drag_started = Rc::new(Cell::new(false));
let d = drag_started.clone();
let mut tree = WidgetTree::new();
let child = tree.add(FillWidget::new().on_tap(move |_p, _c| { }));
let _parent = tree.add(
StackWidget::new()
.add_child(child)
.on_drag(move |phase, _c| {
if matches!(phase, DragPhase::Started { .. }) {
d.set(true);
}
}),
);
tree.layout(SizeProposal::exact(100.0, 50.0));
tree.dispatch_event(WidgetEvent::PointerDown {
position: Point::new(50.0, 25.0),
button: PointerButton::Primary,
modifiers: Modifiers::NONE,
});
tree.dispatch_event(WidgetEvent::PointerUp {
position: Point::new(50.0, 25.0),
button: PointerButton::Primary,
modifiers: Modifiers::NONE,
});
tree.dispatch_event(WidgetEvent::PointerMove {
position: Point::new(85.0, 25.0),
});
assert!(
!drag_started.get(),
"a hover after a click must not start a phantom ancestor drag"
);
}
}