Add a context menu to the project panel (#3393)

Max Brunsfeld created

This PR adds a context menu to the project panel in Zed2.

* [x] Allow the context menu to extend outside of the project panel's
bounds
* [x] Add keyboard shortcuts to the context menu
* [x] Dismiss the context menu
    * [x] when running an action
    * [x] when changing selection in the project panel

Release Notes:

NA

Change summary

crates/auto_update2/src/update_notification.rs       |   2 
crates/collab_ui2/src/collab_panel/contact_finder.rs |   2 
crates/command_palette2/src/command_palette.rs       |   2 
crates/file_finder2/src/file_finder.rs               |   6 
crates/go_to_line2/src/go_to_line.rs                 |   6 
crates/gpui2/src/app/async_context.rs                |   5 
crates/gpui2/src/app/test_context.rs                 |   2 
crates/gpui2/src/window.rs                           |  20 
crates/project_panel2/src/project_panel.rs           | 181 +++++++-----
crates/terminal_view2/src/terminal_view.rs           |   9 
crates/theme_selector2/src/theme_selector.rs         |   2 
crates/ui2/src/components/context_menu.rs            | 187 ++++++++++---
crates/ui2/src/components/list.rs                    |  12 
crates/ui2/src/components/stories/context_menu.rs    |   4 
crates/welcome2/src/base_keymap_picker.rs            |   2 
crates/workspace2/src/dock.rs                        |   2 
crates/workspace2/src/notifications.rs               |   8 
17 files changed, 277 insertions(+), 175 deletions(-)

Detailed changes

crates/collab_ui2/src/collab_panel/contact_finder.rs 🔗

@@ -161,7 +161,7 @@ impl PickerDelegate for ContactFinderDelegate {
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
         //cx.emit(PickerEvent::Dismiss);
         self.parent
-            .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
+            .update(cx, |_, cx| cx.emit(DismissEvent))
             .log_err();
     }
 

crates/command_palette2/src/command_palette.rs 🔗

@@ -269,7 +269,7 @@ impl PickerDelegate for CommandPaletteDelegate {
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
         self.command_palette
-            .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
+            .update(cx, |_, cx| cx.emit(DismissEvent))
             .log_err();
     }
 

crates/file_finder2/src/file_finder.rs 🔗

@@ -687,9 +687,7 @@ impl PickerDelegate for FileFinderDelegate {
                                 .log_err();
                         }
                     }
-                    finder
-                        .update(&mut cx, |_, cx| cx.emit(DismissEvent::Dismiss))
-                        .ok()?;
+                    finder.update(&mut cx, |_, cx| cx.emit(DismissEvent)).ok()?;
 
                     Some(())
                 })
@@ -700,7 +698,7 @@ impl PickerDelegate for FileFinderDelegate {
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
         self.file_finder
-            .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
+            .update(cx, |_, cx| cx.emit(DismissEvent))
             .log_err();
     }
 

crates/go_to_line2/src/go_to_line.rs 🔗

@@ -90,7 +90,7 @@ impl GoToLine {
     ) {
         match event {
             // todo!() this isn't working...
-            editor::EditorEvent::Blurred => cx.emit(DismissEvent::Dismiss),
+            editor::EditorEvent::Blurred => cx.emit(DismissEvent),
             editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
             _ => {}
         }
@@ -125,7 +125,7 @@ impl GoToLine {
     }
 
     fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(DismissEvent::Dismiss);
+        cx.emit(DismissEvent);
     }
 
     fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
@@ -142,7 +142,7 @@ impl GoToLine {
             self.prev_scroll_position.take();
         }
 
-        cx.emit(DismissEvent::Dismiss);
+        cx.emit(DismissEvent);
     }
 }
 

crates/gpui2/src/app/async_context.rs 🔗

@@ -325,8 +325,7 @@ impl VisualContext for AsyncWindowContext {
     where
         V: crate::ManagedView,
     {
-        self.window.update(self, |_, cx| {
-            view.update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
-        })
+        self.window
+            .update(self, |_, cx| view.update(cx, |_, cx| cx.emit(DismissEvent)))
     }
 }

crates/gpui2/src/app/test_context.rs 🔗

