Merge pull request #563 from zed-industries/mouse-history-navigation

Keith Simmons created

Add missing mouse button events and mouse history navigation

Change summary

crates/gpui/src/elements/event_handler.rs | 42 +++++++++++++
crates/gpui/src/gpui.rs                   |  2 
crates/gpui/src/platform.rs               |  2 
crates/gpui/src/platform/event.rs         | 30 ++++++++++
crates/gpui/src/platform/mac/event.rs     | 64 +++++++++++++++++++++
crates/gpui/src/platform/mac/window.rs    | 16 +++++
crates/workspace/src/pane.rs              | 74 ++++++++++++++++++------
crates/zed/src/zed.rs                     | 40 ++++++++++---
8 files changed, 238 insertions(+), 32 deletions(-)

Detailed changes

crates/gpui/src/elements/event_handler.rs 🔗

@@ -3,13 +3,15 @@ use serde_json::json;
 
 use crate::{
     geometry::vector::Vector2F, DebugContext, Element, ElementBox, Event, EventContext,
-    LayoutContext, PaintContext, SizeConstraint,
+    LayoutContext, NavigationDirection, PaintContext, SizeConstraint,
 };
 
 pub struct EventHandler {
     child: ElementBox,
     capture: Option<Box<dyn FnMut(&Event, RectF, &mut EventContext) -> bool>>,
     mouse_down: Option<Box<dyn FnMut(&mut EventContext) -> bool>>,
+    right_mouse_down: Option<Box<dyn FnMut(&mut EventContext) -> bool>>,
+    navigate_mouse_down: Option<Box<dyn FnMut(NavigationDirection, &mut EventContext) -> bool>>,
 }
 
 impl EventHandler {
@@ -18,6 +20,8 @@ impl EventHandler {
             child,
             capture: None,
             mouse_down: None,
+            right_mouse_down: None,
+            navigate_mouse_down: None,
         }
     }
 
@@ -29,6 +33,22 @@ impl EventHandler {
         self
     }
 
+    pub fn on_right_mouse_down<F>(mut self, callback: F) -> Self
+    where
+        F: 'static + FnMut(&mut EventContext) -> bool,
+    {
+        self.right_mouse_down = Some(Box::new(callback));
+        self
+    }
+
+    pub fn on_navigate_mouse_down<F>(mut self, callback: F) -> Self
+    where
+        F: 'static + FnMut(NavigationDirection, &mut EventContext) -> bool,
+    {
+        self.navigate_mouse_down = Some(Box::new(callback));
+        self
+    }
+
     pub fn capture<F>(mut self, callback: F) -> Self
     where
         F: 'static + FnMut(&Event, RectF, &mut EventContext) -> bool,
@@ -87,6 +107,26 @@ impl Element for EventHandler {
                     }
                     false
                 }
+                Event::RightMouseDown { position, .. } => {
+                    if let Some(callback) = self.right_mouse_down.as_mut() {
+                        if bounds.contains_point(*position) {
+                            return callback(cx);
+                        }
+                    }
+                    false
+                }
+                Event::NavigateMouseDown {
+                    position,
+                    direction,
+                    ..
+                } => {
+                    if let Some(callback) = self.navigate_mouse_down.as_mut() {
+                        if bounds.contains_point(*position) {
+                            return callback(*direction, cx);
+                        }
+                    }
+                    false
+                }
                 _ => false,
             }
         }

crates/gpui/src/gpui.rs 🔗

@@ -29,7 +29,7 @@ pub mod keymap;
 pub mod platform;
 pub use gpui_macros::test;
 pub use platform::FontSystem;
-pub use platform::{Event, PathPromptOptions, Platform, PromptLevel};
+pub use platform::{Event, NavigationDirection, PathPromptOptions, Platform, PromptLevel};
 pub use presenter::{
     Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt,
 };

crates/gpui/src/platform.rs 🔗

@@ -19,7 +19,7 @@ use crate::{
 };
 use anyhow::Result;
 use async_task::Runnable;
