Progress on ContextMenu

Conrad Irwin created

Change summary

crates/gpui2/src/elements/overlay.rs       |   9 
crates/terminal_view2/src/terminal_view.rs |  14 
crates/ui2/src/components/context_menu.rs  | 238 ++++++++++++++++++++---
crates/ui2/src/components/list.rs          |  24 ++
crates/workspace2/src/dock.rs              | 118 -----------
5 files changed, 246 insertions(+), 157 deletions(-)

Detailed changes

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

@@ -1,8 +1,9 @@
 use smallvec::SmallVec;
+use taffy::style::Position;
 
 use crate::{
-    point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels,
-    Point, Size, Style,
+    point, px, AbsoluteLength, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId,
+    ParentComponent, Pixels, Point, Size, Style,
 };
 
 pub struct OverlayState {
@@ -72,8 +73,9 @@ impl<V: 'static> Element<V> for Overlay<V> {
             .iter_mut()
             .map(|child| child.layout(view_state, cx))
             .collect::<SmallVec<_>>();
+
         let mut overlay_style = Style::default();
-        overlay_style.position = crate::Position::Absolute;
+        overlay_style.position = Position::Absolute;
 
         let layout_id = cx.request_layout(&overlay_style, child_layout_ids.iter().copied());
 
@@ -106,6 +108,7 @@ impl<V: 'static> Element<V> for Overlay<V> {
             origin: Point::zero(),
             size: cx.viewport_size(),
         };
