1use std::{cell::RefCell, rc::Rc};
2
3use gpui::{
4 anchored, deferred, div, prelude::FluentBuilder, px, AnyElement, App, Context, Corner,
5 DismissEvent, Element, ElementId, Entity, Focusable, GlobalElementId, InspectorElementId,
6 InteractiveElement, IntoElement, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
7 StyleRefinement, Styled, Subscription, Window,
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<Box<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(Box::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()
172 .position(position)
173 .snap_to_window_with_margin(px(8.))
174 .anchor(anchor)
175 .when_some(menu_view, |this, menu| {
176 if !menu.focus_handle(cx).contains_focused(window, cx) {
178 menu.focus_handle(cx).focus(window);
179 }
180
181 this.child(div().occlude().child(menu.clone()))
182 }),
183 )
184 .with_priority(1)
185 .into_any(),
186 );
187 }
188 }
189
190 let mut element = this
191 .element
192 .take()
193 .expect("Element should exists.")
194 .children(menu_element)
195 .into_any_element();
196
197 let layout_id = element.request_layout(window, cx);
198
199 (
200 layout_id,
201 ContextMenuState {
202 element: Some(element),
203 ..Default::default()
204 },
205 )
206 },
207 )
208 }
209
210 fn prepaint(
211 &mut self,
212 _: Option<&gpui::GlobalElementId>,
213 _: Option<&InspectorElementId>,
214 _: gpui::Bounds<gpui::Pixels>,
215 request_layout: &mut Self::RequestLayoutState,
216 window: &mut Window,
217 cx: &mut App,
218 ) -> Self::PrepaintState {
219 if let Some(element) = &mut request_layout.element {
220 element.prepaint(window, cx);
221 }
222 }
223
224 fn paint(
225 &mut self,
226 id: Option<&gpui::GlobalElementId>,
227 _: Option<&InspectorElementId>,
228 bounds: gpui::Bounds<gpui::Pixels>,
229 request_layout: &mut Self::RequestLayoutState,
230 _: &mut Self::PrepaintState,
231 window: &mut Window,
232 cx: &mut App,
233 ) {
234 if let Some(element) = &mut request_layout.element {
235 element.paint(window, cx);
236 }
237
238 let Some(builder) = self.menu.take() else {
239 return;
240 };
241
242 self.with_element_state(
243 id.unwrap(),
244 window,
245 cx,
246 |_view, state: &mut ContextMenuState, window, _| {
247 let shared_state = state.shared_state.clone();
248
249 window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
251 if phase.bubble()
252 && event.button == MouseButton::Right
253 && bounds.contains(&event.position)
254 {
255 {
256 let mut shared_state = shared_state.borrow_mut();
257 shared_state.position = event.position;
258 shared_state.open = true;
259 }
260
261 let menu = PopupMenu::build(window, cx, |menu, window, cx| {
262 (builder)(menu, window, cx)
263 })
264 .into_element();
265
266 let _subscription = window.subscribe(&menu, cx, {
267 let shared_state = shared_state.clone();
268 move |_, _: &DismissEvent, window, _| {
269 shared_state.borrow_mut().open = false;
270 window.refresh();
271 }
272 });
273
274 shared_state.borrow_mut().menu_view = Some(menu.clone());
275 shared_state.borrow_mut()._subscription = Some(_subscription);
276 window.refresh();
277 }
278 });
279 },
280 );
281 }
282}