gpui: Add TouchDragEvent API (#63645)
Adds `TouchDragEvent` and coresponding APIs. This allows web/touch-based code to capture long press events without having to go through scroll-like APIs --- Release Notes: - N/A or Added/Fixed/Improved ...
This commit is contained in:
1 parent
8482496a16
commit
81808cb5db
3 files changed
+234
-14
No files matched your search
+157
-9
@@ -380,6 +380,27 @@ impl GestureKinds {
|
||||
};
|
||||
}
|
||||
|
||||
/// A direct touch drag claimed by an element before touch input becomes a tap,
|
||||
/// long press, or scrolling gesture.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TouchDragEvent {
|
||||
/// The phase of the touch drag.
|
||||
pub phase: TouchPhase,
|
||||
/// The position where the touch started.
|
||||
pub start_position: Point<Pixels>,
|
||||
/// The touch's current position.
|
||||
pub position: Point<Pixels>,
|
||||
}
|
||||
|
||||
impl Sealed for TouchDragEvent {}
|
||||
impl InputEvent for TouchDragEvent {
|
||||
fn to_platform_input(self) -> PlatformInput {
|
||||
PlatformInput::TouchDrag(self)
|
||||
}
|
||||
}
|
||||
impl GestureEvent for TouchDragEvent {}
|
||||
impl MouseEvent for TouchDragEvent {}
|
||||
|
||||
/// A phased long-press gesture recognized from a touch.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LongPressEvent {
|
||||
@@ -485,6 +506,7 @@ pub(crate) enum RecognizedTouchGesture {
|
||||
down: MouseDownEvent,
|
||||
up: MouseUpEvent,
|
||||
},
|
||||
TouchDrag(TouchDragEvent),
|
||||
LongPress(LongPressEvent),
|
||||
}
|
||||
|
||||
@@ -495,7 +517,8 @@ enum TouchGestureState {
|
||||
Pending {
|
||||
touch: ActiveTouch,
|
||||
deadline: Instant,
|
||||
offered: bool,
|
||||
long_press_offered: bool,
|
||||
touch_drag_offered: bool,
|
||||
},
|
||||
/// The touch exceeded `touch_slop`: it is a pan until it ends, and its
|
||||
/// movement flows out as scroll events.
|
||||
@@ -504,6 +527,7 @@ enum TouchGestureState {
|
||||
axis: Axis,
|
||||
},
|
||||
LongPressing(ActiveTouch),
|
||||
TouchDragging(ActiveTouch),
|
||||
}
|
||||
|
||||
struct ActiveTouch {
|
||||
@@ -609,7 +633,8 @@ impl TouchGestureRecognizer {
|
||||
self.state = TouchGestureState::Pending {
|
||||
touch,
|
||||
deadline: now + self.tuning.long_press_duration,
|
||||
offered: false,
|
||||
long_press_offered: false,
|
||||
touch_drag_offered: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -618,7 +643,8 @@ impl TouchGestureRecognizer {
|
||||
TouchGestureState::Pending {
|
||||
mut touch,
|
||||
deadline,
|
||||
offered,
|
||||
long_press_offered,
|
||||
touch_drag_offered,
|
||||
} if touch.id == event.id => {
|
||||
touch.velocity_tracker.push(now, event.position);
|
||||
touch.last_position = event.position;
|
||||
@@ -649,7 +675,8 @@ impl TouchGestureRecognizer {
|
||||
self.state = TouchGestureState::Pending {
|
||||
touch,
|
||||
deadline,
|
||||
offered,
|
||||
long_press_offered,
|
||||
touch_drag_offered,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -693,6 +720,15 @@ impl TouchGestureRecognizer {
|
||||
}));
|
||||
self.state = TouchGestureState::LongPressing(touch);
|
||||
}
|
||||
TouchGestureState::TouchDragging(mut touch) if touch.id == event.id => {
|
||||
touch.last_position = event.position;
|
||||
recognized.push(RecognizedTouchGesture::TouchDrag(TouchDragEvent {
|
||||
phase: TouchPhase::Moved,
|
||||
start_position: touch.start_position,
|
||||
position: event.position,
|
||||
}));
|
||||
self.state = TouchGestureState::TouchDragging(touch);
|
||||
}
|
||||
other => self.state = other,
|
||||
},
|
||||
TouchPhase::Ended => match mem::replace(&mut self.state, TouchGestureState::Idle) {
|
||||
@@ -801,6 +837,13 @@ impl TouchGestureRecognizer {
|
||||
position: event.position,
|
||||
}));
|
||||
}
|
||||
TouchGestureState::TouchDragging(touch) if touch.id == event.id => {
|
||||
recognized.push(RecognizedTouchGesture::TouchDrag(TouchDragEvent {
|
||||
phase: TouchPhase::Ended,
|
||||
start_position: touch.start_position,
|
||||
position: event.position,
|
||||
}));
|
||||
}
|
||||
other => self.state = other,
|
||||
},
|
||||
TouchPhase::Cancelled => match mem::replace(&mut self.state, TouchGestureState::Idle) {
|
||||
@@ -819,6 +862,13 @@ impl TouchGestureRecognizer {
|
||||
position: event.position,
|
||||
}));
|
||||
}
|
||||
TouchGestureState::TouchDragging(touch) if touch.id == event.id => {
|
||||
recognized.push(RecognizedTouchGesture::TouchDrag(TouchDragEvent {
|
||||
phase: TouchPhase::Cancelled,
|
||||
start_position: touch.start_position,
|
||||
position: event.position,
|
||||
}));
|
||||
}
|
||||
other => self.state = other,
|
||||
},
|
||||
}
|
||||
@@ -829,7 +879,8 @@ impl TouchGestureRecognizer {
|
||||
let TouchGestureState::Pending {
|
||||
touch,
|
||||
deadline,
|
||||
offered: false,
|
||||
long_press_offered: false,
|
||||
..
|
||||
} = &self.state
|
||||
else {
|
||||
return None;
|
||||
@@ -838,13 +889,18 @@ impl TouchGestureRecognizer {
|
||||
}
|
||||
|
||||
pub(crate) fn offer_long_press(&mut self, id: TouchId) -> Option<RecognizedTouchGesture> {
|
||||
let TouchGestureState::Pending { touch, offered, .. } = &mut self.state else {
|
||||
let TouchGestureState::Pending {
|
||||
touch,
|
||||
long_press_offered,
|
||||
..
|
||||
} = &mut self.state
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
if touch.id != id || *offered {
|
||||
if touch.id != id || *long_press_offered {
|
||||
return None;
|
||||
}
|
||||
*offered = true;
|
||||
*long_press_offered = true;
|
||||
Some(RecognizedTouchGesture::LongPress(LongPressEvent {
|
||||
phase: TouchPhase::Started,
|
||||
start_position: touch.start_position,
|
||||
@@ -860,13 +916,48 @@ impl TouchGestureRecognizer {
|
||||
self.state = match state {
|
||||
TouchGestureState::Pending {
|
||||
touch,
|
||||
offered: true,
|
||||
long_press_offered: true,
|
||||
..
|
||||
} => TouchGestureState::LongPressing(touch),
|
||||
other => other,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn offer_touch_drag(&mut self, id: TouchId) -> Option<RecognizedTouchGesture> {
|
||||
let TouchGestureState::Pending {
|
||||
touch,
|
||||
touch_drag_offered,
|
||||
..
|
||||
} = &mut self.state
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
if touch.id != id || *touch_drag_offered {
|
||||
return None;
|
||||
}
|
||||
*touch_drag_offered = true;
|
||||
Some(RecognizedTouchGesture::TouchDrag(TouchDragEvent {
|
||||
phase: TouchPhase::Started,
|
||||
start_position: touch.start_position,
|
||||
position: touch.last_position,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_touch_drag(&mut self, claimed: bool) {
|
||||
if !claimed {
|
||||
return;
|
||||
}
|
||||
let state = mem::replace(&mut self.state, TouchGestureState::Idle);
|
||||
self.state = match state {
|
||||
TouchGestureState::Pending {
|
||||
touch,
|
||||
touch_drag_offered: true,
|
||||
..
|
||||
} => TouchGestureState::TouchDragging(touch),
|
||||
other => other,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn has_momentum(&self) -> bool {
|
||||
self.momentum.is_some()
|
||||
}
|
||||
@@ -1894,6 +1985,63 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claimed_touch_drag_emits_phased_stream_without_pan_or_tap() {
|
||||
let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
|
||||
let touch = TouchId(1);
|
||||
let now = Instant::now();
|
||||
recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 10., 20.), now);
|
||||
let Some(RecognizedTouchGesture::TouchDrag(started)) = recognizer.offer_touch_drag(touch)
|
||||
else {
|
||||
panic!("expected touch drag");
|
||||
};
|
||||
assert_eq!(started.phase, TouchPhase::Started);
|
||||
assert_eq!(started.start_position, point(px(10.), px(20.)));
|
||||
recognizer.resolve_touch_drag(true);
|
||||
|
||||
let moved = recognizer.handle_event_at(
|
||||
&touch_event(touch, TouchPhase::Moved, 40., 50.),
|
||||
now + Duration::from_millis(10),
|
||||
);
|
||||
let [RecognizedTouchGesture::TouchDrag(moved)] = moved.as_slice() else {
|
||||
panic!("expected moved touch drag, got {moved:?}");
|
||||
};
|
||||
assert_eq!(moved.phase, TouchPhase::Moved);
|
||||
assert_eq!(moved.position, point(px(40.), px(50.)));
|
||||
|
||||
let ended = recognizer.handle_event_at(
|
||||
&touch_event(touch, TouchPhase::Ended, 45., 55.),
|
||||
now + Duration::from_millis(20),
|
||||
);
|
||||
let [RecognizedTouchGesture::TouchDrag(ended)] = ended.as_slice() else {
|
||||
panic!("expected ended touch drag, got {ended:?}");
|
||||
};
|
||||
assert_eq!(ended.phase, TouchPhase::Ended);
|
||||
assert_eq!(ended.position, point(px(45.), px(55.)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unclaimed_touch_drag_remains_a_pan_candidate() {
|
||||
let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
|
||||
let touch = TouchId(1);
|
||||
let now = Instant::now();
|
||||
recognizer.handle_event_at(&touch_event(touch, TouchPhase::Started, 0., 0.), now);
|
||||
assert!(recognizer.offer_touch_drag(touch).is_some());
|
||||
recognizer.resolve_touch_drag(false);
|
||||
|
||||
let moved = recognizer.handle_event_at(
|
||||
&touch_event(touch, TouchPhase::Moved, 20., 0.),
|
||||
now + Duration::from_millis(10),
|
||||
);
|
||||
assert!(matches!(
|
||||
moved.as_slice(),
|
||||
[RecognizedTouchGesture::Scroll(ScrollWheelEvent {
|
||||
touch_phase: TouchPhase::Started,
|
||||
..
|
||||
})]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claimed_long_press_emits_phased_stream_without_tap() {
|
||||
let mut recognizer = TouchGestureRecognizer::new(GestureTuning::default());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
Bounds, Capslock, Context, Empty, IntoElement, Keystroke, LongPressEvent, Modifiers, Pixels,
|
||||
Point, Render, Window, point, seal::Sealed,
|
||||
Point, Render, TouchDragEvent, Window, point, seal::Sealed,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{any::Any, fmt::Debug, ops::Deref, path::PathBuf};
|
||||
@@ -791,6 +791,8 @@ pub enum PlatformInput {
|
||||
Pinch(PinchEvent),
|
||||
/// A long-press gesture recognized from touch input.
|
||||
LongPress(LongPressEvent),
|
||||
/// A direct touch drag claimed by an element.
|
||||
TouchDrag(TouchDragEvent),
|
||||
/// Files were dragged and dropped onto the window.
|
||||
FileDrop(FileDropEvent),
|
||||
/// A raw touch event on a touch screen.
|
||||
@@ -811,6 +813,7 @@ impl PlatformInput {
|
||||
PlatformInput::ScrollWheel(event) => Some(event),
|
||||
PlatformInput::Pinch(event) => Some(event),
|
||||
PlatformInput::LongPress(event) => Some(event),
|
||||
PlatformInput::TouchDrag(event) => Some(event),
|
||||
PlatformInput::FileDrop(event) => Some(event),
|
||||
PlatformInput::Touch(_) => None,
|
||||
}
|
||||
@@ -829,6 +832,7 @@ impl PlatformInput {
|
||||
PlatformInput::ScrollWheel(_) => None,
|
||||
PlatformInput::Pinch(_) => None,
|
||||
PlatformInput::LongPress(_) => None,
|
||||
PlatformInput::TouchDrag(_) => None,
|
||||
PlatformInput::FileDrop(_) => None,
|
||||
PlatformInput::Touch(_) => None,
|
||||
}
|
||||
@@ -849,6 +853,7 @@ impl PlatformInput {
|
||||
PlatformInput::ScrollWheel(_) => "scroll_wheel",
|
||||
PlatformInput::Pinch(_) => "pinch",
|
||||
PlatformInput::LongPress(_) => "long_press",
|
||||
PlatformInput::TouchDrag(_) => "touch_drag",
|
||||
PlatformInput::FileDrop(_) => "file_drop",
|
||||
PlatformInput::Touch(_) => "touch",
|
||||
}
|
||||
|
||||
@@ -5197,6 +5197,10 @@ impl Window {
|
||||
}
|
||||
PlatformInput::LongPress(long_press)
|
||||
}
|
||||
PlatformInput::TouchDrag(touch_drag) => {
|
||||
self.mouse_position = touch_drag.start_position;
|
||||
PlatformInput::TouchDrag(touch_drag)
|
||||
}
|
||||
PlatformInput::KeyDown(_) | PlatformInput::KeyUp(_) => event,
|
||||
};
|
||||
|
||||
@@ -5289,6 +5293,11 @@ impl Window {
|
||||
event.predicted_position = None;
|
||||
}
|
||||
let recognized_gestures = self.touch_gestures.handle_event(&event);
|
||||
if event.phase == crate::TouchPhase::Started
|
||||
&& let Some(touch_drag) = self.touch_gestures.offer_touch_drag(event.id)
|
||||
{
|
||||
self.dispatch_recognized_touch_gesture(touch_drag, cx);
|
||||
}
|
||||
if event.phase == crate::TouchPhase::Started
|
||||
&& self.touch_gestures.pending_long_press().is_some()
|
||||
{
|
||||
@@ -5331,6 +5340,17 @@ impl Window {
|
||||
cx.propagate_event = true;
|
||||
self.dispatch_mouse_event(&up, cx);
|
||||
}
|
||||
RecognizedTouchGesture::TouchDrag(touch_drag) => {
|
||||
self.mouse_position = touch_drag.start_position;
|
||||
cx.propagate_event = true;
|
||||
self.default_prevented = false;
|
||||
let started = touch_drag.phase == crate::TouchPhase::Started;
|
||||
self.dispatch_mouse_event(&touch_drag, cx);
|
||||
if started {
|
||||
self.touch_gestures
|
||||
.resolve_touch_drag(self.default_prevented);
|
||||
}
|
||||
}
|
||||
RecognizedTouchGesture::LongPress(long_press) => {
|
||||
self.mouse_position = if long_press.phase == crate::TouchPhase::Started {
|
||||
long_press.start_position
|
||||
@@ -7126,8 +7146,8 @@ mod tests {
|
||||
ExternalDragPayload, ExternalPaths, FileDragPaths, FileDropEvent, FocusHandle,
|
||||
InputEvent as _, InteractiveElement as _, IntoElement, LongPressEvent, MouseButton,
|
||||
MouseDownEvent, MouseMoveEvent, ParentElement, Pixels, Point, Render, RequestFrameOptions,
|
||||
StatefulInteractiveElement as _, Styled, TestAppContext, TouchEvent, TouchId, TouchPhase,
|
||||
Window, WindowAppearance, WindowOptions, canvas, div, point, px, size,
|
||||
StatefulInteractiveElement as _, Styled, TestAppContext, TouchDragEvent, TouchEvent,
|
||||
TouchId, TouchPhase, Window, WindowAppearance, WindowOptions, canvas, div, point, px, size,
|
||||
};
|
||||
|
||||
struct EmptyView;
|
||||
@@ -7830,6 +7850,53 @@ mod tests {
|
||||
assert_eq!(b_focus_count.get(), 1);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn claimed_touch_drag_receives_movement_and_release(cx: &mut TestAppContext) {
|
||||
let events = Rc::new(RefCell::new(Vec::new()));
|
||||
let window = cx.add_window({
|
||||
let events = events.clone();
|
||||
move |_, _| TouchDragListener { events }
|
||||
});
|
||||
let touch = TouchId(1);
|
||||
|
||||
dispatch_touch(window, cx, touch, TouchPhase::Started, 10.);
|
||||
dispatch_touch(window, cx, touch, TouchPhase::Moved, 30.);
|
||||
dispatch_touch(window, cx, touch, TouchPhase::Ended, 40.);
|
||||
|
||||
assert_eq!(
|
||||
events.borrow().as_slice(),
|
||||
[
|
||||
(TouchPhase::Started, px(10.)),
|
||||
(TouchPhase::Moved, px(30.)),
|
||||
(TouchPhase::Ended, px(40.)),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
struct TouchDragListener {
|
||||
events: Rc<RefCell<Vec<(TouchPhase, Pixels)>>>,
|
||||
}
|
||||
|
||||
impl Render for TouchDragListener {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let events = self.events.clone();
|
||||
canvas(
|
||||
|_, _, _| {},
|
||||
move |_, _, window, _| {
|
||||
window.on_mouse_event(move |event: &TouchDragEvent, phase, window, _cx| {
|
||||
if phase != DispatchPhase::Bubble {
|
||||
return;
|
||||
}
|
||||
events.borrow_mut().push((event.phase, event.position.x));
|
||||
if event.phase == TouchPhase::Started {
|
||||
window.prevent_default();
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn long_press_is_claimed_only_when_started_prevents_default(cx: &mut TestAppContext) {
|
||||
for response in [
|
||||
@@ -7985,8 +8052,8 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_touch(
|
||||
window: crate::WindowHandle<LongPressListener>,
|
||||
fn dispatch_touch<T: 'static>(
|
||||
window: crate::WindowHandle<T>,
|
||||
cx: &mut TestAppContext,
|
||||
id: TouchId,
|
||||
phase: TouchPhase,
|
||||
|
||||
Reference in new issue
Block a user