@@ -611,7 +611,7 @@ impl<'a> VisualContext for VisualTestContext<'a> {
     {
         self.window
             .update(self.cx, |_, cx| {
-                view.update(cx, |_, cx| cx.emit(crate::DismissEvent::Dismiss))
+                view.update(cx, |_, cx| cx.emit(crate::DismissEvent))
             })
             .unwrap()
     }

crates/gpui2/src/window.rs 🔗

@@ -197,9 +197,7 @@ pub trait ManagedView: FocusableView + EventEmitter<DismissEvent> {}
 
 impl<M: FocusableView + EventEmitter<DismissEvent>> ManagedView for M {}
 
-pub enum DismissEvent {
-    Dismiss,
-}
+pub struct DismissEvent;
 
 // Holds the state for a specific window.
 pub struct Window {
@@ -1482,13 +1480,15 @@ impl<'a> WindowContext<'a> {
         }
     }
 
-    pub fn constructor_for<V: Render, R>(
+    pub fn handler_for<V: Render>(
         &self,
         view: &View<V>,
-        f: impl Fn(&mut V, &mut ViewContext<V>) -> R + 'static,
-    ) -> impl Fn(&mut WindowContext) -> R + 'static {
-        let view = view.clone();
-        move |cx: &mut WindowContext| view.update(cx, |view, cx| f(view, cx))
+        f: impl Fn(&mut V, &mut ViewContext<V>) + 'static,
+    ) -> impl Fn(&mut WindowContext) {
+        let view = view.downgrade();
+        move |cx: &mut WindowContext| {
+            view.update(cx, |view, cx| f(view, cx)).ok();
+        }
     }
 
     //========== ELEMENT RELATED FUNCTIONS ===========
@@ -1699,7 +1699,7 @@ impl VisualContext for WindowContext<'_> {
     where
         V: ManagedView,
     {
-        self.update_view(view, |_, cx| cx.emit(DismissEvent::Dismiss))
+        self.update_view(view, |_, cx| cx.emit(DismissEvent))
     }
 }
 
