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