1#![allow(missing_docs)]
2
3use std::{cell::RefCell, rc::Rc};
4
5use gpui::{
6 anchored, deferred, div, point, prelude::FluentBuilder, px, size, AnyElement, Bounds, Corner,
7 DismissEvent, DispatchPhase, Element, ElementId, GlobalElementId, HitboxId, InteractiveElement,
8 IntoElement, LayoutId, Length, ManagedView, MouseDownEvent, ParentElement, Pixels, Point,
9 Style, View, VisualContext, WindowContext,
10};
11
12use crate::prelude::*;
13
14pub trait PopoverTrigger: IntoElement + Clickable + Toggleable + 'static {}
15
16impl<T: IntoElement + Clickable + Toggleable + 'static> PopoverTrigger for T {}
17
18pub struct PopoverMenuHandle<M>(Rc<RefCell<Option<PopoverMenuHandleState<M>>>>);
19
20impl<M> Clone for PopoverMenuHandle<M> {
21 fn clone(&self) -> Self {
22 Self(self.0.clone())
23 }
24}
25
26impl<M> Default for PopoverMenuHandle<M> {
27 fn default() -> Self {
28 Self(Rc::default())
29 }
30}
31
32struct PopoverMenuHandleState<M> {
33 menu_builder: Rc<dyn Fn(&mut WindowContext) -> Option<View<M>>>,
34 menu: Rc<RefCell<Option<View<M>>>>,
35}
36
37impl<M: ManagedView> PopoverMenuHandle<M> {
38 pub fn show(&self, cx: &mut WindowContext) {
39 if let Some(state) = self.0.borrow().as_ref() {
40 show_menu(&state.menu_builder, &state.menu, cx);
41 }
42 }
43
44 pub fn hide(&self, cx: &mut WindowContext) {
45 if let Some(state) = self.0.borrow().as_ref() {
46 if let Some(menu) = state.menu.borrow().as_ref() {
47 menu.update(cx, |_, cx| cx.emit(DismissEvent));
48 }
49 }
50 }
51
52 pub fn toggle(&self, cx: &mut WindowContext) {
53 if let Some(state) = self.0.borrow().as_ref() {
54 if state.menu.borrow().is_some() {
55 self.hide(cx);
56 } else {
57 self.show(cx);
58 }
59 }
60 }
61
62 pub fn is_deployed(&self) -> bool {
63 self.0
64 .borrow()
65 .as_ref()
66 .map_or(false, |state| state.menu.borrow().as_ref().is_some())
67 }
68
69 pub fn is_focused(&self, cx: &WindowContext) -> bool {
70 self.0.borrow().as_ref().map_or(false, |state| {
71 state
72 .menu
73 .borrow()
74 .as_ref()
75 .map_or(false, |view| view.focus_handle(cx).is_focused(cx))
76 })
77 }
78}
79
80pub struct PopoverMenu<M: ManagedView> {
81 id: ElementId,
82 child_builder: Option<
83 Box<
84 dyn FnOnce(
85 Rc<RefCell<Option<View<M>>>>,
86 Option<Rc<dyn Fn(&mut WindowContext) -> Option<View<M>> + 'static>>,
87 ) -> AnyElement
88 + 'static,
89 >,
90 >,
91 menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> Option<View<M>> + 'static>>,
92 anchor: Corner,
93 attach: Option<Corner>,
94 offset: Option<Point<Pixels>>,
95 trigger_handle: Option<PopoverMenuHandle<M>>,
96 full_width: bool,
97}
98
99impl<M: ManagedView> PopoverMenu<M> {
100 /// Returns a new [`PopoverMenu`].
101 pub fn new(id: impl Into<ElementId>) -> Self {
102 Self {
103 id: id.into(),
104 child_builder: None,
105 menu_builder: None,
106 anchor: Corner::TopLeft,
107 attach: None,
108 offset: None,
109 trigger_handle: None,
110 full_width: false,
111 }
112 }
113
114 pub fn full_width(mut self, full_width: bool) -> Self {
115 self.full_width = full_width;
116 self
117 }
118
119 pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> Option<View<M>> + 'static) -> Self {
120 self.menu_builder = Some(Rc::new(f));
121 self
122 }
123
124 pub fn with_handle(mut self, handle: PopoverMenuHandle<M>) -> Self {
125 self.trigger_handle = Some(handle);
126 self
127 }
128
129 pub fn trigger<T: PopoverTrigger>(mut self, t: T) -> Self {
130 self.child_builder = Some(Box::new(|menu, builder| {
131 let open = menu.borrow().is_some();
132 t.toggle_state(open)
133 .when_some(builder, |el, builder| {
134 el.on_click(move |_, cx| show_menu(&builder, &menu, cx))
135 })
136 .into_any_element()
137 }));
138 self
139 }
140
141 /// anchor defines which corner of the menu to anchor to the attachment point
142 /// (by default the cursor position, but see attach)
143 pub fn anchor(mut self, anchor: Corner) -> Self {
144 self.anchor = anchor;
145 self
146 }
147
148 /// attach defines which corner of the handle to attach the menu's anchor to
149 pub fn attach(mut self, attach: Corner) -> Self {
150 self.attach = Some(attach);
151 self
152 }
153
154 /// offset offsets the position of the content by that many pixels.
155 pub fn offset(mut self, offset: Point<Pixels>) -> Self {
156 self.offset = Some(offset);
157 self
158 }
159
160 fn resolved_attach(&self) -> Corner {
161 self.attach.unwrap_or(match self.anchor {
162 Corner::TopLeft => Corner::BottomLeft,
163 Corner::TopRight => Corner::BottomRight,
164 Corner::BottomLeft => Corner::TopLeft,
165 Corner::BottomRight => Corner::TopRight,
166 })
167 }
168
169 fn resolved_offset(&self, cx: &WindowContext) -> Point<Pixels> {
170 self.offset.unwrap_or_else(|| {
171 // Default offset = 4px padding + 1px border
172 let offset = rems_from_px(5.) * cx.rem_size();
173 match self.anchor {
174 Corner::TopRight | Corner::BottomRight => point(offset, px(0.)),
175 Corner::TopLeft | Corner::BottomLeft => point(-offset, px(0.)),
176 }
177 })
178 }
179}
180
181fn show_menu<M: ManagedView>(
182 builder: &Rc<dyn Fn(&mut WindowContext) -> Option<View<M>>>,
183 menu: &Rc<RefCell<Option<View<M>>>>,
184 cx: &mut WindowContext,
185) {
186 let Some(new_menu) = (builder)(cx) else {
187 return;
188 };
189 let menu2 = menu.clone();
190 let previous_focus_handle = cx.focused();
191
192 cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| {
193 if modal.focus_handle(cx).contains_focused(cx) {
194 if let Some(previous_focus_handle) = previous_focus_handle.as_ref() {
195 cx.focus(previous_focus_handle);
196 }
197 }
198 *menu2.borrow_mut() = None;
199 cx.refresh();
200 })
201 .detach();
202 cx.focus_view(&new_menu);
203 *menu.borrow_mut() = Some(new_menu);
204 cx.refresh();
205}
206
207pub struct PopoverMenuElementState<M> {
208 menu: Rc<RefCell<Option<View<M>>>>,
209 child_bounds: Option<Bounds<Pixels>>,
210}
211
212impl<M> Clone for PopoverMenuElementState<M> {
213 fn clone(&self) -> Self {
214 Self {
215 menu: Rc::clone(&self.menu),
216 child_bounds: self.child_bounds,
217 }
218 }
219}
220
221impl<M> Default for PopoverMenuElementState<M> {
222 fn default() -> Self {
223 Self {
224 menu: Rc::default(),
225 child_bounds: None,
226 }
227 }
228}
229
230pub struct PopoverMenuFrameState<M: ManagedView> {
231 child_layout_id: Option<LayoutId>,
232 child_element: Option<AnyElement>,
233 menu_element: Option<AnyElement>,
234 menu_handle: Rc<RefCell<Option<View<M>>>>,
235}
236
237impl<M: ManagedView> Element for PopoverMenu<M> {
238 type RequestLayoutState = PopoverMenuFrameState<M>;
239 type PrepaintState = Option<HitboxId>;
240
241 fn id(&self) -> Option<ElementId> {
242 Some(self.id.clone())
243 }
244
245 fn request_layout(
246 &mut self,
247 global_id: Option<&GlobalElementId>,
248 cx: &mut WindowContext,
249 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
250 cx.with_element_state(
251 global_id.unwrap(),
252 |element_state: Option<PopoverMenuElementState<M>>, cx| {
253 let element_state = element_state.unwrap_or_default();
254 let mut menu_layout_id = None;
255
256 let menu_element = element_state.menu.borrow_mut().as_mut().map(|menu| {
257 let mut anchored = anchored()
258 .snap_to_window_with_margin(px(8.))
259 .anchor(self.anchor);
260 if let Some(child_bounds) = element_state.child_bounds {
261 anchored = anchored.position(
262 child_bounds.corner(self.resolved_attach()) + self.resolved_offset(cx),
263 );
264 }
265 let mut element = deferred(anchored.child(div().occlude().child(menu.clone())))
266 .with_priority(1)
267 .into_any();
268
269 menu_layout_id = Some(element.request_layout(cx));
270 element
271 });
272
273 let mut child_element = self.child_builder.take().map(|child_builder| {
274 (child_builder)(element_state.menu.clone(), self.menu_builder.clone())
275 });
276
277 if let Some(trigger_handle) = self.trigger_handle.take() {
278 if let Some(menu_builder) = self.menu_builder.clone() {
279 *trigger_handle.0.borrow_mut() = Some(PopoverMenuHandleState {
280 menu_builder,
281 menu: element_state.menu.clone(),
282 });
283 }
284 }
285
286 let child_layout_id = child_element
287 .as_mut()
288 .map(|child_element| child_element.request_layout(cx));
289
290 let mut style = Style::default();
291 if self.full_width {
292 style.size = size(relative(1.).into(), Length::Auto);
293 }
294
295 let layout_id =
296 cx.request_layout(style, menu_layout_id.into_iter().chain(child_layout_id));
297
298 (
299 (
300 layout_id,
301 PopoverMenuFrameState {
302 child_element,
303 child_layout_id,
304 menu_element,
305 menu_handle: element_state.menu.clone(),
306 },
307 ),
308 element_state,
309 )
310 },
311 )
312 }
313
314 fn prepaint(
315 &mut self,
316 global_id: Option<&GlobalElementId>,
317 _bounds: Bounds<Pixels>,
318 request_layout: &mut Self::RequestLayoutState,
319 cx: &mut WindowContext,
320 ) -> Option<HitboxId> {
321 if let Some(child) = request_layout.child_element.as_mut() {
322 child.prepaint(cx);
323 }
324
325 if let Some(menu) = request_layout.menu_element.as_mut() {
326 menu.prepaint(cx);
327 }
328
329 request_layout.child_layout_id.map(|layout_id| {
330 let bounds = cx.layout_bounds(layout_id);
331 cx.with_element_state(global_id.unwrap(), |element_state, _cx| {
332 let mut element_state: PopoverMenuElementState<M> = element_state.unwrap();
333 element_state.child_bounds = Some(bounds);
334 ((), element_state)
335 });
336
337 cx.insert_hitbox(bounds, false).id
338 })
339 }
340
341 fn paint(
342 &mut self,
343 _id: Option<&GlobalElementId>,
344 _: Bounds<gpui::Pixels>,
345 request_layout: &mut Self::RequestLayoutState,
346 child_hitbox: &mut Option<HitboxId>,
347 cx: &mut WindowContext,
348 ) {
349 if let Some(mut child) = request_layout.child_element.take() {
350 child.paint(cx);
351 }
352
353 if let Some(mut menu) = request_layout.menu_element.take() {
354 menu.paint(cx);
355
356 if let Some(child_hitbox) = *child_hitbox {
357 let menu_handle = request_layout.menu_handle.clone();
358 // Mouse-downing outside the menu dismisses it, so we don't
359 // want a click on the toggle to re-open it.
360 cx.on_mouse_event(move |_: &MouseDownEvent, phase, cx| {
361 if phase == DispatchPhase::Bubble && child_hitbox.is_hovered(cx) {
362 if let Some(menu) = menu_handle.borrow().as_ref() {
363 menu.update(cx, |_, cx| {
364 cx.emit(DismissEvent);
365 });
366 }
367 cx.stop_propagation();
368 }
369 })
370 }
371 }
372 }
373}
374
375impl<M: ManagedView> IntoElement for PopoverMenu<M> {
376 type Element = Self;
377
378 fn into_element(self) -> Self::Element {
379 self
380 }
381}