Popover

Popover component for displaying floating content that appears when interacting with a trigger element. Supports multiple positioning options, custom content, different trigger methods, and automatic dismissal behaviors. Perfect for tooltips, menus, forms, and other contextual information.

Install

#Import

use gpui_kit::component::popover::{Popover};
// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { Popover } from "popover.slint";
use iced::widget::{column, container, mouse_area, row, scrollable, svg, text};
use iced::{Alignment, Element, Length, Task, Theme};

#Usage

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// The whole of a popover: a trigger, the content, the caller's flag, and the
// one message that moves it. A trigger click, an outside click and Escape all
// report through `on_open_change`.
let usage_section = column![
    usage,
    muted_line(
        "`open` is the caller's, and `on_open_change` is the only thing the popover reports."
    ),
]
.spacing(8.0);

#Click

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// A click outside the panel dismisses it; so does Escape. The press is
// consumed by the overlay, so it does not also reach whatever is under it.
let click_section = column![
    click,
    muted_line("Click outside the panel, or press Escape, to dismiss it."),
]
.spacing(8.0);

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.

use gpui_kit::MouseButton;

Popover::new("context-menu")
    .anchor(Anchor::BottomRight)
    .mouse_button(MouseButton::Right)
    .trigger(Button::new("right-click").label("Right Click Me").outline())
    .child("Context Menu")
    .child(Separator::horizontal())
    .child("This is a custom context menu.")
Popover {
    trigger-text: "Open popover";
    trigger-icon: "chevron-down";
    panel-width: 280px;
    VerticalLayout {
        spacing: 6px;
        Text {
            text: "Click outside to dismiss.";
            color: Theme.foreground;
            font-size: Theme.font-size-sm;
        }
        Text {
            text: "The panel is a popup, so it is not clipped by this card.";
            color: Theme.muted-foreground;
            font-size: Theme.font-size-xs;
            wrap: word-wrap;
        }
    }
}

#Basic Popover

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
let basic_section = body_box(basic);
use gpui_kit::ParentElement as _;
use gpui_kit::component::{button::Button, popover::Popover};

Popover::new("basic-popover")
    .trigger(Button::new("trigger").label("Click me").outline())
    .child("Hello, this is a popover!")
    .child("It appears when you click the button.")
Popover {
    trigger-text: "Click me";
    panel-width: 260px;
    VerticalLayout {
        spacing: 4px;
        Text {
            text: "Hello, this is a popover!";
            color: Theme.foreground;
            font-size: Theme.font-size-sm;
        }
        Text {
            text: "It appears when you click the button.";
            color: Theme.muted-foreground;
            font-size: Theme.font-size-xs;
            wrap: word-wrap;
        }
    }
}

#Popover with Custom Positioning

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// Every side is named, and each panel resolves its own: the preferred side
// when the window has room, the opposite side when it does not, and a clamp
// into the window when neither fits. The arrow GPUI's popover draws is not
// part of the Slint component, so there is none to place.
let positioning = row![below, right, above, left]
    .spacing(24.0)
    .align_y(Alignment::Center);

The anchor method names the popover’s own anchor, not the trigger’s corner. Top* anchors open below the trigger, Bottom* anchors open above it, LeftCenter opens to the right, and RightCenter opens to the left. The popup clamps to the window without changing its anchor or flipping.

use gpui_kit::{Anchor, px};
use gpui_kit::component::popover::Popover;

Popover::new("anchored")
    .anchor(Anchor::TopCenter)
    .offset(px(8.))
    .arrow(true)
    .trigger(Button::new("details").label("Details"))
    .child("Contextual details")
HorizontalLayout {
    spacing: 24px;
    alignment: center;
    Popover {
        placement: Placement.bottom;
        trigger-text: "Below";
        panel-width: 220px;
        Text {
            text: "Placement.bottom";
            color: Theme.foreground;
            font-size: Theme.font-size-xs;
        }
    }
    Popover {
        placement: Placement.right;
        trigger-text: "Right";
        panel-width: 220px;
        Text {
            text: "Placement.right";
            color: Theme.foreground;
            font-size: Theme.font-size-xs;
        }
    }
    Popover {
        placement: Placement.top;
        trigger-text: "Above";
        panel-width: 220px;
        Text {
            text: "Placement.top";
            color: Theme.foreground;
            font-size: Theme.font-size-xs;
        }
    }
}
OptionMeaningDefault
anchor(Anchor)Popup anchor, including TopCenter and BottomCenterTopLeft
offset(Pixels)Gap from trigger to surface, or to arrow tip when enabled0.25rem
arrow(bool)Show an arrow on the edge selected by the anchorfalse

