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}