Allow passing a handler function to context menu items

Antonio Scandurra created

Change summary

crates/collab_ui/src/collab_titlebar_item.rs |  14 ++
crates/context_menu/src/context_menu.rs      | 101 ++++++++++++++++-----
crates/copilot_button/src/copilot_button.rs  |  12 +-
crates/editor/src/mouse_context_menu.rs      |  12 +-
crates/project_panel/src/project_panel.rs    |  24 ++--
crates/terminal_view/src/terminal_button.rs  |   4 
crates/terminal_view/src/terminal_view.rs    |   4 
crates/workspace/src/pane.rs                 |  44 ++++----
8 files changed, 137 insertions(+), 78 deletions(-)

Detailed changes

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -301,13 +301,19 @@ impl CollabTitlebarItem {
                             .with_style(item_style.container)
                             .into_any()
                     })),
-                    ContextMenuItem::item("Sign out", SignOut),
-                    ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
+                    ContextMenuItem::action("Sign out", SignOut),
+                    ContextMenuItem::action(
+                        "Send Feedback",
+                        feedback::feedback_editor::GiveFeedback,
+                    ),
                 ]
             } else {
                 vec![
-                    ContextMenuItem::item("Sign in", SignIn),
-                    ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
+                    ContextMenuItem::action("Sign in", SignIn),
+                    ContextMenuItem::action(
+                        "Send Feedback",
+                        feedback::feedback_editor::GiveFeedback,
+                    ),
                 ]
             };
 

crates/context_menu/src/context_menu.rs 🔗

@@ -10,7 +10,7 @@ use gpui::{
 };
 use menu::*;
 use settings::Settings;
-use std::{any::TypeId, borrow::Cow, time::Duration};
+use std::{any::TypeId, borrow::Cow, sync::Arc, time::Duration};
 
 #[derive(Copy, Clone, PartialEq)]
 struct Clicked;
@@ -64,20 +64,44 @@ where
     }
 }
 
