gpui: Handle Swipe events to support navigation buttons on some mice (#23332)

Szymon Piechaczek created

Closes #14170

To fix this, Zed needs to handle swipe events on its NSView. Logitech
mice don't send the usual Mouse4 and Mouse5 buttons but emulate swipe
gestures according to these websites:
- https://superuser.com/a/1216049
- https://sensible-side-buttons.archagon.net/

Of course, the user can map these buttons to something else in the
device's driver. Most IDEs (VSCode, IntelliJ) handle that correctly by
default so it would be good to follow that pattern.

Since it's my first contribution here, please let me know if I need to
enhance this PR to make it good enough for the main branch.

Release Notes:
 - Fixed mouse navigation buttons on some devices (Logitech, Mac OS)

Change summary

crates/gpui/src/platform/mac/events.rs | 27 +++++++++++++++++++++++++++
crates/gpui/src/platform/mac/window.rs |  4 ++++
2 files changed, 31 insertions(+)

Detailed changes

crates/gpui/src/platform/mac/events.rs 🔗

@@ -158,6 +158,33 @@ impl PlatformInput {
                     })
                 })
             }
+            // Some mice (like Logitech MX Master) send navigation buttons as swipe events
+            NSEventType::NSEventTypeSwipe => {
+                let navigation_direction = match native_event.phase() {
+                    NSEventPhase::NSEventPhaseEnded => match native_event.deltaX() {
+                        x if x > 0.0 => Some(NavigationDirection::Back),
+                        x if x < 0.0 => Some(NavigationDirection::Forward),
+                        _ => return None,
+                    },
+                    _ => return None,
+                };
+
+                match navigation_direction {
+                    Some(direction) => window_height.map(|window_height| {
+                        Self::MouseDown(MouseDownEvent {
+                            button: MouseButton::Navigate(direction),
+                            position: point(
+                                px(native_event.locationInWindow().x as f32),
+                                window_height - px(native_event.locationInWindow().y as f32),
+                            ),
+                            modifiers: read_modifiers(native_event),
+                            click_count: 1,
+                            first_mouse: false,
+                        })
+                    }),
+                    _ => None,
+                }
+            }
             NSEventType::NSScrollWheel => window_height.map(|window_height| {
                 let phase = match native_event.phase() {
                     NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => {

crates/gpui/src/platform/mac/window.rs 🔗

@@ -148,6 +148,10 @@ unsafe fn build_classes() {
             sel!(scrollWheel:),
             handle_view_event as extern "C" fn(&Object, Sel, id),
         );
+        decl.add_method(
+            sel!(swipeWithEvent:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
         decl.add_method(
             sel!(flagsChanged:),
             handle_view_event as extern "C" fn(&Object, Sel, id),