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