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