Render a context menu when right-clicking in project panel

Nathan Sobo created

It doesn't currently do anything, but I managed to get it rendering in an absolutely positioned way.

Change summary

crates/auto_update/src/auto_update.rs           |   2 
crates/chat_panel/src/chat_panel.rs             |   2 
crates/contacts_panel/src/contacts_panel.rs     |  14 +-
crates/contacts_panel/src/notifications.rs      |  15 -
crates/diagnostics/src/items.rs                 |   4 
crates/editor/src/editor.rs                     |   6 
crates/gpui/src/elements/list.rs                |  24 ++-
crates/gpui/src/elements/mouse_event_handler.rs |  71 ++++++++++
crates/gpui/src/elements/overlay.rs             |  30 ++++
crates/gpui/src/platform/event.rs               |   3 
crates/gpui/src/platform/mac/event.rs           |   1 
crates/gpui/src/presenter.rs                    |   9 
crates/gpui/src/views/select.rs                 |   6 
crates/picker/src/picker.rs                     |   2 
crates/project_panel/src/project_panel.rs       | 118 +++++++++++++-----
crates/search/src/buffer_search.rs              |   4 
crates/search/src/project_search.rs             |   4 
crates/theme/src/theme.rs                       |   9 +
crates/workspace/src/lsp_status.rs              |   3 
crates/workspace/src/pane.rs                    |   2 
crates/workspace/src/sidebar.rs                 |   2 
crates/workspace/src/workspace.rs               |   4 
styles/src/styleTree/projectPanel.ts            |  16 ++
23 files changed, 260 insertions(+), 91 deletions(-)

Detailed changes

crates/auto_update/src/auto_update.rs 🔗

@@ -270,7 +270,7 @@ impl View for AutoUpdateIndicator {
                         )
                         .boxed()
                     })
-                    .on_click(|_, cx| cx.dispatch_action(DismissErrorMessage))
+                    .on_click(|_, _, cx| cx.dispatch_action(DismissErrorMessage))
                     .boxed()
                 }
                 AutoUpdateStatus::Idle => Empty::new().boxed(),

crates/chat_panel/src/chat_panel.rs 🔗

