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