1use std::{cell::RefCell, rc::Rc};
2
3use gpui::{
4 overlay, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase, Element,
5 ElementContext, ElementId, InteractiveBounds, IntoElement, LayoutId, ManagedView, MouseButton,
6 MouseDownEvent, ParentElement, Pixels, Point, View, VisualContext, WindowContext,
7};
8
9pub struct RightClickMenu<M: ManagedView> {
10 id: ElementId,
11 child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement + 'static>>,
12 menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
13 anchor: Option<AnchorCorner>,
14 attach: Option<AnchorCorner>,
15}
16
17impl<M: ManagedView> RightClickMenu<M> {
18 pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View<M> + 'static) -> Self {
19 self.menu_builder = Some(Rc::new(f));
20 self
21 }
22
23 pub fn trigger<E: IntoElement + 'static>(mut self, e: E) -> Self {
24 self.child_builder = Some(Box::new(move |_| e.into_any_element()));
25 self
26 }
27
28 /// anchor defines which corner of the menu to anchor to the attachment point
29 /// (by default the cursor position, but see attach)
30 pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
31 self.anchor = Some(anchor);
32 self
33 }
34
35 /// attach defines which corner of the handle to attach the menu's anchor to
36 pub fn attach(mut self, attach: AnchorCorner) -> Self {
37 self.attach = Some(attach);
38 self
39 }
40}
41
42/// Creates a [`RightClickMenu`]
43pub fn right_click_menu<M: ManagedView>(id: impl Into<ElementId>) -> RightClickMenu<M> {
44 RightClickMenu {
45 id: id.into(),
46 child_builder: None,
47 menu_builder: None,
48 anchor: None,
49 attach: None,
50 }
51}
52
53pub struct MenuHandleState<M> {
54 menu: Rc<RefCell<Option<View<M>>>>,
55 position: Rc<RefCell<Point<Pixels>>>,
56 child_layout_id: Option<LayoutId>,
57 child_element: Option<AnyElement>,
58 menu_element: Option<AnyElement>,
59}
60
61impl<M: ManagedView> Element for RightClickMenu<M> {
62 type State = MenuHandleState<M>;
63
64 fn request_layout(
65 &mut self,
66 element_state: Option<Self::State>,
67 cx: &mut ElementContext,
68 ) -> (gpui::LayoutId, Self::State) {
69 let (menu, position) = if let Some(element_state) = element_state {
70 (element_state.menu, element_state.position)
71 } else {
72 (Rc::default(), Rc::default())
73 };
74
75 let mut menu_layout_id = None;
76
77 let menu_element = menu.borrow_mut().as_mut().map(|menu| {
78 let mut overlay = overlay().snap_to_window();
79 if let Some(anchor) = self.anchor {
80 overlay = overlay.anchor(anchor);
81 }
82 overlay = overlay.position(*position.borrow());
83
84 let mut element = overlay.child(menu.clone()).into_any();
85 menu_layout_id = Some(element.request_layout(cx));
86 element
87 });
88
89 let mut child_element = self
90 .child_builder
91 .take()
92 .map(|child_builder| (child_builder)(menu.borrow().is_some()));
93
94 let child_layout_id = child_element
95 .as_mut()
96 .map(|child_element| child_element.request_layout(cx));
97
98 let layout_id = cx.request_layout(
99 &gpui::Style::default(),
100 menu_layout_id.into_iter().chain(child_layout_id),
101 );
102
103 (
104 layout_id,
105 MenuHandleState {
106 menu,
107 position,
108 child_element,
109 child_layout_id,
110 menu_element,
111 },
112 )
113 }
114
115 fn paint(
116 &mut self,
117 bounds: Bounds<gpui::Pixels>,
118 element_state: &mut Self::State,
119 cx: &mut ElementContext,
120 ) {
121 if let Some(mut child) = element_state.child_element.take() {
122 child.paint(cx);
123 }
124
125 if let Some(mut menu) = element_state.menu_element.take() {
126 menu.paint(cx);
127 return;
128 }
129
130 let Some(builder) = self.menu_builder.take() else {
131 return;
132 };
133 let menu = element_state.menu.clone();
134 let position = element_state.position.clone();
135 let attach = self.attach;
136 let child_layout_id = element_state.child_layout_id;
137 let child_bounds = cx.layout_bounds(child_layout_id.unwrap());
138
139 let interactive_bounds = InteractiveBounds {
140 bounds: bounds.intersect(&cx.content_mask().bounds),
141 stacking_order: cx.stacking_order().clone(),
142 };
143 cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
144 if phase == DispatchPhase::Bubble
145 && event.button == MouseButton::Right
146 && interactive_bounds.visibly_contains(&event.position, cx)
147 {
148 cx.stop_propagation();
149 cx.prevent_default();
150
151 let new_menu = (builder)(cx);
152 let menu2 = menu.clone();
153 let previous_focus_handle = cx.focused();
154
155 cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| {
156 if modal.focus_handle(cx).contains_focused(cx) {
157 if let Some(previous_focus_handle) = previous_focus_handle.as_ref() {
158 cx.focus(previous_focus_handle);
159 }
160 }
161 *menu2.borrow_mut() = None;
162 cx.refresh();
163 })
164 .detach();
165 cx.focus_view(&new_menu);
166 *menu.borrow_mut() = Some(new_menu);
167
168 *position.borrow_mut() =
169 if let Some(attach) = attach.filter(|_| child_layout_id.is_some()) {
170 attach.corner(child_bounds)
171 } else {
172 cx.mouse_position()
173 };
174 cx.refresh();
175 }
176 });
177 }
178}
179
180impl<M: ManagedView> IntoElement for RightClickMenu<M> {
181 type Element = Self;
182
183 fn element_id(&self) -> Option<gpui::ElementId> {
184 Some(self.id.clone())
185 }
186
187 fn into_element(self) -> Self::Element {
188 self
189 }
190}