@@ -320,7 +320,7 @@ impl ChatPanel {
                 .boxed()
             })
             .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(move |_, cx| {
+            .on_click(move |_, _, cx| {
                 let rpc = rpc.clone();
                 let this = this.clone();
                 cx.spawn(|mut cx| async move {

crates/contacts_panel/src/contacts_panel.rs 🔗

@@ -302,7 +302,7 @@ impl ContactsPanel {
                 .boxed()
         })
         .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(move |_, cx| cx.dispatch_action(ToggleExpanded(section)))
+        .on_click(move |_, _, cx| cx.dispatch_action(ToggleExpanded(section)))
         .boxed()
     }
 
@@ -445,7 +445,7 @@ impl ContactsPanel {
         } else {
             CursorStyle::Arrow
         })
-        .on_click(move |_, cx| {
+        .on_click(move |_, _, cx| {
             if !is_host {
                 cx.dispatch_global_action(JoinProject {
                     contact: contact.clone(),
@@ -507,7 +507,7 @@ impl ContactsPanel {
                         .boxed()
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(move |_, cx| {
+                .on_click(move |_, _, cx| {
                     cx.dispatch_action(RespondToContactRequest {
                         user_id,
                         accept: false,
@@ -529,7 +529,7 @@ impl ContactsPanel {
                         .boxed()
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(move |_, cx| {
+                .on_click(move |_, _, cx| {
                     cx.dispatch_action(RespondToContactRequest {
                         user_id,
                         accept: true,
@@ -552,7 +552,7 @@ impl ContactsPanel {
                 })
                 .with_padding(Padding::uniform(2.))
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(move |_, cx| cx.dispatch_action(RemoveContact(user_id)))
+                .on_click(move |_, _, cx| cx.dispatch_action(RemoveContact(user_id)))
                 .flex_float()
                 .boxed(),
             );
@@ -865,7 +865,7 @@ impl View for ContactsPanel {
                                     .boxed()
                             })
                             .with_cursor_style(CursorStyle::PointingHand)
-                            .on_click(|_, cx| cx.dispatch_action(contact_finder::Toggle))
+                            .on_click(|_, _, cx| cx.dispatch_action(contact_finder::Toggle))
                             .boxed(),
                         )
                         .constrained()
@@ -913,7 +913,7 @@ impl View for ContactsPanel {
                                         },
                                     )
                                     .with_cursor_style(CursorStyle::PointingHand)
-                                    .on_click(move |_, cx| {
+                                    .on_click(move |_, _, cx| {
                                         cx.write_to_clipboard(ClipboardItem::new(
                                             info.url.to_string(),
                                         ));

crates/contacts_panel/src/notifications.rs 🔗

@@ -61,7 +61,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
                     })
                     .with_cursor_style(CursorStyle::PointingHand)
                     .with_padding(Padding::uniform(5.))
-                    .on_click(move |_, cx| cx.dispatch_any_action(dismiss_action.boxed_clone()))
+                    .on_click(move |_, _, cx| cx.dispatch_any_action(dismiss_action.boxed_clone()))
                     .aligned()
                     .constrained()
                     .with_height(
@@ -76,13 +76,10 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
                 .named("contact notification header"),
         )
         .with_children(body.map(|body| {
-            Label::new(
-                body.to_string(),
-                theme.body_message.text.clone(),
-            )
-            .contained()
-            .with_style(theme.body_message.container)
-            .boxed()
+            Label::new(body.to_string(), theme.body_message.text.clone())
+                .contained()
+                .with_style(theme.body_message.container)
+                .boxed()
         }))
         .with_children(if buttons.is_empty() {
             None
@@ -99,7 +96,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
                                     .boxed()
                             })
                             .with_cursor_style(CursorStyle::PointingHand)
-                            .on_click(move |_, cx| cx.dispatch_any_action(action.boxed_clone()))
+                            .on_click(move |_, _, cx| cx.dispatch_any_action(action.boxed_clone()))
                             .boxed()
                         },
                     ))

crates/diagnostics/src/items.rs 🔗

@@ -159,7 +159,7 @@ impl View for DiagnosticIndicator {
                     .boxed()
             })
             .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(|_, cx| cx.dispatch_action(crate::Deploy))
+            .on_click(|_, _, cx| cx.dispatch_action(crate::Deploy))
             .aligned()
             .boxed(),
         );
@@ -192,7 +192,7 @@ impl View for DiagnosticIndicator {
                     .boxed()
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(|_, cx| cx.dispatch_action(GoToNextDiagnostic))
+                .on_click(|_, _, cx| cx.dispatch_action(GoToNextDiagnostic))
                 .boxed(),
             );
         }

crates/editor/src/editor.rs 🔗

@@ -672,7 +672,7 @@ impl CompletionsMenu {
                         },
                     )
                     .with_cursor_style(CursorStyle::PointingHand)
-                    .on_mouse_down(move |cx| {
+                    .on_mouse_down(move |_, cx| {
                         cx.dispatch_action(ConfirmCompletion {
                             item_ix: Some(item_ix),
                         });
@@ -800,7 +800,7 @@ impl CodeActionsMenu {
                                 .boxed()
                         })
                         .with_cursor_style(CursorStyle::PointingHand)
-                        .on_mouse_down(move |cx| {
+                        .on_mouse_down(move |_, cx| {
                             cx.dispatch_action(ConfirmCodeAction {
                                 item_ix: Some(item_ix),
                             });
@@ -2590,7 +2590,7 @@ impl Editor {
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
                 .with_padding(Padding::uniform(3.))
-                .on_mouse_down(|cx| {
+                .on_mouse_down(|_, cx| {
                     cx.dispatch_action(ToggleCodeActions {
                         deployed_from_indicator: true,
                     });

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

@@ -612,7 +612,10 @@ mod tests {
         });
 
         let mut list = List::new(state.clone());
-        let (size, _) = list.layout(constraint, &mut presenter.build_layout_context(false, cx));
+        let (size, _) = list.layout(
+            constraint,
+            &mut presenter.build_layout_context(vec2f(100., 40.), false, cx),
+        );
         assert_eq!(size, vec2f(100., 40.));
         assert_eq!(
             state.0.borrow().items.summary().clone(),
@@ -634,8 +637,10 @@ mod tests {
             true,
             &mut presenter.build_event_context(cx),
         );
-        let (_, logical_scroll_top) =
-            list.layout(constraint, &mut presenter.build_layout_context(false, cx));
+        let (_, logical_scroll_top) = list.layout(
+            constraint,
+            &mut presenter.build_layout_context(vec2f(100., 40.), false, cx),
+        );
         assert_eq!(
             logical_scroll_top,
             ListOffset {
@@ -659,8 +664,10 @@ mod tests {
             }
         );
 
-        let (size, logical_scroll_top) =
-            list.layout(constraint, &mut presenter.build_layout_context(false, cx));
+        let (size, logical_scroll_top) = list.layout(
+            constraint,
+            &mut presenter.build_layout_context(vec2f(100., 40.), false, cx),
+        );
         assert_eq!(size, vec2f(100., 40.));
         assert_eq!(
             state.0.borrow().items.summary().clone(),
@@ -770,11 +777,12 @@ mod tests {
             }
 
             let mut list = List::new(state.clone());
+            let window_size = vec2f(width, height);
             let (size, logical_scroll_top) = list.layout(
-                SizeConstraint::new(vec2f(0., 0.), vec2f(width, height)),
-                &mut presenter.build_layout_context(false, cx),
+                SizeConstraint::new(vec2f(0., 0.), window_size),
+                &mut presenter.build_layout_context(window_size, false, cx),
             );
-            assert_eq!(size, vec2f(width, height));
+            assert_eq!(size, window_size);
             last_logical_scroll_top = Some(logical_scroll_top);
 
             let state = state.0.borrow();

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

@@ -14,9 +14,11 @@ pub struct MouseEventHandler {
     state: ElementStateHandle<MouseState>,
     child: ElementBox,
     cursor_style: Option<CursorStyle>,
-    mouse_down_handler: Option<Box<dyn FnMut(&mut EventContext)>>,
-    click_handler: Option<Box<dyn FnMut(usize, &mut EventContext)>>,
+    mouse_down_handler: Option<Box<dyn FnMut(Vector2F, &mut EventContext)>>,
+    click_handler: Option<Box<dyn FnMut(Vector2F, usize, &mut EventContext)>>,
     drag_handler: Option<Box<dyn FnMut(Vector2F, &mut EventContext)>>,
+    right_mouse_down_handler: Option<Box<dyn FnMut(Vector2F, &mut EventContext)>>,
+    right_click_handler: Option<Box<dyn FnMut(Vector2F, usize, &mut EventContext)>>,
     padding: Padding,
 }
 
@@ -24,6 +26,7 @@ pub struct MouseEventHandler {
 pub struct MouseState {
     pub hovered: bool,
     pub clicked: bool,
+    pub right_clicked: bool,
     prev_drag_position: Option<Vector2F>,
 }
 
@@ -43,6 +46,8 @@ impl MouseEventHandler {
             mouse_down_handler: None,
             click_handler: None,
             drag_handler: None,
+            right_mouse_down_handler: None,
+            right_click_handler: None,
             padding: Default::default(),
         }
     }
@@ -52,12 +57,18 @@ impl MouseEventHandler {
         self
     }
 
-    pub fn on_mouse_down(mut self, handler: impl FnMut(&mut EventContext) + 'static) -> Self {
+    pub fn on_mouse_down(
+        mut self,
+        handler: impl FnMut(Vector2F, &mut EventContext) + 'static,
+    ) -> Self {
         self.mouse_down_handler = Some(Box::new(handler));
         self
     }
 
-    pub fn on_click(mut self, handler: impl FnMut(usize, &mut EventContext) + 'static) -> Self {
+    pub fn on_click(
+        mut self,
+        handler: impl FnMut(Vector2F, usize, &mut EventContext) + 'static,
+    ) -> Self {
         self.click_handler = Some(Box::new(handler));
         self
     }
@@ -67,6 +78,22 @@ impl MouseEventHandler {
         self
     }
 
+    pub fn on_right_mouse_down(
+        mut self,
+        handler: impl FnMut(Vector2F, &mut EventContext) + 'static,
+    ) -> Self {
+        self.right_mouse_down_handler = Some(Box::new(handler));
+        self
+    }
+
+    pub fn on_right_click(
+        mut self,
+        handler: impl FnMut(Vector2F, usize, &mut EventContext) + 'static,
+    ) -> Self {
+        self.right_click_handler = Some(Box::new(handler));
+        self
+    }
+
     pub fn with_padding(mut self, padding: Padding) -> Self {
         self.padding = padding;
         self
@@ -120,6 +147,8 @@ impl Element for MouseEventHandler {
         let mouse_down_handler = self.mouse_down_handler.as_mut();
         let click_handler = self.click_handler.as_mut();
         let drag_handler = self.drag_handler.as_mut();
+        let right_mouse_down_handler = self.right_mouse_down_handler.as_mut();
+        let right_click_handler = self.right_click_handler.as_mut();
 
         let handled_in_child = self.child.dispatch_event(event, cx);
 
@@ -144,7 +173,7 @@ impl Element for MouseEventHandler {
                     state.prev_drag_position = Some(*position);
                     cx.notify();
                     if let Some(handler) = mouse_down_handler {
-                        handler(cx);
+                        handler(*position, cx);
                     }
                     true
                 } else {
@@ -162,7 +191,7 @@ impl Element for MouseEventHandler {
                     cx.notify();
                     if let Some(handler) = click_handler {
                         if hit_bounds.contains_point(*position) {
-                            handler(*click_count, cx);
+                            handler(*position, *click_count, cx);
                         }
                     }
                     true
@@ -184,6 +213,36 @@ impl Element for MouseEventHandler {
                     handled_in_child
                 }
             }
+            Event::RightMouseDown { position, .. } => {
+                if !handled_in_child && hit_bounds.contains_point(*position) {
+                    state.right_clicked = true;
+                    cx.notify();
+                    if let Some(handler) = right_mouse_down_handler {
+                        handler(*position, cx);
+                    }
+                    true
+                } else {
+                    handled_in_child
+                }
+            }
+            Event::RightMouseUp {
+                position,
+                click_count,
+                ..
+            } => {
+                if !handled_in_child && state.right_clicked {
+                    state.right_clicked = false;
+                    cx.notify();
+                    if let Some(handler) = right_click_handler {
+                        if hit_bounds.contains_point(*position) {
+                            handler(*position, *click_count, cx);
+                        }
+                    }
+                    true
+                } else {
+                    handled_in_child
+                }
+            }
             _ => handled_in_child,
         })
     }

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

@@ -1,16 +1,28 @@
+use serde_json::json;
+
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
+    json::ToJson,
     DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
     SizeConstraint,
 };
 
 pub struct Overlay {
     child: ElementBox,
+    abs_position: Option<Vector2F>,
 }
 
 impl Overlay {
     pub fn new(child: ElementBox) -> Self {
-        Self { child }
+        Self {
+            child,
+            abs_position: None,
+        }
+    }
+
+    pub fn with_abs_position(mut self, position: Vector2F) -> Self {
+        self.abs_position = Some(position);
+        self
     }
 }
 
@@ -23,6 +35,11 @@ impl Element for Overlay {
         constraint: SizeConstraint,
         cx: &mut LayoutContext,
     ) -> (Vector2F, Self::LayoutState) {
+        let constraint = if self.abs_position.is_some() {
+            SizeConstraint::new(Vector2F::zero(), cx.window_size)
+        } else {
+            constraint
+        };
         let size = self.child.layout(constraint, cx);
         (Vector2F::zero(), size)
     }
@@ -34,9 +51,10 @@ impl Element for Overlay {
         size: &mut Self::LayoutState,
         cx: &mut PaintContext,
     ) {
-        let bounds = RectF::new(bounds.origin(), *size);
+        let origin = self.abs_position.unwrap_or(bounds.origin());
+        let visible_bounds = RectF::new(origin, *size);
         cx.scene.push_stacking_context(None);
-        self.child.paint(bounds.origin(), bounds, cx);
+        self.child.paint(origin, visible_bounds, cx);
         cx.scene.pop_stacking_context();
     }
 
@@ -59,6 +77,10 @@ impl Element for Overlay {
         _: &Self::PaintState,
         cx: &DebugContext,
     ) -> serde_json::Value {
-        self.child.debug(cx)
+        json!({
+            "type": "Overlay",
+            "abs_position": self.abs_position.to_json(),
+            "child": self.child.debug(cx),
+        })
     }
 }

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

@@ -43,6 +43,7 @@ pub enum Event {
     },
     RightMouseUp {
         position: Vector2F,
+        click_count: usize,
     },
     NavigateMouseDown {
         position: Vector2F,
@@ -72,7 +73,7 @@ impl Event {
             | Event::LeftMouseUp { position, .. }
             | Event::LeftMouseDragged { position }
             | Event::RightMouseDown { position, .. }
-            | Event::RightMouseUp { position }
+            | Event::RightMouseUp { position, .. }
             | Event::NavigateMouseDown { position, .. }
             | Event::NavigateMouseUp { position, .. }
             | Event::MouseMoved { position, .. } => Some(*position),

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

@@ -178,6 +178,7 @@ impl Event {
                     native_event.locationInWindow().x as f32,
                     window_height - native_event.locationInWindow().y as f32,
                 ),
+                click_count: native_event.clickCount() as usize,
             }),
             NSEventType::NSOtherMouseDown => {
                 let direction = match native_event.buttonNumber() {

crates/gpui/src/presenter.rs 🔗

@@ -134,15 +134,16 @@ impl Presenter {
         scene
     }
 
-    fn layout(&mut self, size: Vector2F, refreshing: bool, cx: &mut MutableAppContext) {
+    fn layout(&mut self, window_size: Vector2F, refreshing: bool, cx: &mut MutableAppContext) {
         if let Some(root_view_id) = cx.root_view_id(self.window_id) {
-            self.build_layout_context(refreshing, cx)
-                .layout(root_view_id, SizeConstraint::strict(size));
+            self.build_layout_context(window_size, refreshing, cx)
+                .layout(root_view_id, SizeConstraint::strict(window_size));
         }
     }
 
     pub fn build_layout_context<'a>(
         &'a mut self,
+        window_size: Vector2F,
         refreshing: bool,
         cx: &'a mut MutableAppContext,
     ) -> LayoutContext<'a> {
@@ -150,6 +151,7 @@ impl Presenter {
             rendered_views: &mut self.rendered_views,
             parents: &mut self.parents,
             refreshing,
+            window_size,
             font_cache: &self.font_cache,
             font_system: cx.platform().fonts(),
             text_layout_cache: &self.text_layout_cache,
@@ -259,6 +261,7 @@ pub struct LayoutContext<'a> {
     parents: &'a mut HashMap<usize, usize>,
     view_stack: Vec<usize>,
     pub refreshing: bool,
+    pub window_size: Vector2F,
     pub font_cache: &'a Arc<FontCache>,
     pub font_system: Arc<dyn FontSystem>,
     pub text_layout_cache: &'a TextLayoutCache,

crates/gpui/src/views/select.rs 🔗

@@ -119,7 +119,7 @@ impl View for Select {
                 .with_style(style.header)
                 .boxed()
             })
-            .on_click(move |_, cx| cx.dispatch_action(ToggleSelect))
+            .on_click(move |_, _, cx| cx.dispatch_action(ToggleSelect))
             .boxed(),
         );
         if self.is_open {
@@ -153,7 +153,9 @@ impl View for Select {
                                                 )
                                             },
                                         )
-                                        .on_click(move |_, cx| cx.dispatch_action(SelectItem(ix)))
+                                        .on_click(move |_, _, cx| {
+                                            cx.dispatch_action(SelectItem(ix))
+                                        })
                                         .boxed()
                                     }))
                                 },

crates/picker/src/picker.rs 🔗

@@ -90,7 +90,7 @@ impl<D: PickerDelegate> View for Picker<D> {
                                         .read(cx)
                                         .render_match(ix, state, ix == selected_ix, cx)
                                 })
-                                .on_mouse_down(move |cx| cx.dispatch_action(SelectIndex(ix)))
+                                .on_mouse_down(move |_, cx| cx.dispatch_action(SelectIndex(ix)))
                                 .with_cursor_style(CursorStyle::PointingHand)
                                 .boxed()
                             }));

crates/project_panel/src/project_panel.rs 🔗

@@ -4,13 +4,14 @@ use gpui::{
     actions,
     anyhow::{anyhow, Result},
     elements::{
-        ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement,
-        ScrollTarget, Svg, UniformList, UniformListState,
+        ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, Overlay, ParentElement,
+        ScrollTarget, Stack, Svg, UniformList, UniformListState,
     },
+    geometry::vector::Vector2F,
     impl_internal_actions, keymap,
     platform::CursorStyle,
-    AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel, Task,
-    View, ViewContext, ViewHandle, WeakViewHandle,
+    AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel,
+    RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
 use settings::Settings;
@@ -36,6 +37,7 @@ pub struct ProjectPanel {
     selection: Option<Selection>,
     edit_state: Option<EditState>,
     filename_editor: ViewHandle<Editor>,
+    context_menu: Option<ContextMenu>,
     handle: WeakViewHandle<Self>,
 }
 
@@ -75,6 +77,17 @@ pub struct Open {
     pub change_focus: bool,
 }
 
+#[derive(Clone)]
+pub struct DeployContextMenu {
+    pub position: Vector2F,
+    pub entry_id: Option<ProjectEntryId>,
+}
+
+pub struct ContextMenu {
+    pub position: Vector2F,
+    pub entry_id: Option<ProjectEntryId>,
+}
+
 actions!(
     project_panel,
     [
@@ -86,9 +99,10 @@ actions!(
         Rename
     ]
 );
-impl_internal_actions!(project_panel, [Open, ToggleExpanded]);
+impl_internal_actions!(project_panel, [Open, ToggleExpanded, DeployContextMenu]);
 
 pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(ProjectPanel::deploy_context_menu);
     cx.add_action(ProjectPanel::expand_selected_entry);
     cx.add_action(ProjectPanel::collapse_selected_entry);
     cx.add_action(ProjectPanel::toggle_expanded);
@@ -156,6 +170,7 @@ impl ProjectPanel {
                 selection: None,
                 edit_state: None,
                 filename_editor,
+                context_menu: None,
                 handle: cx.weak_handle(),
             };
             this.update_visible_entries(None, cx);
@@ -195,6 +210,14 @@ impl ProjectPanel {
         project_panel
     }
 
+    fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
+        self.context_menu = Some(ContextMenu {
+            position: action.position,
+            entry_id: action.entry_id,
+        });
+        cx.notify();
+    }
+
     fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
         if let Some((worktree, entry)) = self.selected_entry(cx) {
             let expanded_dir_ids =
@@ -841,7 +864,7 @@ impl ProjectPanel {
                 .with_padding_left(padding)
                 .boxed()
         })
-        .on_click(move |click_count, cx| {
+        .on_click(move |_, click_count, cx| {
             if kind == EntryKind::Dir {
                 cx.dispatch_action(ToggleExpanded(entry_id))
             } else {
@@ -851,9 +874,33 @@ impl ProjectPanel {
                 })
             }
         })
+        .on_right_mouse_down(move |position, cx| {
+            cx.dispatch_action(DeployContextMenu {
+                entry_id: Some(entry_id),
+                position,
+            })
+        })
         .with_cursor_style(CursorStyle::PointingHand)
         .boxed()
     }
+
+    fn render_context_menu(&self, cx: &mut RenderContext<Self>) -> Option<ElementBox> {
+        self.context_menu.as_ref().map(|menu| {
+            let style = &cx.global::<Settings>().theme.project_panel.context_menu;
+
+            Overlay::new(
+                Flex::column()
+                    .with_child(Label::new("Add File".to_string(), style.label.clone()).boxed())
+                    .contained()
+                    .with_style(style.container)
+                    // .constrained()
+                    // .with_width(style.width)
+                    .boxed(),
+            )
+            .with_abs_position(menu.position)
+            .named("Project Panel Context Menu")
+        })
+    }
 }
 
 impl View for ProjectPanel {
@@ -866,33 +913,38 @@ impl View for ProjectPanel {
         let mut container_style = theme.container;
         let padding = std::mem::take(&mut container_style.padding);
         let handle = self.handle.clone();
-        UniformList::new(
-            self.list.clone(),
-            self.visible_entries
-                .iter()
-                .map(|(_, worktree_entries)| worktree_entries.len())
-                .sum(),
-            move |range, items, cx| {
-                let theme = cx.global::<Settings>().theme.clone();
-                let this = handle.upgrade(cx).unwrap();
-                this.update(cx.app, |this, cx| {
-                    this.for_each_visible_entry(range.clone(), cx, |id, details, cx| {
-                        items.push(Self::render_entry(
-                            id,
-                            details,
-                            &this.filename_editor,
-                            &theme.project_panel,
-                            cx,
-                        ));
-                    });
-                })
-            },
-        )
-        .with_padding_top(padding.top)
-        .with_padding_bottom(padding.bottom)
-        .contained()
-        .with_style(container_style)
-        .boxed()
+        Stack::new()
+            .with_child(
+                UniformList::new(
+                    self.list.clone(),
+                    self.visible_entries
+                        .iter()
+                        .map(|(_, worktree_entries)| worktree_entries.len())
+                        .sum(),
+                    move |range, items, cx| {
+                        let theme = cx.global::<Settings>().theme.clone();
+                        let this = handle.upgrade(cx).unwrap();
+                        this.update(cx.app, |this, cx| {
+                            this.for_each_visible_entry(range.clone(), cx, |id, details, cx| {
+                                items.push(Self::render_entry(
+                                    id,
+                                    details,
+                                    &this.filename_editor,
+                                    &theme.project_panel,
+                                    cx,
+                                ));
+                            });
+                        })
+                    },
+                )
+                .with_padding_top(padding.top)
+                .with_padding_bottom(padding.bottom)
+                .contained()
+                .with_style(container_style)
+                .boxed(),
+            )
+            .with_children(self.render_context_menu(cx))
+            .boxed()
     }
 
     fn keymap_context(&self, _: &AppContext) -> keymap::Context {

crates/search/src/buffer_search.rs 🔗

@@ -290,7 +290,7 @@ impl BufferSearchBar {
                 .with_style(style.container)
                 .boxed()
         })
-        .on_click(move |_, cx| cx.dispatch_action(ToggleSearchOption(search_option)))
+        .on_click(move |_, _, cx| cx.dispatch_action(ToggleSearchOption(search_option)))
         .with_cursor_style(CursorStyle::PointingHand)
         .boxed()
     }
@@ -314,7 +314,7 @@ impl BufferSearchBar {
                 .with_style(style.container)
                 .boxed()
         })
-        .on_click(move |_, cx| match direction {
+        .on_click(move |_, _, cx| match direction {
             Direction::Prev => cx.dispatch_action(SelectPrevMatch),
             Direction::Next => cx.dispatch_action(SelectNextMatch),
         })

crates/search/src/project_search.rs 🔗

@@ -672,7 +672,7 @@ impl ProjectSearchBar {
                 .with_style(style.container)
                 .boxed()
         })
-        .on_click(move |_, cx| match direction {
+        .on_click(move |_, _, cx| match direction {
             Direction::Prev => cx.dispatch_action(SelectPrevMatch),
             Direction::Next => cx.dispatch_action(SelectNextMatch),
         })
@@ -699,7 +699,7 @@ impl ProjectSearchBar {
                 .with_style(style.container)
                 .boxed()
         })
-        .on_click(move |_, cx| cx.dispatch_action(ToggleSearchOption(option)))
+        .on_click(move |_, _, cx| cx.dispatch_action(ToggleSearchOption(option)))
         .with_cursor_style(CursorStyle::PointingHand)
         .boxed()
     }

crates/theme/src/theme.rs 🔗

@@ -226,6 +226,7 @@ pub struct ProjectPanel {
     pub ignored_entry_fade: f32,
     pub filename_editor: FieldEditor,
     pub indent_width: f32,
+    pub context_menu: ContextMenu,
 }
 
 #[derive(Clone, Debug, Deserialize, Default)]
@@ -239,6 +240,14 @@ pub struct ProjectPanelEntry {
     pub icon_spacing: f32,
 }
 
+#[derive(Clone, Debug, Deserialize, Default)]
+pub struct ContextMenu {
+    pub width: f32,
+    #[serde(flatten)]
+    pub container: ContainerStyle,
+    pub label: TextStyle,
+}
+
 #[derive(Debug, Deserialize, Default)]
 pub struct CommandPalette {
     pub key: Interactive<ContainedLabel>,

crates/workspace/src/lsp_status.rs 🔗

@@ -168,7 +168,8 @@ impl View for LspStatus {
                     self.failed.join(", "),
                     if self.failed.len() > 1 { "s" } else { "" }
                 );
-                handler = Some(|_, cx: &mut EventContext| cx.dispatch_action(DismissErrorMessage));
+                handler =
+                    Some(|_, _, cx: &mut EventContext| cx.dispatch_action(DismissErrorMessage));
             } else {
                 return Empty::new().boxed();
             }

crates/workspace/src/pane.rs 🔗

@@ -788,7 +788,7 @@ impl Pane {
                                             .with_cursor_style(CursorStyle::PointingHand)
                                             .on_click({
                                                 let pane = pane.clone();
-                                                move |_, cx| {
+                                                move |_, _, cx| {
                                                     cx.dispatch_action(CloseItem {
                                                         item_id,
                                                         pane: pane.clone(),

crates/workspace/src/sidebar.rs 🔗

@@ -293,7 +293,7 @@ impl View for SidebarButtons {
                                 .boxed()
                         })
                         .with_cursor_style(CursorStyle::PointingHand)
-                        .on_click(move |_, cx| {
+                        .on_click(move |_, _, cx| {
                             cx.dispatch_action(ToggleSidebarItem {
                                 side,
                                 item_index: ix,

crates/workspace/src/workspace.rs 🔗

@@ -1730,7 +1730,7 @@ impl Workspace {
                         .with_style(style.container)
                         .boxed()
                 })
-                .on_click(|_, cx| cx.dispatch_action(Authenticate))
+                .on_click(|_, _, cx| cx.dispatch_action(Authenticate))
                 .with_cursor_style(CursorStyle::PointingHand)
                 .aligned()
                 .boxed(),
@@ -1781,7 +1781,7 @@ impl Workspace {
         if let Some(peer_id) = peer_id {
             MouseEventHandler::new::<ToggleFollow, _, _>(replica_id.into(), cx, move |_, _| content)
                 .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(move |_, cx| cx.dispatch_action(ToggleFollow(peer_id)))
+                .on_click(move |_, _, cx| cx.dispatch_action(ToggleFollow(peer_id)))
                 .boxed()
         } else {
             content

styles/src/styleTree/projectPanel.ts 🔗

@@ -1,6 +1,6 @@
 import Theme from "../themes/common/theme";
 import { panel } from "./app";
-import { backgroundColor, iconColor, player, text } from "./components";
+import { backgroundColor, iconColor, player, shadow, text } from "./components";
 
 export default function projectPanel(theme: Theme) {
   return {
@@ -32,5 +32,19 @@ export default function projectPanel(theme: Theme) {
       text: text(theme, "mono", "primary", { size: "sm" }),
       selection: player(theme, 1).selection,
     },
+    contextMenu: {
+      width: 100,
+      // background: "#ff0000",
+      background: backgroundColor(theme, 300, "base"),
+      cornerRadius: 6,
+      padding: {
+        bottom: 2,
+        left: 6,
+        right: 6,
+        top: 2,
+      },
+      label: text(theme, "sans", "secondary", { size: "sm" }),
+      shadow: shadow(theme),
+    }
   };
 }