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