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