Skip to main content

iced_runtime/platform_specific/wayland/
popup.rs

1//! Popup actions and types for Wayland xdg_popup surfaces.
2//!
3//! This module provides types for creating and managing popup surfaces
4//! that can appear outside the parent window bounds.
5
6use iced_core::Rectangle;
7use iced_core::layout::Limits;
8use iced_core::window::Id;
9use std::collections::BTreeSet;
10use std::fmt;
11use std::hash::{Hash, Hasher};
12use std::sync::{Mutex, PoisonError};
13
14/// Anchor position on the parent surface for popup positioning.
15///
16/// Based on `xdg_positioner::anchor` from the xdg-shell protocol.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
18#[repr(u32)]
19pub enum Anchor {
20    /// No anchor (use anchor_rect center)
21    #[default]
22    None = 0,
23    /// Top edge of anchor_rect
24    Top = 1,
25    /// Bottom edge of anchor_rect
26    Bottom = 2,
27    /// Left edge of anchor_rect
28    Left = 3,
29    /// Right edge of anchor_rect
30    Right = 4,
31    /// Top-left corner of anchor_rect
32    TopLeft = 5,
33    /// Bottom-left corner of anchor_rect
34    BottomLeft = 6,
35    /// Top-right corner of anchor_rect
36    TopRight = 7,
37    /// Bottom-right corner of anchor_rect
38    BottomRight = 8,
39}
40
41/// Gravity direction for the popup surface.
42///
43/// Based on `xdg_positioner::gravity` from the xdg-shell protocol.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
45#[repr(u32)]
46pub enum Gravity {
47    /// No gravity (centered on anchor point)
48    #[default]
49    None = 0,
50    /// Popup above anchor point
51    Top = 1,
52    /// Popup below anchor point
53    Bottom = 2,
54    /// Popup left of anchor point
55    Left = 3,
56    /// Popup right of anchor point
57    Right = 4,
58    /// Popup above-left of anchor point
59    TopLeft = 5,
60    /// Popup below-left of anchor point
61    BottomLeft = 6,
62    /// Popup above-right of anchor point
63    TopRight = 7,
64    /// Popup below-right of anchor point
65    BottomRight = 8,
66}
67
68/// Constraint adjustment flags for popup repositioning.
69///
70/// When the popup would be constrained (partially outside screen bounds),
71/// the compositor can adjust its position based on these flags.
72pub mod constraint_adjustment {
73    /// No adjustment, the popup is clipped or fails
74    pub const NONE: u32 = 0;
75    /// Slide the popup along the X axis
76    pub const SLIDE_X: u32 = 1;
77    /// Slide the popup along the Y axis
78    pub const SLIDE_Y: u32 = 2;
79    /// Flip the anchor and gravity on the X axis
80    pub const FLIP_X: u32 = 4;
81    /// Flip the anchor and gravity on the Y axis
82    pub const FLIP_Y: u32 = 8;
83    /// Resize the popup on the X axis
84    pub const RESIZE_X: u32 = 16;
85    /// Resize the popup on the Y axis
86    pub const RESIZE_Y: u32 = 32;
87}
88
89/// Settings for creating a popup surface.
90#[derive(Debug, Clone)]
91pub struct PopupSettings {
92    /// ID of the parent surface (must exist)
93    ///
94    /// Only `iced_winit` also takes a popup here, to nest under; other backends need a window.
95    /// The parent popup must have been drawn, or the new popup is reported closed at once.
96    /// Nested popups should use `grab: false`: xdg-shell only lets a popup grab under the topmost
97    /// grab, so a grab is dropped otherwise. `positioner.anchor_rect` is relative to the parent
98    /// popup, and hiding or replacing a popup closes the popups nested under it first.
99    pub parent: Id,
100    /// Unique ID for this popup
101    pub id: Id,
102    /// Positioning information for the popup
103    pub positioner: Positioner,
104    /// Whether to grab keyboard and pointer input
105    pub grab: bool,
106    /// Ask the compositor to blur what is behind the popup.
107    ///
108    /// Needs a compositor implementing a blur protocol, and content that is
109    /// translucent — an opaque card has nothing to show the blur through. The
110    /// blurred area is the whole surface, so pair this with `shadow` rather
111    /// than padding the surface out for a client-drawn one.
112    pub blur: bool,
113    /// Ask the compositor to draw a drop shadow behind the popup.
114    ///
115    /// The alternative to `Positioner::shadow_padding` and drawing one, and
116    /// the only one that combines with `blur`.
117    pub shadow: bool,
118    /// Corner radius hinted to the compositor, in logical pixels.
119    ///
120    /// Rounds the blur and the shadow to match the content drawn on the
121    /// surface; without it both are square while the content is rounded.
122    pub corner_radius: Option<u32>,
123    /// When true, the popup surface will have an empty input region so
124    /// pointer events pass through to the surface below. Useful for tooltips.
125    pub input_passthrough: bool,
126    /// When set, the compositor will position this popup at the pointer cursor
127    /// plus the given (x, y) offset, eliminating client-side reposition round-trips.
128    /// Requires compositor support for zcosmic_tooltip_manager_v1.
129    pub tooltip_offset: Option<(i32, i32)>,
130    /// Tooltip anchor corner (protocol enum value):
131    /// 0 = top_left (default), 1 = top_right, 2 = bottom_left, 3 = bottom_right.
132    /// Determines which corner of the tooltip aligns to the computed position.
133    pub tooltip_anchor: Option<u32>,
134    /// Show delay in milliseconds. When set to a non-zero value, the tooltip
135    /// appears only after the pointer has hovered over the parent surface for
136    /// this duration. The tooltip is positioned at the pointer location when
137    /// the delay expires and stays fixed until hidden.
138    /// 0 or None = immediate (follows pointer).
139    pub tooltip_delay_ms: Option<u32>,
140}
141
142impl Hash for PopupSettings {
143    fn hash<H: Hasher>(&self, state: &mut H) {
144        self.id.hash(state);
145    }
146}
147
148/// Positioner configuration for popup placement.
149#[derive(Debug, Clone)]
150pub struct Positioner {
151    /// Size of the popup (if None, will be auto-sized)
152    pub size: Option<(u32, u32)>,
153    /// Size limits for auto-sizing
154    pub size_limits: Limits,
155    /// Rectangle on the parent surface to anchor to
156    pub anchor_rect: Rectangle<i32>,
157    /// Where on the anchor_rect to attach
158    pub anchor: Anchor,
159    /// Direction the popup extends from the anchor point
160    pub gravity: Gravity,
161    /// How to adjust if popup would be constrained
162    pub constraint_adjustment: u32,
163    /// Offset from the calculated position
164    pub offset: (i32, i32),
165    /// Whether popup should reposition when parent moves
166    pub reactive: bool,
167    /// Window geometry (x, y, width, height) in logical pixels.
168    ///
169    /// Tells the compositor which part of the surface is visible content
170    /// (excluding shadows/decorations). The compositor uses this for
171    /// constraint adjustment (SLIDE/FLIP) — only the geometry rect is
172    /// kept on-screen, not the full surface.
173    pub window_geometry: Option<(i32, i32, i32, i32)>,
174    /// Shadow padding in logical pixels.
175    ///
176    /// When set and `size` is None (auto-sizing), iced will automatically
177    /// compute `window_geometry` from the measured content size minus this
178    /// padding on each side. This tells the compositor to only constrain
179    /// the visible content area, not the shadow.
180    pub shadow_padding: u32,
181}
182
183impl Hash for Positioner {
184    fn hash<H: Hasher>(&self, state: &mut H) {
185        self.size.hash(state);
186        self.anchor_rect.x.hash(state);
187        self.anchor_rect.y.hash(state);
188        self.anchor_rect.width.hash(state);
189        self.anchor_rect.height.hash(state);
190        self.anchor.hash(state);
191        self.gravity.hash(state);
192        self.constraint_adjustment.hash(state);
193        self.offset.hash(state);
194        self.reactive.hash(state);
195        self.window_geometry.hash(state);
196        self.shadow_padding.hash(state);
197    }
198}
199
200impl Default for Positioner {
201    fn default() -> Self {
202        Self {
203            size: None,
204            size_limits: Limits::NONE
205                .min_height(1.0)
206                .min_width(1.0)
207                .max_width(400.0)
208                .max_height(600.0),
209            anchor_rect: Rectangle {
210                x: 0,
211                y: 0,
212                width: 1,
213                height: 1,
214            },
215            anchor: Anchor::None,
216            gravity: Gravity::None,
217            constraint_adjustment: constraint_adjustment::SLIDE_X
218                | constraint_adjustment::SLIDE_Y
219                | constraint_adjustment::FLIP_X
220                | constraint_adjustment::FLIP_Y,
221            offset: (0, 0),
222            reactive: true,
223            window_geometry: None,
224            shadow_padding: 0,
225        }
226    }
227}
228
229/// Actions for popup surfaces.
230#[derive(Clone)]
231pub enum Action {
232    /// Create a popup surface
233    Show {
234        /// Popup configuration
235        settings: PopupSettings,
236    },
237    /// Destroy a popup surface
238    Hide {
239        /// ID of the popup to destroy
240        id: Id,
241    },
242    /// Resize a popup surface
243    Resize {
244        /// ID of the popup
245        id: Id,
246        /// New width
247        width: u32,
248        /// New height
249        height: u32,
250    },
251    /// Reposition a popup surface using xdg_popup.reposition (v3).
252    ///
253    /// Updates the popup's positioner and asks the compositor to
254    /// reposition it accordingly. Useful for tooltips that follow
255    /// the cursor or need to adjust position dynamically.
256    Reposition {
257        /// ID of the popup to reposition
258        id: Id,
259        /// New positioner configuration
260        positioner: Positioner,
261    },
262}
263
264impl fmt::Debug for Action {
265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        match self {
267            Action::Show { settings } => f
268                .debug_struct("Action::Show")
269                .field("settings", settings)
270                .finish(),
271            Action::Hide { id } => f.debug_struct("Action::Hide").field("id", id).finish(),
272            Action::Resize { id, width, height } => f
273                .debug_struct("Action::Resize")
274                .field("id", id)
275                .field("width", width)
276                .field("height", height)
277                .finish(),
278            Action::Reposition { id, positioner } => f
279                .debug_struct("Action::Reposition")
280                .field("id", id)
281                .field("positioner", positioner)
282                .finish(),
283        }
284    }
285}
286
287// ============================================================================
288// Task Helpers
289// ============================================================================
290
291use crate::Task;
292use crate::platform_specific;
293use crate::task;
294
295/// Show a popup surface.
296///
297/// The popup will be positioned relative to the parent surface using
298/// the positioner settings.
299pub fn show<Message>(settings: PopupSettings) -> Task<Message> {
300    task::effect(crate::Action::PlatformSpecific(
301        platform_specific::Action::Wayland(super::Action::Popup(Action::Show { settings })),
302    ))
303}
304
305/// Hide (destroy) a popup surface.
306pub fn hide<Message>(id: Id) -> Task<Message> {
307    task::effect(crate::Action::PlatformSpecific(
308        platform_specific::Action::Wayland(super::Action::Popup(Action::Hide { id })),
309    ))
310}
311
312/// Resize a popup surface.
313pub fn resize<Message>(id: Id, width: u32, height: u32) -> Task<Message> {
314    task::effect(crate::Action::PlatformSpecific(
315        platform_specific::Action::Wayland(super::Action::Popup(Action::Resize {
316            id,
317            width,
318            height,
319        })),
320    ))
321}
322
323/// Reposition a popup surface.
324///
325/// Uses xdg_popup.reposition (protocol v3) to update the popup's
326/// position without destroying and recreating it.
327pub fn reposition<Message>(id: Id, positioner: Positioner) -> Task<Message> {
328    task::effect(crate::Action::PlatformSpecific(
329        platform_specific::Action::Wayland(super::Action::Popup(Action::Reposition {
330            id,
331            positioner,
332        })),
333    ))
334}
335
336/// Popups that take keyboard input, by ID.
337///
338/// A register rather than a [`PopupSettings`] field or an [`Action`] variant: backends match that
339/// enum and apps build that struct field by field, so either would stop them compiling.
340static KEYBOARD_INPUT: Mutex<BTreeSet<Id>> = Mutex::new(BTreeSet::new());
341
342/// Deliver or drop the keyboard input of the popup `id`, which is dropped by default.
343///
344/// Delivered, the popup's key presses, modifiers and focus changes reach its widgets, and
345/// subscriptions under its own ID, while the compositor gives it keyboard focus (a grabbing
346/// popup, on most). Set it before [`show`]; only `iced_winit` reads it.
347pub fn keyboard_input(id: Id, enabled: bool) {
348    let mut popups = KEYBOARD_INPUT
349        .lock()
350        .unwrap_or_else(PoisonError::into_inner);
351    let _ = if enabled {
352        popups.insert(id)
353    } else {
354        popups.remove(&id)
355    };
356}
357
358/// Whether the popup `id` takes keyboard input; a backend asks as it creates the popup.
359pub fn takes_keyboard_input(id: Id) -> bool {
360    KEYBOARD_INPUT
361        .lock()
362        .unwrap_or_else(PoisonError::into_inner)
363        .contains(&id)
364}
365
366/// Forget the popup `id`'s keyboard input; a backend drops it with the popup.
367pub fn forget_keyboard_input(id: Id) {
368    let _ = KEYBOARD_INPUT
369        .lock()
370        .unwrap_or_else(PoisonError::into_inner)
371        .remove(&id);
372}