@@ -2386,7 +2386,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     where
         V: ManagedView,
     {
-        self.defer(|_, cx| cx.emit(DismissEvent::Dismiss))
+        self.defer(|_, cx| cx.emit(DismissEvent))
     }
 
     pub fn listener<E>(

crates/project_panel2/src/project_panel.rs 🔗

@@ -8,10 +8,11 @@ use file_associations::FileAssociations;
 
 use anyhow::{anyhow, Result};
 use gpui::{
-    actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
-    ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement,
-    Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, Stateful, Styled,
-    Task, UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext,
+    actions, div, overlay, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
+    ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView,
+    InteractiveElement, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
+    PromptLevel, Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View,
+    ViewContext, VisualContext as _, WeakView, WindowContext,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
 use project::{
@@ -29,7 +30,7 @@ use std::{
     sync::Arc,
 };
 use theme::ActiveTheme as _;
-use ui::{v_stack, IconElement, Label, ListItem};
+use ui::{v_stack, ContextMenu, IconElement, Label, ListItem};
 use unicase::UniCase;
 use util::{maybe, ResultExt, TryFutureExt};
 use workspace::{
@@ -49,6 +50,7 @@ pub struct ProjectPanel {
     last_worktree_root_id: Option<ProjectEntryId>,
     expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
     selection: Option<Selection>,
+    context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
     edit_state: Option<EditState>,
     filename_editor: View<Editor>,
     clipboard_entry: Option<ClipboardEntry>,
@@ -231,6 +233,7 @@ impl ProjectPanel {
                 expanded_dir_ids: Default::default(),
                 selection: None,
                 edit_state: None,
+                context_menu: None,
                 filename_editor,
                 clipboard_entry: None,
                 // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
@@ -366,80 +369,93 @@ impl ProjectPanel {
 
     fn deploy_context_menu(
         &mut self,
-        _position: Point<Pixels>,
-        _entry_id: ProjectEntryId,
-        _cx: &mut ViewContext<Self>,
+        position: Point<Pixels>,
+        entry_id: ProjectEntryId,
+        cx: &mut ViewContext<Self>,
     ) {
-        // todo!()
-        //     let project = self.project.read(cx);
+        let this = cx.view().clone();
+        let project = self.project.read(cx);
 
-        //     let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
-        //         id
-        //     } else {
-        //         return;
-        //     };
+        let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
+            id
+        } else {
+            return;
+        };
 
-        //     self.selection = Some(Selection {
-        //         worktree_id,
-        //         entry_id,
-        //     });
-
-        //     let mut menu_entries = Vec::new();
-        //     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::action(
-        //                 "Add Folder to Project",
-        //                 workspace::AddFolderToProject,
-        //             ));
-        //             if is_root {
-        //                 let project = self.project.clone();
-        //                 menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| {
-        //                     project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
-        //                 }));
-        //             }
-        //         }
-        //         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::action("Cut", Cut));
-        //         menu_entries.push(ContextMenuItem::action("Copy", Copy));
-        //         if let Some(clipboard_entry) = self.clipboard_entry {
-        //             if clipboard_entry.worktree_id() == worktree.id() {
-        //                 menu_entries.push(ContextMenuItem::action("Paste", Paste));
-        //             }
-        //         }
-        //         menu_entries.push(ContextMenuItem::Separator);
-        //         menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath));
-        //         menu_entries.push(ContextMenuItem::action(
-        //             "Copy Relative Path",
-        //             CopyRelativePath,
-        //         ));
-
-        //         if entry.is_dir() {
-        //             menu_entries.push(ContextMenuItem::Separator);
-        //         }
-        //         menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder));
-        //         if entry.is_dir() {
-        //             menu_entries.push(ContextMenuItem::action("Open in Terminal", OpenInTerminal));
-        //             menu_entries.push(ContextMenuItem::action(
-        //                 "Search Inside",
-        //                 NewSearchInDirectory,
-        //             ));
-        //         }
-
-        //         menu_entries.push(ContextMenuItem::Separator);
-        //         menu_entries.push(ContextMenuItem::action("Rename", Rename));
-        //         if !is_root {
-        //             menu_entries.push(ContextMenuItem::action("Delete", Delete));
-        //         }
-        //     }
-
-        //     // self.context_menu.update(cx, |menu, cx| {
-        //     //     menu.show(position, AnchorCorner::TopLeft, menu_entries, cx);
-        //     // });
-
-        //     cx.notify();
+        self.selection = Some(Selection {
+            worktree_id,
+            entry_id,
+        });
+
+        if let Some((worktree, entry)) = self.selected_entry(cx) {
+            let is_root = Some(entry) == worktree.root_entry();
+            let is_dir = entry.is_dir();
+            let worktree_id = worktree.id();
+            let is_local = project.is_local();
+
+            let context_menu = ContextMenu::build(cx, |mut menu, cx| {
+                if is_local {
+                    menu = menu.action(
+                        "Add Folder to Project",
+                        Box::new(workspace::AddFolderToProject),
+                        cx,
+                    );
+                    if is_root {
+                        menu = menu.entry(
+                            "Remove from Project",
+                            cx.handler_for(&this, move |this, cx| {
+                                this.project.update(cx, |project, cx| {
+                                    project.remove_worktree(worktree_id, cx)
+                                });
+                            }),
+                        );
+                    }
+                }
+
+                menu = menu
+                    .action("New File", Box::new(NewFile), cx)
+                    .action("New Folder", Box::new(NewDirectory), cx)
+                    .separator()
+                    .action("Cut", Box::new(Cut), cx)
+                    .action("Copy", Box::new(Copy), cx);
+
+                if let Some(clipboard_entry) = self.clipboard_entry {
+                    if clipboard_entry.worktree_id() == worktree_id {
+                        menu = menu.action("Paste", Box::new(Paste), cx);
+                    }
+                }
+
+                menu = menu
+                    .separator()
+                    .action("Copy Path", Box::new(CopyPath), cx)
+                    .action("Copy Relative Path", Box::new(CopyRelativePath), cx)
+                    .separator()
+                    .action("Reveal in Finder", Box::new(RevealInFinder), cx);
+
+                if is_dir {
+                    menu = menu
+                        .action("Open in Terminal", Box::new(OpenInTerminal), cx)
+                        .action("Search Inside", Box::new(NewSearchInDirectory), cx)
+                }
+
+                menu = menu.separator().action("Rename", Box::new(Rename), cx);
+
+                if !is_root {
+                    menu = menu.action("Delete", Box::new(Delete), cx);
+                }
+
+                menu
+            });
+
+            cx.focus_view(&context_menu);
+            let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
+                this.context_menu.take();
+                cx.notify();
+            });
+            self.context_menu = Some((context_menu, position, subscription));
+        }
+
+        cx.notify();
     }
 
     fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
@@ -643,7 +659,6 @@ impl ProjectPanel {
     }
 
     fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
-        dbg!("odd");
         self.edit_state = None;
         self.update_visible_entries(None, cx);
         cx.focus(&self.focus_handle);
@@ -1370,7 +1385,7 @@ impl ProjectPanel {
             })
             .child(
                 if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
-                    div().w_full().child(editor.clone())
+                    div().h_full().w_full().child(editor.clone())
                 } else {
                     div()
                         .text_color(filename_text_color)
@@ -1379,6 +1394,9 @@ impl ProjectPanel {
                 .ml_1(),
             )
             .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| {
+                if event.down.button == MouseButton::Right {
+                    return;
+                }
                 if !show_editor {
                     if kind.is_dir() {
                         this.toggle_expanded(entry_id, cx);
@@ -1415,6 +1433,7 @@ impl Render for ProjectPanel {
             div()
                 .id("project-panel")
                 .size_full()
+                .relative()
                 .key_context("ProjectPanel")
                 .on_action(cx.listener(Self::select_next))
                 .on_action(cx.listener(Self::select_prev))
@@ -1458,6 +1477,12 @@ impl Render for ProjectPanel {
                     .size_full()
                     .track_scroll(self.list.clone()),
                 )
+                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
+                    overlay()
+                        .position(*position)
+                        .anchor(gpui::AnchorCorner::BottomLeft)
+                        .child(menu.clone())
+                }))
         } else {
             v_stack()
                 .id("empty-project_panel")

crates/terminal_view2/src/terminal_view.rs 🔗

@@ -298,9 +298,12 @@ impl TerminalView {
         position: gpui::Point<Pixels>,
         cx: &mut ViewContext<Self>,
     ) {
-        self.context_menu = Some(ContextMenu::build(cx, |menu, _| {
-            menu.action("Clear", Box::new(Clear))
-                .action("Close", Box::new(CloseActiveItem { save_intent: None }))
+        self.context_menu = Some(ContextMenu::build(cx, |menu, cx| {
+            menu.action("Clear", Box::new(Clear), cx).action(
+                "Close",
+                Box::new(CloseActiveItem { save_intent: None }),
+                cx,
+            )
         }));
         dbg!(&position);
         // todo!()

crates/ui2/src/components/context_menu.rs 🔗

@@ -1,23 +1,28 @@
-use std::cell::RefCell;
-use std::rc::Rc;
-
-use crate::{prelude::*, v_stack, Label, List};
-use crate::{ListItem, ListSeparator, ListSubHeader};
+use crate::{
+    h_stack, prelude::*, v_stack, KeyBinding, Label, List, ListItem, ListSeparator, ListSubHeader,
+};
 use gpui::{
-    overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DismissEvent,
-    DispatchPhase, Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId,
-    ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, VisualContext,
+    overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, DismissEvent, DispatchPhase,
+    Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, ManagedView, MouseButton,
+    MouseDownEvent, Pixels, Point, Render, View, VisualContext,
 };
+use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
+use std::{cell::RefCell, rc::Rc};
 
 pub enum ContextMenuItem {
     Separator,
     Header(SharedString),
-    Entry(SharedString, Rc<dyn Fn(&ClickEvent, &mut WindowContext)>),
+    Entry {
+        label: SharedString,
+        handler: Rc<dyn Fn(&mut WindowContext)>,
+        key_binding: Option<KeyBinding>,
+    },
 }
 
 pub struct ContextMenu {
     items: Vec<ContextMenuItem>,
     focus_handle: FocusHandle,
+    selected_index: Option<usize>,
 }
 
 impl FocusableView for ContextMenu {
@@ -39,6 +44,7 @@ impl ContextMenu {
                 Self {
                     items: Default::default(),
                     focus_handle: cx.focus_handle(),
+                    selected_index: None,
                 },
                 cx,
             )
@@ -58,27 +64,90 @@ impl ContextMenu {
     pub fn entry(
         mut self,
         label: impl Into<SharedString>,
-        on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
+        on_click: impl Fn(&mut WindowContext) + 'static,
     ) -> Self {
-        self.items
-            .push(ContextMenuItem::Entry(label.into(), Rc::new(on_click)));
+        self.items.push(ContextMenuItem::Entry {
+            label: label.into(),
+            handler: Rc::new(on_click),
+            key_binding: None,
+        });
         self
     }
 
-    pub fn action(self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
-        // todo: add the keybindings to the list entry
-        self.entry(label.into(), move |_, cx| {
-            cx.dispatch_action(action.boxed_clone())
-        })
+    pub fn action(
+        mut self,
+        label: impl Into<SharedString>,
+        action: Box<dyn Action>,
+        cx: &mut WindowContext,
+    ) -> Self {
+        self.items.push(ContextMenuItem::Entry {
+            label: label.into(),
+            key_binding: KeyBinding::for_action(&*action, cx),
+            handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
+        });
+        self
     }
 
     pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
-        // todo!()
-        cx.emit(DismissEvent::Dismiss);
+        if let Some(ContextMenuItem::Entry { handler, .. }) =
+            self.selected_index.and_then(|ix| self.items.get(ix))
+        {
+            (handler)(cx)
+        }
+        cx.emit(DismissEvent);
     }
 
     pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(DismissEvent::Dismiss);
+        cx.emit(DismissEvent);
+    }
+
+    fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
+        self.selected_index = self.items.iter().position(|item| item.is_selectable());
+        cx.notify();
+    }
+
+    fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
+        for (ix, item) in self.items.iter().enumerate().rev() {
+            if item.is_selectable() {
+                self.selected_index = Some(ix);
+                cx.notify();
+                break;
+            }
+        }
+    }
+
+    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
+        if let Some(ix) = self.selected_index {
+            for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
+                if item.is_selectable() {
+                    self.selected_index = Some(ix);
+                    cx.notify();
+                    break;
+                }
+            }
+        } else {
+            self.select_first(&Default::default(), cx);
+        }
+    }
+
+    pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
+        if let Some(ix) = self.selected_index {
+            for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
+                if item.is_selectable() {
+                    self.selected_index = Some(ix);
+                    cx.notify();
+                    break;
+                }
+            }
+        } else {
+            self.select_last(&Default::default(), cx);
+        }
+    }
+}
+
+impl ContextMenuItem {
+    fn is_selectable(&self) -> bool {
+        matches!(self, Self::Entry { .. })
     }
 }
 
@@ -90,38 +159,51 @@ impl Render for ContextMenu {
             v_stack()
                 .min_w(px(200.))
                 .track_focus(&self.focus_handle)
-                .on_mouse_down_out(
-                    cx.listener(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx)),
-                )
-                // .on_action(ContextMenu::select_first)
-                // .on_action(ContextMenu::select_last)
-                // .on_action(ContextMenu::select_next)
-                // .on_action(ContextMenu::select_prev)
+                .on_mouse_down_out(cx.listener(|this, _, cx| this.cancel(&Default::default(), cx)))
+                .key_context("menu")
+                .on_action(cx.listener(ContextMenu::select_first))
+                .on_action(cx.listener(ContextMenu::select_last))
+                .on_action(cx.listener(ContextMenu::select_next))
+                .on_action(cx.listener(ContextMenu::select_prev))
                 .on_action(cx.listener(ContextMenu::confirm))
                 .on_action(cx.listener(ContextMenu::cancel))
                 .flex_none()
-                // .bg(cx.theme().colors().elevated_surface_background)
-                // .border()
-                // .border_color(cx.theme().colors().border)
                 .child(
-                    List::new().children(self.items.iter().map(|item| match item {
-                        ContextMenuItem::Separator => ListSeparator::new().into_any_element(),
-                        ContextMenuItem::Header(header) => {
-                            ListSubHeader::new(header.clone()).into_any_element()
-                        }
-                        ContextMenuItem::Entry(entry, callback) => {
-                            let callback = callback.clone();
-                            let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent::Dismiss));
-
-                            ListItem::new(entry.clone())
-                                .child(Label::new(entry.clone()))
-                                .on_click(move |event, cx| {
-                                    callback(event, cx);
-                                    dismiss(event, cx)
-                                })
-                                .into_any_element()
-                        }
-                    })),
+                    List::new().children(self.items.iter().enumerate().map(
+                        |(ix, item)| match item {
+                            ContextMenuItem::Separator => ListSeparator::new().into_any_element(),
+                            ContextMenuItem::Header(header) => {
+                                ListSubHeader::new(header.clone()).into_any_element()
+                            }
+                            ContextMenuItem::Entry {
+                                label: entry,
+                                handler: callback,
+                                key_binding,
+                            } => {
+                                let callback = callback.clone();
+                                let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent));
+
+                                ListItem::new(entry.clone())
+                                    .child(
+                                        h_stack()
+                                            .w_full()
+                                            .justify_between()
+                                            .child(Label::new(entry.clone()))
+                                            .children(
+                                                key_binding
+                                                    .clone()
+                                                    .map(|binding| div().ml_1().child(binding)),
+                                            ),
+                                    )
+                                    .selected(Some(ix) == self.selected_index)
+                                    .on_click(move |event, cx| {
+                                        callback(cx);
+                                        dismiss(event, cx)
+                                    })
+                                    .into_any_element()
+                            }
+                        },
+                    )),
                 ),
         )
     }
