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}