Skip to main content

gpui_authoring/
interactive.rs

1use crate::{
2    Bounds, Context, Empty, ExternalPaths, FileDropEvent, IntoElement, KeyDownEvent, KeyUpEvent,
3    Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent,
4    MousePressureEvent, MouseUpEvent, PinchEvent, Pixels, PlatformInput, Point, Render,
5    ScrollWheelEvent, TouchEvent, Window, seal::Sealed,
6};
7use std::fmt::Debug;
8
9/// An event from a platform input source.
10pub trait InputEvent: Sealed + 'static {
11    /// Convert this event into the platform input enum.
12    fn to_platform_input(self) -> PlatformInput;
13}
14
15/// A key event from the platform.
16pub trait KeyEvent: InputEvent {}
17
18/// A mouse event from the platform.
19pub trait MouseEvent: InputEvent {}
20
21/// A gesture event from the platform.
22pub trait GestureEvent: InputEvent {}
23
24impl Sealed for KeyDownEvent {}
25impl InputEvent for KeyDownEvent {
26    fn to_platform_input(self) -> PlatformInput {
27        PlatformInput::KeyDown(self)
28    }
29}
30impl KeyEvent for KeyDownEvent {}
31
32impl Sealed for KeyUpEvent {}
33impl InputEvent for KeyUpEvent {
34    fn to_platform_input(self) -> PlatformInput {
35        PlatformInput::KeyUp(self)
36    }
37}
38impl KeyEvent for KeyUpEvent {}
39
40impl Sealed for ModifiersChangedEvent {}
41impl InputEvent for ModifiersChangedEvent {
42    fn to_platform_input(self) -> PlatformInput {
43        PlatformInput::ModifiersChanged(self)
44    }
45}
46impl KeyEvent for ModifiersChangedEvent {}
47
48impl Sealed for TouchEvent {}
49impl InputEvent for TouchEvent {
50    fn to_platform_input(self) -> PlatformInput {
51        PlatformInput::Touch(self)
52    }
53}
54
55impl Sealed for MouseDownEvent {}
56impl InputEvent for MouseDownEvent {
57    fn to_platform_input(self) -> PlatformInput {
58        PlatformInput::MouseDown(self)
59    }
60}
61impl MouseEvent for MouseDownEvent {}
62
63impl Sealed for MouseUpEvent {}
64impl InputEvent for MouseUpEvent {
65    fn to_platform_input(self) -> PlatformInput {
66        PlatformInput::MouseUp(self)
67    }
68}
69
70impl MouseEvent for MouseUpEvent {}
71
72/// A click event, generated when a mouse button is pressed and released.
73#[derive(Clone, Debug, Default)]
74pub struct MouseClickEvent {
75    /// The mouse event when the button was pressed.
76    pub down: MouseDownEvent,
77
78    /// The mouse event when the button was released.
79    pub up: MouseUpEvent,
80}
81
82impl Sealed for MousePressureEvent {}
83impl InputEvent for MousePressureEvent {
84    fn to_platform_input(self) -> PlatformInput {
85        PlatformInput::MousePressure(self)
86    }
87}
88impl MouseEvent for MousePressureEvent {}
89
90/// A click event that was generated by a keyboard button being pressed and released.
91#[derive(Clone, Debug, Default)]
92pub struct KeyboardClickEvent {
93    /// The keyboard button that was pressed to trigger the click.
94    pub button: KeyboardButton,
95
96    /// The bounds of the element that was clicked.
97    pub bounds: Bounds<Pixels>,
98}
99
100/// A click event that was generated by a recognized tap gesture on a touch
101/// screen.
102#[derive(Clone, Debug, Default)]
103pub struct TouchClickEvent {
104    /// The position of the tap in window coordinates.
105    pub position: Point<Pixels>,
106    /// The number of consecutive taps at this location (double tap = 2),
107    /// analogous to the mouse `click_count`.
108    pub tap_count: usize,
109    /// Whether this was a long press rather than a tap. Long presses are
110    /// touch's secondary activation: they are delivered to aux-click
111    /// listeners alongside right clicks, not to primary click listeners.
112    pub long_press: bool,
113}
114
115/// A click event, generated when a mouse button or keyboard button is pressed and released,
116/// or when a tap gesture is recognized on a touch screen.
117#[derive(Clone, Debug)]
118pub enum ClickEvent {
119    /// A click event trigger by a mouse button being pressed and released.
120    Mouse(MouseClickEvent),
121    /// A click event trigger by a keyboard button being pressed and released.
122    Keyboard(KeyboardClickEvent),
123    /// A click event triggered by a recognized tap gesture on a touch screen.
124    Touch(TouchClickEvent),
125}
126
127impl Default for ClickEvent {
128    fn default() -> Self {
129        ClickEvent::Keyboard(KeyboardClickEvent::default())
130    }
131}
132
133impl ClickEvent {
134    /// Returns the modifiers that were held during the click event
135    ///
136    /// `Keyboard`: The keyboard click events never have modifiers.
137    /// `Mouse`: Modifiers that were held during the mouse key up event.
138    pub fn modifiers(&self) -> Modifiers {
139        match self {
140            // Click events are only generated from keyboard events _without any modifiers_, so we know the modifiers are always Default
141            ClickEvent::Keyboard(_) => Modifiers::default(),
142            // Click events on the web only reflect the modifiers for the keyup event,
143            // tested via observing the behavior of the `ClickEvent.shiftKey` field in Chrome 138
144            // under various combinations of modifiers and keyUp / keyDown events.
145            ClickEvent::Mouse(event) => event.up.modifiers,
146            // Touch screens have no modifier keys.
147            ClickEvent::Touch(_) => Modifiers::default(),
148        }
149    }
150
151    /// Returns the position of the click event
152    ///
153    /// `Keyboard`: The bottom left corner of the clicked hitbox
154    /// `Mouse`: The position of the mouse when the button was released.
155    /// `Touch`: The position of the tap.
156    pub fn position(&self) -> Point<Pixels> {
157        match self {
158            ClickEvent::Keyboard(event) => event.bounds.bottom_left(),
159            ClickEvent::Mouse(event) => event.up.position,
160            ClickEvent::Touch(event) => event.position,
161        }
162    }
163
164    /// Returns the mouse position of the click event
165    ///
166    /// `Keyboard`: None
167    /// `Mouse`: The position of the mouse when the button was released.
168    /// `Touch`: None, touches are not mouse input and there is no cursor.
169    pub fn mouse_position(&self) -> Option<Point<Pixels>> {
170        match self {
171            ClickEvent::Keyboard(_) => None,
172            ClickEvent::Mouse(event) => Some(event.up.position),
173            ClickEvent::Touch(_) => None,
174        }
175    }
176
177    /// Returns if this was a right click
178    ///
179    /// `Keyboard`: false
180    /// `Mouse`: Whether the right button was pressed and released
181    pub fn is_right_click(&self) -> bool {
182        match self {
183            ClickEvent::Keyboard(_) => false,
184            ClickEvent::Mouse(event) => {
185                event.down.button == MouseButton::Right && event.up.button == MouseButton::Right
186            }
187            ClickEvent::Touch(_) => false,
188        }
189    }
190
191    /// Returns if this was a middle click
192    ///
193    /// `Keyboard`: false
194    /// `Mouse`: Whether the middle button was pressed and released
195    pub fn is_middle_click(&self) -> bool {
196        match self {
197            ClickEvent::Keyboard(_) => false,
198            ClickEvent::Mouse(event) => {
199                event.down.button == MouseButton::Middle && event.up.button == MouseButton::Middle
200            }
201            ClickEvent::Touch(_) => false,
202        }
203    }
204
205    /// Returns whether the click is a secondary activation, i.e. a context
206    /// menu trigger: a right click from a mouse (macOS ctrl-clicks arrive
207    /// already converted to right clicks by the platform layer), or a long
208    /// press on a touch screen.
209    pub fn is_secondary(&self) -> bool {
210        match self {
211            ClickEvent::Keyboard(_) => false,
212            ClickEvent::Mouse(event) => {
213                event.down.button == MouseButton::Right && event.up.button == MouseButton::Right
214            }
215            ClickEvent::Touch(event) => event.long_press,
216        }
217    }
218
219    /// Returns whether the click was a standard click
220    ///
221    /// `Keyboard`: Always true
222    /// `Mouse`: Left button pressed and released
223    /// `Touch`: A tap, but not a long press
224    pub fn standard_click(&self) -> bool {
225        match self {
226            ClickEvent::Keyboard(_) => true,
227            ClickEvent::Mouse(event) => {
228                event.down.button == MouseButton::Left && event.up.button == MouseButton::Left
229            }
230            ClickEvent::Touch(event) => !event.long_press,
231        }
232    }
233
234    /// Returns whether the click focused the element
235    ///
236    /// `Keyboard`: false, keyboard clicks only work if an element is already focused
237    /// `Mouse`: Whether this was the first focusing click
238    /// `Touch`: false, mobile windows are already active when tappable
239    pub fn first_focus(&self) -> bool {
240        match self {
241            ClickEvent::Keyboard(_) => false,
242            ClickEvent::Mouse(event) => event.down.first_mouse,
243            ClickEvent::Touch(_) => false,
244        }
245    }
246
247    /// Returns the click count of the click event
248    ///
249    /// `Keyboard`: Always 1
250    /// `Mouse`: Count of clicks from MouseUpEvent
251    /// `Touch`: Count of consecutive taps
252    pub fn click_count(&self) -> usize {
253        match self {
254            ClickEvent::Keyboard(_) => 1,
255            ClickEvent::Mouse(event) => event.up.click_count,
256            ClickEvent::Touch(event) => event.tap_count,
257        }
258    }
259
260    /// Returns whether the click event is generated by a keyboard event
261    pub fn is_keyboard(&self) -> bool {
262        match self {
263            ClickEvent::Mouse(_) | ClickEvent::Touch(_) => false,
264            ClickEvent::Keyboard(_) => true,
265        }
266    }
267}
268
269/// An enum representing the keyboard button that was pressed for a click event.
270#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, Default)]
271pub enum KeyboardButton {
272    /// Enter key was clicked
273    #[default]
274    Enter,
275    /// Space key was clicked
276    Space,
277}
278
279impl Sealed for MouseMoveEvent {}
280impl InputEvent for MouseMoveEvent {
281    fn to_platform_input(self) -> PlatformInput {
282        PlatformInput::MouseMove(self)
283    }
284}
285impl MouseEvent for MouseMoveEvent {}
286
287impl Sealed for ScrollWheelEvent {}
288impl InputEvent for ScrollWheelEvent {
289    fn to_platform_input(self) -> PlatformInput {
290        PlatformInput::ScrollWheel(self)
291    }
292}
293impl MouseEvent for ScrollWheelEvent {}
294
295impl Sealed for PinchEvent {}
296impl InputEvent for PinchEvent {
297    fn to_platform_input(self) -> PlatformInput {
298        PlatformInput::Pinch(self)
299    }
300}
301impl GestureEvent for PinchEvent {}
302impl MouseEvent for PinchEvent {}
303
304impl Sealed for MouseExitEvent {}
305impl InputEvent for MouseExitEvent {
306    fn to_platform_input(self) -> PlatformInput {
307        PlatformInput::MouseExited(self)
308    }
309}
310
311impl MouseEvent for MouseExitEvent {}
312
313impl Render for ExternalPaths {
314    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
315        // the platform will render icons for the dragged files
316        Empty
317    }
318}
319
320impl Sealed for FileDropEvent {}
321impl InputEvent for FileDropEvent {
322    fn to_platform_input(self) -> PlatformInput {
323        PlatformInput::FileDrop(self)
324    }
325}
326impl MouseEvent for FileDropEvent {}
327
328#[cfg(test)]
329mod test {
330
331    use crate::{
332        self as gpui, AppContext as _, Context, FocusHandle, InteractiveElement, IntoElement,
333        KeyBinding, Keystroke, Modifiers, ParentElement, Render, TestAppContext, Window, div,
334    };
335
336    struct TestView {
337        saw_key_down: bool,
338        saw_action: bool,
339        focus_handle: FocusHandle,
340    }
341
342    actions!(test_only, [TestAction]);
343
344    impl Render for TestView {
345        fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
346            div().id("testview").child(
347                div()
348                    .key_context("parent")
349                    .on_key_down(cx.listener(|this, _, _, cx| {
350                        cx.stop_propagation();
351                        this.saw_key_down = true
352                    }))
353                    .on_action(cx.listener(|this: &mut TestView, _: &TestAction, _, _| {
354                        this.saw_action = true
355                    }))
356                    .child(
357                        div()
358                            .key_context("nested")
359                            .track_focus(&self.focus_handle)
360                            .into_element(),
361                    ),
362            )
363        }
364    }
365
366    #[gpui::test]
367    fn test_on_events(cx: &mut TestAppContext) {
368        let window = cx.update(|cx| {
369            cx.open_window(Default::default(), |_, cx| {
370                cx.new(|cx| TestView {
371                    saw_key_down: false,
372                    saw_action: false,
373                    focus_handle: cx.focus_handle(),
374                })
375            })
376            .unwrap()
377        });
378
379        cx.update(|cx| {
380            cx.bind_keys(vec![KeyBinding::new("ctrl-g", TestAction, Some("parent"))]);
381        });
382
383        window
384            .update(cx, |test_view, window, cx| {
385                window.focus(&test_view.focus_handle, cx)
386            })
387            .unwrap();
388
389        cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap());
390        cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap());
391
392        window
393            .update(cx, |test_view, _, _| {
394                assert!(test_view.saw_key_down || test_view.saw_action);
395                assert!(test_view.saw_key_down);
396                assert!(test_view.saw_action);
397            })
398            .unwrap();
399    }
400
401    #[gpui::test]
402    fn test_multi_modifier_gesture_does_not_dispatch_standalone_modifier_binding(
403        cx: &mut TestAppContext,
404    ) {
405        let (test_view, cx) = cx.add_window_view(|_, cx| TestView {
406            saw_key_down: false,
407            saw_action: false,
408            focus_handle: cx.focus_handle(),
409        });
410
411        cx.update(|_, cx| {
412            cx.bind_keys(vec![KeyBinding::new("shift", TestAction, None)]);
413        });
414        test_view.update_in(cx, |test_view, window, cx| {
415            window.focus(&test_view.focus_handle, cx);
416        });
417
418        cx.simulate_modifiers_change(Modifiers::alt());
419        cx.simulate_modifiers_change(Modifiers::alt() | Modifiers::shift());
420        cx.simulate_modifiers_change(Modifiers::shift());
421        cx.simulate_modifiers_change(Modifiers::none());
422        assert!(!test_view.read_with(cx, |test_view, _| test_view.saw_action));
423
424        cx.simulate_modifiers_change(Modifiers::shift());
425        cx.simulate_modifiers_change(Modifiers::none());
426        assert!(test_view.read_with(cx, |test_view, _| test_view.saw_action));
427    }
428}