@@ -177,6 +259,7 @@ pub struct MenuHandleState<M> {
     child_element: Option<AnyElement>,
     menu_element: Option<AnyElement>,
 }
+
 impl<M: ManagedView> Element for MenuHandle<M> {
     type State = MenuHandleState<M>;
 
@@ -264,11 +347,9 @@ impl<M: ManagedView> Element for MenuHandle<M> {
 
                 let new_menu = (builder)(cx);
                 let menu2 = menu.clone();
-                cx.subscribe(&new_menu, move |_modal, e, cx| match e {
-                    &DismissEvent::Dismiss => {
-                        *menu2.borrow_mut() = None;
-                        cx.notify();
-                    }
+                cx.subscribe(&new_menu, move |_modal, _: &DismissEvent, cx| {
+                    *menu2.borrow_mut() = None;
+                    cx.notify();
                 })
                 .detach();
                 cx.focus_view(&new_menu);

crates/ui2/src/components/list.rs 🔗

@@ -374,17 +374,15 @@ impl RenderOnce for List {
     type Rendered = Div;
 
     fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
-        let list_content = match (self.children.is_empty(), self.toggle) {
-            (false, _) => div().children(self.children),
-            (true, Toggle::Toggled(false)) => div(),
-            (true, _) => div().child(Label::new(self.empty_message.clone()).color(Color::Muted)),
-        };
-
         v_stack()
             .w_full()
             .py_1()
             .children(self.header.map(|header| header))
-            .child(list_content)
+            .map(|this| match (self.children.is_empty(), self.toggle) {
+                (false, _) => this.children(self.children),
+                (true, Toggle::Toggled(false)) => this,
+                (true, _) => this.child(Label::new(self.empty_message.clone()).color(Color::Muted)),
+            })
     }
 }
 

crates/ui2/src/components/stories/context_menu.rs 🔗

@@ -10,11 +10,11 @@ fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<C
     ContextMenu::build(cx, |menu, _| {
         menu.header(header)
             .separator()
-            .entry("Print current time", |_event, cx| {
+            .entry("Print current time", |cx| {
                 println!("dispatching PrintCurrentTime action");
                 cx.dispatch_action(PrintCurrentDate.boxed_clone())
             })
-            .entry("Print best foot", |_event, cx| {
+            .entry("Print best foot", |cx| {
                 cx.dispatch_action(PrintBestFood.boxed_clone())
             })
     })

crates/workspace2/src/dock.rs 🔗

@@ -721,7 +721,7 @@ impl Render for PanelButtons {
                                         && panel.position_is_valid(position, cx)
                                     {
                                         let panel = panel.clone();
-                                        menu = menu.entry(position.to_label(), move |_, cx| {
+                                        menu = menu.entry(position.to_label(), move |cx| {
                                             panel.set_position(position, cx);
                                         })
                                     }

crates/workspace2/src/notifications.rs 🔗

@@ -106,10 +106,8 @@ impl Workspace {
             let notification = build_notification(cx);
             cx.subscribe(
                 &notification,
-                move |this, handle, event: &DismissEvent, cx| match event {
-                    DismissEvent::Dismiss => {
-                        this.dismiss_notification_internal(type_id, id, cx);
-                    }
+                move |this, handle, event: &DismissEvent, cx| {
+                    this.dismiss_notification_internal(type_id, id, cx);
                 },
             )
             .detach();
@@ -260,7 +258,7 @@ pub mod simple_message_notification {
         }
 
         pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
-            cx.emit(DismissEvent::Dismiss);
+            cx.emit(DismissEvent);
         }
     }