1use std::{cell::RefCell, rc::Rc};
2
3use gpui::{
4 overlay, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase, Element, ElementId,
5 IntoElement, LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
6 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 WindowContext,
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 WindowContext,
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.clone();
136 let child_layout_id = element_state.child_layout_id.clone();
137
138 cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
139 if phase == DispatchPhase::Bubble
140 && event.button == MouseButton::Right
141 && bounds.contains(&event.position)
142 {
143 cx.stop_propagation();
144 cx.prevent_default();
145
146 let new_menu = (builder)(cx);
147 let menu2 = menu.clone();
148 let previous_focus_handle = cx.focused();
149
150 cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| {
151 if modal.focus_handle(cx).contains_focused(cx) {
152 if previous_focus_handle.is_some() {
153 cx.focus(&previous_focus_handle.as_ref().unwrap())
154 }
155 }
156 *menu2.borrow_mut() = None;
157 cx.notify();
158 })
159 .detach();
160 cx.focus_view(&new_menu);
161 *menu.borrow_mut() = Some(new_menu);
162
163 *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
164 attach
165 .unwrap()
166 .corner(cx.layout_bounds(child_layout_id.unwrap()))
167 } else {
168 cx.mouse_position()
169 };
170 cx.notify();
171 }
172 });
173 }
174}
175
176impl<M: ManagedView> IntoElement for RightClickMenu<M> {
177 type Element = Self;
178
179 fn element_id(&self) -> Option<gpui::ElementId> {
180 Some(self.id.clone())
181 }
182
183 fn into_element(self) -> Self::Element {
184 self
185 }
186}