1use std::cell::RefCell;
2use std::rc::Rc;
3
4use crate::prelude::*;
5use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader};
6use gpui::{
7 overlay, px, Action, AnchorCorner, AnyElement, Bounds, DispatchPhase, Div, EventEmitter,
8 FocusHandle, FocusableView, LayoutId, MouseButton, MouseDownEvent, Pixels, Point, Render, View,
9};
10
11pub struct ContextMenu {
12 items: Vec<ListItem>,
13 focus_handle: FocusHandle,
14}
15
16pub enum MenuEvent {
17 Dismissed,
18}
19
20impl EventEmitter<MenuEvent> for ContextMenu {}
21impl FocusableView for ContextMenu {
22 fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle {
23 self.focus_handle.clone()
24 }
25}
26
27impl ContextMenu {
28 pub fn new(cx: &mut WindowContext) -> Self {
29 Self {
30 items: Default::default(),
31 focus_handle: cx.focus_handle(),
32 }
33 }
34
35 pub fn header(mut self, title: impl Into<SharedString>) -> Self {
36 self.items.push(ListItem::Header(ListSubHeader::new(title)));
37 self
38 }
39
40 pub fn separator(mut self) -> Self {
41 self.items.push(ListItem::Separator(ListSeparator));
42 self
43 }
44
45 pub fn entry(mut self, label: Label, action: Box<dyn Action>) -> Self {
46 self.items.push(ListEntry::new(label).action(action).into());
47 self
48 }
49
50 pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
51 // todo!()
52 cx.emit(MenuEvent::Dismissed);
53 }
54
55 pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
56 cx.emit(MenuEvent::Dismissed);
57 }
58}
59
60impl Render for ContextMenu {
61 type Element = Div<Self>;
62 // todo!()
63 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
64 div().elevation_2(cx).flex().flex_row().child(
65 v_stack()
66 .min_w(px(200.))
67 .track_focus(&self.focus_handle)
68 .on_mouse_down_out(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx))
69 // .on_action(ContextMenu::select_first)
70 // .on_action(ContextMenu::select_last)
71 // .on_action(ContextMenu::select_next)
72 // .on_action(ContextMenu::select_prev)
73 .on_action(ContextMenu::confirm)
74 .on_action(ContextMenu::cancel)
75 .flex_none()
76 // .bg(cx.theme().colors().elevated_surface_background)
77 // .border()
78 // .border_color(cx.theme().colors().border)
79 .child(List::new(self.items.clone())),
80 )
81 }
82}
83
84pub struct MenuHandle<V: 'static> {
85 id: Option<ElementId>,
86 child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement<V> + 'static>>,
87 menu_builder: Option<Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> View<ContextMenu> + 'static>>,
88
89 anchor: Option<AnchorCorner>,
90 attach: Option<AnchorCorner>,
91}
92
93impl<V: 'static> MenuHandle<V> {
94 pub fn id(mut self, id: impl Into<ElementId>) -> Self {
95 self.id = Some(id.into());
96 self
97 }
98
99 pub fn menu(
100 mut self,
101 f: impl Fn(&mut V, &mut ViewContext<V>) -> View<ContextMenu> + 'static,
102 ) -> Self {
103 self.menu_builder = Some(Rc::new(f));
104 self
105 }
106
107 pub fn child<R: Component<V>>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
108 self.child_builder = Some(Box::new(|b| f(b).render()));
109 self
110 }
111
112 /// anchor defines which corner of the menu to anchor to the attachment point
113 /// (by default the cursor position, but see attach)
114 pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
115 self.anchor = Some(anchor);
116 self
117 }
118
119 /// attach defines which corner of the handle to attach the menu's anchor to
120 pub fn attach(mut self, attach: AnchorCorner) -> Self {
121 self.attach = Some(attach);
122 self
123 }
124}
125
126pub fn menu_handle<V: 'static>() -> MenuHandle<V> {
127 MenuHandle {
128 id: None,
129 child_builder: None,
130 menu_builder: None,
131 anchor: None,
132 attach: None,
133 }
134}
135
136pub struct MenuHandleState<V> {
137 menu: Rc<RefCell<Option<View<ContextMenu>>>>,
138 position: Rc<RefCell<Point<Pixels>>>,
139 child_layout_id: Option<LayoutId>,
140 child_element: Option<AnyElement<V>>,
141 menu_element: Option<AnyElement<V>>,
142}
143impl<V: 'static> Element<V> for MenuHandle<V> {
144 type ElementState = MenuHandleState<V>;
145
146 fn element_id(&self) -> Option<gpui::ElementId> {
147 Some(self.id.clone().expect("menu_handle must have an id()"))
148 }
149
150 fn layout(
151 &mut self,
152 view_state: &mut V,
153 element_state: Option<Self::ElementState>,
154 cx: &mut crate::ViewContext<V>,
155 ) -> (gpui::LayoutId, Self::ElementState) {
156 let (menu, position) = if let Some(element_state) = element_state {
157 (element_state.menu, element_state.position)
158 } else {
159 (Rc::default(), Rc::default())
160 };
161
162 let mut menu_layout_id = None;
163
164 let menu_element = menu.borrow_mut().as_mut().map(|menu| {
165 let mut overlay = overlay::<V>().snap_to_window();
166 if let Some(anchor) = self.anchor {
167 overlay = overlay.anchor(anchor);
168 }
169 overlay = overlay.position(*position.borrow());
170
171 let mut view = overlay.child(menu.clone()).render();
172 menu_layout_id = Some(view.layout(view_state, cx));
173 view
174 });
175
176 let mut child_element = self
177 .child_builder
178 .take()
179 .map(|child_builder| (child_builder)(menu.borrow().is_some()));
180
181 let child_layout_id = child_element
182 .as_mut()
183 .map(|child_element| child_element.layout(view_state, cx));
184
185 let layout_id = cx.request_layout(
186 &gpui::Style::default(),
187 menu_layout_id.into_iter().chain(child_layout_id),
188 );
189
190 (
191 layout_id,
192 MenuHandleState {
193 menu,
194 position,
195 child_element,
196 child_layout_id,
197 menu_element,
198 },
199 )
200 }
201
202 fn paint(
203 &mut self,
204 bounds: Bounds<gpui::Pixels>,
205 view_state: &mut V,
206 element_state: &mut Self::ElementState,
207 cx: &mut crate::ViewContext<V>,
208 ) {
209 if let Some(child) = element_state.child_element.as_mut() {
210 child.paint(view_state, cx);
211 }
212
213 if let Some(menu) = element_state.menu_element.as_mut() {
214 menu.paint(view_state, cx);
215 return;
216 }
217
218 let Some(builder) = self.menu_builder.clone() else {
219 return;
220 };
221 let menu = element_state.menu.clone();
222 let position = element_state.position.clone();
223 let attach = self.attach.clone();
224 let child_layout_id = element_state.child_layout_id.clone();
225
226 cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| {
227 if phase == DispatchPhase::Bubble
228 && event.button == MouseButton::Right
229 && bounds.contains_point(&event.position)
230 {
231 cx.stop_propagation();
232 cx.prevent_default();
233
234 let new_menu = (builder)(view_state, cx);
235 let menu2 = menu.clone();
236 cx.subscribe(&new_menu, move |this, modal, e, cx| match e {
237 MenuEvent::Dismissed => {
238 *menu2.borrow_mut() = None;
239 cx.notify();
240 }
241 })
242 .detach();
243 *menu.borrow_mut() = Some(new_menu);
244
245 *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
246 attach
247 .unwrap()
248 .corner(cx.layout_bounds(child_layout_id.unwrap()))
249 } else {
250 cx.mouse_position()
251 };
252 cx.notify();
253 }
254 });
255 }
256}
257
258impl<V: 'static> Component<V> for MenuHandle<V> {
259 fn render(self) -> AnyElement<V> {
260 AnyElement::new(self)
261 }
262}
263
264#[cfg(feature = "stories")]
265pub use stories::*;
266
267#[cfg(feature = "stories")]
268mod stories {
269 use super::*;
270 use crate::story::Story;
271 use gpui::{actions, Div, Render, VisualContext};
272
273 actions!(PrintCurrentDate);
274
275 fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<ContextMenu> {
276 cx.build_view(|cx| {
277 ContextMenu::new(cx).header(header).separator().entry(
278 Label::new("Print current time"),
279 PrintCurrentDate.boxed_clone(),
280 )
281 })
282 }
283
284 pub struct ContextMenuStory;
285
286 impl Render for ContextMenuStory {
287 type Element = Div<Self>;
288
289 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
290 Story::container(cx)
291 .on_action(|_, _: &PrintCurrentDate, _| {
292 if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
293 println!("Current Unix time is {:?}", unix_time.as_secs());
294 }
295 })
296 .flex()
297 .flex_row()
298 .justify_between()
299 .child(
300 div()
301 .flex()
302 .flex_col()
303 .justify_between()
304 .child(
305 menu_handle()
306 .id("test2")
307 .child(|is_open| {
308 Label::new(if is_open {
309 "TOP LEFT"
310 } else {
311 "RIGHT CLICK ME"
312 })
313 .render()
314 })
315 .menu(move |_, cx| build_menu(cx, "top left")),
316 )
317 .child(
318 menu_handle()
319 .id("test1")
320 .child(|is_open| {
321 Label::new(if is_open {
322 "BOTTOM LEFT"
323 } else {
324 "RIGHT CLICK ME"
325 })
326 .render()
327 })
328 .anchor(AnchorCorner::BottomLeft)
329 .attach(AnchorCorner::TopLeft)
330 .menu(move |_, cx| build_menu(cx, "bottom left")),
331 ),
332 )
333 .child(
334 div()
335 .flex()
336 .flex_col()
337 .justify_between()
338 .child(
339 menu_handle()
340 .id("test3")
341 .child(|is_open| {
342 Label::new(if is_open {
343 "TOP RIGHT"
344 } else {
345 "RIGHT CLICK ME"
346 })
347 .render()
348 })
349 .anchor(AnchorCorner::TopRight)
350 .menu(move |_, cx| build_menu(cx, "top right")),
351 )
352 .child(
353 menu_handle()
354 .id("test4")
355 .child(|is_open| {
356 Label::new(if is_open {
357 "BOTTOM RIGHT"
358 } else {
359 "RIGHT CLICK ME"
360 })
361 .render()
362 })
363 .anchor(AnchorCorner::BottomRight)
364 .attach(AnchorCorner::TopRight)
365 .menu(move |_, cx| build_menu(cx, "bottom right")),
366 ),
367 )
368 }
369 }
370}