WIP

Antonio Scandurra created

Change summary

Cargo.lock                                |   2 
crates/collab/src/main.rs                 |   1 
crates/context_menu/Cargo.toml            |   1 
crates/context_menu/src/context_menu.rs   | 115 +++++++++++++++++++++++-
crates/project_panel/Cargo.toml           |   1 
crates/project_panel/src/project_panel.rs |  55 +++++------
crates/theme/src/theme.rs                 |   3 
styles/src/styleTree/projectPanel.ts      |   4 
8 files changed, 140 insertions(+), 42 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -979,6 +979,7 @@ name = "context_menu"
 version = "0.1.0"
 dependencies = [
  "gpui",
+ "settings",
  "theme",
 ]
 
@@ -3457,6 +3458,7 @@ dependencies = [
 name = "project_panel"
 version = "0.1.0"
 dependencies = [
+ "context_menu",
  "editor",
  "futures",
  "gpui",

crates/collab/src/main.rs 🔗

@@ -149,6 +149,7 @@ pub fn init_tracing(config: &Config) -> Option<()> {
     use tracing_subscriber::layer::SubscriberExt;
     let rust_log = config.rust_log.clone()?;
 
+    println!("HEY!");
     LogTracer::init().log_err()?;
 
     let open_telemetry_layer = config

crates/context_menu/Cargo.toml 🔗

@@ -9,4 +9,5 @@ doctest = false
 
 [dependencies]
 gpui = { path = "../gpui" }
+settings = { path = "../settings" }
 theme = { path = "../theme" }

crates/context_menu/src/context_menu.rs 🔗

@@ -1,6 +1,10 @@
-use gpui::{Entity, View};
+use gpui::{
+    elements::*, geometry::vector::Vector2F, Action, Entity, RenderContext, View, ViewContext,
+};
+use settings::Settings;
+use std::{marker::PhantomData, sync::Arc};
 
-enum ContextMenuItem {
+pub enum ContextMenuItem {
     Item {
         label: String,
         action: Box<dyn Action>,
@@ -8,21 +12,116 @@ enum ContextMenuItem {
     Separator,
 }
 
-pub struct ContextMenu {
+pub struct ContextMenu<T> {
     position: Vector2F,
-    items: Vec<ContextMenuItem>,
+    items: Arc<[ContextMenuItem]>,
+    state: UniformListState,
+    selected_index: Option<usize>,
+    widest_item_index: Option<usize>,
+    visible: bool,
+    _phantom: PhantomData<T>,
 }
 
-impl Entity for ContextMenu {
+impl<T: 'static> Entity for ContextMenu<T> {
     type Event = ();
 }
 
-impl View for ContextMenu {
+impl<T: 'static> View for ContextMenu<T> {
     fn ui_name() -> &'static str {
         "ContextMenu"
     }
 
-    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
-        Overlay::new().with_abs_position(self.position).boxed()
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        if !self.visible {
+            return Empty::new().boxed();
+        }
+
+        let theme = &cx.global::<Settings>().theme;
+        let menu_style = &theme.project_panel.context_menu;
+        let separator_style = menu_style.separator;
+        let item_style = menu_style.item.clone();
+        let items = self.items.clone();
+        let selected_ix = self.selected_index;
+        Overlay::new(
+            UniformList::new(
+                self.state.clone(),
+                self.items.len(),
+                move |range, elements, cx| {
+                    let start = range.start;
+                    elements.extend(items[range].iter().enumerate().map(|(ix, item)| {
+                        let item_ix = start + ix;
+                        match item {
+                            ContextMenuItem::Item { label, action } => {
+                                let action = action.boxed_clone();
+                                MouseEventHandler::new::<T, _, _>(item_ix, cx, |state, _| {
+                                    let style =
+                                        item_style.style_for(state, Some(item_ix) == selected_ix);
+                                    Flex::row()
+                                        .with_child(
+                                            Label::new(label.to_string(), style.label.clone())
+                                                .boxed(),
+                                        )
+                                        .boxed()
+                                })
+                                .on_click(move |_, _, cx| {
+                                    cx.dispatch_any_action(action.boxed_clone())
+                                })
+                                .boxed()
+                            }
+                            ContextMenuItem::Separator => {
+                                Empty::new().contained().with_style(separator_style).boxed()
+                            }
+                        }
+                    }))
+                },
+            )
+            .with_width_from_item(self.widest_item_index)
+            .boxed(),
+        )
+        .with_abs_position(self.position)
+        .contained()
+        .with_style(menu_style.container)
+        .boxed()
+    }
+
+    fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
+        self.visible = false;
+        cx.notify();
+    }
+}
+
+impl<T: 'static> ContextMenu<T> {
+    pub fn new() -> Self {
+        Self {
+            position: Default::default(),
+            items: Arc::from([]),
+            state: Default::default(),
+            selected_index: Default::default(),
+            widest_item_index: Default::default(),
+            visible: false,
+            _phantom: PhantomData,
+        }
+    }
+
+    pub fn show(
+        &mut self,
+        position: Vector2F,
+        items: impl IntoIterator<Item = ContextMenuItem>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.items = items.into_iter().collect();
+        self.widest_item_index = self
+            .items
+            .iter()
+            .enumerate()
+            .max_by_key(|(_, item)| match item {
+                ContextMenuItem::Item { label, .. } => label.chars().count(),
+                ContextMenuItem::Separator => 0,
+            })
+            .map(|(ix, _)| ix);
+        self.position = position;
+        self.visible = true;
+        cx.focus_self();
+        cx.notify();
     }
 }

crates/project_panel/Cargo.toml 🔗

@@ -8,6 +8,7 @@ path = "src/project_panel.rs"
 doctest = false
 
 [dependencies]
+context_menu = { path = "../context_menu" }
 editor = { path = "../editor" }
 gpui = { path = "../gpui" }
 project = { path = "../project" }

crates/project_panel/src/project_panel.rs 🔗

@@ -1,17 +1,18 @@
+use context_menu::{ContextMenu, ContextMenuItem};
 use editor::{Cancel, Editor};
 use futures::stream::StreamExt;
 use gpui::{
     actions,
     anyhow::{anyhow, Result},
     elements::{
-        ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, Overlay, ParentElement,
+        ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement,
         ScrollTarget, Stack, Svg, UniformList, UniformListState,
     },
     geometry::vector::Vector2F,
     impl_internal_actions, keymap,
     platform::CursorStyle,
-    AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel,
-    RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel, Task,
+    View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
 use settings::Settings;
@@ -37,7 +38,7 @@ pub struct ProjectPanel {
     selection: Option<Selection>,
     edit_state: Option<EditState>,
     filename_editor: ViewHandle<Editor>,
-    context_menu: Option<ContextMenu>,
+    context_menu: ViewHandle<ContextMenu<Self>>,
     handle: WeakViewHandle<Self>,
 }
 
@@ -83,11 +84,6 @@ pub struct DeployContextMenu {
     pub entry_id: Option<ProjectEntryId>,
 }
 
-pub struct ContextMenu {
-    pub position: Vector2F,
-    pub entry_id: Option<ProjectEntryId>,
-}
-
 actions!(
     project_panel,
     [
@@ -170,7 +166,7 @@ impl ProjectPanel {
                 selection: None,
                 edit_state: None,
                 filename_editor,
-                context_menu: None,
+                context_menu: cx.add_view(|_| ContextMenu::new()),
                 handle: cx.weak_handle(),
             };
             this.update_visible_entries(None, cx);
@@ -211,9 +207,22 @@ impl ProjectPanel {
     }
 
     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,
+        self.context_menu.update(cx, |menu, cx| {
+            menu.show(
+                action.position,
+                [
+                    ContextMenuItem::Item {
+                        label: "New File".to_string(),
+                        action: Box::new(AddFile),
+                    },
+                    ContextMenuItem::Item {
+                        label: "New Directory".to_string(),
+                        action: Box::new(AddDirectory),
+                    },
+                    ContextMenuItem::Separator,
+                ],
+                cx,
+            );
         });
         cx.notify();
     }
@@ -883,24 +892,6 @@ impl ProjectPanel {
         .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.item.label.clone()).boxed(),
-                    )
-                    .contained()
-                    .with_style(style.container)
-                    .boxed(),
-            )
-            .with_abs_position(menu.position)
-            .named("Project Panel Context Menu")
-        })
-    }
 }
 
 impl View for ProjectPanel {
@@ -943,7 +934,7 @@ impl View for ProjectPanel {
                 .with_style(container_style)
                 .boxed(),
             )
-            .with_children(self.render_context_menu(cx))
+            .with_child(ChildView::new(&self.context_menu).boxed())
             .boxed()
     }
 

crates/theme/src/theme.rs 🔗

@@ -244,7 +244,8 @@ pub struct ProjectPanelEntry {
 pub struct ContextMenu {
     #[serde(flatten)]
     pub container: ContainerStyle,
-    pub item: ContextMenuItem,
+    pub item: Interactive<ContextMenuItem>,
+    pub separator: ContainerStyle,
 }
 
 #[derive(Clone, Debug, Deserialize, Default)]

styles/src/styleTree/projectPanel.ts 🔗

@@ -43,7 +43,9 @@ export default function projectPanel(theme: Theme) {
         right: 6,
         top: 2,
       },
-      label: text(theme, "sans", "secondary", { size: "sm" }),
+      item: {
+        label: text(theme, "sans", "secondary", { size: "sm" }),
+      },
       shadow: shadow(theme),
     }
   };