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