+        dbg!(bounds, desired, limits);
 
         match self.fit_mode {
             OverlayFitMode::SnapToWindow => {

crates/terminal_view2/src/terminal_view.rs 🔗

@@ -87,7 +87,7 @@ pub struct TerminalView {
     has_new_content: bool,
     //Currently using iTerm bell, show bell emoji in tab until input is received
     has_bell: bool,
-    context_menu: Option<ContextMenu>,
+    context_menu: Option<View<ContextMenu>>,
     blink_state: bool,
     blinking_on: bool,
     blinking_paused: bool,
@@ -302,10 +302,14 @@ impl TerminalView {
         position: gpui::Point<Pixels>,
         cx: &mut ViewContext<Self>,
     ) {
-        self.context_menu = Some(ContextMenu::new(vec![
-            ContextMenuItem::entry(Label::new("Clear"), Clear),
-            ContextMenuItem::entry(Label::new("Close"), CloseActiveItem { save_intent: None }),
-        ]));
+        self.context_menu = Some(cx.build_view(|cx| {
+            ContextMenu::new(cx)
+                .entry(Label::new("Clear"), Box::new(Clear))
+                .entry(
+                    Label::new("Close"),
+                    Box::new(CloseActiveItem { save_intent: None }),
+                )
+        }));
         dbg!(&position);
         // todo!()
         //     self.context_menu

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

@@ -1,5 +1,13 @@
-use crate::{prelude::*, ListItemVariant};
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use crate::{h_stack, prelude::*, ListItemVariant};
 use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader};
+use gpui::{
+    overlay, px, Action, AnyElement, Bounds, DispatchPhase, Div, EventEmitter, FocusHandle,
+    Focusable, FocusableView, LayoutId, MouseButton, MouseDownEvent, Overlay, Render, View,
+};
+use smallvec::SmallVec;
 
 pub enum ContextMenuItem {
     Header(SharedString),
@@ -19,12 +27,12 @@ impl Clone for ContextMenuItem {
     }
 }
 impl ContextMenuItem {
-    fn to_list_item<V: 'static>(self) -> ListItem {
+    fn to_list_item(self) -> ListItem {
         match self {
             ContextMenuItem::Header(label) => ListSubHeader::new(label).into(),
             ContextMenuItem::Entry(label, action) => ListEntry::new(label)
                 .variant(ListItemVariant::Inset)
-                .on_click(action)
+                .action(action)
                 .into(),
             ContextMenuItem::Separator => ListSeparator::new().into(),
         }
@@ -43,40 +51,196 @@ impl ContextMenuItem {
     }
 }
 
-#[derive(Component, Clone)]
 pub struct ContextMenu {
-    items: Vec<ContextMenuItem>,
+    items: Vec<ListItem>,
+    focus_handle: FocusHandle,
+}
+
+pub enum MenuEvent {
+    Dismissed,
+}
+
+impl EventEmitter<MenuEvent> for ContextMenu {}
+impl FocusableView for ContextMenu {
+    fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle {
+        self.focus_handle.clone()
+    }
 }
 
 impl ContextMenu {
-    pub fn new(items: impl IntoIterator<Item = ContextMenuItem>) -> Self {
+    pub fn new(cx: &mut WindowContext) -> Self {
         Self {
-            items: items.into_iter().collect(),
+            items: Default::default(),
+            focus_handle: cx.focus_handle(),
         }
     }
+
+    pub fn header(mut self, title: impl Into<SharedString>) -> Self {
+        self.items.push(ListItem::Header(ListSubHeader::new(title)));
+        self
+    }
+
+    pub fn separator(mut self) -> Self {
+        self.items.push(ListItem::Separator(ListSeparator));
+        self
+    }
+
+    pub fn entry(mut self, label: Label, action: Box<dyn Action>) -> Self {
+        self.items.push(ListEntry::new(label).action(action).into());
+        self
+    }
+
+    pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
+        // todo!()
+        cx.emit(MenuEvent::Dismissed);
+    }
+
+    pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
+        cx.emit(MenuEvent::Dismissed);
+    }
+}
+
+impl Render for ContextMenu {
+    type Element = Overlay<Self>;
     // todo!()
-    // cx.add_action(ContextMenu::select_first);
-    // cx.add_action(ContextMenu::select_last);
-    // cx.add_action(ContextMenu::select_next);
-    // cx.add_action(ContextMenu::select_prev);
-    // cx.add_action(ContextMenu::confirm);
-    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        v_stack()
-            .flex()
-            .bg(cx.theme().colors().elevated_surface_background)
-            .border()
-            .border_color(cx.theme().colors().border)
-            .child(List::new(
-                self.items
-                    .into_iter()
-                    .map(ContextMenuItem::to_list_item::<V>)
-                    .collect(),
-            ))
-            .on_mouse_down_out(|_, _, cx| cx.dispatch_action(Box::new(menu::Cancel)))
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        overlay().child(
+            div().elevation_2(cx).flex().flex_row().child(
+                v_stack()
+                    .min_w(px(200.))
+                    .track_focus(&self.focus_handle)
+                    .on_mouse_down_out(|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_action(ContextMenu::confirm)
+                    .on_action(ContextMenu::cancel)
+                    .flex_none()
+                    // .bg(cx.theme().colors().elevated_surface_background)
+                    // .border()
+                    // .border_color(cx.theme().colors().border)
+                    .child(List::new(self.items.clone())),
+            ),
+        )
+    }
+}
+
+pub struct MenuHandle<V: 'static> {
+    id: ElementId,
+    children: SmallVec<[AnyElement<V>; 2]>,
+    builder: Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> View<ContextMenu> + 'static>,
+}
+
+impl<V: 'static> ParentComponent<V> for MenuHandle<V> {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+        &mut self.children
+    }
+}
+
+impl<V: 'static> MenuHandle<V> {
+    pub fn new(
+        id: impl Into<ElementId>,
+        builder: impl Fn(&mut V, &mut ViewContext<V>) -> View<ContextMenu> + 'static,
+    ) -> Self {
+        Self {
+            id: id.into(),
+            children: SmallVec::new(),
+            builder: Rc::new(builder),
+        }
+    }
+}
+
+pub struct MenuHandleState<V> {
+    menu: Rc<RefCell<Option<View<ContextMenu>>>>,
+    menu_element: Option<AnyElement<V>>,
+}
+impl<V: 'static> Element<V> for MenuHandle<V> {
+    type ElementState = MenuHandleState<V>;
+
+    fn element_id(&self) -> Option<gpui::ElementId> {
+        Some(self.id.clone())
+    }
+
+    fn layout(
+        &mut self,
+        view_state: &mut V,
+        element_state: Option<Self::ElementState>,
+        cx: &mut crate::ViewContext<V>,
+    ) -> (gpui::LayoutId, Self::ElementState) {
+        let mut child_layout_ids = self
+            .children
+            .iter_mut()
+            .map(|child| child.layout(view_state, cx))
+            .collect::<SmallVec<[LayoutId; 2]>>();
+
+        let menu = if let Some(element_state) = element_state {
+            element_state.menu
+        } else {
+            Rc::new(RefCell::new(None))
+        };
+
+        let menu_element = menu.borrow_mut().as_mut().map(|menu| {
+            let mut view = menu.clone().render();
+            child_layout_ids.push(view.layout(view_state, cx));
+            view
+        });
+
+        let layout_id = cx.request_layout(&gpui::Style::default(), child_layout_ids.into_iter());
+
+        (layout_id, MenuHandleState { menu, menu_element })
+    }
+
+    fn paint(
+        &mut self,
+        bounds: Bounds<gpui::Pixels>,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut crate::ViewContext<V>,
+    ) {
+        for child in &mut self.children {
+            child.paint(view_state, cx);
+        }
+
+        if let Some(menu) = element_state.menu_element.as_mut() {
+            menu.paint(view_state, cx);
+            return;
+        }
+
+        let menu = element_state.menu.clone();
+        let builder = self.builder.clone();
+        cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| {
+            if phase == DispatchPhase::Bubble
+                && event.button == MouseButton::Right
+                && bounds.contains_point(&event.position)
+            {
+                cx.stop_propagation();
+                cx.prevent_default();
+
+                let new_menu = (builder)(view_state, cx);
+                let menu2 = menu.clone();
+                cx.subscribe(&new_menu, move |this, modal, e, cx| match e {
+                    MenuEvent::Dismissed => {
+                        *menu2.borrow_mut() = None;
+                        cx.notify();
+                    }
+                })
+                .detach();
+                *menu.borrow_mut() = Some(new_menu);
+                cx.notify();
+            }
+        });
+    }
+}
+
+impl<V: 'static> Component<V> for MenuHandle<V> {
+    fn render(self) -> AnyElement<V> {
+        AnyElement::new(self)
     }
 }
 
