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