Skip to main content

gpui/platform/
popup.rs

1use bitflags::bitflags;
2use thiserror::Error;
3
4use crate::{AnyWindowHandle, Bounds, Pixels, Point, util::FluentBuilder};
5
6/// Describes where a native popup goes, relative to a rectangle on its parent window.
7///
8/// Use a popup for menus, comboboxes, context menus and tooltips that should be able to leave
9/// the parent window's bounds. Use an in-window element for everything else. Pass the popup to
10/// [`WindowKind::AnchoredPopup`](crate::WindowKind::AnchoredPopup) in [`WindowOptions::kind`](crate::WindowOptions::kind).
11///
12/// The popup is placed relative to `anchor_rect` rather than at an absolute screen position.
13/// The platform resolves the final position. That works on Wayland, where the compositor places
14/// windows, and on platforms with absolute coordinates.
15///
16/// The popup's size comes from `window_bounds` in [`WindowOptions`](crate::WindowOptions).
17/// Wayland uses only the size, not the origin. All coordinates are in logical pixels.
18///
19/// Native popups are available on Wayland only. On macOS, Windows, X11 and the web, opening one
20/// returns a [`PopupNotSupportedError`] and you can draw the same content in the window instead.
21///
22/// Pass the parent window and anchor rectangle to [`PopupOptions::new`], then chain setters to
23/// configure placement and input grabbing.
24///
25/// # Examples
26///
27/// ```
28/// # use gpui::{Context, Render, TestApp, Window, div, prelude::*, px, point, size, Bounds};
29/// use gpui::popup::{PopupAnchor, PopupGravity, PopupOptions};
30/// # struct Root;
31/// # impl Render for Root {
32/// #     fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement { div() }
33/// # }
34/// # let mut app = TestApp::new();
35/// # let _window = app.open_window(|_, _| Root);
36/// # let parent = app.windows()[0];
37/// // A dropdown below a button whose bounds are `anchor_rect`.
38/// let anchor_rect = Bounds::new(point(px(10.), px(20.)), size(px(80.), px(24.)));
39/// let options = PopupOptions::new(parent, anchor_rect)
40///     .anchor(PopupAnchor::BottomLeft)
41///     .gravity(PopupGravity::BottomRight)
42///     .grab(true);
43///
44/// assert_eq!(options.anchor, PopupAnchor::BottomLeft);
45/// assert_eq!(options.gravity, PopupGravity::BottomRight);
46/// assert!(options.grab);
47/// ```
48#[derive(Clone, Debug, PartialEq, Eq, derive_setters::Setters)]
49#[setters(into)]
50pub struct PopupOptions {
51    /// The window the popup is anchored to.
52    pub parent: AnyWindowHandle,
53
54    /// The rectangle the popup is positioned relative to, in the parent window's coordinate
55    /// space (the same space element bounds are in). For example, a dropdown menu uses the
56    /// bounds of the button that opened it.
57    pub anchor_rect: Bounds<Pixels>,
58
59    /// Which point of [`Self::anchor_rect`] the popup is anchored to.
60    pub anchor: PopupAnchor,
61
62    /// The direction in which the popup extends away from the anchor point. A dropdown that
63    /// drops below its button anchors to [`PopupAnchor::BottomLeft`] with a gravity of
64    /// [`PopupGravity::BottomRight`] so it grows down and to the right.
65    pub gravity: PopupGravity,
66
67    /// How the platform may adjust the popup if the requested placement would put it off-screen.
68    pub constraint_adjustment: PopupConstraintAdjustment,
69
70    /// An additional offset applied to the popup after anchoring.
71    pub offset: Point<Pixels>,
72
73    /// Whether the popup should take an explicit input grab.
74    ///
75    /// Grabbing popups behave like menus: they take keyboard focus and are dismissed when the
76    /// user clicks outside of them or presses a dismissing key. Use it for menus and comboboxes,
77    /// not for tooltips or other passive popups.
78    ///
79    /// On Wayland, the grab uses the serial of the most recent mouse or key press, so open a
80    /// grabbing popup in response to a press. If the window hasn't received a press yet, the
81    /// popup opens without a grab.
82    ///
83    /// Automatic dismissal only covers input aimed at other applications. A click elsewhere in
84    /// your own application still reaches it as usual, so closing the popup in that case is up
85    /// to you. Nested grabbing popups must be closed in the reverse order they were opened.
86    pub grab: bool,
87}
88
89impl PopupOptions {
90    /// Creates popup options for a parent window and anchor rectangle.
91    ///
92    /// Defaults to centered placement, zero offset, no constraint adjustments, and no input grab.
93    pub fn new(parent: impl Into<AnyWindowHandle>, anchor_rect: Bounds<Pixels>) -> Self {
94        Self {
95            parent: parent.into(),
96            anchor_rect,
97            anchor: PopupAnchor::default(),
98            gravity: PopupGravity::default(),
99            constraint_adjustment: PopupConstraintAdjustment::default(),
100            offset: Point::default(),
101            grab: false,
102        }
103    }
104}
105
106impl FluentBuilder for PopupOptions {}
107
108/// The point of the anchor rectangle that a popup is anchored to.
109#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
110pub enum PopupAnchor {
111    /// Anchor to the center of the anchor rectangle.
112    #[default]
113    Center,
114    /// Anchor to the center of the top edge.
115    Top,
116    /// Anchor to the center of the bottom edge.
117    Bottom,
118    /// Anchor to the center of the left edge.
119    Left,
120    /// Anchor to the center of the right edge.
121    Right,
122    /// Anchor to the top-left corner.
123    TopLeft,
124    /// Anchor to the bottom-left corner.
125    BottomLeft,
126    /// Anchor to the top-right corner.
127    TopRight,
128    /// Anchor to the bottom-right corner.
129    BottomRight,
130}
131
132/// The direction in which a popup extends away from its anchor point.
133///
134/// For instance, a gravity of [`PopupGravity::BottomRight`] places the popup below and to the
135/// right of the anchor point.
136#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
137pub enum PopupGravity {
138    /// The popup is centered over the anchor point.
139    #[default]
140    Center,
141    /// The popup extends upwards from the anchor point.
142    Top,
143    /// The popup extends downwards from the anchor point.
144    Bottom,
145    /// The popup extends to the left of the anchor point.
146    Left,
147    /// The popup extends to the right of the anchor point.
148    Right,
149    /// The popup extends up and to the left of the anchor point.
150    TopLeft,
151    /// The popup extends down and to the left of the anchor point.
152    BottomLeft,
153    /// The popup extends up and to the right of the anchor point.
154    TopRight,
155    /// The popup extends down and to the right of the anchor point.
156    BottomRight,
157}
158
159bitflags! {
160    /// How a popup may be adjusted by the platform if the requested placement would put it
161    /// off-screen. If no flags are set, the popup is placed exactly as requested and may be
162    /// clipped.
163    #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
164    pub struct PopupConstraintAdjustment: u32 {
165        /// The popup may be slid horizontally to stay on-screen.
166        const SLIDE_X = 1;
167        /// The popup may be slid vertically to stay on-screen.
168        const SLIDE_Y = 2;
169        /// The popup's anchor and gravity may be flipped horizontally to stay on-screen.
170        const FLIP_X = 4;
171        /// The popup's anchor and gravity may be flipped vertically to stay on-screen.
172        const FLIP_Y = 8;
173        /// The popup may be shrunk horizontally to stay on-screen.
174        const RESIZE_X = 16;
175        /// The popup may be shrunk vertically to stay on-screen.
176        const RESIZE_Y = 32;
177    }
178}
179
180/// Returned when a platform can't open a native popup window.
181///
182/// Opening a [`WindowKind::AnchoredPopup`](crate::WindowKind::AnchoredPopup) window fails with
183/// this error on macOS, Windows, X11 and the web. Wayland supports native popups.
184///
185/// Native popups are separate from gpui's in-window popovers, which are drawn as elements inside
186/// an existing window. To show a popup on every platform, catch this error and draw the same
187/// content in the parent window instead.
188#[derive(Debug, Error)]
189#[error("popups are not supported on this platform")]
190pub struct PopupNotSupportedError;