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