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