use std::cell::RefCell; use std::rc::Rc; use gpui::prelude::FluentBuilder as _; use gpui::{ Animation, AnimationExt as _, AnyElement, App, Bounds, DispatchPhase, ElementId, InteractiveElement as _, IntoElement, KeyDownEvent, MouseButton, MouseDownEvent, ParentElement as _, Pixels, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Window, anchored, canvas, deferred, div, ease_out_quint, }; use gpui_native_theme::ActiveTheme as _; use crate::common::v_flex; /// Reports the next open state of a [`Popover`]: `true` to open, `false` to close. type ChangeHandler = Rc; type TriggerFn = Box AnyElement>; type ContentFn = Box AnyElement>; /// Bounds captured during the last paint, shared with the dismissal listener. type BoundsCell = Rc>>>; /// A floating panel anchored below its trigger. #[derive(IntoElement)] pub struct Popover { id: ElementId, open: bool, a11y_label: Option, on_change: Option, trigger: Option, content: Option, } impl Popover { /// Creates a closed popover. pub fn new(id: I) -> Self where I: Into, { Self { id: id.into(), open: false, a11y_label: None, on_change: None, trigger: None, content: None, } } /// Sets the current open state. pub fn open(mut self, open: bool) -> Self { self.open = open; self } /// Reports the next open state, from dismissals and app changes alike. pub fn on_change(mut self, handler: F) -> Self where F: Fn(bool, &mut Window, &mut App) + 'static, { self.on_change = Some(Rc::new(handler)); self } /// Sets the element the panel anchors below, rendered in place. pub fn trigger(mut self, trigger: F) -> Self where F: Fn(&mut Window, &mut App) -> AnyElement + 'static, { self.trigger = Some(Box::new(trigger)); self } /// Sets the panel's contents. pub fn content(mut self, content: F) -> Self where F: Fn(&mut Window, &mut App) -> AnyElement + 'static, { self.content = Some(Box::new(content)); self } /// Overrides the accessibility label of the panel. pub fn accessibility_label(mut self, label: L) -> Self where L: Into, { self.a11y_label = Some(label.into()); self } } impl RenderOnce for Popover { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let (colors, radius, shadows, enter_duration) = { let theme = cx.theme(); ( theme.colors, theme.radius, theme.elevations.popover.shadows(), theme.motion.overlay, ) }; let trigger_bounds: BoundsCell = Rc::new(RefCell::new(None)); let panel_bounds: BoundsCell = Rc::new(RefCell::new(None)); let fallback_label = self .a11y_label .clone() .unwrap_or_else(|| SharedString::from(self.id.to_string())); let mut wrapper = div() .relative() .child( canvas( { let cell = trigger_bounds.clone(); move |bounds, _window, _cx| *cell.borrow_mut() = Some(bounds) }, { let on_change = self.on_change.clone(); let open = self.open; let cell = trigger_bounds.clone(); move |_, _, window, _cx| { window.on_mouse_event({ let on_change = on_change.clone(); let trigger_bounds = cell; move |event: &MouseDownEvent, phase, window, cx| { if phase != DispatchPhase::Bubble || event.button != MouseButton::Left { return; } let on_trigger = trigger_bounds .borrow() .is_some_and(|bounds| bounds.contains(&event.position)); if !on_trigger { return; } if let Some(on_change) = &on_change { on_change(!open, window, cx); } cx.stop_propagation(); } }); } }, ) .absolute() .inset_0(), ) .when_some(self.trigger, |wrapper, trigger| { wrapper.child(trigger(window, cx)) }); if self.open { let on_change = self.on_change; let label = fallback_label.clone(); let focus_handle = window.use_keyed_state("popover-focus", cx, |_, cx| cx.focus_handle()); let content = self.content.map(|content| content(window, cx)); let panel = v_flex() .id("popover-panel") .track_focus(&focus_handle.read(cx).clone()) .role(Role::Dialog) .aria_label(label) .mt_1() .min_w_32() .bg(colors.surface) .text_color(colors.surface_foreground) .rounded(radius.overlay) .border_1() .border_color(colors.border) .shadow(shadows) .overflow_hidden() .on_key_down({ let on_change = on_change.clone(); move |event: &KeyDownEvent, window, cx| { if event.keystroke.key == "escape" { if let Some(on_change) = &on_change { on_change(false, window, cx); } cx.stop_propagation(); } } }) .when_some(content, |panel, content| panel.child(content)) .child( canvas( { let cell = panel_bounds.clone(); move |bounds, _window, _cx| *cell.borrow_mut() = Some(bounds) }, { let on_change = on_change.clone(); let focus = focus_handle; move |_, _, window, cx| { if !focus.read(cx).contains_focused(window, cx) { window.focus(&focus.read(cx).clone(), cx); } window.on_mouse_event({ let on_change = on_change.clone(); move |event: &MouseDownEvent, phase, window, cx| { if phase != DispatchPhase::Bubble || event.button != MouseButton::Left { return; } let on_panel = panel_bounds .borrow() .is_some_and(|bounds| bounds.contains(&event.position)); let on_trigger = trigger_bounds .borrow() .is_some_and(|bounds| bounds.contains(&event.position)); if on_panel || on_trigger { return; } if let Some(on_change) = &on_change { on_change(false, window, cx); } cx.stop_propagation(); } }); } }, ) .absolute() .inset_0(), ); wrapper = wrapper.child(deferred(anchored().child(panel.with_animation( "popover-enter", Animation::new(enter_duration).with_easing(ease_out_quint()), |panel, delta| panel.opacity(delta), )))); } wrapper } }