context_menu.rs

  1use std::cell::RefCell;
  2use std::rc::Rc;
  3
  4use crate::{prelude::*, ListItemVariant};
  5use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader};
  6use gpui::{
  7    overlay, px, Action, AnyElement, Bounds, DispatchPhase, EventEmitter, FocusHandle,
  8    FocusableView, LayoutId, MouseButton, MouseDownEvent, Overlay, Render, View,
  9};
 10use smallvec::SmallVec;
 11
 12pub enum ContextMenuItem {
 13    Header(SharedString),
 14    Entry(Label, Box<dyn gpui::Action>),
 15    Separator,
 16}
 17
 18impl Clone for ContextMenuItem {
 19    fn clone(&self) -> Self {
 20        match self {
 21            ContextMenuItem::Header(name) => ContextMenuItem::Header(name.clone()),
 22            ContextMenuItem::Entry(label, action) => {
 23                ContextMenuItem::Entry(label.clone(), action.boxed_clone())
 24            }
 25            ContextMenuItem::Separator => ContextMenuItem::Separator,
 26        }
 27    }
 28}
 29impl ContextMenuItem {
 30    fn to_list_item(self) -> ListItem {
 31        match self {
 32            ContextMenuItem::Header(label) => ListSubHeader::new(label).into(),
 33            ContextMenuItem::Entry(label, action) => ListEntry::new(label)
 34                .variant(ListItemVariant::Inset)
 35                .action(action)
 36                .into(),
 37            ContextMenuItem::Separator => ListSeparator::new().into(),
 38        }
 39    }
 40
 41    pub fn header(label: impl Into<SharedString>) -> Self {
 42        Self::Header(label.into())
 43    }
 44
 45    pub fn separator() -> Self {
 46        Self::Separator
 47    }
 48
 49    pub fn entry(label: Label, action: impl Action) -> Self {
 50        Self::Entry(label, Box::new(action))
 51    }
 52}
 53
 54pub struct ContextMenu {
 55    items: Vec<ListItem>,
 56    focus_handle: FocusHandle,
 57}
 58
 59pub enum MenuEvent {
 60    Dismissed,
 61}
 62
 63impl EventEmitter<MenuEvent> for ContextMenu {}
 64impl FocusableView for ContextMenu {
 65    fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle {
 66        self.focus_handle.clone()
 67    }
 68}
 69
 70impl ContextMenu {
 71    pub fn new(cx: &mut WindowContext) -> Self {
 72        Self {
 73            items: Default::default(),
 74            focus_handle: cx.focus_handle(),
 75        }
 76    }
 77
 78    pub fn header(mut self, title: impl Into<SharedString>) -> Self {
 79        self.items.push(ListItem::Header(ListSubHeader::new(title)));
 80        self
 81    }
 82
 83    pub fn separator(mut self) -> Self {
 84        self.items.push(ListItem::Separator(ListSeparator));
 85        self
 86    }
 87
 88    pub fn entry(mut self, label: Label, action: Box<dyn Action>) -> Self {
 89        self.items.push(ListEntry::new(label).action(action).into());
 90        self
 91    }
 92
 93    pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
 94        // todo!()
 95        cx.emit(MenuEvent::Dismissed);
 96    }
 97
 98    pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
 99        cx.emit(MenuEvent::Dismissed);
