Skip to main content

gpui_core/
interactive.rs

1use crate::{
2    Axis, Bounds, Capslock, Context, Empty, IntoElement, IsZero, Keystroke, Modifiers, Pixels,
3    Point, Render, Window, point, px, seal::Sealed,
4};
5use smallvec::SmallVec;
6use std::{
7    any::Any,
8    fmt::Debug,
9    ops::Deref,
10    path::PathBuf,
11    time::{Duration, Instant},
12};
13
14/// An event from a platform input source.
15pub trait InputEvent: Sealed + 'static {
16    /// Convert this event into the platform input enum.
17    fn to_platform_input(self) -> PlatformInput;
18}
19
20/// A key event from the platform.
21pub trait KeyEvent: InputEvent {}
22
23/// A mouse event from the platform.
24pub trait MouseEvent: InputEvent {}
25
26/// A gesture event from the platform.
27pub trait GestureEvent: InputEvent {}
28
29/// The key down event equivalent for the platform.
30#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct KeyDownEvent {
32    /// The keystroke that was generated.
33    pub keystroke: Keystroke,
34
35    /// Whether the key is currently held down.
36    pub is_held: bool,
37}
38
39impl Sealed for KeyDownEvent {}
40impl InputEvent for KeyDownEvent {
41    fn to_platform_input(self) -> PlatformInput {
42        PlatformInput::KeyDown(self)
43    }
44}
45impl KeyEvent for KeyDownEvent {}
46
47/// The key up event equivalent for the platform.
48#[derive(Clone, Debug)]
49pub struct KeyUpEvent {
50    /// The keystroke that was released.
51    pub keystroke: Keystroke,
52}
53
54impl Sealed for KeyUpEvent {}
55impl InputEvent for KeyUpEvent {
56    fn to_platform_input(self) -> PlatformInput {
57        PlatformInput::KeyUp(self)
58    }
59}
60impl KeyEvent for KeyUpEvent {}
61
62/// The modifiers changed event equivalent for the platform.
63#[derive(Clone, Debug, Default)]
64pub struct ModifiersChangedEvent {
65    /// The new state of the modifier keys
66    pub modifiers: Modifiers,
67    /// The new state of the capslock key
68    pub capslock: Capslock,
69}
70
71impl Sealed for ModifiersChangedEvent {}
72impl InputEvent for ModifiersChangedEvent {
73    fn to_platform_input(self) -> PlatformInput {
74        PlatformInput::ModifiersChanged(self)
75    }
76}
77impl KeyEvent for ModifiersChangedEvent {}
78
79impl Deref for ModifiersChangedEvent {
80    type Target = Modifiers;
81
82    fn deref(&self) -> &Self::Target {
83        &self.modifiers
84    }
85}
86
87/// The phase of a touch motion event.
88/// Based on the winit enum of the same name.
89#[derive(Clone, Copy, Debug, Default)]
90pub enum TouchPhase {
91    /// The touch started.
92    Started,
93    /// The touch event is moving.
94    #[default]
95    Moved,
96    /// The touch phase has ended
97    Ended,
98}
99
100/// A mouse down event from the platform
101#[derive(Clone, Debug, Default)]
102pub struct MouseDownEvent {
103    /// Which mouse button was pressed.
104    pub button: MouseButton,
105
106    /// The position of the mouse on the window.
107    pub position: Point<Pixels>,
108
109    /// The modifiers that were held down when the mouse was pressed.
110    pub modifiers: Modifiers,
111
112    /// The number of times the button has been clicked.
113    pub click_count: usize,
114
115    /// Whether this is the first, focusing click.
116    pub first_mouse: bool,
117}
118
119impl Sealed for MouseDownEvent {}
120impl InputEvent for MouseDownEvent {
121    fn to_platform_input(self) -> PlatformInput {
122        PlatformInput::MouseDown(self)
123    }
124}
125impl MouseEvent for MouseDownEvent {}
126
127/// A mouse up event from the platform
128#[derive(Clone, Debug, Default)]
129pub struct MouseUpEvent {
130    /// Which mouse button was released.
131    pub button: MouseButton,
132
133    /// The position of the mouse on the window.
134    pub position: Point<Pixels>,
135
136    /// The modifiers that were held down when the mouse was released.
137    pub modifiers: Modifiers,
138
139    /// The number of times the button has been clicked.
140    pub click_count: usize,
141}
142
143impl Sealed for MouseUpEvent {}
144impl InputEvent for MouseUpEvent {
145    fn to_platform_input(self) -> PlatformInput {
146        PlatformInput::MouseUp(self)
147    }
148}
149impl MouseEvent for MouseUpEvent {}
150
151/// A click event, generated when a mouse button is pressed and released.
152#[derive(Clone, Debug, Default)]
153pub struct MouseClickEvent {
154    /// The mouse event when the button was pressed.
155    pub down: MouseDownEvent,
156
157    /// The mouse event when the button was released.
158    pub up: MouseUpEvent,
159}
160
161/// A click event that was generated by a keyboard button being pressed and released.
162#[derive(Clone, Debug, Default)]
163pub struct KeyboardClickEvent {
164    /// The keyboard button that was pressed to trigger the click.
165    pub button: KeyboardButton,
166
167    /// The bounds of the element that was clicked.
168    pub bounds: Bounds<Pixels>,
169}
170
171/// A click event, generated when a mouse button or keyboard button is pressed and released.
172#[derive(Clone, Debug)]
173pub enum ClickEvent {
174    /// A click event trigger by a mouse button being pressed and released.
175    Mouse(MouseClickEvent),
176    /// A click event trigger by a keyboard button being pressed and released.
177    Keyboard(KeyboardClickEvent),
178}
179
180impl Default for ClickEvent {
181    fn default() -> Self {
182        ClickEvent::Keyboard(KeyboardClickEvent::default())
183    }
184}
185
186impl ClickEvent {
187    /// Returns the modifiers that were held during the click event
188    ///
189    /// `Keyboard`: The keyboard click events never have modifiers.
190    /// `Mouse`: Modifiers that were held during the mouse key up event.
191    pub fn modifiers(&self) -> Modifiers {
192        match self {
193            // Click events are only generated from keyboard events _without any modifiers_, so we know the modifiers are always Default
194            ClickEvent::Keyboard(_) => Modifiers::default(),
195            // Click events on the web only reflect the modifiers for the keyup event,
196            // tested via observing the behavior of the `ClickEvent.shiftKey` field in Chrome 138
197            // under various combinations of modifiers and keyUp / keyDown events.
198            ClickEvent::Mouse(event) => event.up.modifiers,
199        }
200    }
201
202    /// Returns the position of the click event
203    ///
204    /// `Keyboard`: The bottom left corner of the clicked hitbox
205    /// `Mouse`: The position of the mouse when the button was released.
206    pub fn position(&self) -> Point<Pixels> {
207        match self {
208            ClickEvent::Keyboard(event) => event.bounds.bottom_left(),
209            ClickEvent::Mouse(event) => event.up.position,
210        }
211    }
212
213    /// Returns the mouse position of the click event
214    ///
215    /// `Keyboard`: None
216    /// `Mouse`: The position of the mouse when the button was released.
217    pub fn mouse_position(&self) -> Option<Point<Pixels>> {
218        match self {
219            ClickEvent::Keyboard(_) => None,
220            ClickEvent::Mouse(event) => Some(event.up.position),
221        }
222    }
223
224    /// Returns if this was a right click
225    ///
226    /// `Keyboard`: false
227    /// `Mouse`: Whether the right button was pressed and released
228    pub fn is_right_click(&self) -> bool {
229        match self {
230            ClickEvent::Keyboard(_) => false,
231            ClickEvent::Mouse(event) => {
232                event.down.button == MouseButton::Right && event.up.button == MouseButton::Right
233            }
234        }
235    }
236
237    /// Returns whether the click was a standard click
238    ///
239    /// `Keyboard`: Always true
240    /// `Mouse`: Left button pressed and released
241    pub fn standard_click(&self) -> bool {
242        match self {
243            ClickEvent::Keyboard(_) => true,
244            ClickEvent::Mouse(event) => {
245                event.down.button == MouseButton::Left && event.up.button == MouseButton::Left
246            }
247        }
248    }
249
250    /// Returns whether the click focused the element
251    ///
252    /// `Keyboard`: false, keyboard clicks only work if an element is already focused
253    /// `Mouse`: Whether this was the first focusing click
254    pub fn first_focus(&self) -> bool {
255        match self {
256            ClickEvent::Keyboard(_) => false,
257            ClickEvent::Mouse(event) => event.down.first_mouse,
258        }
259    }
260
261    /// Returns the click count of the click event
262    ///
263    /// `Keyboard`: Always 1
264    /// `Mouse`: Count of clicks from MouseUpEvent
265    pub fn click_count(&self) -> usize {
266        match self {
267            ClickEvent::Keyboard(_) => 1,
268            ClickEvent::Mouse(event) => event.up.click_count,
269        }
270    }
271
272    /// Returns whether the click event is generated by a keyboard event
273    pub fn is_keyboard(&self) -> bool {
274        match self {
275            ClickEvent::Mouse(_) => false,
276            ClickEvent::Keyboard(_) => true,
277        }
278    }
279}
280
281/// An enum representing the keyboard button that was pressed for a click event.
282#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, Default)]
283pub enum KeyboardButton {
284    /// Enter key was clicked
285    #[default]
286    Enter,
287    /// Space key was clicked
288    Space,
289}
290
291/// An enum representing the mouse button that was pressed.
292#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, Default)]
293pub enum MouseButton {
294    /// The left mouse button.
295    #[default]
296    Left,
297
298    /// The right mouse button.
299    Right,
300
301    /// The middle mouse button.
302    Middle,
303
304    /// A navigation button, such as back or forward.
305    Navigate(NavigationDirection),
306}
307
308impl MouseButton {
309    /// Get all the mouse buttons in a list.
310    pub fn all() -> Vec<Self> {
311        vec![
312            MouseButton::Left,
313            MouseButton::Right,
314            MouseButton::Middle,
315            MouseButton::Navigate(NavigationDirection::Back),
316            MouseButton::Navigate(NavigationDirection::Forward),
317        ]
318    }
319}
320
321/// A navigation direction, such as back or forward.
322#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, Default)]
323pub enum NavigationDirection {
324    /// The back button.
325    #[default]
326    Back,
327
328    /// The forward button.
329    Forward,
330}
331
332/// A mouse move event from the platform
333#[derive(Clone, Debug, Default)]
334pub struct MouseMoveEvent {
335    /// The position of the mouse on the window.
336    pub position: Point<Pixels>,
337
338    /// The mouse button that was pressed, if any.
339    pub pressed_button: Option<MouseButton>,
340
341    /// The modifiers that were held down when the mouse was moved.
342    pub modifiers: Modifiers,
343}
344
345impl Sealed for MouseMoveEvent {}
346impl InputEvent for MouseMoveEvent {
347    fn to_platform_input(self) -> PlatformInput {
348        PlatformInput::MouseMove(self)
349    }
350}
351impl MouseEvent for MouseMoveEvent {}
352
353impl MouseMoveEvent {
354    /// Returns true if the left mouse button is currently held down.
355    pub fn dragging(&self) -> bool {
356        self.pressed_button == Some(MouseButton::Left)
357    }
358}
359
360/// A mouse wheel event from the platform
361#[derive(Clone, Debug, Default)]
362pub struct ScrollWheelEvent {
363    /// The position of the mouse on the window.
364    pub position: Point<Pixels>,
365
366    /// The change in scroll wheel position for this event.
367    pub delta: ScrollDelta,
368
369    /// The modifiers that were held down when the mouse was moved.
370    pub modifiers: Modifiers,
371
372    /// The phase of the touch event.
373    pub touch_phase: TouchPhase,
374}
375
376impl Sealed for ScrollWheelEvent {}
377impl InputEvent for ScrollWheelEvent {
378    fn to_platform_input(self) -> PlatformInput {
379        PlatformInput::ScrollWheel(self)
380    }
381}
382impl MouseEvent for ScrollWheelEvent {}
383
384impl Deref for ScrollWheelEvent {
385    type Target = Modifiers;
386
387    fn deref(&self) -> &Self::Target {
388        &self.modifiers
389    }
390}
391
392/// The scroll delta for a scroll wheel event.
393#[derive(Clone, Copy, Debug)]
394pub enum ScrollDelta {
395    /// An exact scroll delta in pixels.
396    Pixels(Point<Pixels>),
397    /// An inexact scroll delta in lines.
398    Lines(Point<f32>),
399}
400
401impl Default for ScrollDelta {
402    fn default() -> Self {
403        Self::Lines(Default::default())
404    }
405}
406
407/// A pinch gesture event from the platform, generated when the user performs
408/// a pinch-to-zoom gesture.
409#[derive(Clone, Debug, Default)]
410pub struct PinchEvent {
411    /// The position of the pinch center on the window.
412    pub position: Point<Pixels>,
413
414    /// The zoom delta for this event. Positive values indicate zooming in,
415    /// negative values indicate zooming out. Native pinch gestures report
416    /// the change in scale since the previous event. Wheel-based synthetic
417    /// pinch gestures report the normalized wheel-notch delta.
418    pub delta: f32,
419
420    /// The modifiers that were held down during the pinch gesture.
421    pub modifiers: Modifiers,
422
423    /// The phase of the pinch gesture.
424    pub phase: TouchPhase,
425}
426
427impl Sealed for PinchEvent {}
428impl InputEvent for PinchEvent {
429    fn to_platform_input(self) -> PlatformInput {
430        PlatformInput::Pinch(self)
431    }
432}
433impl GestureEvent for PinchEvent {}
434impl MouseEvent for PinchEvent {}
435
436impl Deref for PinchEvent {
437    type Target = Modifiers;
438
439    fn deref(&self) -> &Self::Target {
440        &self.modifiers
441    }
442}
443
444impl ScrollDelta {
445    /// Returns true if this is a precise scroll delta in pixels.
446    pub fn precise(&self) -> bool {
447        match self {
448            ScrollDelta::Pixels(_) => true,
449            ScrollDelta::Lines(_) => false,
450        }
451    }
452
453    /// Converts this scroll event into exact pixels.
454    pub fn pixel_delta(&self, line_height: Pixels) -> Point<Pixels> {
455        match self {
456            ScrollDelta::Pixels(delta) => *delta,
457            ScrollDelta::Lines(delta) => point(line_height * delta.x, line_height * delta.y),
458        }
459    }
460
461    /// Combines two scroll deltas into one.
462    /// If the signs of the deltas are the same (both positive or both negative),
463    /// the deltas are added together. If the signs are opposite, the second delta
464    /// (other) is used, effectively overriding the first delta.
465    pub fn coalesce(self, other: ScrollDelta) -> ScrollDelta {
466        match (self, other) {
467            (ScrollDelta::Pixels(a), ScrollDelta::Pixels(b)) => {
468                let x = if a.x.signum() == b.x.signum() {
469                    a.x + b.x
470                } else {
471                    b.x
472                };
473
474                let y = if a.y.signum() == b.y.signum() {
475                    a.y + b.y
476                } else {
477                    b.y
478                };
479
480                ScrollDelta::Pixels(point(x, y))
481            }
482
483            (ScrollDelta::Lines(a), ScrollDelta::Lines(b)) => {
484                let x = if a.x.signum() == b.x.signum() {
485                    a.x + b.x
486                } else {
487                    b.x
488                };
489
490                let y = if a.y.signum() == b.y.signum() {
491                    a.y + b.y
492                } else {
493                    b.y
494                };
495
496                ScrollDelta::Lines(point(x, y))
497            }
498
499            _ => other,
500        }
501    }
502}
503
504const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
505
506/// Tracks the dominant axis across the events in a scroll gesture.
507#[derive(Clone, Copy, Debug, Default)]
508pub struct OngoingScroll {
509    last_event: Option<Instant>,
510    axis: Option<Axis>,
511}
512
513impl OngoingScroll {
514    /// Filters the given delta to the dominant axis of the current scroll gesture.
515    ///
516    /// Gestures are delimited by their touch phase when available, with a timeout
517    /// fallback for platforms that only emit [`TouchPhase::Moved`].
518    pub fn filter(&mut self, delta: &mut Point<Pixels>, touch_phase: TouchPhase) {
519        self.filter_at(delta, touch_phase, Instant::now())
520    }
521
522    fn filter_at(&mut self, delta: &mut Point<Pixels>, touch_phase: TouchPhase, now: Instant) {
523        const UNLOCK_PERCENT: f32 = 1.9;
524        const UNLOCK_LOWER_BOUND: Pixels = px(6.);
525
526        if matches!(touch_phase, TouchPhase::Ended) {
527            self.last_event = None;
528            self.axis = None;
529            return;
530        }
531
532        let x = delta.x.abs();
533        let y = delta.y.abs();
534        if x.is_zero() && y.is_zero() {
535            if matches!(touch_phase, TouchPhase::Started) {
536                self.last_event = None;
537                self.axis = None;
538            }
539            return;
540        }
541
542        let starts_new_gesture = matches!(touch_phase, TouchPhase::Started)
543            || self
544                .last_event
545                .is_none_or(|last_event| now.duration_since(last_event) >= SCROLL_EVENT_SEPARATION);
546        let mut axis = self.axis;
547        if starts_new_gesture {
548            axis = if x <= y {
549                Some(Axis::Vertical)
550            } else {
551                Some(Axis::Horizontal)
552            };
553        } else if x.max(y) >= UNLOCK_LOWER_BOUND {
554            match axis {
555                Some(Axis::Vertical) if x > y && x >= y * UNLOCK_PERCENT => {
556                    axis = None;
557                }
558                Some(Axis::Horizontal) if y > x && y >= x * UNLOCK_PERCENT => {
559                    axis = None;
560                }
561                _ => {}
562            }
563        }
564
565        self.last_event = Some(now);
566        self.axis = axis;
567        match axis {
568            Some(Axis::Vertical) => delta.x = Pixels::ZERO,
569            Some(Axis::Horizontal) => delta.y = Pixels::ZERO,
570            None => {}
571        }
572    }
573}
574
575/// A mouse exit event from the platform, generated when the mouse leaves the window.
576#[derive(Clone, Debug, Default)]
577pub struct MouseExitEvent {
578    /// The position of the mouse relative to the window.
579    pub position: Point<Pixels>,
580    /// The mouse button that was pressed, if any.
581    pub pressed_button: Option<MouseButton>,
582    /// The modifiers that were held down when the mouse was moved.
583    pub modifiers: Modifiers,
584}
585
586impl Sealed for MouseExitEvent {}
587impl InputEvent for MouseExitEvent {
588    fn to_platform_input(self) -> PlatformInput {
589        PlatformInput::MouseExited(self)
590    }
591}
592impl MouseEvent for MouseExitEvent {}
593
594impl Deref for MouseExitEvent {
595    type Target = Modifiers;
596
597    fn deref(&self) -> &Self::Target {
598        &self.modifiers
599    }
600}
601
602/// A collection of paths from the platform, such as from a file drop.
603#[derive(Debug, Clone, Default)]
604#[allow(missing_docs)]
605pub struct ExternalPaths(pub SmallVec<[PathBuf; 2]>);
606
607impl ExternalPaths {
608    /// Convert this collection of paths into a slice.
609    pub fn paths(&self) -> &[PathBuf] {
610        &self.0
611    }
612}
613
614impl Render for ExternalPaths {
615    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
616        // the platform will render icons for the dragged files
617        Empty
618    }
619}
620
621/// A file drop event from the platform, generated when files are dragged and dropped onto the window.
622#[derive(Debug, Clone)]
623pub enum FileDropEvent {
624    /// The files have entered the window.
625    Entered {
626        /// The position of the mouse relative to the window.
627        position: Point<Pixels>,
628        /// The paths of the files that are being dragged.
629        paths: ExternalPaths,
630    },
631    /// The files are being dragged over the window
632    Pending {
633        /// The position of the mouse relative to the window.
634        position: Point<Pixels>,
635    },
636    /// The files have been dropped onto the window.
637    Submit {
638        /// The position of the mouse relative to the window.
639        position: Point<Pixels>,
640    },
641    /// The user has stopped dragging the files over the window.
642    Exited,
643}
644
645impl Sealed for FileDropEvent {}
646impl InputEvent for FileDropEvent {
647    fn to_platform_input(self) -> PlatformInput {
648        PlatformInput::FileDrop(self)
649    }
650}
651impl MouseEvent for FileDropEvent {}
652
653/// An enum corresponding to all kinds of platform input events.
654#[derive(Clone, Debug)]
655pub enum PlatformInput {
656    /// A key was pressed.
657    KeyDown(KeyDownEvent),
658    /// A key was released.
659    KeyUp(KeyUpEvent),
660    /// The keyboard modifiers were changed.
661    ModifiersChanged(ModifiersChangedEvent),
662    /// The mouse was pressed.
663    MouseDown(MouseDownEvent),
664    /// The mouse was released.
665    MouseUp(MouseUpEvent),
666    /// The mouse was moved.
667    MouseMove(MouseMoveEvent),
668    /// The mouse exited the window.
669    MouseExited(MouseExitEvent),
670    /// The scroll wheel was used.
671    ScrollWheel(ScrollWheelEvent),
672    /// A pinch gesture was performed.
673    Pinch(PinchEvent),
674    /// Files were dragged and dropped onto the window.
675    FileDrop(FileDropEvent),
676}
677
678impl PlatformInput {
679    pub(crate) fn mouse_event(&self) -> Option<&dyn Any> {
680        match self {
681            PlatformInput::KeyDown { .. } => None,
682            PlatformInput::KeyUp { .. } => None,
683            PlatformInput::ModifiersChanged { .. } => None,
684            PlatformInput::MouseDown(event) => Some(event),
685            PlatformInput::MouseUp(event) => Some(event),
686            PlatformInput::MouseMove(event) => Some(event),
687            PlatformInput::MouseExited(event) => Some(event),
688            PlatformInput::ScrollWheel(event) => Some(event),
689            PlatformInput::Pinch(event) => Some(event),
690            PlatformInput::FileDrop(event) => Some(event),
691        }
692    }
693
694    pub(crate) fn keyboard_event(&self) -> Option<&dyn Any> {
695        match self {
696            PlatformInput::KeyDown(event) => Some(event),
697            PlatformInput::KeyUp(event) => Some(event),
698            PlatformInput::ModifiersChanged(event) => Some(event),
699            PlatformInput::MouseDown(_) => None,
700            PlatformInput::MouseUp(_) => None,
701            PlatformInput::MouseMove(_) => None,
702            PlatformInput::MouseExited(_) => None,
703            PlatformInput::ScrollWheel(_) => None,
704            PlatformInput::Pinch(_) => None,
705            PlatformInput::FileDrop(_) => None,
706        }
707    }
708}
709
710#[cfg(test)]
711mod test {
712
713    use crate::{
714        self as gpui, AppContext as _, Context, FocusHandle, InteractiveElement, IntoElement,
715        KeyBinding, Keystroke, Modifiers, ParentElement, Render, TestAppContext, Window, div,
716    };
717
718    struct TestView {
719        saw_key_down: bool,
720        saw_action: bool,
721        focus_handle: FocusHandle,
722    }
723
724    actions!(test_only, [TestAction]);
725
726    impl Render for TestView {
727        fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
728            div().id("testview").child(
729                div()
730                    .key_context("parent")
731                    .on_key_down(cx.listener(|this, _, _, cx| {
732                        cx.stop_propagation();
733                        this.saw_key_down = true
734                    }))
735                    .on_action(cx.listener(|this: &mut TestView, _: &TestAction, _, _| {
736                        this.saw_action = true
737                    }))
738                    .child(
739                        div()
740                            .key_context("nested")
741                            .track_focus(&self.focus_handle)
742                            .into_element(),
743                    ),
744            )
745        }
746    }
747
748    #[gpui::test]
749    fn test_on_events(cx: &mut TestAppContext) {
750        let window = cx.update(|cx| {
751            cx.open_window(Default::default(), |_, cx| {
752                cx.new(|cx| TestView {
753                    saw_key_down: false,
754                    saw_action: false,
755                    focus_handle: cx.focus_handle(),
756                })
757            })
758            .unwrap()
759        });
760
761        cx.update(|cx| {
762            cx.bind_keys(vec![KeyBinding::new("ctrl-g", TestAction, Some("parent"))]);
763        });
764
765        window
766            .update(cx, |test_view, window, _cx| {
767                window.focus(&test_view.focus_handle)
768            })
769            .unwrap();
770
771        cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap());
772        cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap());
773
774        window
775            .update(cx, |test_view, _, _| {
776                assert!(test_view.saw_key_down || test_view.saw_action);
777                assert!(test_view.saw_key_down);
778                assert!(test_view.saw_action);
779            })
780            .unwrap();
781    }
782
783    #[gpui::test]
784    fn test_multi_modifier_gesture_does_not_dispatch_standalone_modifier_binding(
785        cx: &mut TestAppContext,
786    ) {
787        let (test_view, cx) = cx.add_window_view(|_, cx| TestView {
788            saw_key_down: false,
789            saw_action: false,
790            focus_handle: cx.focus_handle(),
791        });
792
793        cx.update(|_, cx| {
794            cx.bind_keys(vec![KeyBinding::new("shift", TestAction, None)]);
795        });
796        test_view.update_in(cx, |test_view, window, _cx| {
797            window.focus(&test_view.focus_handle);
798        });
799
800        cx.simulate_modifiers_change(Modifiers::alt());
801        cx.simulate_modifiers_change(Modifiers::alt() | Modifiers::shift());
802        cx.simulate_modifiers_change(Modifiers::shift());
803        cx.simulate_modifiers_change(Modifiers::none());
804        assert!(!test_view.read_with(cx, |test_view, _| test_view.saw_action));
805
806        cx.simulate_modifiers_change(Modifiers::shift());
807        cx.simulate_modifiers_change(Modifiers::none());
808        assert!(test_view.read_with(cx, |test_view, _| test_view.saw_action));
809    }
810}
811
812#[cfg(test)]
813mod ongoing_scroll_tests {
814    use super::*;
815
816    #[test]
817    fn ongoing_scroll_locks_to_dominant_axis() {
818        let now = Instant::now();
819        let mut ongoing_scroll = OngoingScroll::default();
820        let mut horizontal_delta = point(px(10.), px(2.));
821        ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now);
822        assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal));
823        assert_eq!(horizontal_delta, point(px(10.), px(0.)));
824
825        let mut continued_delta = point(px(3.), px(2.));
826        ongoing_scroll.filter_at(
827            &mut continued_delta,
828            TouchPhase::Moved,
829            now + Duration::from_millis(1),
830        );
831        assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal));
832        assert_eq!(continued_delta, point(px(3.), px(0.)));
833    }
834
835    #[test]
836    fn ongoing_scroll_unlocks_when_direction_changes() {
837        let now = Instant::now();
838        let mut ongoing_scroll = OngoingScroll::default();
839        let mut horizontal_delta = point(px(10.), px(2.));
840        ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now);
841
842        let mut vertical_delta = point(px(2.), px(10.));
843        ongoing_scroll.filter_at(
844            &mut vertical_delta,
845            TouchPhase::Moved,
846            now + Duration::from_millis(1),
847        );
848        assert_eq!(ongoing_scroll.axis, None);
849        assert_eq!(vertical_delta, point(px(2.), px(10.)));
850    }
851
852    #[test]
853    fn ongoing_scroll_starts_new_gesture_at_timeout_boundary() {
854        let now = Instant::now();
855        let mut ongoing_scroll = OngoingScroll::default();
856        let mut horizontal_delta = point(px(10.), px(2.));
857        ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Moved, now);
858
859        let mut vertical_delta = point(px(2.), px(10.));
860        ongoing_scroll.filter_at(
861            &mut vertical_delta,
862            TouchPhase::Moved,
863            now + SCROLL_EVENT_SEPARATION,
864        );
865        assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical));
866        assert_eq!(vertical_delta, point(px(0.), px(10.)));
867    }
868
869    #[test]
870    fn ongoing_scroll_ignores_zero_delta_and_resets_when_ended() {
871        let now = Instant::now();
872        let mut ongoing_scroll = OngoingScroll::default();
873        let mut horizontal_delta = point(px(10.), px(2.));
874        ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now);
875
876        let mut zero_delta = Point::default();
877        ongoing_scroll.filter_at(
878            &mut zero_delta,
879            TouchPhase::Ended,
880            now + Duration::from_millis(1),
881        );
882        assert_eq!(ongoing_scroll.axis, None);
883
884        let mut vertical_delta = point(px(2.), px(3.));
885        ongoing_scroll.filter_at(
886            &mut vertical_delta,
887            TouchPhase::Moved,
888            now + Duration::from_millis(2),
889        );
890        assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical));
891        assert_eq!(vertical_delta, point(px(0.), px(3.)));
892    }
893
894    #[test]
895    fn ongoing_scroll_ignores_zero_delta_movement() {
896        let now = Instant::now();
897        let mut ongoing_scroll = OngoingScroll::default();
898        let mut horizontal_delta = point(px(10.), px(2.));
899        ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now);
900
901        let mut zero_delta = Point::default();
902        ongoing_scroll.filter_at(
903            &mut zero_delta,
904            TouchPhase::Moved,
905            now + SCROLL_EVENT_SEPARATION,
906        );
907
908        let mut vertical_delta = point(px(2.), px(10.));
909        ongoing_scroll.filter_at(
910            &mut vertical_delta,
911            TouchPhase::Moved,
912            now + SCROLL_EVENT_SEPARATION,
913        );
914        assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical));
915        assert_eq!(vertical_delta, point(px(0.), px(10.)));
916    }
917
918    #[test]
919    fn ongoing_scroll_supports_moved_only_platforms() {
920        let now = Instant::now();
921        let mut ongoing_scroll = OngoingScroll::default();
922        let mut horizontal_delta = point(px(10.), px(2.));
923        ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Moved, now);
924        assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal));
925        assert_eq!(horizontal_delta, point(px(10.), px(0.)));
926    }
927}