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