1use std::{cell::RefCell, rc::Rc};
2
3use gpui::{
4 AnyElement, App, Context, Corner, DismissEvent, Element, ElementId, Entity, Focusable,
5 GlobalElementId, InspectorElementId, InteractiveElement, IntoElement, MouseButton,
6 MouseDownEvent, ParentElement, Pixels, Point, StyleRefinement, Styled, Subscription, Window,
7 anchored, deferred, div, prelude::FluentBuilder, px,
8};
9
10use crate::menu::PopupMenu;
11
12pub trait ContextMenuExt: ParentElement + Styled {
14 fn context_menu(
19 self,
20 f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
21 ) -> ContextMenu<Self> {
22 ContextMenu::new("context-menu", self).menu(f)
23 }
24}
25
26impl<E: ParentElement + Styled> ContextMenuExt for E {}
27
28pub struct ContextMenu<E: ParentElement + Styled + Sized> {
30 id: ElementId,
31 element: Option<E>,
32 menu: Option<Rc<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu>>,
33 _ignore_style: StyleRefinement,
35 anchor: Corner,
36}
37
38impl<E: ParentElement + Styled> ContextMenu<E> {
39 pub fn new(id: impl Into<ElementId>, element: E) -> Self {
41 Self {
42 id: id.into(),
43 element: Some(element),
44 menu: None,
45 anchor: Corner::TopLeft,
46 _ignore_style: StyleRefinement::default(),
47 }
48 }
49
50 #[must_use]
52 fn menu<F>(mut self, builder: F) -> Self
53 where
54 F: Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
55 {
56 self.menu = Some(Rc::new(builder));
57 self
58 }
59
60 fn with_element_state<R>(
61 &mut self,
62 id: &GlobalElementId,
63 window: &mut Window,
64 cx: &mut App,
65 f: impl FnOnce(&mut Self, &mut ContextMenuState, &mut Window, &mut App) -> R,
66 ) -> R {
67 window.with_optional_element_state::<ContextMenuState, _>(
68 Some(id),
69 |element_state, window| {
70 let mut element_state = element_state.unwrap().unwrap_or_default();
71 let result = f(self, &mut element_state, window, cx);
72 (result, Some(element_state))
73 },
74 )
75 }
76}
77
78impl<E: ParentElement + Styled> ParentElement for ContextMenu<E> {
79 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
80 if let Some(element) = &mut self.element {
81 element.extend(elements);
82 }
83 }
84}
85
86impl<E: ParentElement + Styled> Styled for ContextMenu<E> {
87 fn style(&mut self) -> &mut StyleRefinement {
88 if let Some(element) = &mut self.element {
89 element.style()
90 } else {
91 &mut self._ignore_style
92 }
93 }
94}
95
96impl<E: ParentElement + Styled + IntoElement + 'static> IntoElement for ContextMenu<E> {
97 type Element = Self;
98
99 fn into_element(self) -> Self::Element {
100 self
101 }
102}
103
104struct ContextMenuSharedState {
105 menu_view: Option<Entity<PopupMenu>>,
106 open: bool,
107 position: Point<Pixels>,
108 _subscription: Option<Subscription>,
109}
110
111pub struct ContextMenuState {
112 element: Option<AnyElement>,
113 shared_state: Rc<RefCell<ContextMenuSharedState>>,
114}
115
116impl Default for ContextMenuState {
117 fn default() -> Self {
118 Self {
119 element: None,
120 shared_state: Rc::new(RefCell::new(ContextMenuSharedState {
121 menu_view: None,
122 open: false,
123 position: Default::default(),
124 _subscription: None,
125 })),
126 }
127 }
128}
129
130impl<E: ParentElement + Styled + IntoElement + 'static> Element for ContextMenu<E> {
131 type RequestLayoutState = ContextMenuState;
132 type PrepaintState = ();
133
134 fn id(&self) -> Option<ElementId> {
135 Some(self.id.clone())
136 }
137
138 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
139 None
140 }
141
142 fn request_layout(
143 &mut self,
144 id: Option<&gpui::GlobalElementId>,
145 _: Option<&gpui::InspectorElementId>,
146 window: &mut Window,
147 cx: &mut App,
148 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
149 let anchor = self.anchor;
150
151 self.with_element_state(
152 id.unwrap(),
153 window,
154 cx,
155 |this, state: &mut ContextMenuState, window, cx| {
156 let (position, open) = {
157 let shared_state = state.shared_state.borrow();
158 (shared_state.position, shared_state.open)
159 };
160 let menu_view = state.shared_state.borrow().menu_view.clone();
161 let mut menu_element = None;
162 if open {
163 let has_menu_item = menu_view
164 .as_ref()
165 .map(|menu| !menu.read(cx).is_empty())
166 .unwrap_or(false);
167
168 if has_menu_item {
169 menu_element = Some(
170 deferred(
171 anchored().child(
172 div()
173 .w(window.bounds().size.width)
174 .h(window.bounds().size.height)
175 .occlude()
176 .child(
177 anchored()
178 .position(position)
179 .snap_to_window_with_margin(px(8.))
180 .anchor(anchor)
181 .when_some(menu_view, |this, menu| {
182 if !menu
184 .focus_handle(cx)
185 .contains_focused(window, cx)
186 {
187 menu.focus_handle(cx).focus(window);
188 }
189
190 this.child(menu.clone())
191 }),
192 ),
193 ),
194 )
195 .with_priority(1)
196 .into_any(),
197 );
198 }
199 }
200
201 let mut element = this
202 .element
203 .take()
204 .expect("Element should exists.")
205 .children(menu_element)
206 .into_any_element();
207
208 let layout_id = element.request_layout(window, cx);
209
210 (
211 layout_id,
212 ContextMenuState {
213 element: Some(element),
214 ..Default::default()
215 },
216 )
217 },
218 )
219 }
220
221 fn prepaint(
222 &mut self,
223 _: Option<&gpui::GlobalElementId>,
224 _: Option<&InspectorElementId>,
225 _: gpui::Bounds<gpui::Pixels>,
226 request_layout: &mut Self::RequestLayoutState,
227 window: &mut Window,
228 cx: &mut App,
229 ) -> Self::PrepaintState {
230 if let Some(element) = &mut request_layout.element {
231 element.prepaint(window, cx);
232 }
233 }
234
235 fn paint(
236 &mut self,
237 id: Option<&gpui::GlobalElementId>,
238 _: Option<&InspectorElementId>,
239 bounds: gpui::Bounds<gpui::Pixels>,
240 request_layout: &mut Self::RequestLayoutState,
241 _: &mut Self::PrepaintState,
242 window: &mut Window,
243 cx: &mut App,
244 ) {
245 if let Some(element) = &mut request_layout.element {
246 element.paint(window, cx);
247 }
248
249 let builder = self.menu.clone();
251
252 self.with_element_state(
253 id.unwrap(),
254 window,
255 cx,
256 |_view, state: &mut ContextMenuState, window, _| {
257 let shared_state = state.shared_state.clone();
258
259 window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
261 if phase.bubble()
262 && event.button == MouseButton::Right
263 && bounds.contains(&event.position)
264 {
265 {
266 let mut shared_state = shared_state.borrow_mut();
267 shared_state.menu_view = None;
270 shared_state._subscription = None;
271 shared_state.position = event.position;
272 shared_state.open = true;
273 }
274
275 window.defer(cx, {
277 let shared_state = shared_state.clone();
278 let builder = builder.clone();
279 move |window, cx| {
280 let menu = PopupMenu::build(window, cx, move |menu, window, cx| {
281 let Some(build) = &builder else {
282 return menu;
283 };
284 build(menu, window, cx)
285 });
286
287 let _subscription = window.subscribe(&menu, cx, {
289 let shared_state = shared_state.clone();
290 move |_, _: &DismissEvent, window, _cx| {
291 shared_state.borrow_mut().open = false;
292 window.refresh();
293 }
294 });
295
296 {
298 let mut state = shared_state.borrow_mut();
299 state.menu_view = Some(menu.clone());
300 state._subscription = Some(_subscription);
301 window.refresh();
302 }
303 }
304 });
305 }
306 });
307 },
308 );
309 }
310}