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;