-use gpui::Action;
 #[cfg(feature = "stories")]
 pub use stories::*;
 
@@ -84,7 +248,7 @@ pub use stories::*;
 mod stories {
     use super::*;
     use crate::story::Story;
-    use gpui::{action, Div, Render};
+    use gpui::{action, Div, Render, VisualContext};
 
     pub struct ContextMenuStory;
 
@@ -97,17 +261,25 @@ mod stories {
 
             Story::container(cx)
                 .child(Story::title_for::<_, ContextMenu>(cx))
-                .child(Story::label(cx, "Default"))
-                .child(ContextMenu::new([
-                    ContextMenuItem::header("Section header"),
-                    ContextMenuItem::Separator,
-                    ContextMenuItem::entry(Label::new("Print current time"), PrintCurrentDate {}),
-                ]))
                 .on_action(|_, _: &PrintCurrentDate, _| {
                     if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
                         println!("Current Unix time is {:?}", unix_time.as_secs());
                     }
                 })
+                .child(
+                    MenuHandle::new("test", move |_, cx| {
+                        cx.build_view(|cx| {
+                            ContextMenu::new(cx)
+                                .header("Section header")
+                                .separator()
+                                .entry(
+                                    Label::new("Print current time"),
+                                    PrintCurrentDate {}.boxed_clone(),
+                                )
+                        })
+                    })
+                    .child(Label::new("RIGHT CLICK ME")),
+                )
         }
     }
 }

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

@@ -117,7 +117,7 @@ impl ListHeader {
     }
 }
 