-pub use event::Event;
+pub use event::{Event, NavigationDirection};
 use postage::oneshot;
 use std::{
     any::Any,

crates/gpui/src/platform/event.rs 🔗

@@ -1,5 +1,11 @@
 use crate::{geometry::vector::Vector2F, keymap::Keystroke};
 
+#[derive(Copy, Clone, Debug)]
+pub enum NavigationDirection {
+    Back,
+    Forward,
+}
+
 #[derive(Clone, Debug)]
 pub enum Event {
     KeyDown {
@@ -26,6 +32,30 @@ pub enum Event {
     LeftMouseDragged {
         position: Vector2F,
     },
+    RightMouseDown {
+        position: Vector2F,
+        ctrl: bool,
+        alt: bool,
+        shift: bool,
+        cmd: bool,
+        click_count: usize,
+    },
+    RightMouseUp {
+        position: Vector2F,
+    },
+    NavigateMouseDown {
+        position: Vector2F,
+        direction: NavigationDirection,
+        ctrl: bool,
+        alt: bool,
+        shift: bool,
+        cmd: bool,
+        click_count: usize,
+    },
+    NavigateMouseUp {
+        position: Vector2F,
+        direction: NavigationDirection,
+    },
     MouseMoved {
         position: Vector2F,
         left_mouse_down: bool,

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

@@ -1,4 +1,8 @@
-use crate::{geometry::vector::vec2f, keymap::Keystroke, platform::Event};
+use crate::{
+    geometry::vector::vec2f,
+    keymap::Keystroke,
+    platform::{Event, NavigationDirection},
+};
 use cocoa::{
     appkit::{NSEvent, NSEventModifierFlags, NSEventType},
     base::{id, nil, YES},
@@ -125,6 +129,64 @@ impl Event {
                     window_height - native_event.locationInWindow().y as f32,
                 ),
             }),
+            NSEventType::NSRightMouseDown => {
+                let modifiers = native_event.modifierFlags();
+                window_height.map(|window_height| Self::RightMouseDown {
+                    position: vec2f(
+                        native_event.locationInWindow().x as f32,
+                        window_height - native_event.locationInWindow().y as f32,
+                    ),
+                    ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
+                    alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
+                    shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
+                    cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
+                    click_count: native_event.clickCount() as usize,
+                })
+            }
+            NSEventType::NSRightMouseUp => window_height.map(|window_height| Self::RightMouseUp {
+                position: vec2f(
+                    native_event.locationInWindow().x as f32,
+                    window_height - native_event.locationInWindow().y as f32,
+                ),
+            }),
+            NSEventType::NSOtherMouseDown => {
+                let direction = match native_event.buttonNumber() {
+                    3 => NavigationDirection::Back,
+                    4 => NavigationDirection::Forward,
+                    // Other mouse buttons aren't tracked currently
+                    _ => return None,
+                };
+
+                let modifiers = native_event.modifierFlags();
+                window_height.map(|window_height| Self::NavigateMouseDown {
+                    position: vec2f(
+                        native_event.locationInWindow().x as f32,
+                        window_height - native_event.locationInWindow().y as f32,
+                    ),
+                    direction,
+                    ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
+                    alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
+                    shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
+                    cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
+                    click_count: native_event.clickCount() as usize,
+                })
+            }
+            NSEventType::NSOtherMouseUp => {
+                let direction = match native_event.buttonNumber() {
+                    3 => NavigationDirection::Back,
+                    4 => NavigationDirection::Forward,
+                    // Other mouse buttons aren't tracked currently
+                    _ => return None,
+                };
+
+                window_height.map(|window_height| Self::NavigateMouseUp {
+                    position: vec2f(
+                        native_event.locationInWindow().x as f32,
+                        window_height - native_event.locationInWindow().y as f32,
+                    ),
+                    direction,
+                })
+            }
             NSEventType::NSLeftMouseDragged => {
                 window_height.map(|window_height| Self::LeftMouseDragged {
                     position: vec2f(

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

@@ -95,6 +95,22 @@ unsafe fn build_classes() {
             sel!(mouseUp:),
             handle_view_event as extern "C" fn(&Object, Sel, id),
         );
+        decl.add_method(
+            sel!(rightMouseDown:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(rightMouseUp:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(otherMouseDown:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(otherMouseUp:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
         decl.add_method(
             sel!(mouseMoved:),
             handle_view_event as extern "C" fn(&Object, Sel, id),

crates/workspace/src/pane.rs 🔗

@@ -6,9 +6,9 @@ use gpui::{
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
     keymap::Binding,
-    platform::CursorStyle,
+    platform::{CursorStyle, NavigationDirection},
     AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext,
-    ViewHandle,
+    ViewHandle, WeakViewHandle,
 };
 use postage::watch;
 use project::ProjectPath;
@@ -27,8 +27,8 @@ action!(ActivateNextItem);
 action!(CloseActiveItem);
 action!(CloseInactiveItems);
 action!(CloseItem, usize);
-action!(GoBack);
-action!(GoForward);
+action!(GoBack, Option<WeakViewHandle<Pane>>);
+action!(GoForward, Option<WeakViewHandle<Pane>>);
 
 const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
@@ -54,11 +54,27 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(|pane: &mut Pane, action: &Split, cx| {
         pane.split(action.0, cx);
     });
-    cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| {
-        Pane::go_back(workspace, cx).detach();
+    cx.add_action(|workspace: &mut Workspace, action: &GoBack, cx| {
+        Pane::go_back(
+            workspace,
+            action
+                .0
+                .as_ref()
+                .and_then(|weak_handle| weak_handle.upgrade(cx)),
+            cx,
+        )
+        .detach();
     });
-    cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| {
-        Pane::go_forward(workspace, cx).detach();
+    cx.add_action(|workspace: &mut Workspace, action: &GoForward, cx| {
+        Pane::go_forward(
+            workspace,
+            action
+                .0
+                .as_ref()
+                .and_then(|weak_handle| weak_handle.upgrade(cx)),
+            cx,
+        )
+        .detach();
     });
 
     cx.add_bindings(vec![
@@ -70,8 +86,8 @@ pub fn init(cx: &mut MutableAppContext) {
         Binding::new("cmd-k down", Split(SplitDirection::Down), Some("Pane")),
         Binding::new("cmd-k left", Split(SplitDirection::Left), Some("Pane")),
         Binding::new("cmd-k right", Split(SplitDirection::Right), Some("Pane")),
-        Binding::new("ctrl--", GoBack, Some("Pane")),
-        Binding::new("shift-ctrl-_", GoForward, Some("Pane")),
+        Binding::new("ctrl--", GoBack(None), Some("Pane")),
+        Binding::new("shift-ctrl-_", GoForward(None), Some("Pane")),
     ]);
 }
 
@@ -163,19 +179,27 @@ impl Pane {
         cx.emit(Event::Activate);
     }
 
-    pub fn go_back(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Task<()> {
+    pub fn go_back(
+        workspace: &mut Workspace,
+        pane: Option<ViewHandle<Pane>>,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Task<()> {
         Self::navigate_history(
             workspace,
-            workspace.active_pane().clone(),
+            pane.unwrap_or_else(|| workspace.active_pane().clone()),
             NavigationMode::GoingBack,
             cx,
         )
     }
 
-    pub fn go_forward(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Task<()> {
+    pub fn go_forward(
+        workspace: &mut Workspace,
+        pane: Option<ViewHandle<Pane>>,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Task<()> {
         Self::navigate_history(
             workspace,
-            workspace.active_pane().clone(),
+            pane.unwrap_or_else(|| workspace.active_pane().clone()),
             NavigationMode::GoingForward,
             cx,
         )
@@ -187,6 +211,8 @@ impl Pane {
         mode: NavigationMode,
         cx: &mut ViewContext<Workspace>,
     ) -> Task<()> {
+        workspace.activate_pane(pane.clone(), cx);
+
         let to_load = pane.update(cx, |pane, cx| {
             // Retrieve the weak item handle from the history.
             let entry = pane.nav_history.borrow_mut().pop(mode)?;
@@ -634,7 +660,9 @@ impl View for Pane {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        if let Some(active_item) = self.active_item() {
+        let this = cx.handle();
+
+        EventHandler::new(if let Some(active_item) = self.active_item() {
             Flex::column()
                 .with_child(self.render_tabs(cx))
                 .with_children(
@@ -643,10 +671,20 @@ impl View for Pane {
                         .map(|view| ChildView::new(view).boxed()),
                 )
                 .with_child(ChildView::new(active_item).flexible(1., true).boxed())
-                .named("pane")
+                .boxed()
         } else {
-            Empty::new().named("pane")
-        }
+            Empty::new().boxed()
+        })
+        .on_navigate_mouse_down(move |direction, cx| {
+            let this = this.clone();
+            match direction {
+                NavigationDirection::Back => cx.dispatch_action(GoBack(Some(this))),
+                NavigationDirection::Forward => cx.dispatch_action(GoForward(Some(this))),
+            }
+
+            true
+        })
+        .named("pane")
     }
 
     fn on_focus(&mut self, cx: &mut ViewContext<Self>) {

crates/zed/src/zed.rs 🔗

@@ -747,44 +747,58 @@ mod tests {
             (file3.clone(), DisplayPoint::new(15, 0))
         );
 
-        workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await;
+        workspace
+            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .await;
         assert_eq!(
             active_location(&workspace, cx),
             (file3.clone(), DisplayPoint::new(0, 0))
         );
 
-        workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await;
+        workspace
+            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .await;
         assert_eq!(
             active_location(&workspace, cx),
             (file2.clone(), DisplayPoint::new(0, 0))
         );
 
-        workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await;
+        workspace
+            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .await;
         assert_eq!(
             active_location(&workspace, cx),
             (file1.clone(), DisplayPoint::new(10, 0))
         );
 
-        workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await;
+        workspace
+            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .await;
         assert_eq!(
             active_location(&workspace, cx),
             (file1.clone(), DisplayPoint::new(0, 0))
         );
 
         // Go back one more time and ensure we don't navigate past the first item in the history.
-        workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await;
+        workspace
+            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .await;
         assert_eq!(
             active_location(&workspace, cx),
             (file1.clone(), DisplayPoint::new(0, 0))
         );
 
-        workspace.update(cx, |w, cx| Pane::go_forward(w, cx)).await;
+        workspace
+            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
+            .await;
         assert_eq!(
             active_location(&workspace, cx),
             (file1.clone(), DisplayPoint::new(10, 0))
         );
 
-        workspace.update(cx, |w, cx| Pane::go_forward(w, cx)).await;
+        workspace
+            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
+            .await;
         assert_eq!(
             active_location(&workspace, cx),
             (file2.clone(), DisplayPoint::new(0, 0))
@@ -798,7 +812,9 @@ mod tests {
                 .update(cx, |pane, cx| pane.close_item(editor3.id(), cx));
             drop(editor3);
         });
-        workspace.update(cx, |w, cx| Pane::go_forward(w, cx)).await;
+        workspace
+            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
+            .await;
         assert_eq!(
             active_location(&workspace, cx),
             (file3.clone(), DisplayPoint::new(0, 0))
@@ -818,12 +834,16 @@ mod tests {
             })
             .await
             .unwrap();
-        workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await;
+        workspace
+            .update(cx, |w, cx| Pane::go_back(w, None, cx))
+            .await;
         assert_eq!(
             active_location(&workspace, cx),
             (file1.clone(), DisplayPoint::new(10, 0))
         );
-        workspace.update(cx, |w, cx| Pane::go_forward(w, cx)).await;
+        workspace
+            .update(cx, |w, cx| Pane::go_forward(w, None, cx))
+            .await;
         assert_eq!(
             active_location(&workspace, cx),
             (file3.clone(), DisplayPoint::new(0, 0))