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`.
This commit is contained in:
Jason Lee 2025-11-11 17:45:30 +08:00 committed by GitHub
parent 13f25bc4a9
commit 2dbfba3490
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 626 additions and 1068 deletions

View file

@ -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<PopoverStory>,
input1: Entity<InputState>,
}
impl Form {
fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
fn new(parent: Entity<PopoverStory>, window: &mut Window, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self {
parent,
input1: cx.new(|cx| InputState::new(window, cx)),
})
}
@ -63,17 +67,24 @@ impl EventEmitter<DismissEvent> for Form {}
impl Render for Form {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> 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>,
form_open: bool,
checked: bool,
message: String,
}
@ -105,13 +117,14 @@ impl PopoverStory {
}
fn new(window: &mut Window, cx: &mut Context<Self>) -> 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."),
),
),
),
)
}
}

View file

@ -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>) -> PopupMenu + 'static,
) -> Popover<PopupMenu> {
) -> DropdownMenuPopover<Self> {
self.dropdown_menu_with_anchor(Corner::TopLeft, f)
}
@ -17,19 +22,116 @@ pub trait DropdownMenu: Styled + Selectable + InteractiveElement + IntoElement +
mut self,
anchor: impl Into<Corner>,
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
) -> Popover<PopupMenu> {
) -> DropdownMenuPopover<Self> {
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<T: Selectable + IntoElement + 'static> {
id: ElementId,
style: StyleRefinement,
anchor: Corner,
trigger: T,
builder: Rc<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu>,
}
impl<T> DropdownMenuPopover<T>
where
T: Selectable + IntoElement + 'static,
{
fn new(
id: ElementId,
anchor: impl Into<Corner>,
trigger: T,
builder: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> 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<Corner>) -> 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<Entity<PopupMenu>>,
}
impl<T> RenderOnce for DropdownMenuPopover<T>
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()
})
}
}

View file

@ -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<dyn Fn(&mut Window, &mut Context<Self>) -> AnyElement>,
}
impl PopoverContent {
pub fn new<B>(_: &mut Window, cx: &mut App, content: B) -> Self
where
B: Fn(&mut Window, &mut Context<Self>) -> AnyElement + 'static,
{
let focus_handle = cx.focus_handle();
Self {
style: StyleRefinement::default(),
focus_handle,
content: Rc::new(content),
}
}
}
impl EventEmitter<DismissEvent> 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<Self>) -> 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<M: ManagedView> {
#[derive(IntoElement)]
pub struct Popover {
id: ElementId,
style: StyleRefinement,
anchor: Corner,
default_open: bool,
open: Option<bool>,
tracked_focus_handle: Option<FocusHandle>,
trigger: Option<Box<dyn FnOnce(bool, &Window, &App) -> AnyElement + 'static>>,
content: Option<Rc<dyn Fn(&mut Window, &mut App) -> Entity<M> + 'static>>,
content: Option<
Rc<
dyn Fn(&mut PopoverState, &mut Window, &mut Context<PopoverState>) -> AnyElement
+ 'static,
>,
>,
children: Vec<AnyElement>,
/// Style for trigger element.
/// This is used for hotfix the trigger element style to support w_full.
trigger_style: Option<StyleRefinement>,
mouse_button: MouseButton,
appearance: bool,
on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
}
impl<M> Popover<M>
where
M: ManagedView,
{
impl Popover {
/// Create a new Popover with `view` mode.
pub fn new(id: impl Into<ElementId>) -> 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<F>(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<C>(mut self, content: C) -> Self
pub fn content<F, E>(mut self, content: F) -> Self
where
C: Fn(&mut Window, &mut App) -> Entity<M> + 'static,
E: IntoElement,
F: Fn(&mut PopoverState, &mut Window, &mut Context<PopoverState>) -> 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<Pixels>) -> Point<Pixels> {
bounds.corner(match self.anchor {
fn resolved_corner(anchor: Corner, bounds: Bounds<Pixels>) -> Point<Pixels> {
bounds.corner(match anchor {
Corner::TopLeft => Corner::BottomLeft,
Corner::TopRight => Corner::BottomRight,
Corner::BottomLeft => Corner::TopLeft,
Corner::BottomRight => Corner::TopRight,
})
}
fn with_element_state<R>(
&mut self,
id: &GlobalElementId,
window: &mut Window,
cx: &mut App,
f: impl FnOnce(&mut Self, &mut PopoverElementState<M>, &mut Window, &mut App) -> R,
) -> R {
window.with_optional_element_state::<PopoverElementState<M>, _>(
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<M> IntoElement for Popover<M>
where
M: ManagedView,
{
type Element = Self;
fn into_element(self) -> Self::Element {
self
impl ParentElement for Popover {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements);
}
}
pub struct PopoverElementState<M> {
trigger_layout_id: Option<LayoutId>,
popover_layout_id: Option<LayoutId>,
popover_element: Option<AnyElement>,
trigger_element: Option<AnyElement>,
content_view: Rc<RefCell<Option<Entity<M>>>>,
/// 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<FocusHandle>,
trigger_bounds: Option<Bounds<Pixels>>,
previous_focus: Option<FocusHandle>,
open: bool,
on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
_dismiss_subscription: Option<Subscription>,
}
impl<M> Default for PopoverElementState<M> {
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<Bounds<Pixels>>,
}
impl<M: ManagedView> Element for Popover<M> {
type RequestLayoutState = PopoverElementState<M>;
type PrepaintState = PrepaintState;
fn id(&self) -> Option<ElementId> {
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<Self>) {
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<Self>) {
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>) {
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>) {
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<Self>) -> impl IntoElement {
div()
}
}
impl EventEmitter<DismissEvent> 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<gpui::Pixels>,
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<Pixels>,
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();
}
});
},
);
}
}

View file

@ -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<T>` 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<PopoverState>`.
```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<PopoverState>` 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<PopoverContent>) -> 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<PopoverContent>) -> 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