gpui_component/
popover.rs

1use gpui::{
2    anchored, deferred, div, prelude::FluentBuilder as _, px, AnyElement, App, Bounds, Context,
3    Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, EventEmitter, FocusHandle,
4    Focusable, GlobalElementId, Hitbox, InteractiveElement as _, IntoElement, KeyBinding, LayoutId,
5    ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, Style,
6    StyleRefinement, Styled, Window,
7};
8use std::{cell::RefCell, rc::Rc};
9
10use crate::{actions::Cancel, Selectable, StyledExt as _};
11
12const CONTEXT: &str = "Popover";
13
14pub(crate) fn init(cx: &mut App) {
15    cx.bind_keys([KeyBinding::new("escape", Cancel, Some(CONTEXT))])
16}
17
18pub struct PopoverContent {
19    style: StyleRefinement,
20    focus_handle: FocusHandle,
21    content: Rc<dyn Fn(&mut Window, &mut Context<Self>) -> AnyElement>,
22}
23
24impl PopoverContent {
25    pub fn new<B>(_: &mut Window, cx: &mut App, content: B) -> Self
26    where
27        B: Fn(&mut Window, &mut Context<Self>) -> AnyElement + 'static,
28    {
29        let focus_handle = cx.focus_handle();
30
31        Self {
32            style: StyleRefinement::default(),
33            focus_handle,
34            content: Rc::new(content),
35        }
36    }
37}
38impl EventEmitter<DismissEvent> for PopoverContent {}
39
40impl Focusable for PopoverContent {
41    fn focus_handle(&self, _cx: &App) -> FocusHandle {
42        self.focus_handle.clone()
43    }
44}
45
46impl Styled for PopoverContent {
47    fn style(&mut self) -> &mut StyleRefinement {
48        &mut self.style
49    }
50}
51
52impl Render for PopoverContent {
53    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
54        div()
55            .p_2()
56            .refine_style(&self.style)
57            .track_focus(&self.focus_handle)
58            .key_context(CONTEXT)
59            .on_action(cx.listener(|_, _: &Cancel, _, cx| {
60                cx.propagate();
61                cx.emit(DismissEvent);
62            }))
63            .child(self.content.clone()(window, cx))
64    }
65}
66
67pub struct Popover<M: ManagedView> {
68    id: ElementId,
69    anchor: Corner,
70    trigger: Option<Box<dyn FnOnce(bool, &Window, &App) -> AnyElement + 'static>>,
71    content: Option<Rc<dyn Fn(&mut Window, &mut App) -> Entity<M> + 'static>>,
72    /// Style for trigger element.
73    /// This is used for hotfix the trigger element style to support w_full.
74    trigger_style: Option<StyleRefinement>,
75    mouse_button: MouseButton,
76    no_style: bool,
77}
78
79impl<M> Popover<M>
80where
81    M: ManagedView,
82{
83    /// Create a new Popover with `view` mode.
84    pub fn new(id: impl Into<ElementId>) -> Self {
85        Self {
86            id: id.into(),
87            anchor: Corner::TopLeft,
88            trigger: None,
89            trigger_style: None,
90            content: None,
91            mouse_button: MouseButton::Left,
92            no_style: false,
93        }
94    }
95
96    pub fn anchor(mut self, anchor: Corner) -> Self {
97        self.anchor = anchor;
98        self
99    }
100
101    /// Set the mouse button to trigger the popover, default is `MouseButton::Left`.
102    pub fn mouse_button(mut self, mouse_button: MouseButton) -> Self {
103        self.mouse_button = mouse_button;
104        self
105    }
106
107    pub fn trigger<T>(mut self, trigger: T) -> Self
108    where
109        T: Selectable + IntoElement + 'static,
110    {
111        self.trigger = Some(Box::new(|is_open, _, _| {
112            let selected = trigger.is_selected();
113            trigger.selected(selected || is_open).into_any_element()
114        }));
115        self
116    }
117
118    pub fn trigger_style(mut self, style: StyleRefinement) -> Self {
119        self.trigger_style = Some(style);
120        self
121    }
122
123    /// Set the content of the popover.
124    ///
125    /// The `content` is a closure that returns an `AnyElement`.
126    pub fn content<C>(mut self, content: C) -> Self
127    where
128        C: Fn(&mut Window, &mut App) -> Entity<M> + 'static,
129    {
130        self.content = Some(Rc::new(content));
131        self
132    }
133
134    /// Set whether the popover no style, default is `false`.
135    ///
136    /// If no style:
137    ///
138    /// - The popover will not have a bg, border, shadow, or padding.
139    /// - The click out of the popover will not dismiss it.
140    pub fn no_style(mut self) -> Self {
141        self.no_style = true;
142        self
143    }
144
145    fn render_trigger(&mut self, open: bool, window: &mut Window, cx: &mut App) -> AnyElement {
146        let Some(trigger) = self.trigger.take() else {
147            return div().into_any_element();
148        };
149
150        (trigger)(open, window, cx)
151    }
152
153    fn resolved_corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
154        bounds.corner(match self.anchor {
155            Corner::TopLeft => Corner::BottomLeft,
156            Corner::TopRight => Corner::BottomRight,
157            Corner::BottomLeft => Corner::TopLeft,
158            Corner::BottomRight => Corner::TopRight,
159        })
160    }
161
162    fn with_element_state<R>(
163        &mut self,
164        id: &GlobalElementId,
165        window: &mut Window,
166        cx: &mut App,
167        f: impl FnOnce(&mut Self, &mut PopoverElementState<M>, &mut Window, &mut App) -> R,
168    ) -> R {
169        window.with_optional_element_state::<PopoverElementState<M>, _>(
170            Some(id),
171            |element_state, window| {
172                let mut element_state = element_state.unwrap().unwrap_or_default();
173                let result = f(self, &mut element_state, window, cx);
174                (result, Some(element_state))
175            },
176        )
177    }
178}
179
180impl<M> IntoElement for Popover<M>
181where
182    M: ManagedView,
183{
184    type Element = Self;
185
186    fn into_element(self) -> Self::Element {
187        self
188    }
189}
190
191pub struct PopoverElementState<M> {
192    trigger_layout_id: Option<LayoutId>,
193    popover_layout_id: Option<LayoutId>,
194    popover_element: Option<AnyElement>,
195    trigger_element: Option<AnyElement>,
196    content_view: Rc<RefCell<Option<Entity<M>>>>,
197    /// Trigger bounds for positioning the popover.
198    trigger_bounds: Option<Bounds<Pixels>>,
199}
200
201impl<M> Default for PopoverElementState<M> {
202    fn default() -> Self {
203        Self {
204            trigger_layout_id: None,
205            popover_layout_id: None,
206            popover_element: None,
207            trigger_element: None,
208            content_view: Rc::new(RefCell::new(None)),
209            trigger_bounds: None,
210        }
211    }
212}
213
214pub struct PrepaintState {
215    hitbox: Hitbox,
216    /// Trigger bounds for limit a rect to handle mouse click.
217    trigger_bounds: Option<Bounds<Pixels>>,
218}
219
220impl<M: ManagedView> Element for Popover<M> {
221    type RequestLayoutState = PopoverElementState<M>;
222    type PrepaintState = PrepaintState;
223
224    fn id(&self) -> Option<ElementId> {
225        Some(self.id.clone())
226    }
227
228    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
229        None
230    }
231
232    fn request_layout(
233        &mut self,
234        id: Option<&gpui::GlobalElementId>,
235        _: Option<&gpui::InspectorElementId>,
236        window: &mut Window,
237        cx: &mut App,
238    ) -> (gpui::LayoutId, Self::RequestLayoutState) {
239        let mut style = Style::default();
240
241        // FIXME: Remove this and find a better way to handle this.
242        // Apply trigger style, for support w_full for trigger.
243        //
244        // If remove this, the trigger will not support w_full.
245        if let Some(trigger_style) = self.trigger_style.clone() {
246            if let Some(width) = trigger_style.size.width {
247                style.size.width = width;
248            }
249            if let Some(display) = trigger_style.display {
250                style.display = display;
251            }
252        }
253
254        self.with_element_state(
255            id.unwrap(),
256            window,
257            cx,
258            |view, element_state, window, cx| {
259                let mut popover_layout_id = None;
260                let mut popover_element = None;
261                let mut is_open = false;
262
263                if let Some(content_view) = element_state.content_view.borrow_mut().as_mut() {
264                    is_open = true;
265
266                    let mut anchored = anchored()
267                        .snap_to_window_with_margin(px(8.))
268                        .anchor(view.anchor);
269                    if let Some(trigger_bounds) = element_state.trigger_bounds {
270                        anchored = anchored.position(view.resolved_corner(trigger_bounds));
271                    }
272
273                    let mut element = {
274                        let content_view_mut = element_state.content_view.clone();
275                        let anchor = view.anchor;
276                        let no_style = view.no_style;
277                        deferred(
278                            anchored.child(
279                                div()
280                                    .size_full()
281                                    .occlude()
282                                    .tab_group()
283                                    .when(!no_style, |this| this.popover_style(cx))
284                                    .map(|this| match anchor {
285                                        Corner::TopLeft | Corner::TopRight => this.top_1(),
286                                        Corner::BottomLeft | Corner::BottomRight => this.bottom_1(),
287                                    })
288                                    .child(content_view.clone())
289                                    .when(!no_style, |this| {
290                                        this.on_mouse_down_out(move |_, window, _| {
291                                            // Update the element_state.content_view to `None`,
292                                            // so that the `paint`` method will not paint it.
293                                            *content_view_mut.borrow_mut() = None;
294                                            window.refresh();
295                                        })
296                                    }),
297                            ),
298                        )
299                        .with_priority(1)
300                        .into_any()
301                    };
302
303                    popover_layout_id = Some(element.request_layout(window, cx));
304                    popover_element = Some(element);
305                }
306
307                let mut trigger_element = view.render_trigger(is_open, window, cx);
308                let trigger_layout_id = trigger_element.request_layout(window, cx);
309
310                let layout_id = window.request_layout(
311                    style,
312                    Some(trigger_layout_id).into_iter().chain(popover_layout_id),
313                    cx,
314                );
315
316                (
317                    layout_id,
318                    PopoverElementState {
319                        trigger_layout_id: Some(trigger_layout_id),
320                        popover_layout_id,
321                        popover_element,
322                        trigger_element: Some(trigger_element),
323                        ..Default::default()
324                    },
325                )
326            },
327        )
328    }
329
330    fn prepaint(
331        &mut self,
332        _id: Option<&gpui::GlobalElementId>,
333        _: Option<&gpui::InspectorElementId>,
334        _bounds: gpui::Bounds<gpui::Pixels>,
335        request_layout: &mut Self::RequestLayoutState,
336        window: &mut Window,
337        cx: &mut App,
338    ) -> Self::PrepaintState {
339        if let Some(element) = &mut request_layout.trigger_element {
340            element.prepaint(window, cx);
341        }
342        if let Some(element) = &mut request_layout.popover_element {
343            element.prepaint(window, cx);
344        }
345
346        let trigger_bounds = request_layout
347            .trigger_layout_id
348            .map(|id| window.layout_bounds(id));
349
350        // Prepare the popover, for get the bounds of it for open window size.
351        let _ = request_layout
352            .popover_layout_id
353            .map(|id| window.layout_bounds(id));
354
355        let hitbox = window.insert_hitbox(
356            trigger_bounds.unwrap_or_default(),
357            gpui::HitboxBehavior::Normal,
358        );
359
360        PrepaintState {
361            trigger_bounds,
362            hitbox,
363        }
364    }
365
366    fn paint(
367        &mut self,
368        id: Option<&GlobalElementId>,
369        _: Option<&gpui::InspectorElementId>,
370        _bounds: Bounds<Pixels>,
371        request_layout: &mut Self::RequestLayoutState,
372        prepaint: &mut Self::PrepaintState,
373        window: &mut Window,
374        cx: &mut App,
375    ) {
376        self.with_element_state(
377            id.unwrap(),
378            window,
379            cx,
380            |this, element_state, window, cx| {
381                element_state.trigger_bounds = prepaint.trigger_bounds;
382
383                if let Some(mut element) = request_layout.trigger_element.take() {
384                    element.paint(window, cx);
385                }
386
387                if let Some(mut element) = request_layout.popover_element.take() {
388                    element.paint(window, cx);
389                    return;
390                }
391
392                // When mouse click down in the trigger bounds, open the popover.
393                let Some(content_build) = this.content.take() else {
394                    return;
395                };
396                let old_content_view = element_state.content_view.clone();
397                let hitbox_id = prepaint.hitbox.id;
398                let mouse_button = this.mouse_button;
399                window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
400                    if phase == DispatchPhase::Bubble
401                        && event.button == mouse_button
402                        && hitbox_id.is_hovered(window)
403                    {
404                        cx.stop_propagation();
405                        window.prevent_default();
406
407                        let new_content_view = (content_build)(window, cx);
408                        let old_content_view1 = old_content_view.clone();
409
410                        let previous_focus_handle = window.focused(cx);
411
412                        window
413                            .subscribe(
414                                &new_content_view,
415                                cx,
416                                move |modal, _: &DismissEvent, window, cx| {
417                                    if modal.focus_handle(cx).contains_focused(window, cx) {
418                                        if let Some(previous_focus_handle) =
419                                            previous_focus_handle.as_ref()
420                                        {
421                                            window.focus(previous_focus_handle);
422                                        }
423                                    }
424                                    *old_content_view1.borrow_mut() = None;
425
426                                    window.refresh();
427                                },
428                            )
429                            .detach();
430
431                        window.focus(&new_content_view.focus_handle(cx));
432                        *old_content_view.borrow_mut() = Some(new_content_view);
433                        window.refresh();
434                    }
435                });
436            },
437        );
438    }
439}