1#![allow(missing_docs)]
2
3use std::{cell::RefCell, rc::Rc};
4
5use gpui::{
6 anchored, deferred, div, point, prelude::FluentBuilder, px, size, AnyElement, App, Bounds,
7 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 /// anchor defines which corner of the menu to anchor to the attachment point
182 /// (by default the cursor position, but see attach)
183 pub fn anchor(mut self, anchor: Corner) -> Self {
184 self.anchor = anchor;
185 self
186 }
187
188 /// attach defines which corner of the handle to attach the menu's anchor to
189 pub fn attach(mut self, attach: Corner) -> Self {
190 self.attach = Some(attach);
191 self
192 }
193
194 /// offset offsets the position of the content by that many pixels.
195 pub fn offset(mut self, offset: Point<Pixels>) -> Self {
196 self.offset = Some(offset);
197 self
198 }
199
200 /// attach something upon opening the menu
201 pub fn on_open(mut self, on_open: Rc<dyn Fn(&mut Window, &mut App)>) -> Self {
202 self.on_open = Some(on_open);
203 self
204 }
205
206 fn resolved_attach(&self) -> Corner {
207 self.attach.unwrap_or(match self.anchor {
208 Corner::TopLeft => Corner::BottomLeft,
209 Corner::TopRight => Corner::BottomRight,
210 Corner::BottomLeft => Corner::TopLeft,
211 Corner::BottomRight => Corner::TopRight,
212 })
213 }
214
215 fn resolved_offset(&self, window: &mut Window) -> Point<Pixels> {
216 self.offset.unwrap_or_else(|| {
217 // Default offset = 4px padding + 1px border
218 let offset = rems_from_px(5.) * window.rem_size();
219 match self.anchor {
220 Corner::TopRight | Corner::BottomRight => point(offset, px(0.)),
221 Corner::TopLeft | Corner::BottomLeft => point(-offset, px(0.)),
222 }
223 })
224 }
225}
226
227fn show_menu<M: ManagedView>(
228 builder: &Rc<dyn Fn(&mut Window, &mut App) -> Option<Entity<M>>>,
229 menu: &Rc<RefCell<Option<Entity<M>>>>,
230 on_open: Option<Rc<dyn Fn(&mut Window, &mut App)>>,
231 window: &mut Window,
232 cx: &mut App,
233) {
234 let Some(new_menu) = (builder)(window, cx) else {
235 return;
236 };
237 let menu2 = menu.clone();
238 let previous_focus_handle = window.focused(cx);
239
240 window
241 .subscribe(&new_menu, cx, move |modal, _: &DismissEvent, window, cx| {
242 if modal.focus_handle(cx).contains_focused(window, cx) {
243 if let Some(previous_focus_handle) = previous_focus_handle.as_ref() {
244 window.focus(previous_focus_handle);
245 }
246 }
247 *menu2.borrow_mut() = None;
248 window.refresh();
249 })
250 .detach();
251 window.focus(&new_menu.focus_handle(cx));
252 *menu.borrow_mut() = Some(new_menu);
253 window.refresh();
254
255 if let Some(on_open) = on_open {
256 on_open(window, cx);
257 }
258}
259
260pub struct PopoverMenuElementState<M> {
261 menu: Rc<RefCell<Option<Entity<M>>>>,
262 child_bounds: Option<Bounds<Pixels>>,
263}
264
265impl<M> Clone for PopoverMenuElementState<M> {
266 fn clone(&self) -> Self {
267 Self {
268 menu: Rc::clone(&self.menu),
269 child_bounds: self.child_bounds,
270 }
271 }
272}
273
274impl<M> Default for PopoverMenuElementState<M> {
275 fn default() -> Self {
276 Self {
277 menu: Rc::default(),
278 child_bounds: None,
279 }
280 }
281}
282
283pub struct PopoverMenuFrameState<M: ManagedView> {
284 child_layout_id: Option<LayoutId>,
285 child_element: Option<AnyElement>,
286 menu_element: Option<AnyElement>,
287 menu_handle: Rc<RefCell<Option<Entity<M>>>>,
288}
289
290impl<M: ManagedView> Element for PopoverMenu<M> {
291 type RequestLayoutState = PopoverMenuFrameState<M>;
292 type PrepaintState = Option<HitboxId>;
293
294 fn id(&self) -> Option<ElementId> {
295 Some(self.id.clone())
296 }
297
298 fn request_layout(
299 &mut self,
300 global_id: Option<&GlobalElementId>,
301 window: &mut Window,
302 cx: &mut App,
303 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
304 window.with_element_state(
305 global_id.unwrap(),
306 |element_state: Option<PopoverMenuElementState<M>>, window| {
307 let element_state = element_state.unwrap_or_default();
308 let mut menu_layout_id = None;
309
310 let menu_element = element_state.menu.borrow_mut().as_mut().map(|menu| {
311 let offset = self.resolved_offset(window);
312 let mut anchored = anchored()
313 .snap_to_window_with_margin(px(8.))
314 .anchor(self.anchor)
315 .offset(offset);
316 if let Some(child_bounds) = element_state.child_bounds {
317 anchored =
318 anchored.position(child_bounds.corner(self.resolved_attach()) + offset);
319 }
320 let mut element = deferred(anchored.child(div().occlude().child(menu.clone())))
321 .with_priority(1)
322 .into_any();
323
324 menu_layout_id = Some(element.request_layout(window, cx));
325 element
326 });
327
328 let mut child_element = self.child_builder.take().map(|child_builder| {
329 (child_builder)(element_state.menu.clone(), self.menu_builder.clone())
330 });
331
332 if let Some(trigger_handle) = self.trigger_handle.take() {
333 if let Some(menu_builder) = self.menu_builder.clone() {
334 *trigger_handle.0.borrow_mut() = Some(PopoverMenuHandleState {
335 menu_builder,
336 menu: element_state.menu.clone(),
337 on_open: self.on_open.clone(),
338 });
339 }
340 }
341
342 let child_layout_id = child_element
343 .as_mut()
344 .map(|child_element| child_element.request_layout(window, cx));
345
346 let mut style = Style::default();
347 if self.full_width {
348 style.size = size(relative(1.).into(), Length::Auto);
349 }
350
351 let layout_id = window.request_layout(
352 style,
353 menu_layout_id.into_iter().chain(child_layout_id),
354 cx,
355 );
356
357 (
358 (
359 layout_id,
360 PopoverMenuFrameState {
361 child_element,
362 child_layout_id,
363 menu_element,
364 menu_handle: element_state.menu.clone(),
365 },
366 ),
367 element_state,
368 )
369 },
370 )
371 }
372
373 fn prepaint(
374 &mut self,
375 global_id: Option<&GlobalElementId>,
376 _bounds: Bounds<Pixels>,
377 request_layout: &mut Self::RequestLayoutState,
378 window: &mut Window,
379 cx: &mut App,
380 ) -> Option<HitboxId> {
381 if let Some(child) = request_layout.child_element.as_mut() {
382 child.prepaint(window, cx);
383 }
384
385 if let Some(menu) = request_layout.menu_element.as_mut() {
386 menu.prepaint(window, cx);
387 }
388
389 request_layout.child_layout_id.map(|layout_id| {
390 let bounds = window.layout_bounds(layout_id);
391 window.with_element_state(global_id.unwrap(), |element_state, _cx| {
392 let mut element_state: PopoverMenuElementState<M> = element_state.unwrap();
393 element_state.child_bounds = Some(bounds);
394 ((), element_state)
395 });
396
397 window.insert_hitbox(bounds, false).id
398 })
399 }
400
401 fn paint(
402 &mut self,
403 _id: Option<&GlobalElementId>,
404 _: Bounds<gpui::Pixels>,
405 request_layout: &mut Self::RequestLayoutState,
406 child_hitbox: &mut Option<HitboxId>,
407 window: &mut Window,
408 cx: &mut App,
409 ) {
410 if let Some(mut child) = request_layout.child_element.take() {
411 child.paint(window, cx);
412 }
413
414 if let Some(mut menu) = request_layout.menu_element.take() {
415 menu.paint(window, cx);
416
417 if let Some(child_hitbox) = *child_hitbox {
418 let menu_handle = request_layout.menu_handle.clone();
419 // Mouse-downing outside the menu dismisses it, so we don't
420 // want a click on the toggle to re-open it.
421 window.on_mouse_event(move |_: &MouseDownEvent, phase, window, cx| {
422 if phase == DispatchPhase::Bubble && child_hitbox.is_hovered(window) {
423 if let Some(menu) = menu_handle.borrow().as_ref() {
424 menu.update(cx, |_, cx| {
425 cx.emit(DismissEvent);
426 });
427 }
428 cx.stop_propagation();
429 }
430 })
431 }
432 }
433 }
434}
435
436impl<M: ManagedView> IntoElement for PopoverMenu<M> {
437 type Element = Self;
438
439 fn into_element(self) -> Self::Element {
440 self
441 }
442}