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