Add `ContextMenu` component

Marshall Bowers created

Change summary

crates/storybook2/src/stories/components.rs              |  1 
crates/storybook2/src/stories/components/context_menu.rs | 30 ++++
crates/storybook2/src/story_selector.rs                  |  2 
crates/ui2/src/components.rs                             |  2 
crates/ui2/src/components/context_menu.rs                | 67 ++++++++++
5 files changed, 102 insertions(+)

Detailed changes

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

@@ -0,0 +1,30 @@
+use std::marker::PhantomData;
+
+use ui::prelude::*;
+use ui::{ContextMenu, ContextMenuItem, Label};
+
+use crate::story::Story;
+
+#[derive(Element)]
+pub struct ContextMenuStory<S: 'static + Send + Sync + Clone> {
+    state_type: PhantomData<S>,
+}
+
+impl<S: 'static + Send + Sync + Clone> ContextMenuStory<S> {
+    pub fn new() -> Self {
+        Self {
+            state_type: PhantomData,
+        }
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        Story::container(cx)
+            .child(Story::title_for::<_, ContextMenu<S>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(ContextMenu::new([
+                ContextMenuItem::header("Section header"),
+                ContextMenuItem::Separator,
+                ContextMenuItem::entry(Label::new("Some entry")),
+            ]))
+    }
+}

crates/storybook2/src/story_selector.rs 🔗

@@ -42,6 +42,7 @@ pub enum ComponentStory {
     ChatPanel,
     CollabPanel,
     CommandPalette,
+    ContextMenu,
     Facepile,
     Keybinding,
     Palette,
@@ -71,6 +72,7 @@ impl ComponentStory {
             Self::CommandPalette => {
                 components::command_palette::CommandPaletteStory::new().into_any()
             }
+            Self::ContextMenu => components::context_menu::ContextMenuStory::new().into_any(),
             Self::Facepile => components::facepile::FacepileStory::new().into_any(),
             Self::Keybinding => components::keybinding::KeybindingStory::new().into_any(),
             Self::Palette => components::palette::PaletteStory::new().into_any(),

crates/ui2/src/components.rs 🔗

@@ -4,6 +4,7 @@ mod buffer;
 mod chat_panel;
 mod collab_panel;
 mod command_palette;
+mod context_menu;
 mod editor_pane;
 mod facepile;
 mod icon_button;
@@ -29,6 +30,7 @@ pub use buffer::*;
 pub use chat_panel::*;
 pub use collab_panel::*;
 pub use command_palette::*;
+pub use context_menu::*;
 pub use editor_pane::*;
 pub use facepile::*;
 pub use icon_button::*;

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

@@ -0,0 +1,67 @@
+use crate::prelude::*;
+use crate::{
+    theme, v_stack, Label, List, ListEntry, ListItem, ListItemVariant, ListSeparator, ListSubHeader,
+};
+
+#[derive(Clone)]
+pub enum ContextMenuItem<S: 'static + Send + Sync + Clone> {
+    Header(&'static str),
+    Entry(Label<S>),
+    Separator,
+}
+
+impl<S: 'static + Send + Sync + Clone> ContextMenuItem<S> {
+    fn to_list_item(self) -> ListItem<S> {
+        match self {
+            ContextMenuItem::Header(label) => ListSubHeader::new(label).into(),
+            ContextMenuItem::Entry(label) => {
+                ListEntry::new(label).variant(ListItemVariant::Inset).into()
+            }
+            ContextMenuItem::Separator => ListSeparator::new().into(),
+        }
+    }
+
+    pub fn header(label: &'static str) -> Self {
+        Self::Header(label)
+    }
+
+    pub fn separator() -> Self {
+        Self::Separator
+    }
+
+    pub fn entry(label: Label<S>) -> Self {
+        Self::Entry(label)
+    }
+}
+
+#[derive(Element)]
+pub struct ContextMenu<S: 'static + Send + Sync + Clone> {
+    items: Vec<ContextMenuItem<S>>,
+}
+
+impl<S: 'static + Send + Sync + Clone> ContextMenu<S> {
+    pub fn new(items: impl IntoIterator<Item = ContextMenuItem<S>>) -> Self {
+        Self {
+            items: items.into_iter().collect(),
+        }
+    }
+    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        let theme = theme(cx);
+
+        v_stack()
+            .flex()
+            .fill(theme.lowest.base.default.background)
+            .border()
+            .border_color(theme.lowest.base.default.border)
+            .child(
+                List::new(
+                    self.items
+                        .clone()
+                        .into_iter()
+                        .map(ContextMenuItem::to_list_item)
+                        .collect(),
+                )
+                .set_toggle(ToggleState::Toggled),
+            )
+    }
+}