1use crate::{prelude::*, v_stack, KeyBinding, Label, List, ListItem, ListSeparator, ListSubHeader};
2use gpui::{
3 overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DismissEvent,
4 DispatchPhase, Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId,
5 ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, VisualContext,
6};
7use std::{cell::RefCell, rc::Rc};
8
9pub enum ContextMenuItem {
10 Separator,
11 Header(SharedString),
12 Entry {
13 label: SharedString,
14 click_handler: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>,
15 key_binding: Option<KeyBinding>,
16 },
17}
18
19pub struct ContextMenu {
20 items: Vec<ContextMenuItem>,
21 focus_handle: FocusHandle,
22}
23
24impl FocusableView for ContextMenu {
25 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
26 self.focus_handle.clone()
27 }
28}
29
30impl EventEmitter<DismissEvent> for ContextMenu {}
31
32impl ContextMenu {
33 pub fn build(
34 cx: &mut WindowContext,
35 f: impl FnOnce(Self, &mut WindowContext) -> Self,
36 ) -> View<Self> {
37 // let handle = cx.view().downgrade();
38 cx.build_view(|cx| {
39 f(
40 Self {
41 items: Default::default(),
42 focus_handle: cx.focus_handle(),
43 },
44 cx,
45 )
46 })
47 }
48
49 pub fn header(mut self, title: impl Into<SharedString>) -> Self {
50 self.items.push(ContextMenuItem::Header(title.into()));
51 self
52 }
53
54 pub fn separator(mut self) -> Self {
55 self.items.push(ContextMenuItem::Separator);
56 self
57 }
58
59 pub fn entry(
60 mut self,
61 label: impl Into<SharedString>,
62 on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
63 ) -> Self {
64 self.items.push(ContextMenuItem::Entry {
65 label: label.into(),
66 click_handler: Rc::new(on_click),
67 key_binding: None,
68 });
69 self
70 }
71
72 pub fn action(
73 mut self,
74 label: impl Into<SharedString>,
75 action: Box<dyn Action>,
76 cx: &mut WindowContext,
77 ) -> Self {
78 self.items.push(ContextMenuItem::Entry {
79 label: label.into(),
80 key_binding: KeyBinding::for_action(&*action, cx),
81 click_handler: Rc::new(move |_, cx| cx.dispatch_action(action.boxed_clone())),
82 });
83 self
84 }
85
86 pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
87 // todo!()
88 cx.emit(DismissEvent::Dismiss);
89 }
90
91 pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
92 cx.emit(DismissEvent::Dismiss);
93 }
94}
95
96impl Render for ContextMenu {
97 type Element = Div;
98
99 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
100 div().elevation_2(cx).flex().flex_row().child(
101 v_stack()
102 .min_w(px(200.))
103 .track_focus(&self.focus_handle)
104 .on_mouse_down_out(
105 cx.listener(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx)),
106 )
107 // .on_action(ContextMenu::select_first)
108 // .on_action(ContextMenu::select_last)
109 // .on_action(ContextMenu::select_next)
110 // .on_action(ContextMenu::select_prev)
111 .on_action(cx.listener(ContextMenu::confirm))
112 .on_action(cx.listener(ContextMenu::cancel))
113 .flex_none()
114 // .bg(cx.theme().colors().elevated_surface_background)
115 // .border()
116 // .border_color(cx.theme().colors().border)
117 .child(
118 List::new().children(self.items.iter().map(|item| match item {
119 ContextMenuItem::Separator => ListSeparator::new().into_any_element(),
120 ContextMenuItem::Header(header) => {
121 ListSubHeader::new(header.clone()).into_any_element()
122 }
123 ContextMenuItem::Entry {
124 label: entry,
125 click_handler: callback,
126 key_binding,
127 } => {
128 let callback = callback.clone();
129 let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent::Dismiss));
130
131 ListItem::new(entry.clone())
132 .child(Label::new(entry.clone()))
133 .children(key_binding.clone())
134 .on_click(move |event, cx| {
135 callback(event, cx);
136 dismiss(event, cx)
137 })
138 .into_any_element()
139 }
140 })),
141 ),
142 )
143 }
144}
145
146pub struct MenuHandle<M: ManagedView> {
147 id: ElementId,
148 child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement + 'static>>,
149 menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
150 anchor: Option<AnchorCorner>,
151 attach: Option<AnchorCorner>,
152}
153
154impl<M: ManagedView> MenuHandle<M> {
155 pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View<M> + 'static) -> Self {
156 self.menu_builder = Some(Rc::new(f));
157 self
158 }
159
160 pub fn child<R: IntoElement>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
161 self.child_builder = Some(Box::new(|b| f(b).into_element().into_any()));
162 self
163 }
164
165 /// anchor defines which corner of the menu to anchor to the attachment point
166 /// (by default the cursor position, but see attach)
167 pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
168 self.anchor = Some(anchor);
169 self
170 }
171
172 /// attach defines which corner of the handle to attach the menu's anchor to
173 pub fn attach(mut self, attach: AnchorCorner) -> Self {
174 self.attach = Some(attach);
175 self
176 }
177}
178
179pub fn menu_handle<M: ManagedView>(id: impl Into<ElementId>) -> MenuHandle<M> {
180 MenuHandle {
181 id: id.into(),
182 child_builder: None,
183 menu_builder: None,
184 anchor: None,
185 attach: None,
186 }
187}
188
189pub struct MenuHandleState<M> {
190 menu: Rc<RefCell<Option<View<M>>>>,
191 position: Rc<RefCell<Point<Pixels>>>,
192 child_layout_id: Option<LayoutId>,
193 child_element: Option<AnyElement>,
194 menu_element: Option<AnyElement>,
195}
196
197impl<M: ManagedView> Element for MenuHandle<M> {
198 type State = MenuHandleState<M>;
199
200 fn layout(
201 &mut self,
202 element_state: Option<Self::State>,
203 cx: &mut WindowContext,
204 ) -> (gpui::LayoutId, Self::State) {
205 let (menu, position) = if let Some(element_state) = element_state {
206 (element_state.menu, element_state.position)
207 } else {
208 (Rc::default(), Rc::default())
209 };
210
211 let mut menu_layout_id = None;
212
213 let menu_element = menu.borrow_mut().as_mut().map(|menu| {
214 let mut overlay = overlay().snap_to_window();
215 if let Some(anchor) = self.anchor {
216 overlay = overlay.anchor(anchor);
217 }
218 overlay = overlay.position(*position.borrow());
219
220 let mut element = overlay.child(menu.clone()).into_any();
221 menu_layout_id = Some(element.layout(cx));
222 element
223 });
224
225 let mut child_element = self
226 .child_builder
227 .take()
228 .map(|child_builder| (child_builder)(menu.borrow().is_some()));
229
230 let child_layout_id = child_element
231 .as_mut()
232 .map(|child_element| child_element.layout(cx));
233
234 let layout_id = cx.request_layout(
235 &gpui::Style::default(),
236 menu_layout_id.into_iter().chain(child_layout_id),
237 );
238
239 (
240 layout_id,
241 MenuHandleState {
242 menu,
243 position,
244 child_element,
245 child_layout_id,
246 menu_element,
247 },
248 )
249 }
250
251 fn paint(
252 self,
253 bounds: Bounds<gpui::Pixels>,
254 element_state: &mut Self::State,
255 cx: &mut WindowContext,
256 ) {
257 if let Some(child) = element_state.child_element.take() {
258 child.paint(cx);
259 }
260
261 if let Some(menu) = element_state.menu_element.take() {
262 menu.paint(cx);
263 return;
264 }
265
266 let Some(builder) = self.menu_builder else {
267 return;
268 };
269 let menu = element_state.menu.clone();
270 let position = element_state.position.clone();
271 let attach = self.attach.clone();
272 let child_layout_id = element_state.child_layout_id.clone();
273
274 cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
275 if phase == DispatchPhase::Bubble
276 && event.button == MouseButton::Right
277 && bounds.contains_point(&event.position)
278 {
279 cx.stop_propagation();
280 cx.prevent_default();
281
282 let new_menu = (builder)(cx);
283 let menu2 = menu.clone();
284 cx.subscribe(&new_menu, move |_modal, e, cx| match e {
285 &DismissEvent::Dismiss => {
286 *menu2.borrow_mut() = None;
287 cx.notify();
288 }
289 })
290 .detach();
291 cx.focus_view(&new_menu);
292 *menu.borrow_mut() = Some(new_menu);
293
294 *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
295 attach
296 .unwrap()
297 .corner(cx.layout_bounds(child_layout_id.unwrap()))
298 } else {
299 cx.mouse_position()
300 };
301 cx.notify();
302 }
303 });
304 }
305}
306
307impl<M: ManagedView> IntoElement for MenuHandle<M> {
308 type Element = Self;
309
310 fn element_id(&self) -> Option<gpui::ElementId> {
311 Some(self.id.clone())
312 }
313
314 fn into_element(self) -> Self::Element {
315 self
316 }
317}