1use std::{cell::RefCell, rc::Rc};
2
3use gpui::{
4 Anchor, AnyElement, AnyView, App, Bounds, DismissEvent, DispatchPhase, Element, ElementId,
5 Entity, Focusable as _, GlobalElementId, HitboxBehavior, HitboxId, InteractiveElement,
6 IntoElement, LayoutId, Length, ManagedView, MouseDownEvent, ParentElement, Pixels, Point,
7 Style, Window, anchored, deferred, div, point, prelude::FluentBuilder, px, size,
8};
9
10use crate::prelude::*;
11
12pub trait PopoverTrigger: IntoElement + Clickable + Toggleable + 'static {}
13
14impl<T: IntoElement + Clickable + Toggleable + 'static> PopoverTrigger for T {}
15
16impl<T: Clickable> Clickable for gpui::AnimationElement<T>
17where
18 T: Clickable + 'static,
19{
20 fn on_click(
21 self,
22 handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
23 ) -> Self {
24 self.map_element(|e| e.on_click(handler))
25 }
26
27 fn cursor_style(self, cursor_style: gpui::CursorStyle) -> Self {
28 self.map_element(|e| e.cursor_style(cursor_style))
29 }
30}
31
32impl<T: Toggleable> Toggleable for gpui::AnimationElement<T>
33where
34 T: Toggleable + 'static,
35{
36 fn toggle_state(self, selected: bool) -> Self {
37 self.map_element(|e| e.toggle_state(selected))
38 }
39}
40
41pub struct PopoverMenuHandle<M>(Rc<RefCell<Option<PopoverMenuHandleState<M>>>>);
42
43impl<M> Clone for PopoverMenuHandle<M> {
44 fn clone(&self) -> Self {
45 Self(self.0.clone())
46 }
47}
48
49impl<M> Default for PopoverMenuHandle<M> {
50 fn default() -> Self {
51 Self(Rc::default())
52 }
53}
54
55struct PopoverMenuHandleState<M> {
56 menu_builder: Rc<dyn Fn(&mut Window, &mut App) -> Option<Entity<M>>>,
57 menu: Rc<RefCell<Option<Entity<M>>>>,
58 on_open: Option<Rc<dyn Fn(&mut Window, &mut App)>>,
59}
60
61impl<M: ManagedView> PopoverMenuHandle<M> {
62 pub fn show(&self, window: &mut Window, cx: &mut App) {
63 if let Some(state) = self.0.borrow().as_ref() {
64 show_menu(
65 &state.menu_builder,
66 &state.menu,
67 state.on_open.clone(),
68 window,
69 cx,
70 );
71 }
72 }
73
74 pub fn hide(&self, cx: &mut App) {
75 if let Some(state) = self.0.borrow().as_ref()
76 && let Some(menu) = state.menu.borrow().as_ref()
77 {
78 menu.update(cx, |_, cx| cx.emit(DismissEvent));
79 }
80 }
81
82 pub fn toggle(&self, window: &mut Window, cx: &mut App) {
83 if let Some(state) = self.0.borrow().as_ref() {
84 if state.menu.borrow().is_some() {
85 self.hide(cx);
86 } else {
87 self.show(window, cx);
88 }
89 }
90 }
91
92 pub fn is_deployed(&self) -> bool {
93 self.0
94 .borrow()
95 .as_ref()
96 .is_some_and(|state| state.menu.borrow().as_ref().is_some())
97 }
98
99 pub fn is_focused(&self, window: &Window, cx: &App) -> bool {
100 self.0.borrow().as_ref().is_some_and(|state| {
101 state
102 .menu
103 .borrow()
104 .as_ref()
105 .is_some_and(|model| model.focus_handle(cx).is_focused(window))
106 })
107 }
108
109 pub fn refresh_menu(
110 &self,
111 window: &mut Window,
112 cx: &mut App,
113 new_menu_builder: Rc<dyn Fn(&mut Window, &mut App) -> Option<Entity<M>>>,
114 ) {
115 let show_menu = if let Some(state) = self.0.borrow_mut().as_mut() {
116 state.menu_builder = new_menu_builder;
117 state.menu.borrow().is_some()
118 } else {
119 false
120 };
121
122 if show_menu {
123 self.show(window, cx);
124 }
125 }
126}
127
128pub struct PopoverMenu<M: ManagedView> {
129 id: ElementId,
130 child_builder: Option<
131 Box<
132 dyn FnOnce(
133 Rc<RefCell<Option<Entity<M>>>>,
134 Option<Rc<dyn Fn(&mut Window, &mut App) -> Option<Entity<M>> + 'static>>,
135 ) -> AnyElement
136 + 'static,
137 >,
138 >,
139 menu_builder: Option<Rc<dyn Fn(&mut Window, &mut App) -> Option<Entity<M>> + 'static>>,
140 anchor: Anchor,
141 attach: Option<Anchor>,
142 offset: Option<Point<Pixels>>,
143 trigger_handle: Option<PopoverMenuHandle<M>>,
144 on_open: Option<Rc<dyn Fn(&mut Window, &mut App)>>,
145 full_width: bool,
146}
147
148impl<M: ManagedView> PopoverMenu<M> {
149 /// Returns a new [`PopoverMenu`].
150 pub fn new(id: impl Into<ElementId>) -> Self {
151 Self {
152 id: id.into(),
153 child_builder: None,
154 menu_builder: None,
155 anchor: Anchor::TopLeft,
156 attach: None,
157 offset: None,
158 trigger_handle: None,
159 on_open: None,
160 full_width: false,
161 }
162 }
163
164 pub fn full_width(mut self, full_width: bool) -> Self {
165 self.full_width = full_width;
166 self
167 }
168
169 pub fn menu(
170 mut self,
171 f: impl Fn(&mut Window, &mut App) -> Option<Entity<M>> + 'static,
172 ) -> Self {
173 self.menu_builder = Some(Rc::new(f));
174 self
175 }
176
177 pub fn with_handle(mut self, handle: PopoverMenuHandle<M>) -> Self {
178 self.trigger_handle = Some(handle);
179 self
180 }
181
182 pub fn trigger<T: PopoverTrigger>(mut self, t: T) -> Self {
183 let on_open = self.on_open.clone();
184 self.child_builder = Some(Box::new(move |menu, builder| {
185 let open = menu.borrow().is_some();
186 t.toggle_state(open)
187 .when_some(builder, |el, builder| {
188 el.on_click(move |_event, window, cx| {
189 show_menu(&builder, &menu, on_open.clone(), window, cx)
190 })
191 })
192 .into_any_element()
193 }));
194 self
195 }
196
197 /// This method prevents the trigger button tooltip from being seen when the menu is open.
198 pub fn trigger_with_tooltip<T: PopoverTrigger + ButtonCommon>(
199 mut self,
200 t: T,
201 tooltip_builder: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
202 ) -> Self {
203 let on_open = self.on_open.clone();
204 self.child_builder = Some(Box::new(move |menu, builder| {
205 let open = menu.borrow().is_some();
206 t.toggle_state(open)
207 .when_some(builder, |el, builder| {
208 el.on_click(move |_, window, cx| {
209 show_menu(&builder, &menu, on_open.clone(), window, cx)
210 })
211 .when(!open, |t| {
212 t.tooltip(move |window, cx| tooltip_builder(window, cx))
213 })
214 })
215 .into_any_element()
216 }));
217 self
218 }
219
220 /// Defines which corner of the menu to anchor to the attachment point.
221 /// By default, it uses the cursor position. Also see the `attach` method.
222 pub fn anchor(mut self, anchor: Anchor) -> Self {
223 self.anchor = anchor;
224 self
225 }
226
227 /// Defines which corner of the handle to attach the menu's anchor to.
228 pub fn attach(mut self, attach: Anchor) -> Self {
229 self.attach = Some(attach);
230 self
231 }
232
233 /// Offsets the position of the content by that many pixels.
234 pub fn offset(mut self, offset: Point<Pixels>) -> Self {
235 self.offset = Some(offset);
236 self
237 }
238
239 /// Attaches something upon opening the menu.
240 pub fn on_open(mut self, on_open: Rc<dyn Fn(&mut Window, &mut App)>) -> Self {
241 self.on_open = Some(on_open);
242 self
243 }
244
245 fn resolved_attach(&self) -> Anchor {
246 self.attach
247 .unwrap_or(self.attach.unwrap_or(match self.anchor {
248 Anchor::TopLeft => Anchor::BottomLeft,
249 Anchor::TopCenter => Anchor::BottomCenter,
250 Anchor::TopRight => Anchor::BottomRight,
251 Anchor::BottomLeft => Anchor::TopLeft,
252 Anchor::BottomCenter => Anchor::TopCenter,
253 Anchor::BottomRight => Anchor::TopRight,
254 Anchor::LeftCenter => Anchor::LeftCenter,
255 Anchor::RightCenter => Anchor::RightCenter,
256 }))
257 }
258
259 fn resolved_offset(&self, window: &mut Window) -> Point<Pixels> {
260 self.offset.unwrap_or_else(|| {
261 // Default offset = 4px padding + 1px border
262 let offset = rems_from_px(5.) * window.rem_size();
263 match self.anchor {
264 Anchor::TopRight | Anchor::BottomRight | Anchor::RightCenter => {
265 point(offset, px(0.))
266 }
267 Anchor::TopLeft | Anchor::BottomLeft | Anchor::LeftCenter => point(-offset, px(0.)),
268 Anchor::TopCenter | Anchor::BottomCenter => point(px(0.), px(0.)),
269 }
270 })
271 }
272}
273
274fn show_menu<M: ManagedView>(
275 builder: &Rc<dyn Fn(&mut Window, &mut App) -> Option<Entity<M>>>,
276 menu: &Rc<RefCell<Option<Entity<M>>>>,
277 on_open: Option<Rc<dyn Fn(&mut Window, &mut App)>>,
278 window: &mut Window,
279 cx: &mut App,
280) {
281 let previous_focus_handle = window.focused(cx);
282 let Some(new_menu) = (builder)(window, cx) else {
283 return;
284 };
285 let menu2 = menu.clone();
286
287 window
288 .subscribe(&new_menu, cx, move |modal, _: &DismissEvent, window, cx| {
289 if modal.focus_handle(cx).contains_focused(window, cx)
290 && let Some(previous_focus_handle) = previous_focus_handle.as_ref()
291 {
292 window.focus(previous_focus_handle, cx);
293 }
294 *menu2.borrow_mut() = None;
295 window.refresh();
296 })
297 .detach();
298
299 // Since menus are rendered in a deferred fashion, their focus handles are
300 // not linked in the dispatch tree until after the deferred draw callback
301 // runs. We need to wait for that to happen before focusing it, so that
302 // calling `contains_focused` on the parent's focus handle returns `true`
303 // when the menu is focused. This prevents the pane's tab bar buttons from
304 // flickering when opening popover menus.
305 let focus_handle = new_menu.focus_handle(cx);
306 window.on_next_frame(move |window, _cx| {
307 window.on_next_frame(move |window, cx| {
308 window.focus(&focus_handle, cx);
309 });
310 });
311 *menu.borrow_mut() = Some(new_menu);
312 window.refresh();
313
314 if let Some(on_open) = on_open {
315 on_open(window, cx);
316 }
317}
318
319pub struct PopoverMenuElementState<M> {
320 menu: Rc<RefCell<Option<Entity<M>>>>,
321 child_bounds: Option<Bounds<Pixels>>,
322}
323
324impl<M> Clone for PopoverMenuElementState<M> {
325 fn clone(&self) -> Self {
326 Self {
327 menu: Rc::clone(&self.menu),
328 child_bounds: self.child_bounds,
329 }
330 }
331}
332
333impl<M> Default for PopoverMenuElementState<M> {
334 fn default() -> Self {
335 Self {
336 menu: Rc::default(),
337 child_bounds: None,
338 }
339 }
340}
341
342pub struct PopoverMenuFrameState<M: ManagedView> {
343 child_layout_id: Option<LayoutId>,
344 child_element: Option<AnyElement>,
345 menu_element: Option<AnyElement>,
346 menu_handle: Rc<RefCell<Option<Entity<M>>>>,
347}
348
349impl<M: ManagedView> Element for PopoverMenu<M> {
350 type RequestLayoutState = PopoverMenuFrameState<M>;
351 type PrepaintState = Option<HitboxId>;
352
353 fn id(&self) -> Option<ElementId> {
354 Some(self.id.clone())
355 }
356
357 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
358 None
359 }
360
361 fn request_layout(
362 &mut self,
363 global_id: Option<&GlobalElementId>,
364 _inspector_id: Option<&gpui::InspectorElementId>,
365 window: &mut Window,
366 cx: &mut App,
367 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
368 window.with_element_state(
369 global_id.unwrap(),
370 |element_state: Option<PopoverMenuElementState<M>>, window| {
371 let element_state = element_state.unwrap_or_default();
372 let mut menu_layout_id = None;
373
374 let menu_element = element_state.menu.borrow_mut().as_mut().map(|menu| {
375 let offset = self.resolved_offset(window);
376 let mut anchored = anchored()
377 .snap_to_window_with_margin(px(8.))
378 .anchor(self.anchor)
379 .offset(offset);
380 if let Some(child_bounds) = element_state.child_bounds {
381 anchored =
382 anchored.position(child_bounds.corner(self.resolved_attach()) + offset);
383 }
384 let mut element = deferred(anchored.child(div().occlude().child(menu.clone())))
385 .with_priority(1)
386 .into_any();
387
388 menu_layout_id = Some(element.request_layout(window, cx));
389 element
390 });
391
392 let mut child_element = self.child_builder.take().map(|child_builder| {
393 (child_builder)(element_state.menu.clone(), self.menu_builder.clone())
394 });
395
396 if let Some(trigger_handle) = self.trigger_handle.take()
397 && let Some(menu_builder) = self.menu_builder.clone()
398 {
399 *trigger_handle.0.borrow_mut() = Some(PopoverMenuHandleState {
400 menu_builder,
401 menu: element_state.menu.clone(),
402 on_open: self.on_open.clone(),
403 });
404 }
405
406 let child_layout_id = child_element
407 .as_mut()
408 .map(|child_element| child_element.request_layout(window, cx));
409
410 let mut style = Style::default();
411 if self.full_width {
412 style.size = size(relative(1.).into(), Length::Auto);
413 }
414
415 let layout_id = window.request_layout(
416 style,
417 menu_layout_id.into_iter().chain(child_layout_id),
418 cx,
419 );
420
421 (
422 (
423 layout_id,
424 PopoverMenuFrameState {
425 child_element,
426 child_layout_id,
427 menu_element,
428 menu_handle: element_state.menu.clone(),
429 },
430 ),
431 element_state,
432 )
433 },
434 )
435 }
436
437 fn prepaint(
438 &mut self,
439 global_id: Option<&GlobalElementId>,
440 _inspector_id: Option<&gpui::InspectorElementId>,
441 _bounds: Bounds<Pixels>,
442 request_layout: &mut Self::RequestLayoutState,
443 window: &mut Window,
444 cx: &mut App,
445 ) -> Option<HitboxId> {
446 if let Some(child) = request_layout.child_element.as_mut() {
447 child.prepaint(window, cx);
448 }
449
450 if let Some(menu) = request_layout.menu_element.as_mut() {
451 menu.prepaint(window, cx);
452 }
453
454 request_layout.child_layout_id.map(|layout_id| {
455 let bounds = window.layout_bounds(layout_id);
456 window.with_element_state(global_id.unwrap(), |element_state, _cx| {
457 let mut element_state: PopoverMenuElementState<M> = element_state.unwrap();
458 element_state.child_bounds = Some(bounds);
459 ((), element_state)
460 });
461
462 window.insert_hitbox(bounds, HitboxBehavior::Normal).id
463 })
464 }
465
466 fn paint(
467 &mut self,
468 _id: Option<&GlobalElementId>,
469 _inspector_id: Option<&gpui::InspectorElementId>,
470 _: Bounds<gpui::Pixels>,
471 request_layout: &mut Self::RequestLayoutState,
472 child_hitbox: &mut Option<HitboxId>,
473 window: &mut Window,
474 cx: &mut App,
475 ) {
476 if let Some(mut child) = request_layout.child_element.take() {
477 child.paint(window, cx);
478 }
479
480 if let Some(mut menu) = request_layout.menu_element.take() {
481 menu.paint(window, cx);
482
483 if let Some(child_hitbox) = *child_hitbox {
484 let menu_handle = request_layout.menu_handle.clone();
485 // Mouse-downing outside the menu dismisses it, so we don't
486 // want a click on the toggle to re-open it.
487 window.on_mouse_event(move |_: &MouseDownEvent, phase, window, cx| {
488 if phase == DispatchPhase::Bubble && child_hitbox.is_hovered(window) {
489 if let Some(menu) = menu_handle.borrow().as_ref() {
490 menu.update(cx, |_, cx| {
491 cx.emit(DismissEvent);
492 });
493 }
494 cx.stop_propagation();
495 }
496 })
497 }
498 }
499 }
500}
501
502impl<M: ManagedView> IntoElement for PopoverMenu<M> {
503 type Element = Self;
504
505 fn into_element(self) -> Self::Element {
506 self
507 }
508}