-#[derive(Component)]
+#[derive(Component, Clone)]
 pub struct ListSubHeader {
     label: SharedString,
     left_icon: Option<Icon>,
@@ -172,7 +172,7 @@ pub enum ListEntrySize {
     Medium,
 }
 
-#[derive(Component)]
+#[derive(Component, Clone)]
 pub enum ListItem {
     Entry(ListEntry),
     Separator(ListSeparator),
@@ -234,6 +234,24 @@ pub struct ListEntry {
     on_click: Option<Box<dyn Action>>,
 }
 
+impl Clone for ListEntry {
+    fn clone(&self) -> Self {
+        Self {
+            disabled: self.disabled,
+            // TODO: Reintroduce this
+            // disclosure_control_style: DisclosureControlVisibility,
+            indent_level: self.indent_level,
+            label: self.label.clone(),
+            left_slot: self.left_slot.clone(),
+            overflow: self.overflow,
+            size: self.size,
+            toggle: self.toggle,
+            variant: self.variant,
+            on_click: self.on_click.as_ref().map(|opt| opt.boxed_clone()),
+        }
+    }
+}
+
 impl ListEntry {
     pub fn new(label: Label) -> Self {
         Self {
@@ -249,7 +267,7 @@ impl ListEntry {
         }
     }
 
-    pub fn on_click(mut self, action: impl Into<Box<dyn Action>>) -> Self {
+    pub fn action(mut self, action: impl Into<Box<dyn Action>>) -> Self {
         self.on_click = Some(action.into());
         self
     }

crates/workspace2/src/dock.rs 🔗

@@ -10,7 +10,10 @@ use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use smallvec::SmallVec;
 use std::{cell::RefCell, rc::Rc, sync::Arc};
-use ui::{h_stack, IconButton, InteractionState, Label, Tooltip};
+use ui::{
+    h_stack, ContextMenu, ContextMenuItem, IconButton, InteractionState, Label, MenuEvent,
+    MenuHandle, Tooltip,
+};
 
 pub enum PanelEvent {
     ChangePosition,
@@ -659,117 +662,6 @@ impl PanelButtons {
 //     }
 // }
 
-pub struct MenuHandle<V: 'static> {
-    id: ElementId,
-    children: SmallVec<[AnyElement<V>; 2]>,
-    builder: Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>,
-}
-
-impl<V: 'static> ParentComponent<V> for MenuHandle<V> {
-    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
-        &mut self.children
-    }
-}
-
-impl<V: 'static> MenuHandle<V> {
-    fn new(
-        id: impl Into<ElementId>,
-        builder: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
-    ) -> Self {
-        Self {
-            id: id.into(),
-            children: SmallVec::new(),
-            builder: Rc::new(builder),
-        }
-    }
-}
-
-pub struct MenuState<V> {
-    open: Rc<RefCell<bool>>,
-    menu: Option<AnyElement<V>>,
-}
-// Here be dragons
-impl<V: 'static> Element<V> for MenuHandle<V> {
-    type ElementState = MenuState<V>;
-
-    fn element_id(&self) -> Option<gpui::ElementId> {
-        Some(self.id.clone())
-    }
-
-    fn layout(
-        &mut self,
-        view_state: &mut V,
-        element_state: Option<Self::ElementState>,
-        cx: &mut crate::ViewContext<V>,
-    ) -> (gpui::LayoutId, Self::ElementState) {
-        let mut child_layout_ids = self
-            .children
-            .iter_mut()
-            .map(|child| child.layout(view_state, cx))
-            .collect::<SmallVec<[LayoutId; 2]>>();
-
-        let open = if let Some(element_state) = element_state {
-            element_state.open
-        } else {
-            Rc::new(RefCell::new(false))
-        };
-
-        let mut menu = None;
-        if *open.borrow() {
-            let mut view = (self.builder)(view_state, cx).render();
-            child_layout_ids.push(view.layout(view_state, cx));
-            menu.replace(view);
-        }
-        let layout_id = cx.request_layout(&gpui::Style::default(), child_layout_ids.into_iter());
-
-        (layout_id, MenuState { open, menu })
-    }
-
-    fn paint(
-        &mut self,
-        bounds: crate::Bounds<gpui::Pixels>,
-        view_state: &mut V,
-        element_state: &mut Self::ElementState,
-        cx: &mut crate::ViewContext<V>,
-    ) {
-        for child in &mut self.children {
-            child.paint(view_state, cx);
-        }
-
-        if let Some(mut menu) = element_state.menu.as_mut() {
-            menu.paint(view_state, cx);
-            return;
-        }
-
-        let open = element_state.open.clone();
-        cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| {
-            dbg!(&event, &phase);
-            if phase == DispatchPhase::Bubble
-                && event.button == MouseButton::Right
-                && bounds.contains_point(&event.position)
-            {
-                *open.borrow_mut() = true;
-                cx.notify();
-            }
-        });
-    }
-}
-
-impl<V: 'static> Component<V> for MenuHandle<V> {
-    fn render(self) -> AnyElement<V> {
-        AnyElement::new(self)
-    }
-}
-
-struct TestMenu {}
-impl Render for TestMenu {
-    type Element = Div<Self>;
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        div().child("0MG!")
-    }
-}
-
 // here be kittens
 impl Render for PanelButtons {
     type Element = Div<Self>;
@@ -807,7 +699,7 @@ impl Render for PanelButtons {
                 Some(
                     MenuHandle::new(
                         SharedString::from(format!("{} tooltip", name)),
-                        move |_, cx| Tooltip::text("HELLOOOOOOOOOOOOOO", cx),
+                        move |_, cx| cx.build_view(|cx| ContextMenu::new(cx).header("SECTION")),
                     )
                     .child(button),
                 )