1use std::{cell::RefCell, rc::Rc};
2
3use gpui::{
4 overlay, point, prelude::FluentBuilder, px, rems, AnchorCorner, AnyElement, Bounds,
5 DismissEvent, DispatchPhase, Element, ElementContext, ElementId, InteractiveBounds,
6 IntoElement, LayoutId, ManagedView, MouseDownEvent, ParentElement, Pixels, Point, View,
7 VisualContext, WindowContext,
8};
9
10use crate::{Clickable, Selectable};
11
12pub trait PopoverTrigger: IntoElement + Clickable + Selectable + 'static {}
13
14impl<T: IntoElement + Clickable + Selectable + 'static> PopoverTrigger for T {}
15
16pub struct PopoverMenu<M: ManagedView> {
17 id: ElementId,
18 child_builder: Option<
19 Box<
20 dyn FnOnce(
21 Rc<RefCell<Option<View<M>>>>,
22 Option<Rc<dyn Fn(&mut WindowContext) -> Option<View<M>> + 'static>>,
23 ) -> AnyElement
24 + 'static,
25 >,
26 >,
27 menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> Option<View<M>> + 'static>>,
28 anchor: AnchorCorner,
29 attach: Option<AnchorCorner>,
30 offset: Option<Point<Pixels>>,
31}
32
33impl<M: ManagedView> PopoverMenu<M> {
34 pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> Option<View<M>> + 'static) -> Self {
35 self.menu_builder = Some(Rc::new(f));
36 self
37 }
38
39 pub fn trigger<T: PopoverTrigger>(mut self, t: T) -> Self {
40 self.child_builder = Some(Box::new(|menu, builder| {
41 let open = menu.borrow().is_some();
42 t.selected(open)
43 .when_some(builder, |el, builder| {
44 el.on_click({
45 move |_, cx| {
46 let Some(new_menu) = (builder)(cx) else {
47 return;
48 };
49 let menu2 = menu.clone();
50 let previous_focus_handle = cx.focused();
51
52 cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| {
53 if modal.focus_handle(cx).contains_focused(cx) {
54 if previous_focus_handle.is_some() {
55 cx.focus(previous_focus_handle.as_ref().unwrap())
56 }
57 }
58 *menu2.borrow_mut() = None;
59 cx.refresh();
60 })
61 .detach();
62 cx.focus_view(&new_menu);
63 *menu.borrow_mut() = Some(new_menu);
64 }
65 })
66 })
67 .into_any_element()
68 }));
69 self
70 }
71
72 /// anchor defines which corner of the menu to anchor to the attachment point
73 /// (by default the cursor position, but see attach)
74 pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
75 self.anchor = anchor;
76 self
77 }
78
79 /// attach defines which corner of the handle to attach the menu's anchor to
80 pub fn attach(mut self, attach: AnchorCorner) -> Self {
81 self.attach = Some(attach);
82 self
83 }
84
85 /// offset offsets the position of the content by that many pixels.
86 pub fn offset(mut self, offset: Point<Pixels>) -> Self {
87 self.offset = Some(offset);
88 self
89 }
90
91 fn resolved_attach(&self) -> AnchorCorner {
92 self.attach.unwrap_or_else(|| match self.anchor {
93 AnchorCorner::TopLeft => AnchorCorner::BottomLeft,
94 AnchorCorner::TopRight => AnchorCorner::BottomRight,
95 AnchorCorner::BottomLeft => AnchorCorner::TopLeft,
96 AnchorCorner::BottomRight => AnchorCorner::TopRight,
97 })
98 }
99
100 fn resolved_offset(&self, cx: &WindowContext) -> Point<Pixels> {
101 self.offset.unwrap_or_else(|| {
102 // Default offset = 4px padding + 1px border
103 let offset = rems(5. / 16.) * cx.rem_size();
104 match self.anchor {
105 AnchorCorner::TopRight | AnchorCorner::BottomRight => point(offset, px(0.)),
106 AnchorCorner::TopLeft | AnchorCorner::BottomLeft => point(-offset, px(0.)),
107 }
108 })
109 }
110}
111
112/// Creates a [`PopoverMenu`]
113pub fn popover_menu<M: ManagedView>(id: impl Into<ElementId>) -> PopoverMenu<M> {
114 PopoverMenu {
115 id: id.into(),
116 child_builder: None,
117 menu_builder: None,
118 anchor: AnchorCorner::TopLeft,
119 attach: None,
120 offset: None,
121 }
122}
123
124pub struct PopoverMenuState<M> {
125 child_layout_id: Option<LayoutId>,
126 child_element: Option<AnyElement>,
127 child_bounds: Option<Bounds<Pixels>>,
128 menu_element: Option<AnyElement>,
129 menu: Rc<RefCell<Option<View<M>>>>,
130}
131
132impl<M: ManagedView> Element for PopoverMenu<M> {
133 type State = PopoverMenuState<M>;
134
135 fn request_layout(
136 &mut self,
137 element_state: Option<Self::State>,
138 cx: &mut ElementContext,
139 ) -> (gpui::LayoutId, Self::State) {
140 let mut menu_layout_id = None;
141
142 let (menu, child_bounds) = if let Some(element_state) = element_state {
143 (element_state.menu, element_state.child_bounds)
144 } else {
145 (Rc::default(), None)
146 };
147
148 let menu_element = menu.borrow_mut().as_mut().map(|menu| {
149 let mut overlay = overlay().snap_to_window().anchor(self.anchor);
150
151 if let Some(child_bounds) = child_bounds {
152 overlay = overlay.position(
153 self.resolved_attach().corner(child_bounds) + self.resolved_offset(cx),
154 );
155 }
156
157 let mut element = overlay.child(menu.clone()).into_any();
158 menu_layout_id = Some(element.request_layout(cx));
159 element
160 });
161
162 let mut child_element = self
163 .child_builder
164 .take()
165 .map(|child_builder| (child_builder)(menu.clone(), self.menu_builder.clone()));
166
167 let child_layout_id = child_element
168 .as_mut()
169 .map(|child_element| child_element.request_layout(cx));
170
171 let layout_id = cx.request_layout(
172 &gpui::Style::default(),
173 menu_layout_id.into_iter().chain(child_layout_id),
174 );
175
176 (
177 layout_id,
178 PopoverMenuState {
179 menu,
180 child_element,
181 child_layout_id,
182 menu_element,
183 child_bounds,
184 },
185 )
186 }
187
188 fn paint(
189 &mut self,
190 _: Bounds<gpui::Pixels>,
191 element_state: &mut Self::State,
192 cx: &mut ElementContext,
193 ) {
194 if let Some(mut child) = element_state.child_element.take() {
195 child.paint(cx);
196 }
197
198 if let Some(child_layout_id) = element_state.child_layout_id.take() {
199 element_state.child_bounds = Some(cx.layout_bounds(child_layout_id));
200 }
201
202 if let Some(mut menu) = element_state.menu_element.take() {
203 menu.paint(cx);
204
205 if let Some(child_bounds) = element_state.child_bounds {
206 let interactive_bounds = InteractiveBounds {
207 bounds: child_bounds,
208 stacking_order: cx.stacking_order().clone(),
209 };
210
211 // Mouse-downing outside the menu dismisses it, so we don't
212 // want a click on the toggle to re-open it.
213 cx.on_mouse_event(move |e: &MouseDownEvent, phase, cx| {
214 if phase == DispatchPhase::Bubble
215 && interactive_bounds.visibly_contains(&e.position, cx)
216 {
217 cx.stop_propagation()
218 }
219 })
220 }
221 }
222 }
223}
224
225impl<M: ManagedView> IntoElement for PopoverMenu<M> {
226 type Element = Self;
227
228 fn element_id(&self) -> Option<gpui::ElementId> {
229 Some(self.id.clone())
230 }
231
232 fn into_element(self) -> Self::Element {
233 self
234 }
235}