The arrow follows the anchor’s leading, center, or trailing alignment and is inset as needed to avoid rounded corners. It adds 0.375rem to the surface distance and uses the surface background, falling back to the theme’s popover color. Neither offset nor arrow changes the positioning strategy.

For example, Anchor::TopLeft places the popover just below the trigger, left-aligned to it:

[ Trigger ]
┌──────────────┐
│   Popover    │
└──────────────┘
SlintNo Slint version of this example yet.
use gpui_kit::component::Anchor;

// Below the trigger: name the popover's top anchor
Popover::new("top-left")
    .anchor(Anchor::TopLeft)
    .trigger(Button::new("btn").label("Top Left").outline())
    .child("Below the trigger, aligned left")

Popover::new("top-center")
    .anchor(Anchor::TopCenter)
    .trigger(Button::new("btn").label("Top Center").outline())
    .child("Below the trigger, centered")

Popover::new("top-right")
    .anchor(Anchor::TopRight)
    .trigger(Button::new("btn").label("Top Right").outline())
    .child("Below the trigger, aligned right")

// Above the trigger: name the popover's bottom anchor
Popover::new("bottom-left")
    .anchor(Anchor::BottomLeft)
    .trigger(Button::new("btn").label("Bottom Left").outline())
    .child("Above the trigger, aligned left")

Popover::new("bottom-center")
    .anchor(Anchor::BottomCenter)
    .trigger(Button::new("btn").label("Bottom Center").outline())
    .child("Above the trigger, centered")

Popover::new("bottom-right")
    .anchor(Anchor::BottomRight)
    .trigger(Button::new("btn").label("Bottom Right").outline())
    .child("Above the trigger, aligned right")
SlintNo Slint version of this example yet.

#View in Popover

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// Any element can be the panel's content, not just text.
let view_section = body_box(activity);

You can add any Entity<T> that implemented Render as the popover content.

let view = cx.new(|_| MyView::new());

Popover::new("form-popover")
    .anchor(Anchor::BottomLeft)
    .trigger(Button::new("show-form").label("Open Form").outline())
    .child(view.clone())
Popover {
    trigger-text: "Open activity";
    panel-width: 280px;
    VerticalLayout {
        spacing: 10px;
        Text {
            text: "Recent activity";
            color: Theme.muted-foreground;
            font-size: Theme.font-size-xs;
        }
        for row in [
            { title: "Build finished", detail: "2 minutes ago" },
            { title: "New comment", detail: "9 minutes ago" },
            { title: "Deploy failed", detail: "1 hour ago" }
        ]
        : HorizontalLayout {
            spacing: 8px;
            cross-axis-alignment: center;
            Icon {
                name: row.title == "Deploy failed" ? "circle-x" : "circle-check";
                size: 14px;
                color: row.title == "Deploy failed" ? Theme.danger : Theme.success;
            }
            VerticalLayout {
                spacing: 1px;
                Text {
                    text: row.title;
                    color: Theme.foreground;
                    font-size: Theme.font-size-sm;
                }
                Text {
                    text: row.detail;
                    color: Theme.muted-foreground;
                    font-size: Theme.font-size-xs;
                }
            }
        }
    }
}

#Add content by content method

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// A panel can hold interactive children. A control inside it sends an
// ordinary message, exactly as it would anywhere else.
let complex_section = body_box(complex);

The content method allows you to create more complex popover content using a closure. This is useful when you need to build dynamic content or need access to the popover’s context.

This method will let us to have &mut PopoverState, &mut Window and &mut Context<PopoverState> parameters in the closure is to allow you to interact with the popover’s state and the overall application context if needed.

And content will works with child, children methods together.

use gpui_kit::ParentElement as _;
use gpui_kit::component::popover::Popover;

Popover::new("complex-popover")
    .anchor(Anchor::BottomLeft)
    .trigger(Button::new("complex").label("Complex Content").outline())
    .content(|_, _, _| {
        div()
            .child("This popover has complex content.")
            .child(
                Button::new("action-btn")
                    .label("Perform Action")
                    .outline()
            )
    })