100    }
101}
102
103impl Render for ContextMenu {
104    type Element = Overlay<Self>;
105    // todo!()
106    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
107        overlay().child(
108            div().elevation_2(cx).flex().flex_row().child(
109                v_stack()
110                    .min_w(px(200.))
111                    .track_focus(&self.focus_handle)
112                    .on_mouse_down_out(|this: &mut Self, _, cx| {
113                        this.cancel(&Default::default(), cx)
114                    })
115                    // .on_action(ContextMenu::select_first)
116                    // .on_action(ContextMenu::select_last)
117                    // .on_action(ContextMenu::select_next)
118                    // .on_action(ContextMenu::select_prev)
119                    .on_action(ContextMenu::confirm)
120                    .on_action(ContextMenu::cancel)
121                    .flex_none()
122                    // .bg(cx.theme().colors().elevated_surface_background)
123                    // .border()
124                    // .border_color(cx.theme().colors().border)
125                    .child(List::new(self.items.clone())),
126            ),
127        )
128    }
129}
130
131pub struct MenuHandle<V: 'static> {
132    id: ElementId,
133    children: SmallVec<[AnyElement<V>; 2]>,
134    builder: Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> View<ContextMenu> + 'static>,
135}
136
137impl<V: 'static> ParentComponent<V> for MenuHandle<V> {
138    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
139        &mut self.children
140    }
141}
142
143impl<V: 'static> MenuHandle<V> {
144    pub fn new(
145        id: impl Into<ElementId>,
146        builder: impl Fn(&mut V, &mut ViewContext<V>) -> View<ContextMenu> + 'static,
147    ) -> Self {
148        Self {
149            id: id.into(),
150            children: SmallVec::new(),
151            builder: Rc::new(builder),
152        }
153    }
154}
155
156pub struct MenuHandleState<V> {
157    menu: Rc<RefCell<Option<View<ContextMenu>>>>,
158    menu_element: Option<AnyElement<V>>,
159}
160impl<V: 'static> Element<V> for MenuHandle<V> {
161    type ElementState = MenuHandleState<V>;
162
163    fn element_id(&self) -> Option<gpui::ElementId> {
164        Some(self.id.clone())
165    }
166
167    fn layout(
168        &mut self,
169        view_state: &mut V,
170        element_state: Option<Self::ElementState>,
171        cx: &mut crate::ViewContext<V>,
172    ) -> (gpui::LayoutId, Self::ElementState) {
173        let mut child_layout_ids = self
174            .children
175            .iter_mut()
176            .map(|child| child.layout(view_state, cx))
177            .collect::<SmallVec<[LayoutId; 2]>>();
178
179        let menu = if let Some(element_state) = element_state {
180            element_state.menu
181        } else {
182            Rc::new(RefCell::new(None))
183        };
184
185        let menu_element = menu.borrow_mut().as_mut().map(|menu| {
186            let mut view = menu.clone().render();
187            child_layout_ids.push(view.layout(view_state, cx));
188            view
189        });
190
191        let layout_id = cx.request_layout(&gpui::Style::default(), child_layout_ids.into_iter());
192
193        (layout_id, MenuHandleState { menu, menu_element })
194    }
195
196    fn paint(
197        &mut self,
198        bounds: Bounds<gpui::Pixels>,
199        view_state: &mut V,
200        element_state: &mut Self::ElementState,
201        cx: &mut crate::ViewContext<V>,
202    ) {
203        for child in &mut self.children {
204            child.paint(view_state, cx);
205        }
206
207        if let Some(menu) = element_state.menu_element.as_mut() {
208            menu.paint(view_state, cx);
209            return;
210        }
211
212        let menu = element_state.menu.clone();
213        let builder = self.builder.clone();
214        cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| {
215            if phase == DispatchPhase::Bubble
216                && event.button == MouseButton::Right
217                && bounds.contains_point(&event.position)
218            {
219                cx.stop_propagation();
220                cx.prevent_default();
221
222                let new_menu = (builder)(view_state, cx);
223                let menu2 = menu.clone();
224                cx.subscribe(&new_menu, move |this, modal, e, cx| match e {
225                    MenuEvent::Dismissed => {
226                        *menu2.borrow_mut() = None;
227                        cx.notify();
228                    }
229                })
230                .detach();
231                *menu.borrow_mut() = Some(new_menu);
232                cx.notify();
233            }
234        });
235    }
236}
237
238impl<V: 'static> Component<V> for MenuHandle<V> {
239    fn render(self) -> AnyElement<V> {
240        AnyElement::new(self)
241    }
242}
243
244#[cfg(feature = "stories")]
245pub use stories::*;
246
247#[cfg(feature = "stories")]
248mod stories {
249    use super::*;
250    use crate::story::Story;
251    use gpui::{action, Div, Render, VisualContext};
252
253    pub struct ContextMenuStory;
254
255    impl Render for ContextMenuStory {
256        type Element = Div<Self>;
257
258        fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
259            #[action]
260            struct PrintCurrentDate {}
261
262            Story::container(cx)
263                .child(Story::title_for::<_, ContextMenu>(cx))
264                .on_action(|_, _: &PrintCurrentDate, _| {
265                    if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
266                        println!("Current Unix time is {:?}", unix_time.as_secs());
267                    }
268                })
269                .child(
270                    MenuHandle::new("test", move |_, cx| {
271                        cx.build_view(|cx| {
272                            ContextMenu::new(cx)
273                                .header("Section header")
274                                .separator()
275                                .entry(
276                                    Label::new("Print current time"),
277                                    PrintCurrentDate {}.boxed_clone(),
278                                )
279                        })
280                    })
281                    .child(Label::new("RIGHT CLICK ME")),
282                )
283        }
284    }
285}