+pub enum ContextMenuItemAction {
+    Action(Box<dyn Action>),
+    Handler(Arc<dyn Fn(&mut ViewContext<ContextMenu>)>),
+}
+
+impl Clone for ContextMenuItemAction {
+    fn clone(&self) -> Self {
+        match self {
+            Self::Action(action) => Self::Action(action.boxed_clone()),
+            Self::Handler(handler) => Self::Handler(handler.clone()),
+        }
+    }
+}
+
 pub enum ContextMenuItem {
     Item {
         label: ContextMenuItemLabel,
-        action: Box<dyn Action>,
+        action: ContextMenuItemAction,
     },
     Static(StaticItem),
     Separator,
 }
 
 impl ContextMenuItem {
-    pub fn item(label: impl Into<ContextMenuItemLabel>, action: impl 'static + Action) -> Self {
+    pub fn action(label: impl Into<ContextMenuItemLabel>, action: impl 'static + Action) -> Self {
+        Self::Item {
+            label: label.into(),
+            action: ContextMenuItemAction::Action(Box::new(action)),
+        }
+    }
+
+    pub fn handler(
+        label: impl Into<ContextMenuItemLabel>,
+        handler: impl 'static + Fn(&mut ViewContext<ContextMenu>),
+    ) -> Self {
         Self::Item {
             label: label.into(),
-            action: Box::new(action),
+            action: ContextMenuItemAction::Handler(Arc::new(handler)),
         }
     }
 
@@ -91,7 +115,10 @@ impl ContextMenuItem {
 
     fn action_id(&self) -> Option<TypeId> {
         match self {
-            ContextMenuItem::Item { action, .. } => Some(action.id()),
+            ContextMenuItem::Item { action, .. } => match action {
+                ContextMenuItemAction::Action(action) => Some(action.id()),
+                ContextMenuItemAction::Handler(_) => None,
+            },
             ContextMenuItem::Static(..) | ContextMenuItem::Separator => None,
         }
     }
@@ -208,7 +235,17 @@ impl ContextMenu {
     fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
         if let Some(ix) = self.selected_index {
             if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
-                cx.dispatch_any_action(action.boxed_clone());
+                match action {
+                    ContextMenuItemAction::Action(action) => {
+                        let window_id = cx.window_id();
+                        cx.dispatch_any_action_at(
+                            window_id,
+                            self.parent_view_id,
+                            action.boxed_clone(),
+                        );
+                    }
+                    ContextMenuItemAction::Handler(handler) => handler(cx),
+                }
                 self.reset(cx);
             }
         }
@@ -351,13 +388,16 @@ impl ContextMenu {
                                     Some(ix) == self.selected_index,
                                 );
 
-                                KeystrokeLabel::new(
-                                    self.parent_view_id,
-                                    action.boxed_clone(),
-                                    style.keystroke.container,
-                                    style.keystroke.text.clone(),
-                                )
-                                .into_any()
+                                match action {
+                                    ContextMenuItemAction::Action(action) => KeystrokeLabel::new(
+                                        self.parent_view_id,
+                                        action.boxed_clone(),
+                                        style.keystroke.container,
+                                        style.keystroke.text.clone(),
+                                    )
+                                    .into_any(),
+                                    ContextMenuItemAction::Handler(_) => Empty::new().into_any(),
+                                }
                             }
 
                             ContextMenuItem::Static(_) => Empty::new().into_any(),
@@ -389,11 +429,23 @@ impl ContextMenu {
                 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
                     match item {
                         ContextMenuItem::Item { label, action } => {
-                            let action = action.boxed_clone();
+                            let action = action.clone();
                             let view_id = self.parent_view_id;
                             MouseEventHandler::<MenuItem, ContextMenu>::new(ix, cx, |state, _| {
                                 let style =
                                     style.item.style_for(state, Some(ix) == self.selected_index);
+                                let keystroke = match &action {
+                                    ContextMenuItemAction::Action(action) => Some(
+                                        KeystrokeLabel::new(
+                                            view_id,
+                                            action.boxed_clone(),
+                                            style.keystroke.container,
+                                            style.keystroke.text.clone(),
+                                        )
+                                        .flex_float(),
+                                    ),
+                                    ContextMenuItemAction::Handler(_) => None,
+                                };
 
                                 Flex::row()
                                     .with_child(match label {
@@ -406,15 +458,7 @@ impl ContextMenu {
                                             element(state, style)
                                         }
                                     })
-                                    .with_child({
-                                        KeystrokeLabel::new(
-                                            view_id,
-                                            action.boxed_clone(),
-                                            style.keystroke.container,
-                                            style.keystroke.text.clone(),
-                                        )
-                                        .flex_float()
-                                    })
+                                    .with_children(keystroke)
                                     .contained()
                                     .with_style(style.container)
                             })
@@ -424,7 +468,16 @@ impl ContextMenu {
                             .on_click(MouseButton::Left, move |_, _, cx| {
                                 let window_id = cx.window_id();
                                 cx.dispatch_action(Clicked);
-                                cx.dispatch_any_action_at(window_id, view_id, action.boxed_clone());
+                                match &action {
+                                    ContextMenuItemAction::Action(action) => {
+                                        cx.dispatch_any_action_at(
+                                            window_id,
+                                            view_id,
+                                            action.boxed_clone(),
+                                        );
+                                    }
+                                    ContextMenuItemAction::Handler(handler) => handler(cx),
+                                }
                             })
                             .on_drag(MouseButton::Left, |_, _, _| {})
                             .into_any()

crates/copilot_button/src/copilot_button.rs 🔗

@@ -271,8 +271,8 @@ impl CopilotButton {
     ) {
         let mut menu_options = Vec::with_capacity(2);
 
-        menu_options.push(ContextMenuItem::item("Sign In", InitiateSignIn));
-        menu_options.push(ContextMenuItem::item("Disable Copilot", HideCopilot));
+        menu_options.push(ContextMenuItem::action("Sign In", InitiateSignIn));
+        menu_options.push(ContextMenuItem::action("Disable Copilot", HideCopilot));
 
         self.popup_menu.update(cx, |menu, cx| {
             menu.show(
@@ -292,7 +292,7 @@ impl CopilotButton {
         if let Some(language) = &self.language {
             let language_enabled = settings.show_copilot_suggestions(Some(language.as_ref()));
 
-            menu_options.push(ContextMenuItem::item(
+            menu_options.push(ContextMenuItem::action(
                 format!(
                     "{} Suggestions for {}",
                     if language_enabled { "Hide" } else { "Show" },
@@ -305,7 +305,7 @@ impl CopilotButton {
         }
 
         let globally_enabled = cx.global::<Settings>().show_copilot_suggestions(None);
-        menu_options.push(ContextMenuItem::item(
+        menu_options.push(ContextMenuItem::action(
             if globally_enabled {
                 "Hide Suggestions for All Files"
             } else {
@@ -317,7 +317,7 @@ impl CopilotButton {
         menu_options.push(ContextMenuItem::Separator);
 
         let icon_style = settings.theme.copilot.out_link_icon.clone();
-        menu_options.push(ContextMenuItem::item(
+        menu_options.push(ContextMenuItem::action(
             move |state: &mut MouseState, style: &theme::ContextMenuItem| {
                 Flex::row()
                     .with_child(Label::new("Copilot Settings", style.label.clone()))
@@ -328,7 +328,7 @@ impl CopilotButton {
             OsOpen::new(COPILOT_SETTINGS_URL),
         ));
 
-        menu_options.push(ContextMenuItem::item("Sign Out", SignOut));
+        menu_options.push(ContextMenuItem::action("Sign Out", SignOut));
 
         self.popup_menu.update(cx, |menu, cx| {
             menu.show(

crates/editor/src/mouse_context_menu.rs 🔗

@@ -51,18 +51,18 @@ pub fn deploy_context_menu(
             position,
             AnchorCorner::TopLeft,
             vec![
-                ContextMenuItem::item("Rename Symbol", Rename),
-                ContextMenuItem::item("Go to Definition", GoToDefinition),
-                ContextMenuItem::item("Go to Type Definition", GoToTypeDefinition),
-                ContextMenuItem::item("Find All References", FindAllReferences),
-                ContextMenuItem::item(
+                ContextMenuItem::action("Rename Symbol", Rename),
+                ContextMenuItem::action("Go to Definition", GoToDefinition),
+                ContextMenuItem::action("Go to Type Definition", GoToTypeDefinition),
+                ContextMenuItem::action("Find All References", FindAllReferences),
+                ContextMenuItem::action(
                     "Code Actions",
                     ToggleCodeActions {
                         deployed_from_indicator: false,
                     },
                 ),
                 ContextMenuItem::Separator,
-                ContextMenuItem::item("Reveal in Finder", RevealInFinder),
+                ContextMenuItem::action("Reveal in Finder", RevealInFinder),
             ],
             cx,
         );

crates/project_panel/src/project_panel.rs 🔗

@@ -296,38 +296,38 @@ impl ProjectPanel {
         if let Some((worktree, entry)) = self.selected_entry(cx) {
             let is_root = Some(entry) == worktree.root_entry();
             if !project.is_remote() {
-                menu_entries.push(ContextMenuItem::item(
+                menu_entries.push(ContextMenuItem::action(
                     "Add Folder to Project",
                     workspace::AddFolderToProject,
                 ));
                 if is_root {
-                    menu_entries.push(ContextMenuItem::item(
+                    menu_entries.push(ContextMenuItem::action(
                         "Remove from Project",
                         workspace::RemoveWorktreeFromProject(worktree_id),
                     ));
                 }
             }
-            menu_entries.push(ContextMenuItem::item("New File", NewFile));
-            menu_entries.push(ContextMenuItem::item("New Folder", NewDirectory));
+            menu_entries.push(ContextMenuItem::action("New File", NewFile));
+            menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory));
             menu_entries.push(ContextMenuItem::Separator);
-            menu_entries.push(ContextMenuItem::item("Cut", Cut));
-            menu_entries.push(ContextMenuItem::item("Copy", Copy));
+            menu_entries.push(ContextMenuItem::action("Cut", Cut));
+            menu_entries.push(ContextMenuItem::action("Copy", Copy));
             menu_entries.push(ContextMenuItem::Separator);
-            menu_entries.push(ContextMenuItem::item("Copy Path", CopyPath));
-            menu_entries.push(ContextMenuItem::item(
+            menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath));
+            menu_entries.push(ContextMenuItem::action(
                 "Copy Relative Path",
                 CopyRelativePath,
             ));
-            menu_entries.push(ContextMenuItem::item("Reveal in Finder", RevealInFinder));
+            menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder));
             if let Some(clipboard_entry) = self.clipboard_entry {
                 if clipboard_entry.worktree_id() == worktree.id() {
-                    menu_entries.push(ContextMenuItem::item("Paste", Paste));
+                    menu_entries.push(ContextMenuItem::action("Paste", Paste));
                 }
             }
             menu_entries.push(ContextMenuItem::Separator);
-            menu_entries.push(ContextMenuItem::item("Rename", Rename));
+            menu_entries.push(ContextMenuItem::action("Rename", Rename));
             if !is_root {
-                menu_entries.push(ContextMenuItem::item("Delete", Delete));
+                menu_entries.push(ContextMenuItem::action("Delete", Delete));
             }
         }
 

crates/terminal_view/src/terminal_button.rs 🔗

@@ -134,7 +134,7 @@ impl TerminalButton {
         _action: &DeployTerminalMenu,
         cx: &mut ViewContext<Self>,
     ) {
-        let mut menu_options = vec![ContextMenuItem::item("New Terminal", NewTerminal)];
+        let mut menu_options = vec![ContextMenuItem::action("New Terminal", NewTerminal)];
 
         if let Some(workspace) = self.workspace.upgrade(cx) {
             let project = workspace.read(cx).project().read(cx);
@@ -146,7 +146,7 @@ impl TerminalButton {
 
             for local_terminal_handle in local_terminal_handles {
                 if let Some(terminal) = local_terminal_handle.upgrade(cx) {
-                    menu_options.push(ContextMenuItem::item(
+                    menu_options.push(ContextMenuItem::action(
                         terminal.read(cx).title(),
                         FocusTerminal {
                             terminal_handle: local_terminal_handle.clone(),

crates/terminal_view/src/terminal_view.rs 🔗

@@ -199,8 +199,8 @@ impl TerminalView {
 
     pub fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
         let menu_entries = vec![
-            ContextMenuItem::item("Clear", Clear),
-            ContextMenuItem::item("Close", pane::CloseActiveItem),
+            ContextMenuItem::action("Clear", Clear),
+            ContextMenuItem::action("Close", pane::CloseActiveItem),
         ];
 
         self.context_menu.update(cx, |menu, cx| {

crates/workspace/src/pane.rs 🔗

@@ -1229,10 +1229,10 @@ impl Pane {
                 Default::default(),
                 AnchorCorner::TopRight,
                 vec![
-                    ContextMenuItem::item("Split Right", SplitRight),
-                    ContextMenuItem::item("Split Left", SplitLeft),
-                    ContextMenuItem::item("Split Up", SplitUp),
-                    ContextMenuItem::item("Split Down", SplitDown),
+                    ContextMenuItem::action("Split Right", SplitRight),
+                    ContextMenuItem::action("Split Left", SplitLeft),
+                    ContextMenuItem::action("Split Up", SplitUp),
+                    ContextMenuItem::action("Split Down", SplitDown),
                 ],
                 cx,
             );
@@ -1247,9 +1247,9 @@ impl Pane {
                 Default::default(),
                 AnchorCorner::TopRight,
                 vec![
-                    ContextMenuItem::item("Anchor Dock Right", AnchorDockRight),
-                    ContextMenuItem::item("Anchor Dock Bottom", AnchorDockBottom),
-                    ContextMenuItem::item("Expand Dock", ExpandDock),
+                    ContextMenuItem::action("Anchor Dock Right", AnchorDockRight),
+                    ContextMenuItem::action("Anchor Dock Bottom", AnchorDockBottom),
+                    ContextMenuItem::action("Expand Dock", ExpandDock),
                 ],
                 cx,
             );
@@ -1264,9 +1264,9 @@ impl Pane {
                 Default::default(),
                 AnchorCorner::TopRight,
                 vec![
-                    ContextMenuItem::item("New File", NewFile),
-                    ContextMenuItem::item("New Terminal", NewTerminal),
-                    ContextMenuItem::item("New Search", NewSearch),
+                    ContextMenuItem::action("New File", NewFile),
+                    ContextMenuItem::action("New Terminal", NewTerminal),
+                    ContextMenuItem::action("New Search", NewSearch),
                 ],
                 cx,
             );
@@ -1293,40 +1293,40 @@ impl Pane {
                 AnchorCorner::TopLeft,
                 if is_active_item {
                     vec![
-                        ContextMenuItem::item("Close Active Item", CloseActiveItem),
-                        ContextMenuItem::item("Close Inactive Items", CloseInactiveItems),
-                        ContextMenuItem::item("Close Clean Items", CloseCleanItems),
-                        ContextMenuItem::item("Close Items To The Left", CloseItemsToTheLeft),
-                        ContextMenuItem::item("Close Items To The Right", CloseItemsToTheRight),
-                        ContextMenuItem::item("Close All Items", CloseAllItems),
+                        ContextMenuItem::action("Close Active Item", CloseActiveItem),
+                        ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
+                        ContextMenuItem::action("Close Clean Items", CloseCleanItems),
+                        ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
+                        ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight),
+                        ContextMenuItem::action("Close All Items", CloseAllItems),
                     ]
                 } else {
                     // In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command.
                     vec![
-                        ContextMenuItem::item(
+                        ContextMenuItem::action(
                             "Close Inactive Item",
                             CloseItemById {
                                 item_id: target_item_id,
                                 pane: target_pane.clone(),
                             },
                         ),
-                        ContextMenuItem::item("Close Inactive Items", CloseInactiveItems),
-                        ContextMenuItem::item("Close Clean Items", CloseCleanItems),
-                        ContextMenuItem::item(
+                        ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
+                        ContextMenuItem::action("Close Clean Items", CloseCleanItems),
+                        ContextMenuItem::action(
                             "Close Items To The Left",
                             CloseItemsToTheLeftById {
                                 item_id: target_item_id,
                                 pane: target_pane.clone(),
                             },
                         ),
-                        ContextMenuItem::item(
+                        ContextMenuItem::action(
                             "Close Items To The Right",
                             CloseItemsToTheRightById {
                                 item_id: target_item_id,
                                 pane: target_pane.clone(),
                             },
                         ),
-                        ContextMenuItem::item("Close All Items", CloseAllItems),
+                        ContextMenuItem::action("Close All Items", CloseAllItems),
                     ]
                 },
                 cx,