Popover {
    trigger-text: "Complex content";
    panel-width: 280px;
    VerticalLayout {
        spacing: 10px;
        Text {
            text: "This popover has complex content.";
            color: Theme.foreground;
            font-size: Theme.font-size-sm;
        }
        HorizontalLayout {
            spacing: 8px;
            alignment: start;
            Button {
                text: "Perform action";
                outline: true;
            }
            Button {
                text: "Cancel";
                variant: ButtonVariant.ghost;
            }
        }
    }
}

#Dismiss Popover manually

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// Closing from inside needs a handle on the popover, and the host has one:
// the button sends the same message an outside click would.
let dismiss_section = body_box(dismissable);

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>.

use gpui_kit::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);
                    }))
            )
    })
dismiss := Popover {
    trigger-text: "Dismiss popover";
    panel-width: 300px;
    VerticalLayout {
        spacing: 10px;
        Text {
            text: "Click the button below to dismiss this popover.";
            color: Theme.foreground;
            font-size: Theme.font-size-sm;
            wrap: word-wrap;
        }
        Button {
            text: "Close popover";
            outline: true;
            clicked => {
                dismiss.open = false;
            }
        }
    }
}

#Styling Popover

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// A host that wants its own look states it through `panel` rather than
// reaching into the surface, which it cannot see. This is Slint's
// `panel-background: Theme.primary`, `panel-radius: Theme.radius-xl` and
// `panel-border: false`.
let style_section = body_box(styled);

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.

// For custom styled popovers or when you want full control
Popover::new("custom-popover")
    .appearance(false)
    .trigger(Button::new("custom").label("Custom Style"))
    .bg(cx.theme().accent)
    .text_color(cx.theme().accent_foreground)
    .p_6()
    .rounded_xl()
    .shadow_2xl()
    .child("Fully custom styled popover")
Popover {
    trigger-text: "Custom style";
    panel-width: 300px;
    panel-background: Theme.primary;
    panel-radius: Theme.radius-xl;
    panel-border: false;
    VerticalLayout {
        spacing: 4px;
        Text {
            text: "Fully custom styled popover";
            color: Theme.primary-foreground;
            font-size: Theme.font-size-sm;
            font-weight: 600;
        }
        Text {
            text: "A custom surface, radius, and no border.";
            color: Theme.primary-foreground;
            font-size: Theme.font-size-xs;
            wrap: word-wrap;
        }
    }
}

#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.

use gpui_kit::component::popover::Popover;

struct MyView {
    popover_open: bool,
}

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.")
HorizontalLayout {
    spacing: 12px;
    alignment: start;
    Button {
        text: root.controlled ? "Close" : "Open";
        outline: true;
        clicked => {
            root.controlled = !root.controlled;
        }
    }
    controlled := Popover {
        trigger-text: "Controlled popover";
        open <=> root.controlled;
        panel-width: 280px;
        Text {
            text: "This popover's open state is controlled programmatically.";
            color: Theme.foreground;
            font-size: Theme.font-size-xs;
            wrap: word-wrap;
        }
    }
}

#Default Open

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.

use gpui_kit::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.")
SlintNo Slint version of this example yet.

#Custom Trigger

A trigger is any element that implements Selectable. While the popover is open, it calls open(true) on the trigger — not selected(true) — so a trigger can tell “my popover is showing” apart from “I am the selected item”.

open and is_open default to selected and is_selected, so a trigger that only implements the selected state keeps working unchanged, and a Button trigger looks the same open as it does selected. Override them when your element already uses selected for something else, such as a sidebar row that is selected when it is the current view:

use gpui_kit::component::Selectable;

struct SidebarRow {
    /// This row is the current view.
    selected: bool,
    /// This row's account popover is showing.
    open: bool,
}

impl Selectable for SidebarRow {
    fn selected(mut self, selected: bool) -> Self {
        self.selected = selected;
        self
    }

    fn is_selected(&self) -> bool {
        self.selected
    }

    fn open(mut self, open: bool) -> Self {
        self.open = open;
        self
    }

    fn is_open(&self) -> bool {
        self.open
    }
}
SlintNo Slint version of this example yet.