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