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
42pub fn right_click_menu<M: ManagedView>(id: impl Into<ElementId>) -> RightClickMenu<M> {
43 RightClickMenu {
44 id: id.into(),
45 child_builder: None,
46 menu_builder: None,
47 anchor: None,
48 attach: None,
49 }
50}
51
52pub struct MenuHandleState<M> {
53 menu: Rc<RefCell<Option<View<M>>>>,
54 position: Rc<RefCell<Point<Pixels>>>,
55 child_layout_id: Option<LayoutId>,
56 child_element: Option<AnyElement>,
57 menu_element: Option<AnyElement>,
58}
59
60impl<M: ManagedView> Element for RightClickMenu<M> {
61 type State = MenuHandleState<M>;
62
63 fn layout(
64 &mut self,
65 element_state: Option<Self::State>,
66 cx: &mut WindowContext,
67 ) -> (gpui::LayoutId, Self::State) {
68 let (menu, position) = if let Some(element_state) = element_state {
69 (element_state.menu, element_state.position)
70 } else {
71 (Rc::default(), Rc::default())
72 };
73
74 let mut menu_layout_id = None;
75
76 let menu_element = menu.borrow_mut().as_mut().map(|menu| {
77 let mut overlay = overlay().snap_to_window();
78 if let Some(anchor) = self.anchor {
79 overlay = overlay.anchor(anchor);
80 }
81 overlay = overlay.position(*position.borrow());
82
83 let mut element = overlay.child(menu.clone()).into_any();
84 menu_layout_id = Some(element.layout(cx));
85 element
86 });
87
88 let mut child_element = self
89 .child_builder
90 .take()
91 .map(|child_builder| (child_builder)(menu.borrow().is_some()));
92
93 let child_layout_id = child_element
94 .as_mut()
95 .map(|child_element| child_element.layout(cx));
96
97 let layout_id = cx.request_layout(
98 &gpui::Style::default(),
99 menu_layout_id.into_iter().chain(child_layout_id),
100 );
101
102 (
103 layout_id,
104 MenuHandleState {
105 menu,
106 position,
107 child_element,
108 child_layout_id,
109 menu_element,
110 },
111 )
112 }
113
114 fn paint(
115 self,
116 bounds: Bounds<gpui::Pixels>,
117 element_state: &mut Self::State,
118 cx: &mut WindowContext,
119 ) {
120 if let Some(child) = element_state.child_element.take() {
121 child.paint(cx);
122 }
123
124 if let Some(menu) = element_state.menu_element.take() {
125 menu.paint(cx);
126 return;
127 }
128
129 let Some(builder) = self.menu_builder else {
130 return;
131 };
132 let menu = element_state.menu.clone();
133 let position = element_state.position.clone();
134 let attach = self.attach.clone();
135 let child_layout_id = element_state.child_layout_id.clone();
136
137 cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
138 if phase == DispatchPhase::Bubble
139 && event.button == MouseButton::Right
140 && bounds.contains_point(&event.position)
141 {
142 cx.stop_propagation();
143 cx.prevent_default();
144
145 let new_menu = (builder)(cx);
146 let menu2 = menu.clone();
147 let previous_focus_handle = cx.focused();
148
149 cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| {
150 if modal.focus_handle(cx).contains_focused(cx) {
151 if previous_focus_handle.is_some() {
152 cx.focus(&previous_focus_handle.as_ref().unwrap())
153 }
154 }
155 *menu2.borrow_mut() = None;
156 cx.notify();
157 })
158 .detach();
159 cx.focus_view(&new_menu);
160 *menu.borrow_mut() = Some(new_menu);
161
162 *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
163 attach
164 .unwrap()
165 .corner(cx.layout_bounds(child_layout_id.unwrap()))
166 } else {
167 cx.mouse_position()
168 };
169 cx.notify();
170 }
171 });
172 }
173}
174
175impl<M: ManagedView> IntoElement for RightClickMenu<M> {
176 type Element = Self;
177
178 fn element_id(&self) -> Option<gpui::ElementId> {
179 Some(self.id.clone())
180 }
181
182 fn into_element(self) -> Self::Element {
183 self
184 }
185}