From 2dbfba3490944aee6976ffa55b6f58f4b41f76e7 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Tue, 11 Nov 2025 17:45:30 +0800 Subject: [PATCH] popover: Improve Popover API. (#1545) - Add `open`, `on_open_change` method to control open state. - Add `default_open` method. ## Break Change This PR to rewrite the API of Popover API to make it easy to use. - The `content` method now can receive an element directly. ```diff - .content(|window, cx| { - cx.new(|cx| { - PopoverContent::new(window, cx, |_, _| { - div().child("This popover content.") - }) - }) - }) + .content(|state, window, cx| { + div().child("This popover content.") + }) ``` - And you can also just use `child` and `children` to add child elements. ```rs Popover::new("my-popover") .trigger(Button::new("trigger").label("Open Popover")) .child("This popover content.") ``` - Removed `PopoverContent`, and changed `Popover` default paddings to `p_3`. --- crates/story/src/popover_story.rs | 285 ++++++------ crates/ui/src/menu/dropdown_menu.rs | 124 ++++- crates/ui/src/popover.rs | 607 +++++++++++-------------- docs/docs/components/popover.md | 678 ++++------------------------ 4 files changed, 626 insertions(+), 1068 deletions(-) diff --git a/crates/story/src/popover_story.rs b/crates/story/src/popover_story.rs index 3cba6ab8..7264fb14 100644 --- a/crates/story/src/popover_story.rs +++ b/crates/story/src/popover_story.rs @@ -1,19 +1,21 @@ use gpui::{ - Action, App, AppContext, Context, Corner, DismissEvent, Element, Entity, EventEmitter, - FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding, MouseButton, - ParentElement as _, Render, Styled as _, Window, actions, div, px, + Action, App, AppContext, Context, Corner, DismissEvent, Entity, EventEmitter, FocusHandle, + Focusable, InteractiveElement, IntoElement, KeyBinding, MouseButton, ParentElement as _, + Render, Styled as _, Window, actions, div, px, }; use gpui_component::{ - Sizable, WindowExt, + ActiveTheme, StyledExt, WindowExt, button::{Button, ButtonVariants as _}, divider::Divider, h_flex, input::{Input, InputState}, - popover::{Popover, PopoverContent}, + popover::Popover, v_flex, }; use serde::Deserialize; +use crate::section; + #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = popover_story, no_json)] struct Info(usize); @@ -42,12 +44,14 @@ pub fn init(cx: &mut App) { } struct Form { + parent: Entity, input1: Entity, } impl Form { - fn new(window: &mut Window, cx: &mut App) -> Entity { + fn new(parent: Entity, window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self { + parent, input1: cx.new(|cx| InputState::new(window, cx)), }) } @@ -63,17 +67,24 @@ impl EventEmitter for Form {} impl Render for Form { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let parent = self.parent.clone(); v_flex() - .gap_4() - .p_4() + .gap_2() + .p_3() .size_full() .child("This is a form container.") + .child("Click submit to dismiss the popover.") .child(Input::new(&self.input1)) .child( Button::new("submit") .label("Submit") .primary() - .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), + .on_click(cx.listener(move |_, _, _, cx| { + parent.update(cx, |this, cx| { + this.form_open = false; + cx.notify(); + }) + })), ) } } @@ -81,6 +92,7 @@ impl Render for Form { pub struct PopoverStory { focus_handle: FocusHandle, form: Entity
, + form_open: bool, checked: bool, message: String, } @@ -105,13 +117,14 @@ impl PopoverStory { } fn new(window: &mut Window, cx: &mut Context) -> Self { - let form = Form::new(window, cx); + let form = Form::new(cx.entity(), window, cx); cx.focus_self(window); Self { form, checked: true, + form_open: false, focus_handle: cx.focus_handle(), message: "".to_string(), } @@ -169,138 +182,136 @@ impl Render for PopoverStory { .on_action(cx.listener(Self::on_action_info)) .on_action(cx.listener(Self::on_action_toggle_check)) .size_full() - .min_h(px(400.)) .gap_6() .child( - h_flex() - .items_center() - .justify_between() - .child( - v_flex().gap_4().child( - Popover::new("info-top-left") - .trigger(Button::new("info-top-left").outline().label("Top Left")) - .content(|window, cx| { - cx.new(|cx| { - PopoverContent::new(window, cx, |_, _| { - v_flex() - .gap_4() - .child("Hello, this is a Popover.") - .w(px(400.)) - .child(Divider::horizontal()) - .child( - Button::new("info1") - .primary() - .label("Ok") - .w(px(80.)) - .small(), - ) - .into_any() - }) - .p_4() - .max_w(px(600.)) - }) - }), - ), - ) - .child( - Popover::new("info-top-right") - .anchor(Corner::TopRight) - .trigger(Button::new("info-top-right").outline().label("Top Right")) - .content(|window, cx| { - cx.new(|cx| { - PopoverContent::new(window, cx, |_, _| { - v_flex() - .gap_4() - .w_96() - .child("Hello, this is a Popover on the Top Right.") - .child(Divider::horizontal()) - .child( - Button::new("info1") - .primary() - .label("Ok") - .w(px(80.)) - .small(), - ) - .into_any() - }) - .p_4() - }) - }), - ), - ) - .child( - div().absolute().bottom_4().left_0().w_full().h_10().child( - h_flex() - .items_center() - .justify_between() + section("Basic Popover").child( + Popover::new("popover-0") + .max_w(px(600.)) + .trigger(Button::new("btn").outline().label("Popover")) + .gap_2() + .text_sm() + .w(px(400.)) + .child("Hello, this is a Popover.") + .child(Divider::horizontal()) .child( - Popover::new("info-bottom-left") - .anchor(Corner::BottomLeft) - .trigger( - Button::new("pop") - .outline() - .label("Popup with Form") - .w(px(300.)), - ) - .content(move |_, _| form.clone()), - ) - .child( - Popover::new("info-bottom-right") - .anchor(Corner::BottomRight) - .mouse_button(MouseButton::Right) - .trigger( - Button::new("pop") - .outline() - .label("Mouse Right Click") - .w(px(300.)), - ) - .content(|window, cx| { - cx.new(|cx| { - PopoverContent::new(window, cx, |_, cx| { - v_flex() - .gap_2() - .child( - "Hello, this is a Popover on the Bottom Right.", - ) - .child(Divider::horizontal()) - .child( - h_flex() - .gap_2() - .justify_end() - .child( - Button::new("info1") - .primary() - .label("Ok") - .w(px(80.)) - .small() - .on_click(cx.listener( - |_, _, window, cx| { - window.push_notification( - "You have clicked Ok.", - cx, - ); - cx.emit(DismissEvent); - }, - )), - ) - .child( - Button::new("close") - .label("Cancel") - .small() - .on_click(cx.listener( - |_, _, _, cx| { - cx.emit(DismissEvent); - }, - )), - ), - ) - .into_any() - }) - .p_4() - }) - }), + "You can put any content here, including text,\ + buttons, forms, and more.", ), ), ) + .child( + section("Popover with Form").child( + Popover::new("info-bottom-left") + .p_0() + .text_sm() + .trigger(Button::new("pop").outline().label("Popup Form")) + .track_focus(&form.focus_handle(cx)) + .open(self.form_open) + .on_open_change(cx.listener(move |this, open, _, cx| { + println!("Popover form open changed: {}", open); + this.form_open = *open; + cx.notify(); + })) + .child(form.clone()), + ), + ) + .child( + section("Right click to open Popover").child( + Popover::new("popover-right-click") + .mouse_button(MouseButton::Right) + .trigger(Button::new("btn").outline().label("Right Click Popover")) + .max_w(px(600.)) + .content(|_, _, cx| { + v_flex() + .gap_2() + .child("Hello, this is a Popover on the Bottom Right.") + .child(Divider::horizontal()) + .child( + Button::new("info1") + .primary() + .label("Dismiss") + .w(px(80.)) + .on_click(cx.listener(|_, _, window, cx| { + window.push_notification( + "You have clicked dismiss via DismissEvent.", + cx, + ); + cx.emit(DismissEvent); + })), + ) + }), + ), + ) + .child( + section("Styling Popover").child( + Popover::new("popover-1") + .trigger(Button::new("btn").outline().label("Style Popover")) + .appearance(false) + .py_1() + .px_2() + .bg(cx.theme().primary) + .text_color(cx.theme().primary_foreground) + .max_w(px(600.)) + .rounded_sm() + .text_sm() + .shadow_2xl() + .child("A styled Popover with custom background and text color."), + ), + ) + .child( + section("Default Open").child( + Popover::new("default-open-popover") + .default_open(true) + .trigger( + Button::new("default-open-btn") + .label("Default Open") + .outline(), + ) + .child("This popover is open by default when first rendered."), + ), + ) + .child( + section("Popover Anchor") + .min_h(px(320.)) + .v_flex() + .child( + div().absolute().top_0().left_0().w_full().h_10().child( + h_flex() + .items_center() + .justify_between() + .child( + Popover::new("anchor0") + .max_w(px(600.)) + .trigger(Button::new("btn").outline().label("TopLeft")) + .child("This is a Popover on the Top Left."), + ) + .child( + Popover::new("anchor1") + .anchor(Corner::TopRight) + .trigger(Button::new("btn").outline().label("TopRight")) + .child("This is a Popover on the Top Right."), + ), + ), + ) + .child( + div().absolute().bottom_0().left_0().w_full().h_10().child( + h_flex() + .items_center() + .justify_between() + .child( + Popover::new("anchor2") + .trigger(Button::new("btn").outline().label("BottomLeft")) + .anchor(Corner::BottomLeft) + .child("This is a Popover on the Bottom Left."), + ) + .child( + Popover::new("anchor3") + .anchor(Corner::BottomRight) + .trigger(Button::new("btn").outline().label("BottomRight")) + .child("This is a Popover on the Bottom Right."), + ), + ), + ), + ) } } diff --git a/crates/ui/src/menu/dropdown_menu.rs b/crates/ui/src/menu/dropdown_menu.rs index e8da81f3..e3263254 100644 --- a/crates/ui/src/menu/dropdown_menu.rs +++ b/crates/ui/src/menu/dropdown_menu.rs @@ -1,4 +1,9 @@ -use gpui::{Context, Corner, InteractiveElement, IntoElement, SharedString, Styled, Window}; +use std::rc::Rc; + +use gpui::{ + Context, Corner, DismissEvent, ElementId, Entity, Focusable, InteractiveElement, IntoElement, + RenderOnce, SharedString, StyleRefinement, Styled, Window, +}; use crate::{button::Button, menu::PopupMenu, popover::Popover, Selectable}; @@ -8,7 +13,7 @@ pub trait DropdownMenu: Styled + Selectable + InteractiveElement + IntoElement + fn dropdown_menu( self, f: impl Fn(PopupMenu, &mut Window, &mut Context) -> PopupMenu + 'static, - ) -> Popover { + ) -> DropdownMenuPopover { self.dropdown_menu_with_anchor(Corner::TopLeft, f) } @@ -17,19 +22,116 @@ pub trait DropdownMenu: Styled + Selectable + InteractiveElement + IntoElement + mut self, anchor: impl Into, f: impl Fn(PopupMenu, &mut Window, &mut Context) -> PopupMenu + 'static, - ) -> Popover { + ) -> DropdownMenuPopover { let style = self.style().clone(); let id = self.interactivity().element_id.clone(); - Popover::new(SharedString::from(format!("dropdown-menu:{:?}", id))) - .appearance(false) - .trigger(self) - .trigger_style(style) - .anchor(anchor.into()) - .content(move |window, cx| { - PopupMenu::build(window, cx, |menu, window, cx| f(menu, window, cx)) - }) + DropdownMenuPopover::new(id.unwrap_or(0.into()), anchor, self, f).trigger_style(style) } } impl DropdownMenu for Button {} + +#[derive(IntoElement)] +pub struct DropdownMenuPopover { + id: ElementId, + style: StyleRefinement, + anchor: Corner, + trigger: T, + builder: Rc) -> PopupMenu>, +} + +impl DropdownMenuPopover +where + T: Selectable + IntoElement + 'static, +{ + fn new( + id: ElementId, + anchor: impl Into, + trigger: T, + builder: impl Fn(PopupMenu, &mut Window, &mut Context) -> PopupMenu + 'static, + ) -> Self { + Self { + id: SharedString::from(format!("dropdown-menu:{:?}", id)).into(), + style: StyleRefinement::default(), + anchor: anchor.into(), + trigger, + builder: Rc::new(builder), + } + } + + /// Set the anchor corner for the dropdown menu popover. + pub fn anchor(mut self, anchor: impl Into) -> Self { + self.anchor = anchor.into(); + self + } + + /// Set the style refinement for the dropdown menu trigger. + fn trigger_style(mut self, style: StyleRefinement) -> Self { + self.style = style; + self + } +} + +#[derive(Default)] +struct DropdownMenuState { + menu: Option>, +} + +impl RenderOnce for DropdownMenuPopover +where + T: Selectable + IntoElement + 'static, +{ + fn render(self, window: &mut Window, cx: &mut gpui::App) -> impl IntoElement { + let builder = self.builder.clone(); + let menu_state = + window.use_keyed_state(self.id.clone(), cx, |_, _| DropdownMenuState::default()); + + Popover::new(SharedString::from(format!("popover:{}", self.id))) + .appearance(false) + .trigger(self.trigger) + .trigger_style(self.style) + .anchor(self.anchor) + .content(move |_, window, cx| { + // Here is special logic to only create the PopupMenu once and reuse it. + // Because this `content` will called in every time render, so we need to store the menu + // in state to avoid recreating at every render. + // + // And we also need to rebuild the menu when it is dismissed, to rebuild menu items + // dynamically for support `dropdown_menu` method, so we listen for DismissEvent below. + let menu = match menu_state.read(cx).menu.clone() { + Some(menu) => menu, + None => { + let builder = builder.clone(); + let menu = PopupMenu::build(window, cx, move |menu, window, cx| { + builder(menu, window, cx) + }); + menu_state.update(cx, |state, _| { + state.menu = Some(menu.clone()); + }); + menu.focus_handle(cx).focus(window); + + // Listen for dismiss events from the PopupMenu to close the popover. + let popover_state = cx.entity(); + window + .subscribe(&menu, cx, { + let menu_state = menu_state.clone(); + move |_, _: &DismissEvent, window, cx| { + popover_state.update(cx, |state, cx| { + state.dismiss(window, cx); + }); + menu_state.update(cx, |state, _| { + state.menu = None; + }); + } + }) + .detach(); + + menu.clone() + } + }; + + menu.clone() + }) + } +} diff --git a/crates/ui/src/popover.rs b/crates/ui/src/popover.rs index a23a325c..7432cccb 100644 --- a/crates/ui/src/popover.rs +++ b/crates/ui/src/popover.rs @@ -1,97 +1,60 @@ use gpui::{ - anchored, deferred, div, prelude::FluentBuilder as _, px, AnyElement, App, Bounds, Context, - Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, EventEmitter, FocusHandle, - Focusable, GlobalElementId, Hitbox, InteractiveElement as _, IntoElement, KeyBinding, LayoutId, - ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, Style, - StyleRefinement, Styled, Window, + anchored, canvas, deferred, div, prelude::FluentBuilder as _, px, AnyElement, App, Bounds, + Context, Corner, DismissEvent, ElementId, EventEmitter, FocusHandle, Focusable, + InteractiveElement as _, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point, + Render, RenderOnce, StyleRefinement, Styled, Subscription, Window, }; -use std::{cell::RefCell, rc::Rc}; +use std::rc::Rc; -use crate::{actions::Cancel, Selectable, StyledExt as _}; +use crate::{actions::Cancel, v_flex, Selectable, StyledExt as _}; const CONTEXT: &str = "Popover"; - pub(crate) fn init(cx: &mut App) { cx.bind_keys([KeyBinding::new("escape", Cancel, Some(CONTEXT))]) } -/// The content of the popover. -pub struct PopoverContent { - style: StyleRefinement, - focus_handle: FocusHandle, - content: Rc) -> AnyElement>, -} - -impl PopoverContent { - pub fn new(_: &mut Window, cx: &mut App, content: B) -> Self - where - B: Fn(&mut Window, &mut Context) -> AnyElement + 'static, - { - let focus_handle = cx.focus_handle(); - - Self { - style: StyleRefinement::default(), - focus_handle, - content: Rc::new(content), - } - } -} -impl EventEmitter for PopoverContent {} - -impl Focusable for PopoverContent { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Styled for PopoverContent { - fn style(&mut self) -> &mut StyleRefinement { - &mut self.style - } -} - -impl Render for PopoverContent { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .p_2() - .refine_style(&self.style) - .track_focus(&self.focus_handle) - .key_context(CONTEXT) - .on_action(cx.listener(|_, _: &Cancel, _, cx| { - cx.propagate(); - cx.emit(DismissEvent); - })) - .child(self.content.clone()(window, cx)) - } -} - /// A popover element that can be triggered by a button or any other element. -pub struct Popover { +#[derive(IntoElement)] +pub struct Popover { id: ElementId, + style: StyleRefinement, anchor: Corner, + default_open: bool, + open: Option, + tracked_focus_handle: Option, trigger: Option AnyElement + 'static>>, - content: Option Entity + 'static>>, + 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, + on_open_change: Option>, } -impl Popover -where - M: ManagedView, -{ +impl Popover { /// Create a new Popover with `view` mode. pub fn new(id: impl Into) -> Self { Self { id: id.into(), + style: StyleRefinement::default(), anchor: Corner::TopLeft, trigger: None, trigger_style: None, content: None, + tracked_focus_handle: None, + children: vec![], mouse_button: MouseButton::Left, appearance: true, + default_open: false, + open: None, + on_open_change: None, } } @@ -119,6 +82,39 @@ where 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); @@ -126,13 +122,14 @@ where } /// Set the content of the popover. - /// - /// The `content` is a closure that returns an `AnyElement`. - pub fn content(mut self, content: C) -> Self + pub fn content(mut self, content: F) -> Self where - C: Fn(&mut Window, &mut App) -> Entity + 'static, + E: IntoElement, + F: Fn(&mut PopoverState, &mut Window, &mut Context) -> E + 'static, { - self.content = Some(Rc::new(content)); + self.content = Some(Rc::new(move |state, window, cx| { + content(state, window, cx).into_any_element() + })); self } @@ -147,298 +144,246 @@ where self } - fn render_trigger(&mut self, open: bool, window: &mut Window, cx: &mut App) -> AnyElement { - let Some(trigger) = self.trigger.take() else { - return div().into_any_element(); - }; - - (trigger)(open, window, cx) + /// Bind the focus handle to track focus inside the popover. + /// + /// 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 } - fn resolved_corner(&self, bounds: Bounds) -> Point { - bounds.corner(match self.anchor { + fn resolved_corner(anchor: Corner, bounds: Bounds) -> Point { + bounds.corner(match anchor { Corner::TopLeft => Corner::BottomLeft, Corner::TopRight => Corner::BottomRight, Corner::BottomLeft => Corner::TopLeft, Corner::BottomRight => Corner::TopRight, - }) - } - - fn with_element_state( - &mut self, - id: &GlobalElementId, - window: &mut Window, - cx: &mut App, - f: impl FnOnce(&mut Self, &mut PopoverElementState, &mut Window, &mut App) -> R, - ) -> R { - window.with_optional_element_state::, _>( - Some(id), - |element_state, window| { - let mut element_state = element_state.unwrap().unwrap_or_default(); - let result = f(self, &mut element_state, window, cx); - (result, Some(element_state)) - }, - ) + }) + Point { + x: px(0.), + y: -bounds.size.height, + } } } -impl IntoElement for Popover -where - M: ManagedView, -{ - type Element = Self; - - fn into_element(self) -> Self::Element { - self +impl ParentElement for Popover { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements); } } -pub struct PopoverElementState { - trigger_layout_id: Option, - popover_layout_id: Option, - popover_element: Option, - trigger_element: Option, - content_view: Rc>>>, - /// Trigger bounds for positioning the popover. +impl Styled for Popover { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} + +pub struct PopoverState { + focus_handle: FocusHandle, + pub(crate) tracked_focus_handle: Option, trigger_bounds: Option>, + previous_focus: Option, + open: bool, + on_open_change: Option>, + + _dismiss_subscription: Option, } -impl Default for PopoverElementState { - fn default() -> Self { +impl PopoverState { + pub fn new(default_open: bool, cx: &mut App) -> Self { Self { - trigger_layout_id: None, - popover_layout_id: None, - popover_element: None, - trigger_element: None, - content_view: Rc::new(RefCell::new(None)), + focus_handle: cx.focus_handle(), + tracked_focus_handle: None, trigger_bounds: None, + previous_focus: None, + open: default_open, + on_open_change: None, + _dismiss_subscription: None, } } -} -pub struct PrepaintState { - hitbox: Hitbox, - /// Trigger bounds for limit a rect to handle mouse click. - trigger_bounds: Option>, -} - -impl Element for Popover { - type RequestLayoutState = PopoverElementState; - type PrepaintState = PrepaintState; - - fn id(&self) -> Option { - Some(self.id.clone()) + /// Check if the popover is open. + pub fn is_open(&self) -> bool { + self.open } - fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - id: Option<&gpui::GlobalElementId>, - _: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (gpui::LayoutId, Self::RequestLayoutState) { - let mut style = Style::default(); - - // FIXME: Remove this and find a better way to handle this. - // Apply trigger style, for support w_full for trigger. - // - // If remove this, the trigger will not support w_full. - if let Some(trigger_style) = self.trigger_style.clone() { - if let Some(width) = trigger_style.size.width { - style.size.width = width; - } - if let Some(display) = trigger_style.display { - style.display = display; - } + /// Dismiss the popover if it is open. + pub fn dismiss(&mut self, window: &mut Window, cx: &mut Context) { + if self.open { + self.toggle_open(window, cx); } + } - self.with_element_state( - id.unwrap(), - window, - cx, - |view, element_state, window, cx| { - let mut popover_layout_id = None; - let mut popover_element = None; - let mut is_open = false; + /// Open the popover if it is closed. + pub fn show(&mut self, window: &mut Window, cx: &mut Context) { + if !self.open { + self.toggle_open(window, cx); + } + } - if let Some(content_view) = element_state.content_view.borrow_mut().as_mut() { - is_open = true; + fn toggle_open(&mut self, window: &mut Window, cx: &mut Context) { + self.open = !self.open; + if self.open { + let state = cx.entity(); + self.previous_focus = window.focused(cx); + self.focus_handle(cx).focus(window); - let mut anchored = anchored() - .snap_to_window_with_margin(px(8.)) - .anchor(view.anchor); - if let Some(trigger_bounds) = element_state.trigger_bounds { - anchored = anchored.position(view.resolved_corner(trigger_bounds)); - } - - let mut element = { - let content_view_mut = element_state.content_view.clone(); - let anchor = view.anchor; - let appearance = view.appearance; - deferred( - anchored.child( - div() - .size_full() - .occlude() - .tab_group() - .when(appearance, |this| this.popover_style(cx)) - .map(|this| match anchor { - Corner::TopLeft | Corner::TopRight => this.top_1(), - Corner::BottomLeft | Corner::BottomRight => this.bottom_1(), - }) - .child(content_view.clone()) - .when(appearance, |this| { - this.on_mouse_down_out(move |_, window, _| { - // Update the element_state.content_view to `None`, - // so that the `paint`` method will not paint it. - *content_view_mut.borrow_mut() = None; - window.refresh(); - }) - }), - ), - ) - .with_priority(1) - .into_any() - }; - - popover_layout_id = Some(element.request_layout(window, cx)); - popover_element = Some(element); - } - - let mut trigger_element = view.render_trigger(is_open, window, cx); - let trigger_layout_id = trigger_element.request_layout(window, cx); - - let layout_id = window.request_layout( - style, - Some(trigger_layout_id).into_iter().chain(popover_layout_id), - cx, + self._dismiss_subscription = + Some( + window.subscribe(&cx.entity(), cx, move |_, _: &DismissEvent, window, cx| { + state.update(cx, |state, cx| { + state.dismiss(window, cx); + }); + window.refresh(); + }), ); + } else { + if let Some(previous_focus) = self.previous_focus.take() { + window.focus(&previous_focus); + } + self._dismiss_subscription = None; + } - ( - layout_id, - PopoverElementState { - trigger_layout_id: Some(trigger_layout_id), - popover_layout_id, - popover_element, - trigger_element: Some(trigger_element), - ..Default::default() + if let Some(callback) = self.on_open_change.as_ref() { + callback(&self.open, window, cx); + } + + cx.notify(); + window.refresh(); + } + + fn on_action_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context) { + self.dismiss(window, cx); + } +} + +impl Focusable for PopoverState { + fn focus_handle(&self, _: &App) -> FocusHandle { + if let Some(tracked_focus_handle) = &self.tracked_focus_handle { + tracked_focus_handle.clone() + } else { + self.focus_handle.clone() + } + } +} + +impl Render for PopoverState { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div() + } +} + +impl EventEmitter for PopoverState {} + +impl RenderOnce for Popover { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let force_open = self.open; + let default_open = self.default_open; + let state = window.use_keyed_state(self.id.clone(), cx, |_, cx| { + PopoverState::new(default_open, cx) + }); + if let Some(tracked_focus_handle) = self.tracked_focus_handle.clone() { + state.update(cx, |state, _| { + state.tracked_focus_handle = Some(tracked_focus_handle); + state.on_open_change = self.on_open_change.clone(); + + if let Some(force_open) = force_open { + state.open = force_open; + } + }) + } + + let open = state.read(cx).open; + let focus_handle = state.read(cx).focus_handle.clone(); + let trigger_bounds = state.read(cx).trigger_bounds; + + let Some(trigger) = self.trigger else { + return div().id("empty"); + }; + + let parent_view_id = window.current_view(); + + let el = div() + .id(self.id) + .child((trigger)(open, window, cx)) + .on_mouse_down(self.mouse_button, { + let state = state.clone(); + move |_, window, cx| { + state.update(cx, |state, cx| { + state.toggle_open(window, cx); + }); + cx.notify(parent_view_id); + } + }) + .child( + canvas( + { + let state = state.clone(); + move |bounds, _, cx| { + state.update(cx, |state, _| { + state.trigger_bounds = Some(bounds); + }) + } }, + |_, _, _, _| {}, ) - }, + .absolute() + .size_full(), + ); + + if !open { + return el; + } + + el.child( + deferred( + anchored() + .snap_to_window_with_margin(px(8.)) + .anchor(self.anchor) + .when_some(trigger_bounds, |this, trigger_bounds| { + this.position(Self::resolved_corner(self.anchor, trigger_bounds)) + }) + .child( + v_flex() + .id("content") + .key_context(CONTEXT) + .track_focus(&focus_handle) + .on_action(window.listener_for(&state, PopoverState::on_action_cancel)) + .size_full() + .occlude() + .tab_group() + .when(self.appearance, |this| this.popover_style(cx).p_3()) + .map(|this| match self.anchor { + Corner::TopLeft | Corner::TopRight => this.top_1(), + Corner::BottomLeft | Corner::BottomRight => this.bottom_1(), + }) + .when_some(self.content, |this, content| { + this.child( + state.update(cx, |state, cx| (content)(state, window, cx)), + ) + }) + .children(self.children) + .when(self.appearance, |this| { + let state = state.clone(); + this.on_mouse_down_out(move |_, window, cx| { + state.update(cx, |state, cx| { + state.toggle_open(window, cx); + }); + cx.notify(parent_view_id); + }) + }) + .on_mouse_down_out({ + let state = state.clone(); + move |_, window, cx| { + state.update(cx, |state, cx| { + state.dismiss(window, cx); + }); + cx.notify(parent_view_id); + } + }) + .refine_style(&self.style), + ), + ) + .with_priority(1), ) } - - fn prepaint( - &mut self, - _id: Option<&gpui::GlobalElementId>, - _: Option<&gpui::InspectorElementId>, - _bounds: gpui::Bounds, - request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - if let Some(element) = &mut request_layout.trigger_element { - element.prepaint(window, cx); - } - if let Some(element) = &mut request_layout.popover_element { - element.prepaint(window, cx); - } - - let trigger_bounds = request_layout - .trigger_layout_id - .map(|id| window.layout_bounds(id)); - - // Prepare the popover, for get the bounds of it for open window size. - let _ = request_layout - .popover_layout_id - .map(|id| window.layout_bounds(id)); - - let hitbox = window.insert_hitbox( - trigger_bounds.unwrap_or_default(), - gpui::HitboxBehavior::Normal, - ); - - PrepaintState { - trigger_bounds, - hitbox, - } - } - - fn paint( - &mut self, - id: Option<&GlobalElementId>, - _: Option<&gpui::InspectorElementId>, - _bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - prepaint: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - self.with_element_state( - id.unwrap(), - window, - cx, - |this, element_state, window, cx| { - element_state.trigger_bounds = prepaint.trigger_bounds; - - if let Some(mut element) = request_layout.trigger_element.take() { - element.paint(window, cx); - } - - if let Some(mut element) = request_layout.popover_element.take() { - element.paint(window, cx); - return; - } - - // When mouse click down in the trigger bounds, open the popover. - let Some(content_build) = this.content.take() else { - return; - }; - let old_content_view = element_state.content_view.clone(); - let hitbox_id = prepaint.hitbox.id; - let mouse_button = this.mouse_button; - window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble - && event.button == mouse_button - && hitbox_id.is_hovered(window) - { - cx.stop_propagation(); - window.prevent_default(); - - let new_content_view = (content_build)(window, cx); - let old_content_view1 = old_content_view.clone(); - - let previous_focus_handle = window.focused(cx); - - window - .subscribe( - &new_content_view, - cx, - move |dialog, _: &DismissEvent, window, cx| { - if dialog.focus_handle(cx).contains_focused(window, cx) { - if let Some(previous_focus_handle) = - previous_focus_handle.as_ref() - { - window.focus(previous_focus_handle); - } - } - *old_content_view1.borrow_mut() = None; - - window.refresh(); - }, - ) - .detach(); - - window.focus(&new_content_view.focus_handle(cx)); - *old_content_view.borrow_mut() = Some(new_content_view); - window.refresh(); - } - }); - }, - ); - } } diff --git a/docs/docs/components/popover.md b/docs/docs/components/popover.md index ce1309bd..9bdfb084 100644 --- a/docs/docs/components/popover.md +++ b/docs/docs/components/popover.md @@ -10,62 +10,59 @@ Popover component for displaying floating content that appears when interacting ## Import ```rust -use gpui_component::popover::{Popover, PopoverContent}; +use gpui_component::popover::{Popover}; ``` ## Usage ### Basic Popover +:::info +Any element that implements [Selectable] can be used as a trigger, for example, a [Button]. + +Any element that implements [RenderOnce] or [Render] can be used as popover content, use `.child(...)` to add children directly. +::: + ```rust +use gpui::ParentElement as _; +use gpui_component::{button::Button, popover::Popover}; + Popover::new("basic-popover") .trigger(Button::new("trigger").label("Click me").outline()) - .content(|window, cx| { - cx.new(|cx| { - PopoverContent::new(window, cx, |_, _| { - div() - .p_4() - .child("Hello, this is a popover!") - .into_any() - }) - }) - }) + .child("Hello, this is a popover!") + .child("It appears when you click the button.") ``` ### Popover with Custom Positioning +The `anchor` method allows you to specify where the popover appears relative to the trigger element, this anchor point can be one of the four corners: TopLeft, TopRight, BottomLeft, BottomRight. + ```rust use gpui::Corner; Popover::new("positioned-popover") .anchor(Corner::TopRight) .trigger(Button::new("top-right").label("Top Right").outline()) - .content(|window, cx| { - cx.new(|cx| { - PopoverContent::new(window, cx, |_, _| { - div() - .p_4() - .w_64() - .child("This popover appears at the top right") - .into_any() - }) - }) - }) + .child("This popover appears at the top right") ``` -### Form in Popover +### View in Popover + +You can add any `Entity` that implemented [Render] as the popover content. ```rust -let form = Form::new(window, cx); +let view = cx.new(|_| MyView::new()); Popover::new("form-popover") .anchor(Corner::BottomLeft) .trigger(Button::new("show-form").label("Open Form").outline()) - .content(move |_, _| form.clone()) + .child(view.clone()) ``` ### Right-Click Popover +Sometimes you may want to show a popover on right-click, for example, to create a special your ownen context menu. The `mouse_button` method allows you to specify which mouse button triggers the popover. + ```rust use gpui::MouseButton; @@ -73,595 +70,98 @@ Popover::new("context-menu") .anchor(Corner::BottomRight) .mouse_button(MouseButton::Right) .trigger(Button::new("right-click").label("Right Click Me").outline()) - .content(|window, cx| { - cx.new(|cx| { - PopoverContent::new(window, cx, |_, cx| { - v_flex() - .gap_2() - .child("Context Menu") - .child(Divider::horizontal()) - .child( - Button::new("action") - .label("Perform Action") - .on_click(cx.listener(|_, _, window, cx| { - window.push_notification("Action performed!", cx); - cx.emit(DismissEvent); - })) - ) - .into_any() - }) - .p_4() - }) - }) + .child("Context Menu") + .child(Divider::horizontal()) + .child("This is a custom context menu.") ``` -## Advanced Usage +### Dismiss Popover manually -### Rich Content Popover +If you want to dismiss the popover programmatically from within the content, you can emit a `DismissEvent`. In this case, you should use `content` method to create the popover content so you have access to the `cx: &mut Context`. ```rust -Popover::new("rich-content") - .trigger(Button::new("info").icon(IconName::Info).outline()) - .content(|window, cx| { - cx.new(|cx| { - PopoverContent::new(window, cx, |_, cx| { - v_flex() - .gap_4() - .max_w(px(400.)) - .child( - h_flex() - .gap_2() - .items_center() - .child(Icon::new(IconName::Info).size_5()) - .child("Information") - .text_lg() - .font_semibold() - ) - .child(Divider::horizontal()) - .child( - div() - .child("This is detailed information about the feature.") - .text_sm() - .text_color(cx.theme().muted_foreground) - ) - .child( - h_flex() - .gap_2() - .justify_end() - .child( - Button::new("learn-more") - .label("Learn More") - .small() - .primary() - ) - .child( - Button::new("close") - .label("Close") - .small() - .on_click(cx.listener(|_, _, _, cx| { - cx.emit(DismissEvent); - })) - ) - ) - .into_any() - }) - .p_4() - }) +use gpui_component::{DismissEvent, popover::Popover}; + +Popover::new("dismiss-popover") + .trigger(Button::new("dismiss").label("Dismiss Popover").outline()) + .content(|_, cx| { + div() + .child("Click the button below to dismiss this popover.") + .child( + Button::new("close-btn") + .label("Close Popover") + .on_click(cx.listener(|_, _, _, cx| { + // NOTE: Here `cx` is `&mut Context` type, so we can emit DismissEvent. + cx.emit(DismissEvent); + })) + ) }) ``` -### Unstyled Popover +### Styling Popover + +Like the others components in GPUI Component, the `appearance(false)` method can be used to disable the default styling of the popover, allowing you to fully customize its appearance. + +And the `Popover` has implemented the [Styled] trait, so you can use all the styling methods provided by GPUI to style the popover content as you like. ```rust // For custom styled popovers or when you want full control Popover::new("custom-popover") .appearance(false) .trigger(Button::new("custom").label("Custom Style")) - .content(|window, cx| { - cx.new(|cx| { - PopoverContent::new(window, cx, |_, cx| { - div() - .bg(cx.theme().accent) - .text_color(cx.theme().accent_foreground) - .p_6() - .rounded_xl() - .shadow_2xl() - .child("Fully custom styled popover") - .into_any() - }) - }) - }) + .bg(cx.theme().accent) + .text_color(cx.theme().accent_foreground) + .p_6() + .rounded_xl() + .shadow_2xl() + .child("Fully custom styled popover") ``` -### Popover with Different Triggers +### Control Open State + +There have `open` and `on_open_change` methods to control the open state of the popover programmatically. + +This is useful when you want to synchronize the popover's open state with other UI elements or application state. + +:::tip +When you use `open` to control the popover's open state, that means you have take full control of it, +so you need to update the state in `on_open_change` callback to keep the popover working correctly. +::: ```rust -// Button trigger -Popover::new("button-trigger") - .trigger(Button::new("btn").label("Button Trigger")) - .content(content_fn) +use gpui_component::popover::Popover; -// Custom element trigger -Popover::new("div-trigger") - .trigger( - div() - .p_2() - .bg(cx.theme().muted) - .rounded(px(4.)) - .child("Click this div") - .cursor_pointer() - ) - .content(content_fn) - -// Icon trigger -Popover::new("icon-trigger") - .trigger(Icon::new(IconName::HelpCircle).size_5()) - .content(content_fn) -``` - -### Dismissible Popover with Actions - -```rust -Popover::new("action-popover") - .trigger(Button::new("actions").label("Show Actions")) - .content(|window, cx| { - cx.new(|cx| { - PopoverContent::new(window, cx, |_, cx| { - v_flex() - .gap_2() - .child("Choose an action:") - .child(Divider::horizontal()) - .child( - v_flex() - .gap_1() - .child( - Button::new("copy") - .label("Copy") - .small() - .w_full() - .justify_start() - .on_click(cx.listener(|_, _, window, cx| { - window.push_notification("Copied!", cx); - cx.emit(DismissEvent); - })) - ) - .child( - Button::new("paste") - .label("Paste") - .small() - .w_full() - .justify_start() - .on_click(cx.listener(|_, _, window, cx| { - window.push_notification("Pasted!", cx); - cx.emit(DismissEvent); - })) - ) - .child( - Button::new("delete") - .label("Delete") - .small() - .w_full() - .justify_start() - .destructive() - .on_click(cx.listener(|_, _, window, cx| { - window.push_notification("Deleted!", cx); - cx.emit(DismissEvent); - })) - ) - ) - .into_any() - }) - .p_2() - .min_w(px(120.)) - }) - }) -``` - -### Trigger Styling - -```rust -// Full width trigger -Popover::new("full-width") - .trigger_style(StyleRefinement { - size: Size { width: Some(relative(1.0)), ..Default::default() }, - ..Default::default() - }) - .trigger(Button::new("full").label("Full Width Button")) - .content(content_fn) - -// Custom display -Popover::new("flex-trigger") - .trigger_style(StyleRefinement { - display: Some(Display::Flex), - ..Default::default() - }) - .trigger(Button::new("flex").label("Flex Button")) - .content(content_fn) -``` - -## Positioning and Anchoring - -### Anchor Positions - -```rust -use gpui::Corner; - -// Top left (default) -.anchor(Corner::TopLeft) // Popover appears below trigger, aligned to left - -// Top right -.anchor(Corner::TopRight) // Popover appears below trigger, aligned to right - -// Bottom left -.anchor(Corner::BottomLeft) // Popover appears above trigger, aligned to left - -// Bottom right -.anchor(Corner::BottomRight) // Popover appears above trigger, aligned to right -``` - -### Positioning Behavior - -The popover automatically: - -- Snaps to window edges with 8px margin -- Adjusts position to stay within viewport -- Resolves anchor position relative to trigger bounds -- Handles collision detection with window boundaries - -## Trigger Methods - -### Mouse Button Configuration - -```rust -use gpui::MouseButton; - -// Left click (default) -.mouse_button(MouseButton::Left) - -// Right click for context menus -.mouse_button(MouseButton::Right) - -// Middle click -.mouse_button(MouseButton::Middle) -``` - -### Selectable Triggers - -The trigger element must implement the `Selectable` trait. Most UI components like `Button`, `div`, etc. support this: - -```rust -// Button automatically supports selection state -.trigger(Button::new("btn").label("Click me")) - -// Custom elements with selection state -.trigger(my_custom_element.selected(is_selected)) -``` - -## Custom Content - -### PopoverContent Builder - -The `PopoverContent` provides a flexible way to create popover content: - -```rust -PopoverContent::new(window, cx, |window, cx| { - // Return any element that implements IntoElement - v_flex() - .gap_3() - .child("Content goes here") - .child(Button::new("action").label("Action")) - .into_any() -}) -``` - -### Content Styling - -PopoverContent can be styled using the `Styled` trait: - -```rust -PopoverContent::new(window, cx, content_fn) - .p_6() // Custom padding - .max_w(px(500.)) // Maximum width - .bg(cx.theme().card) // Custom background -``` - -### Reusable Content Components - -```rust -// Create reusable content components -struct InfoPopover { - title: String, - description: String, +struct MyView { + popover_open: bool, } -impl InfoPopover { - fn render(&self, _: &mut Window, cx: &mut Context) -> AnyElement { - v_flex() - .gap_3() - .child( - div() - .text_lg() - .font_semibold() - .child(&self.title) - ) - .child( - div() - .text_sm() - .text_color(cx.theme().muted_foreground) - .child(&self.description) - ) - .into_any() - } -} - -// Use in popover -.content(|window, cx| { - let info = InfoPopover { - title: "Feature Info".to_string(), - description: "This feature helps you...".to_string(), - }; - - cx.new(|cx| { - PopoverContent::new(window, cx, move |window, cx| { - info.render(window, cx) - }) - }) -}) +Popover::new("controlled-popover") + .open(self.open) + .on_open_change(cx.listener(|this, open: &bool, _, cx| { + this.popover_open = *open; + cx.notify(); + })) + .trigger(Button::new("control-btn").label("Control Popover").outline()) + .child("This popover's open state is controlled programmatically.") ``` -### Default Styling +### Default Open -When not using `no_style()`, popovers automatically apply: +The `default_open` method allows you to set the initial open state of the popover when it is first rendered. + +Please note that if you use the `open` method to control the popover's open state, the `default_open` setting will be ignored. ```rust -.bg(cx.theme().popover) // Background color -.text_color(cx.theme().popover_foreground) // Text color -.border_1() // 1px border -.border_color(cx.theme().border) // Border color -.shadow_lg() // Large shadow +use gpui_component::popover::Popover; + +Popover::new("default-open-popover") + .default_open(true) + .trigger(Button::new("default-open-btn").label("Default Open").outline()) + .child("This popover is open by default when first rendered.") ``` -### Dismissal Events - -Popovers can be dismissed by: - -- Clicking outside the popover (when styled) -- Pressing the Escape key -- Emitting a `DismissEvent` from content - -```rust -// Emit DismissEvent to close popover -cx.emit(DismissEvent) - -// Subscribe to dismissal in content -window.subscribe(&content_view, cx, |_, _: &DismissEvent, window, cx| { - // Handle popover dismissal -}); -``` - -## Examples - -### Tooltip-style Popover - -```rust -Popover::new("tooltip") - .trigger( - div() - .child("Hover me") - .p_2() - .cursor_help() - ) - .content(|window, cx| { - cx.new(|cx| { - PopoverContent::new(window, cx, |_, _| { - div() - .p_2() - .text_xs() - .child("This is helpful information") - .into_any() - }) - .max_w(px(200.)) - }) - }) -``` - -### Dropdown Menu - -```rust -Popover::new("dropdown") - .anchor(Corner::BottomLeft) - .trigger( - Button::new("menu") - .label("Menu") - .icon_after(IconName::ChevronDown) - ) - .content(|window, cx| { - cx.new(|cx| { - PopoverContent::new(window, cx, |_, cx| { - v_flex() - .gap_1() - .child(menu_item("New File", IconName::Plus, cx)) - .child(menu_item("Open File", IconName::FolderOpen, cx)) - .child(Divider::horizontal()) - .child(menu_item("Settings", IconName::Settings, cx)) - .into_any() - }) - .p_1() - .min_w(px(150.)) - }) - }) - -fn menu_item(label: &str, icon: IconName, cx: &Context) -> impl IntoElement { - Button::new(label.to_lowercase().replace(" ", "-")) - .icon(icon) - .label(label) - .small() - .ghost() - .w_full() - .justify_start() - .on_click(cx.listener(move |_, _, window, cx| { - window.push_notification(format!("{} clicked", label), cx); - cx.emit(DismissEvent); - })) -} -``` - -### Confirmation Popover - -```rust -Popover::new("confirm-delete") - .anchor(Corner::TopRight) - .trigger( - Button::new("delete") - .icon(IconName::Trash) - .destructive() - ) - .content(|window, cx| { - cx.new(|cx| { - PopoverContent::new(window, cx, |_, cx| { - v_flex() - .gap_3() - .child( - h_flex() - .gap_2() - .items_center() - .child(Icon::new(IconName::AlertTriangle).text_color(cx.theme().warning)) - .child("Confirm Deletion") - .font_semibold() - ) - .child( - div() - .text_sm() - .child("Are you sure you want to delete this item? This action cannot be undone.") - ) - .child( - h_flex() - .gap_2() - .justify_end() - .child( - Button::new("cancel") - .label("Cancel") - .small() - .on_click(cx.listener(|_, _, _, cx| { - cx.emit(DismissEvent); - })) - ) - .child( - Button::new("confirm") - .label("Delete") - .small() - .destructive() - .on_click(cx.listener(|_, _, window, cx| { - window.push_notification("Item deleted", cx); - cx.emit(DismissEvent); - })) - ) - ) - .into_any() - }) - .p_4() - .max_w(px(300.)) - }) - }) -``` - -### User Profile Popover - -```rust -Popover::new("user-profile") - .anchor(Corner::BottomRight) - .trigger( - div() - .flex() - .items_center() - .gap_2() - .p_2() - .rounded(px(6.)) - .hover(|this, cx| this.bg(cx.theme().muted)) - .cursor_pointer() - .child(Avatar::new("user").name("John Doe").size(Size::Small)) - .child("John Doe") - ) - .content(|window, cx| { - cx.new(|cx| { - PopoverContent::new(window, cx, |_, cx| { - v_flex() - .gap_4() - .child( - h_flex() - .gap_3() - .items_center() - .child(Avatar::new("user").name("John Doe")) - .child( - v_flex() - .gap_1() - .child("John Doe") - .child( - div() - .text_sm() - .text_color(cx.theme().muted_foreground) - .child("john.doe@example.com") - ) - ) - ) - .child(Divider::horizontal()) - .child( - v_flex() - .gap_1() - .child(profile_menu_item("Profile", IconName::User, cx)) - .child(profile_menu_item("Settings", IconName::Settings, cx)) - .child(profile_menu_item("Help", IconName::HelpCircle, cx)) - .child(Divider::horizontal()) - .child(profile_menu_item("Sign Out", IconName::LogOut, cx)) - ) - .into_any() - }) - .p_3() - .w(px(240.)) - }) - }) -``` - -## Performance Considerations - -### Efficient Content Creation - -```rust -// Good: Lazy content creation -.content(|window, cx| { - // Content is only created when popover opens - cx.new(|cx| expensive_content_creation(window, cx)) -}) - -// Avoid: Pre-creating content -let content = expensive_content_creation(); // Created immediately -.content(move |_, _| content.clone()) -``` - -### Memory Management - -```rust -// The popover automatically manages content lifecycle: -// - Content is created when popover opens -// - Content is destroyed when popover closes -// - No memory leaks from unclosed popovers - -// For complex content, consider cleanup: -window.subscribe(&content_view, cx, |_, _: &DismissEvent, _, _| { - // Cleanup resources when popover closes -}); -``` - -### Styling Performance - -```rust -// Good: Use theme colors for consistency -.bg(cx.theme().popover) -.text_color(cx.theme().popover_foreground) - -// Good: Minimal custom styling -PopoverContent::new(window, cx, content_fn) - .p_4() // Simple padding - -// Avoid: Complex nested styling in hot paths -``` +[Button]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.Button.html +[Selectable]: https://docs.rs/gpui-component/latest/gpui_component/trait.Selectable.html +[Render]: https://docs.rs/gpui/latest/gpui/trait.Render.html +[RenderOnce]: https://docs.rs/gpui/latest/gpui/trait.RenderOnce.html +[Styled]: https://docs.rs/gpui/latest/gpui/trait.Styled.html