use std::rc::Rc; use gpui::prelude::FluentBuilder as _; use gpui::{ Anchor, AnyElement, App, Context, Div, ElementId, FocusHandle, InteractiveElement as _, IntoElement, MouseButton, ParentElement, RenderOnce, Stateful, StyleRefinement, Styled, Window, }; use gpui_base::Popover as BasePopover; pub use gpui_base::PopoverState; use crate::{Selectable, StyledExt as _, v_flex}; /// A popover element that can be triggered by a button or any other element. #[derive(IntoElement)] pub struct Popover { id: ElementId, style: StyleRefinement, anchor: Anchor, default_open: bool, open: Option, tracked_focus_handle: Option, #[allow(clippy::type_complexity)] trigger: Option AnyElement + 'static>>, #[allow(clippy::type_complexity)] content: Option< Rc< dyn Fn(&mut PopoverState, &mut Window, &mut Context) -> AnyElement + 'static, >, >, children: Vec, /// Style for trigger element. /// This is used for hotfix the trigger element style to support w_full. trigger_style: Option, mouse_button: MouseButton, appearance: bool, overlay_closable: bool, #[allow(clippy::type_complexity)] on_open_change: Option>, } impl Popover { /// Create a new Popover with `view` mode. pub fn new(id: impl Into) -> Self { Self { id: id.into(), style: StyleRefinement::default(), anchor: Anchor::TopLeft, trigger: None, trigger_style: None, content: None, tracked_focus_handle: None, children: vec![], mouse_button: MouseButton::Left, appearance: true, overlay_closable: true, default_open: false, open: None, on_open_change: None, } } /// Set the anchor corner of the popover, default is `Corner::TopLeft`. /// /// This method is kept for backward compatibility with `Corner` type. /// Internally, it converts `Corner` to `Anchor`. pub fn anchor(mut self, anchor: impl Into) -> Self { self.anchor = anchor.into(); self } /// Set the mouse button to trigger the popover, default is `MouseButton::Left`. pub fn mouse_button(mut self, mouse_button: MouseButton) -> Self { self.mouse_button = mouse_button; self } /// Set the trigger element of the popover. pub fn trigger(mut self, trigger: T) -> Self where T: Selectable + IntoElement + 'static, { self.trigger = Some(Box::new(|is_open, _, _| { let selected = trigger.is_selected(); trigger.selected(selected || is_open).into_any_element() })); self } /// Set the trigger from a builder, for elements that have no selected state. /// /// [`Self::trigger`] marks the trigger as selected while the popover is /// open, so it cannot be used with elements whose selection carries a /// different meaning, such as a row that indicates the current room. pub fn trigger_with(mut self, trigger: F) -> Self where F: FnOnce(bool, &Window, &App) -> AnyElement + 'static, { self.trigger = Some(Box::new(trigger)); self } /// Set the default open state of the popover, default is `false`. /// /// This is only used to initialize the open state of the popover. /// /// And please note that if you use the `open` method, this value will be ignored. pub fn default_open(mut self, open: bool) -> Self { self.default_open = open; self } /// Force set the open state of the popover. /// /// If this is set, the popover will be controlled by this value. /// /// NOTE: You must be used in conjunction with `on_open_change` to handle state changes. pub fn open(mut self, open: bool) -> Self { self.open = Some(open); self } /// Add a callback to be called when the open state changes. /// /// The first `&bool` parameter is the **new open state**. /// /// This is useful when using the `open` method to control the popover state. pub fn on_open_change(mut self, callback: F) -> Self where F: Fn(&bool, &mut Window, &mut App) + 'static, { self.on_open_change = Some(Rc::new(callback)); self } /// Set the style for the trigger element. pub fn trigger_style(mut self, style: StyleRefinement) -> Self { self.trigger_style = Some(style); self } /// Set whether clicking outside the popover will dismiss it, default is `true`. pub fn overlay_closable(mut self, closable: bool) -> Self { self.overlay_closable = closable; self } /// Set the content builder for content of the Popover. /// /// This callback will called every time on render the popover. /// So, you should avoid creating new elements or entities in the content closure. pub fn content(mut self, content: F) -> Self where E: IntoElement, F: Fn(&mut PopoverState, &mut Window, &mut Context) -> E + 'static, { self.content = Some(Rc::new(move |state, window, cx| { content(state, window, cx).into_any_element() })); self } /// Set whether the popover no style, default is `false`. /// /// If no style: /// /// - The popover will not have a bg, border, shadow, or padding. /// - The click out of the popover will not dismiss it. pub fn appearance(mut self, appearance: bool) -> Self { self.appearance = appearance; self } /// Bind the focus handle to receive focus when the popover is opened. /// If you not set this, a new focus handle will be created for the popover to /// /// If popover is opened, the focus will be moved to the focus handle. pub fn track_focus(mut self, handle: &FocusHandle) -> Self { self.tracked_focus_handle = Some(handle.clone()); self } } impl ParentElement for Popover { fn extend(&mut self, elements: impl IntoIterator) { self.children.extend(elements); } } impl Styled for Popover { fn style(&mut self) -> &mut StyleRefinement { &mut self.style } } impl Popover { pub(crate) fn render_popover_content( anchor: Anchor, appearance: bool, _: &mut Window, cx: &mut App, ) -> Stateful
{ v_flex() .id("content") .occlude() .tab_group() .when(appearance, |this| this.popover_style(cx).p_3()) .map(|this| match anchor { Anchor::TopLeft | Anchor::TopCenter | Anchor::TopRight => this.top_1(), Anchor::BottomLeft | Anchor::BottomCenter | Anchor::BottomRight => this.bottom_1(), Anchor::LeftCenter | Anchor::RightCenter => this.top_1(), // Fallback for centered }) } } impl RenderOnce for Popover { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { let anchor = self.anchor; let appearance = self.appearance; let style = self.style; let children = self.children; let content = self.content; BasePopover::new(self.id) .anchor(anchor) .mouse_button(self.mouse_button) .default_open(self.default_open) .overlay_closable(self.overlay_closable) .content(move |state, window, cx| { Self::render_popover_content(anchor, appearance, window, cx) .when_some(content, |this, content| { this.child((content)(state, window, cx)) }) .children(children) .refine_style(&style) }) .when_some(self.trigger, |this, trigger| this.trigger_with(trigger)) .when_some(self.open, |this, open| this.open(open)) .when_some(self.tracked_focus_handle, |this, handle| { this.track_focus(&handle) }) .when_some(self.on_open_change, |this, callback| { this.on_open_change(move |open, window, cx| callback(open, window, cx)) }) .into_any_element() } }