1use std::cell::Cell;
46use std::rc::Rc;
47use std::sync::Arc;
48
49use gpui::{
50 px, size, AnyElement, App, Bounds, InteractiveElement, IntoElement, MouseButton, ParentElement,
51 Pixels, RenderOnce, SharedString, Styled, Window,
52};
53use herogpui_core::{element_id, Placement};
54
55use crate::{Menu, MenuItem};
56
57type Callback<T> = Arc<dyn Fn(&T, &mut Window, &mut App) + 'static>;
58type RowContent<T> = Box<dyn Fn(&SharedString, crate::util::InteractiveState) -> T + 'static>;
59
60#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
62#[derive(IntoElement)]
63pub struct ContextMenu {
64 id: gpui::ElementId,
65 child: AnyElement,
66 items: Vec<MenuItem>,
67 disabled_keys: Vec<SharedString>,
68 is_disabled: bool,
69 on_action: Option<Callback<SharedString>>,
70 on_open_change: Option<Callback<bool>>,
71 recipes: Vec<SharedString>,
72 item_content: Option<RowContent<AnyElement>>,
73 item_start_content: Option<RowContent<Option<AnyElement>>>,
74}
75
76impl ContextMenu {
77 pub fn new(
80 id: impl Into<gpui::ElementId>,
81 child: impl IntoElement,
82 items: Vec<MenuItem>,
83 ) -> Self {
84 Self {
85 id: id.into(),
86 child: child.into_any_element(),
87 items,
88 disabled_keys: Vec::new(),
89 is_disabled: false,
90 on_action: None,
91 on_open_change: None,
92 recipes: Vec::new(),
93 item_content: None,
94 item_start_content: None,
95 }
96 }
97
98 pub fn recipe(mut self, name: impl Into<SharedString>) -> Self {
102 self.recipes.push(name.into());
103 self
104 }
105
106 pub fn item_content(
109 mut self,
110 render: impl Fn(&SharedString, crate::util::InteractiveState) -> AnyElement + 'static,
111 ) -> Self {
112 self.item_content = Some(Box::new(render));
113 self
114 }
115
116 pub fn item_start_content(
119 mut self,
120 render: impl Fn(&SharedString, crate::util::InteractiveState) -> Option<AnyElement> + 'static,
121 ) -> Self {
122 self.item_start_content = Some(Box::new(render));
123 self
124 }
125
126 pub fn on_action(mut self, f: impl Fn(&SharedString, &mut Window, &mut App) + 'static) -> Self {
128 self.on_action = Some(Arc::new(f));
129 self
130 }
131
132 pub fn on_open_change(mut self, f: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
134 self.on_open_change = Some(Arc::new(f));
135 self
136 }
137
138 pub fn disabled_keys(
140 mut self,
141 keys: impl IntoIterator<Item = impl Into<SharedString>>,
142 ) -> Self {
143 self.disabled_keys = keys.into_iter().map(Into::into).collect();
144 self
145 }
146
147 pub fn is_disabled(mut self, v: bool) -> Self {
150 self.is_disabled = v;
151 self
152 }
153}
154
155fn set_open(
156 own: &Option<gpui::Entity<bool>>,
157 on_open_change: &Option<Callback<bool>>,
158 open: bool,
159 window: &mut Window,
160 cx: &mut App,
161) {
162 if let Some(held) = own {
163 held.update(cx, |v, cx| {
164 *v = open;
165 cx.notify();
166 });
167 }
168 if let Some(cb) = on_open_change {
169 cb(&open, window, cx);
170 }
171}
172
173fn opens_context_menu(keystroke: &gpui::Keystroke) -> bool {
175 let m = &keystroke.modifiers;
176 if m.control || m.alt || m.platform || m.function {
177 return false;
178 }
179 match keystroke.key.as_str() {
180 "f10" => m.shift,
181 "menu" | "contextmenu" => !m.shift,
182 _ => false,
183 }
184}
185
186impl RenderOnce for ContextMenu {
187 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
188 let base = self.id.clone();
189 let (is_open, own) =
190 crate::util::controlled(window, cx, element_id::scoped(&base, "open"), None, false);
191 let (phase, overlay_token) = crate::util::overlay_scope(
192 window,
193 cx,
194 element_id::scoped(&base, "phase"),
195 is_open,
196 true,
197 );
198 let anchor = window
202 .use_keyed_state(element_id::scoped(&base, "anchor"), cx, |_, _| {
203 Rc::new(Cell::new(None::<Bounds<Pixels>>))
204 })
205 .read(cx)
206 .clone();
207 let focus_first =
208 window.use_keyed_state(element_id::scoped(&base, "focus-first"), cx, |_, _| false);
209
210 let area_focus = window
211 .use_keyed_state(element_id::scoped(&base, "area-focus"), cx, |_, cx| {
212 cx.focus_handle().tab_stop(false)
213 })
214 .read(cx)
215 .clone();
216 let area_bounds = window
218 .use_keyed_state(element_id::scoped(&base, "area-bounds"), cx, |_, _| {
219 Rc::new(Cell::new(None::<Bounds<Pixels>>))
220 })
221 .read(cx)
222 .clone();
223 let restore_focus = window
225 .use_keyed_state(element_id::scoped(&base, "restore-focus"), cx, |_, _| {
226 Rc::new(std::cell::RefCell::new(None::<gpui::FocusHandle>))
227 })
228 .read(cx)
229 .clone();
230
231 let bounds_probe = area_bounds.clone();
232 let mut area = gpui::div()
233 .id(element_id::scoped(&base, "area"))
234 .relative()
235 .track_focus(&area_focus)
236 .child(self.child)
237 .child(
238 gpui::canvas(
239 move |bounds, _, _| bounds_probe.set(Some(bounds)),
240 |_, _, _, _| {},
241 )
242 .absolute()
243 .inset_0(),
244 );
245 if !self.is_disabled {
246 let focus = area_focus;
247 area = area.on_mouse_down(MouseButton::Left, move |_, window, cx| {
248 if !focus.contains_focused(window, cx) {
249 window.focus(&focus, cx);
250 }
251 });
252 let anchor_key = anchor.clone();
253 let own_key = own.clone();
254 let on_open_change_key = self.on_open_change.clone();
255 let focus_first_key = focus_first.clone();
256 let restore_key = restore_focus.clone();
257 area = area.on_key_down(move |event, window, cx| {
258 if is_open || !opens_context_menu(&event.keystroke) {
259 return;
260 }
261 let Some(bounds) = area_bounds.get() else {
262 return;
263 };
264 let target = crate::util::focused_element_bounds(window, cx)
265 .filter(|focused| bounds.intersects(focused))
266 .unwrap_or(Bounds::new(bounds.origin, size(px(0.), px(0.))));
267 anchor_key.set(Some(target));
268 *restore_key.borrow_mut() = window.focused(cx);
269 focus_first_key.update(cx, |focus, _| *focus = true);
270 set_open(&own_key, &on_open_change_key, true, window, cx);
271 cx.stop_propagation();
272 });
273 let anchor = anchor.clone();
274 let own = own.clone();
275 let on_open_change = self.on_open_change.clone();
276 let pointer_focus_first = focus_first.clone();
277 let pointer_restore = restore_focus.clone();
278 area = area.on_mouse_down(MouseButton::Right, move |event, window, cx| {
279 anchor.set(Some(Bounds::new(event.position, size(px(0.), px(0.)))));
280 pointer_focus_first.update(cx, |focus, _| *focus = false);
281 pointer_restore.borrow_mut().take();
282 set_open(&own, &on_open_change, true, window, cx);
283 cx.stop_propagation();
284 });
285 }
286
287 let mut root = gpui::div().relative().child(area);
288 if phase != crate::util::OverlayPhase::Closed {
289 let mut menu = Menu::new(element_id::scoped(&base, "menu-content"), self.items)
290 .id(element_id::scoped(&base, "menu"))
291 .dropdown_composition()
292 .focus_first(focus_first)
293 .exiting(phase == crate::util::OverlayPhase::Exiting)
294 .disabled_keys(self.disabled_keys)
295 .overlay_token(overlay_token);
296 for recipe in self.recipes {
297 menu = menu.recipe(recipe);
298 }
299 if let Some(render) = self.item_content {
300 menu = menu.item_content(render);
301 }
302 if let Some(render) = self.item_start_content {
303 menu = menu.item_start_content(render);
304 }
305 if let Some(on_action) = self.on_action {
306 menu = menu.on_action(move |key, window, cx| on_action(key, window, cx));
307 }
308 let on_open_change = self.on_open_change;
309 menu = menu.on_dismiss(move |_refocus, window, cx| {
310 set_open(&own, &on_open_change, false, window, cx);
311 let restore = restore_focus.borrow_mut().take();
316 if let Some(handle) = restore {
317 window.focus(&handle, cx);
318 }
319 });
320 root = root.child(crate::util::floating(
321 crate::popover::popover_with_resolved_placement(
322 anchor,
323 Placement::BottomStart,
324 px(0.),
325 None,
326 menu.panel_debug_label("context-menu"),
327 ),
328 ));
329 }
330 root
331 }
332}