Skip to main content

crcbl_shell/
role.rs

1//! What kind of window a [`WindowDesc`] asks for: an
2//! application window, a panel, or a popup.
3//!
4//! Most programs want [`WindowRole::Toplevel`], the window every desktop has.
5//! The other two are for desktop furniture — a status bar, a launcher, a
6//! notification, a menu opened from a bar — which every window system has a
7//! different mechanism for, and which no program should have to write three
8//! times:
9//!
10//! | role | Wayland | Win32 | AppKit |
11//! | --- | --- | --- | --- |
12//! | [`Panel`](WindowRole::Panel) | `zwlr_layer_surface_v1` | a topmost tool window; an appbar when it reserves an edge | a non-activating `NSPanel` |
13//! | [`Popup`](WindowRole::Popup) | `xdg_popup` | a `WS_POPUP` tool window | a child `NSPanel` |
14//!
15//! Which roles a backend can build is in its caps —
16//! [`ShellCaps::PANEL`] and
17//! [`ShellCaps::POPUP`] — and asking for one it cannot
18//! is [`ShellError::Unsupported`], never a
19//! toplevel in its place: a bar that silently became a normal window would
20//! cover the desktop it was meant to sit at the edge of.
21
22use crate::geom::LogicalRect;
23use crate::monitor::MonitorId;
24use crate::window::{WindowDesc, WindowId};
25#[cfg(any(target_os = "windows", desktop_unix, test))]
26use crate::{PhysicalRect, PhysicalSize};
27use crate::{ShellBackend, ShellCaps, ShellError};
28
29/// What kind of window to create — see the [module docs](self).
30#[derive(Clone, Copy, Debug, Default, PartialEq)]
31#[non_exhaustive]
32pub enum WindowRole<'a> {
33    /// An ordinary application window: decorated, in the taskbar, moved and
34    /// sized by the user and the window manager.
35    #[default]
36    Toplevel,
37    /// Desktop furniture placed against a monitor's edges — a bar, a dock, a
38    /// launcher, an overlay. See [`PanelDesc`].
39    Panel(PanelDesc<'a>),
40    /// A window anchored to a rectangle of another, above it — a menu, a
41    /// tooltip, a calendar opened from a bar. See [`PopupDesc`].
42    Popup(PopupDesc),
43}
44
45/// Where a [panel](WindowRole::Panel) goes and how it behaves.
46///
47/// Laid out the way `wlr-layer-shell` describes a surface, because that
48/// protocol is the most complete statement of what a panel is: the other
49/// window systems implement a subset, and the backend docs say which.
50///
51/// **Its size is [`WindowDesc::size`], with one
52/// exception: along an axis where it is anchored to both opposite edges, that
53/// size is ignored and the panel stretches between them.** A bar anchored
54/// `TOP | LEFT | RIGHT` with a size of `(0, 50)` is 50 high and as wide as the
55/// monitor. Either way the size it actually gets arrives as the first
56/// [`Resized`](crate::ShellEvent::Resized), as for any window, and is the one
57/// to draw at.
58#[derive(Clone, Copy, Debug, Default, PartialEq)]
59pub struct PanelDesc<'a> {
60    /// The name window-system rules match the panel by: the layer-shell
61    /// namespace on Wayland, which Sway's and Hyprland's per-layer rules key
62    /// on. Empty means [`WindowDesc::app_id`].
63    pub namespace: &'a str,
64    /// The monitor it is placed on, or `None` for the system's choice, which
65    /// is **where the user is** rather than the primary monitor: the
66    /// compositor's pick on Wayland (the focused output on Sway, Hyprland and
67    /// niri), and the monitor under the pointer on Win32 and AppKit. A launcher
68    /// opening on a screen the user is not looking at is the failure this
69    /// avoids.
70    pub monitor: Option<MonitorId>,
71    /// Which band of the stack it lives in.
72    pub layer: PanelLayer,
73    /// The edges it is held against. A panel held against two opposite edges
74    /// stretches between them; one held against none is centred.
75    pub anchor: PanelAnchor,
76    /// Whether, and how, it keeps other windows out of its space.
77    pub reserve: Reserve,
78    /// Space between the panel and the edges it is anchored to, in logical
79    /// units. A margin on an edge it is not anchored to does nothing.
80    pub margins: Margins,
81    /// Whether it takes the keyboard.
82    pub keyboard: PanelKeyboard,
83}
84
85/// The band of the stack a [panel](PanelDesc) lives in, bottom to top.
86#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
87pub enum PanelLayer {
88    /// Below every application window: a wallpaper, a desktop widget.
89    Background,
90    /// Above the background, below application windows.
91    Bottom,
92    /// Above application windows, below full-screen ones: a status bar.
93    #[default]
94    Top,
95    /// Above everything, full-screen windows included: a launcher, a lock
96    /// screen, a notification.
97    ///
98    /// Win32 has one topmost band, which `Top` uses too, so there an overlay
99    /// is above application windows and shares the band with full-screen
100    /// ones that are topmost themselves.
101    Overlay,
102}
103
104bitflags::bitflags! {
105    /// The monitor edges a [panel](PanelDesc) is held against.
106    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
107    pub struct PanelAnchor: u8 {
108        /// The top edge.
109        const TOP = 1 << 0;
110        /// The bottom edge.
111        const BOTTOM = 1 << 1;
112        /// The left edge.
113        const LEFT = 1 << 2;
114        /// The right edge.
115        const RIGHT = 1 << 3;
116    }
117}
118
119/// Whether a [panel](PanelDesc) keeps other windows out of its space.
120///
121/// **Reserving is a request a window system may be unable to grant**, which is
122/// different from being unable to build the panel at all:
123/// [`ShellCaps::PANEL_RESERVE`] says whether
124/// this backend can. Where it cannot — macOS, which lets nothing but the menu
125/// bar and the Dock reserve screen space — the panel is still created and
126/// simply overlaps what is behind it.
127#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
128pub enum Reserve {
129    /// It reserves nothing, and keeps clear of the space other panels have
130    /// reserved — a launcher placed below a bar rather than over it.
131    /// Layer-shell's exclusive zone 0.
132    #[default]
133    None,
134    /// The edge it is anchored to gives up the panel's own extent: maximised
135    /// windows stop at it. A bar is this. Layer-shell's exclusive zone equal
136    /// to the panel's extent across that edge.
137    ///
138    /// **The panel must name one edge**: anchored to exactly one, or to one and
139    /// both of the edges beside it — `TOP`, or `TOP | LEFT | RIGHT`, reserves
140    /// the top. Any other anchoring has no edge to reserve, and creating it is
141    /// [`ShellError::Unsupported`] on every
142    /// backend; see [`PanelDesc::reserved_edge`].
143    OwnExtent,
144    /// It reserves nothing and is placed as if no other panel reserved
145    /// anything — at the very edge, over other bars. Layer-shell's exclusive
146    /// zone -1.
147    IgnoreOthers,
148}
149
150impl PanelDesc<'_> {
151    /// The edge [`Reserve::OwnExtent`] would reserve: the one anchored edge,
152    /// alone or with both of its neighbours. `None` for any other anchoring,
153    /// which has no single edge to give up.
154    #[must_use]
155    pub fn reserved_edge(&self) -> Option<PanelAnchor> {
156        let vertical = PanelAnchor::LEFT | PanelAnchor::RIGHT;
157        let horizontal = PanelAnchor::TOP | PanelAnchor::BOTTOM;
158        [
159            (PanelAnchor::TOP, vertical),
160            (PanelAnchor::BOTTOM, vertical),
161            (PanelAnchor::LEFT, horizontal),
162            (PanelAnchor::RIGHT, horizontal),
163        ]
164        .into_iter()
165        .find(|&(edge, beside)| self.anchor == edge || self.anchor == edge | beside)
166        .map(|(edge, _)| edge)
167    }
168}
169
170/// Space around a [panel](PanelDesc), in logical units.
171#[derive(Clone, Copy, Debug, Default, PartialEq)]
172pub struct Margins {
173    /// From the top edge.
174    pub top: f64,
175    /// From the right edge.
176    pub right: f64,
177    /// From the bottom edge.
178    pub bottom: f64,
179    /// From the left edge.
180    pub left: f64,
181}
182
183/// Whether a [panel](PanelDesc) takes the keyboard.
184#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
185pub enum PanelKeyboard {
186    /// Never: a click leaves focus where it was. A bar is this.
187    #[default]
188    None,
189    /// When clicked, like an application window.
190    OnDemand,
191    /// Takes it on mapping: a launcher, a lock screen. Keys typed the moment
192    /// it appears are its, or
193    /// [`FocusRefused`](crate::ShellEvent::FocusRefused) says the window
194    /// system would not hand it over.
195    ///
196    /// Held while mapped only where
197    /// [`ShellCaps::PANEL_EXCLUSIVE_KEYBOARD`](crate::ShellCaps::PANEL_EXCLUSIVE_KEYBOARD)
198    /// is set. Elsewhere the user can click another window and the panel
199    /// hears [`Focus`](crate::ShellEvent::Focus) with `focused: false`;
200    /// nothing takes the keyboard back, since that would fight the user and
201    /// the system's foreground rules.
202    Exclusive,
203}
204
205/// Where a [popup](WindowRole::Popup) goes.
206#[derive(Clone, Copy, Debug, PartialEq)]
207pub struct PopupDesc {
208    /// The window it belongs to and moves with. Destroying the parent
209    /// destroys the popup.
210    pub parent: WindowId,
211    /// The rectangle it is placed against, in the parent's logical
212    /// coordinates — the module or button that opened it.
213    pub anchor: LogicalRect,
214    /// Which side of the anchor it opens on. The system flips it to the other
215    /// side, or slides it along this one, rather than let it leave the screen.
216    pub placement: PopupPlacement,
217    /// What input it takes while open.
218    pub input: PopupInput,
219}
220
221/// What input a [popup](PopupDesc) takes.
222#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
223pub enum PopupInput {
224    /// None at all: the pointer passes through it to whatever is beneath, and
225    /// it never activates. A tooltip is this, so hovering stays on the bar
226    /// that showed it.
227    #[default]
228    Passthrough,
229    /// The pointer over it, as any window has; never the keyboard, and a click
230    /// elsewhere leaves it open.
231    Pointer,
232    /// The pointer and the keyboard while it is open. A click outside it or
233    /// `Escape` dismisses it, which arrives as a
234    /// [`CloseRequested`](crate::ShellEvent::CloseRequested) for it. A menu or
235    /// a calendar is this.
236    ///
237    /// `Escape` is still delivered: the popup hears the
238    /// [`Key`](crate::ShellEvent::Key) press first and the `CloseRequested`
239    /// after it, so a consumer can tell a dismissal by the keyboard from one by
240    /// a click, and can answer [`CloseReply::Keep`](crate::CloseReply::Keep)
241    /// to back out of a submenu instead of closing the whole menu. Only a
242    /// fresh press dismisses — an auto-repeat or a release does not.
243    Grab,
244}
245
246/// Whether `key`, delivered to a window whose popup input is `popup`, asks
247/// for that window to close: a fresh press of `Escape` on a
248/// [`PopupInput::Grab`] popup, as that variant documents. `popup` is `None`
249/// for a window that is not a popup.
250///
251/// `Escape` is the key's **symbol**, [`Keysym::ESCAPE`](crcbl_core::input::Keysym::ESCAPE),
252/// not its physical key: an xkb remap such as `caps:escape` dismisses the
253/// popup, and a virtual keyboard whose own keymap puts Down on the scancode a
254/// physical Escape has (`wtype` uses evdev 1) does not.
255///
256/// Every backend that builds popups calls this after queueing the key, and on
257/// `true` queues the window's
258/// [`CloseRequested`](crate::ShellEvent::CloseRequested) through its own
259/// close-request path, so a request already pending is not repeated.
260pub(crate) fn escape_dismisses(popup: Option<PopupInput>, key: &crate::ShellEvent) -> bool {
261    popup == Some(PopupInput::Grab)
262        && matches!(
263            key,
264            crate::ShellEvent::Key {
265                keysym: crcbl_core::input::Keysym::ESCAPE,
266                state: crcbl_core::input::ButtonState::Pressed,
267                repeat: false,
268                ..
269            }
270        )
271}
272
273/// What every backend checks before it builds a window: that its caps cover
274/// the role and the transparency asked for, that a reserved panel names one
275/// edge, and that a panel's monitor exists.
276///
277/// The popup's parent is the backend's to check, since only it knows its
278/// windows.
279pub(crate) fn check(
280    desc: &WindowDesc<'_>,
281    caps: ShellCaps,
282    backend: ShellBackend,
283    has_monitor: impl Fn(MonitorId) -> bool,
284) -> Result<(), ShellError> {
285    let unsupported = |what| Err(ShellError::Unsupported { backend, what });
286    if desc.transparent && !caps.contains(ShellCaps::TRANSPARENT) {
287        return unsupported("transparent windows");
288    }
289    match desc.role {
290        WindowRole::Toplevel => Ok(()),
291        WindowRole::Panel(panel) => check_panel(&panel, caps, backend, has_monitor),
292        WindowRole::Popup(_) if !caps.contains(ShellCaps::POPUP) => unsupported("popup windows"),
293        WindowRole::Popup(_) => Ok(()),
294    }
295}
296
297/// The panel half of [`check`], which
298/// [`Shell::set_panel`](crate::Shell::set_panel) applies to a panel's new
299/// descriptor too, so a panel cannot be reconfigured into one it could not
300/// have been created as.
301fn check_panel(
302    panel: &PanelDesc<'_>,
303    caps: ShellCaps,
304    backend: ShellBackend,
305    has_monitor: impl Fn(MonitorId) -> bool,
306) -> Result<(), ShellError> {
307    let unsupported = |what| Err(ShellError::Unsupported { backend, what });
308    if !caps.contains(ShellCaps::PANEL) {
309        return unsupported("panel windows");
310    }
311    if panel.reserve == Reserve::OwnExtent && panel.reserved_edge().is_none() {
312        return unsupported("a reserved panel that is not held against one edge");
313    }
314    match panel.monitor {
315        Some(monitor) if !has_monitor(monitor) => Err(ShellError::NoSuchMonitor(monitor.0)),
316        _ => Ok(()),
317    }
318}
319
320/// What every backend checks before it reconfigures a panel with
321/// [`Shell::set_panel`](crate::Shell::set_panel): that the window is a panel,
322/// whose descriptor is `was`, that the new descriptor keeps its namespace and
323/// its monitor, and that it passes the checks a new panel would.
324///
325/// The namespace and the monitor are fixed by `wlr-layer-shell`, which names
326/// both in `get_layer_surface` and has no request to change either. Refusing
327/// a change on every backend, rather than only where the protocol does, keeps
328/// a panel that works on one system from failing on another.
329pub(crate) fn check_set_panel(
330    window: WindowId,
331    was: Option<PanelDesc<'_>>,
332    panel: &PanelDesc<'_>,
333    caps: ShellCaps,
334    backend: ShellBackend,
335    has_monitor: impl Fn(MonitorId) -> bool,
336) -> Result<(), ShellError> {
337    let Some(was) = was else {
338        return Err(not_a_panel(window));
339    };
340    if panel.namespace != was.namespace {
341        return Err(ShellError::InvalidDescriptor(
342            "set_panel: a panel's namespace is fixed when it is created".to_string(),
343        ));
344    }
345    if panel.monitor != was.monitor {
346        return Err(ShellError::InvalidDescriptor(
347            "set_panel: a panel's monitor is fixed when it is created".to_string(),
348        ));
349    }
350    check_panel(panel, caps, backend, has_monitor)
351}
352
353/// What [`Shell::set_panel`](crate::Shell::set_panel) answers for a window
354/// that is not a panel, in every backend's words alike.
355pub(crate) fn not_a_panel(window: WindowId) -> ShellError {
356    ShellError::InvalidDescriptor(format!("set_panel: {window:?} is not a panel"))
357}
358
359/// A [`PanelDesc`] kept past the call that gave it, for the backends that
360/// have no panel record of their own: the namespace owned, the rest as given.
361#[derive(Clone, Debug, PartialEq)]
362pub(crate) struct KeptPanel {
363    namespace: String,
364    /// The descriptor with an empty namespace, which [`desc`](Self::desc)
365    /// puts back.
366    desc: PanelDesc<'static>,
367}
368
369impl KeptPanel {
370    pub(crate) fn new(desc: &PanelDesc<'_>) -> Self {
371        Self {
372            namespace: desc.namespace.to_string(),
373            desc: PanelDesc {
374                namespace: "",
375                ..*desc
376            },
377        }
378    }
379
380    /// The descriptor as it was given.
381    pub(crate) fn desc(&self) -> PanelDesc<'_> {
382        PanelDesc {
383            namespace: &self.namespace,
384            ..self.desc
385        }
386    }
387}
388
389/// Which side of its anchor a [popup](PopupDesc) opens on.
390#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
391pub enum PopupPlacement {
392    /// Below the anchor, left edges aligned.
393    #[default]
394    Below,
395    /// Above the anchor, left edges aligned.
396    Above,
397    /// To the left of the anchor, top edges aligned.
398    Left,
399    /// To the right of the anchor, top edges aligned: a submenu.
400    Right,
401}
402
403/// A rectangle in a backend's own Y-down space — desktop pixels on Win32,
404/// flipped screen points on AppKit — for [`place_popup`].
405///
406/// Compiled where its callers are: the backends that place popups
407/// themselves.
408#[cfg(any(target_os = "windows", target_os = "macos", desktop_unix, test))]
409#[derive(Clone, Copy, Debug, PartialEq)]
410pub(crate) struct Placed {
411    pub x: f64,
412    pub y: f64,
413    pub width: f64,
414    pub height: f64,
415}
416
417/// Where a popup of `width × height` goes against `anchor`: on the
418/// `placement` side of it, flipped to the other side when it does not fit in
419/// `area` and the other side does, and slid along the side it is on to stay
420/// in `area`.
421///
422/// The placement `xdg_positioner` is asked for on Wayland, worked out here
423/// for the window systems that have no one to ask. Y grows downwards.
424/// What [`Shell::reposition_popup`](crate::Shell::reposition_popup) answers
425/// for a window that is not a popup, in every backend's words alike.
426pub(crate) fn not_a_popup(window: WindowId) -> ShellError {
427    ShellError::InvalidDescriptor(format!("reposition_popup: {window:?} is not a popup"))
428}
429
430#[cfg(any(target_os = "windows", target_os = "macos", desktop_unix, test))]
431pub(crate) fn place_popup(
432    anchor: Placed,
433    (width, height): (f64, f64),
434    placement: PopupPlacement,
435    area: Placed,
436) -> (f64, f64) {
437    let (left, top) = (anchor.x, anchor.y);
438    let (right, bottom) = (left + anchor.width, top + anchor.height);
439    let (area_right, area_bottom) = (area.x + area.width, area.y + area.height);
440    // The near start on the placement's axis, or the flip when only that fits.
441    let side = |near: f64, far: f64, extent: f64, low: f64, high: f64| {
442        if near >= low && near + extent <= high {
443            near
444        } else if far >= low && far + extent <= high {
445            far
446        } else {
447            near
448        }
449    };
450    let slide = |start: f64, extent: f64, low: f64, high: f64| start.min(high - extent).max(low);
451    match placement {
452        PopupPlacement::Below => (
453            slide(left, width, area.x, area_right),
454            side(bottom, top - height, height, area.y, area_bottom),
455        ),
456        PopupPlacement::Above => (
457            slide(left, width, area.x, area_right),
458            side(top - height, bottom, height, area.y, area_bottom),
459        ),
460        PopupPlacement::Left => (
461            side(left - width, right, width, area.x, area_right),
462            slide(top, height, area.y, area_bottom),
463        ),
464        PopupPlacement::Right => (
465            side(right, left - width, width, area.x, area_right),
466            slide(top, height, area.y, area_bottom),
467        ),
468    }
469}
470
471#[cfg(any(target_os = "windows", desktop_unix, test))]
472/// Where a popup of `size` goes against `anchor`, both in desktop pixels:
473/// [`place_popup`]'s placement, flipped or
474/// slid to stay in `area`, in whole pixels.
475#[must_use]
476pub(crate) fn popup_rect(
477    anchor: PhysicalRect,
478    size: PhysicalSize,
479    placement: PopupPlacement,
480    area: PhysicalRect,
481) -> PhysicalRect {
482    let placed = |rect: PhysicalRect| Placed {
483        x: f64::from(rect.x),
484        y: f64::from(rect.y),
485        width: f64::from(rect.width),
486        height: f64::from(rect.height),
487    };
488    let (x, y) = place_popup(
489        placed(anchor),
490        (f64::from(size.width), f64::from(size.height)),
491        placement,
492        placed(area),
493    );
494    // Whole pixels in, whole pixels out: every input is an integer, so the
495    // arithmetic is exact and the cast only narrows the range.
496    #[expect(
497        clippy::cast_possible_truncation,
498        reason = "clamped into i32's range before the cast"
499    )]
500    let narrow = |value: f64| value.clamp(f64::from(i32::MIN), f64::from(i32::MAX)) as i32;
501    PhysicalRect::new(narrow(x), narrow(y), size.width, size.height)
502}
503
504#[cfg(any(target_os = "windows", desktop_unix, test))]
505/// Space around a panel, in physical pixels: [`Margins`]
506/// scaled to the monitor.
507#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
508pub(crate) struct Insets {
509    /// From the top edge.
510    pub top: i32,
511    /// From the right edge.
512    pub right: i32,
513    /// From the bottom edge.
514    pub bottom: i32,
515    /// From the left edge.
516    pub left: i32,
517}
518
519#[cfg(any(target_os = "windows", desktop_unix, test))]
520/// Where a panel of `size` goes in `area`: `insets` away from each edge it is
521/// anchored to, stretched between two opposite anchored edges, and centred on
522/// an axis anchored to neither. That is `wlr-layer-shell`'s placement, which
523/// [`PanelDesc`] is written in.
524#[must_use]
525pub(crate) fn panel_rect(
526    area: PhysicalRect,
527    anchor: PanelAnchor,
528    size: PhysicalSize,
529    insets: Insets,
530) -> PhysicalRect {
531    let (x, width) = place_on_axis(
532        (area.x, area.width),
533        (
534            anchor.contains(PanelAnchor::LEFT),
535            anchor.contains(PanelAnchor::RIGHT),
536        ),
537        (insets.left, insets.right),
538        size.width,
539    );
540    let (y, height) = place_on_axis(
541        (area.y, area.height),
542        (
543            anchor.contains(PanelAnchor::TOP),
544            anchor.contains(PanelAnchor::BOTTOM),
545        ),
546        (insets.top, insets.bottom),
547        size.height,
548    );
549    PhysicalRect::new(x, y, width, height)
550}
551
552/// One axis of [`panel_rect`]: where a panel of `length` starts along an
553/// area's `(start, length)`, and how long it is, given which ends it is
554/// anchored to and its insets from them.
555#[cfg(any(target_os = "windows", desktop_unix, test))]
556fn place_on_axis(
557    (area_start, area_length): (i32, u32),
558    (at_start, at_end): (bool, bool),
559    (inset_start, inset_end): (i32, i32),
560    length: u32,
561) -> (i32, u32) {
562    let area_length = i64::from(area_length);
563    let length = i64::from(length);
564    let (offset, length) = match (at_start, at_end) {
565        (true, true) => (
566            i64::from(inset_start),
567            (area_length - i64::from(inset_start) - i64::from(inset_end)).max(1),
568        ),
569        (true, false) => (i64::from(inset_start), length),
570        (false, true) => (area_length - i64::from(inset_end) - length, length),
571        (false, false) => ((area_length - length) / 2, length),
572    };
573    let start = i64::from(area_start) + offset;
574    (
575        i32::try_from(start).unwrap_or(if start < 0 { i32::MIN } else { i32::MAX }),
576        u32::try_from(length).unwrap_or(u32::MAX),
577    )
578}
579
580#[cfg(test)]
581mod tests {
582    use super::{
583        PanelAnchor, PanelDesc, PopupDesc, PopupInput, PopupPlacement, Reserve, WindowRole,
584    };
585    use crate::{
586        HeadlessShell, LogicalRect, MonitorId, Shell, ShellCaps, ShellError, ShellEvent, WindowDesc,
587    };
588    use crate::{PhysicalRect, PhysicalSize};
589
590    #[test]
591    fn a_popup_opens_on_its_side_and_flips_or_slides_to_stay_on_screen() {
592        let area = PhysicalRect::new(0, 0, 1920, 1040);
593        let button = PhysicalRect::new(100, 100, 80, 30);
594        let menu = PhysicalSize::new(200, 300);
595        assert_eq!(
596            super::popup_rect(button, menu, PopupPlacement::Below, area),
597            PhysicalRect::new(100, 130, 200, 300),
598            "below, left edges aligned"
599        );
600        assert_eq!(
601            super::popup_rect(button, menu, PopupPlacement::Above, area),
602            PhysicalRect::new(100, 130, 200, 300),
603            "no room above, so flipped below"
604        );
605        assert_eq!(
606            super::popup_rect(button, menu, PopupPlacement::Right, area),
607            PhysicalRect::new(180, 100, 200, 300),
608            "a submenu, top edges aligned"
609        );
610        assert_eq!(
611            super::popup_rect(button, menu, PopupPlacement::Left, area),
612            PhysicalRect::new(180, 100, 200, 300),
613            "no room to the left, so flipped right"
614        );
615        let low = PhysicalRect::new(1850, 1000, 60, 30);
616        assert_eq!(
617            super::popup_rect(low, menu, PopupPlacement::Below, area),
618            PhysicalRect::new(1720, 700, 200, 300),
619            "flipped above, and slid back onto the screen"
620        );
621        let tall = PhysicalSize::new(200, 2000);
622        assert_eq!(
623            super::popup_rect(button, tall, PopupPlacement::Below, area).y,
624            130,
625            "fits neither side, so it stays on its own"
626        );
627    }
628
629    #[test]
630    fn a_panel_is_placed_as_layer_shell_places_one() {
631        let area = PhysicalRect::new(100, 40, 1920, 1040);
632        let bar = PhysicalSize::new(0, 30);
633        let insets = super::Insets {
634            top: 4,
635            right: 8,
636            bottom: 12,
637            left: 16,
638        };
639        let across_the_top = PanelAnchor::TOP | PanelAnchor::LEFT | PanelAnchor::RIGHT;
640        assert_eq!(
641            super::panel_rect(area, across_the_top, bar, super::Insets::default()),
642            PhysicalRect::new(100, 40, 1920, 30),
643            "stretched between the sides, at its own height"
644        );
645        assert_eq!(
646            super::panel_rect(area, across_the_top, bar, insets),
647            PhysicalRect::new(116, 44, 1920 - 16 - 8, 30),
648        );
649        let dock = PhysicalSize::new(64, 500);
650        assert_eq!(
651            super::panel_rect(area, PanelAnchor::RIGHT, dock, insets),
652            PhysicalRect::new(100 + 1920 - 8 - 64, 40 + (1040 - 500) / 2, 64, 500),
653            "held off the right edge, centred down it"
654        );
655        assert_eq!(
656            super::panel_rect(area, PanelAnchor::BOTTOM | PanelAnchor::LEFT, dock, insets),
657            PhysicalRect::new(116, 40 + 1040 - 12 - 500, 64, 500),
658            "in the corner"
659        );
660        let launcher = PhysicalSize::new(600, 400);
661        assert_eq!(
662            super::panel_rect(area, PanelAnchor::empty(), launcher, insets),
663            PhysicalRect::new(100 + (1920 - 600) / 2, 40 + (1040 - 400) / 2, 600, 400),
664            "centred, and an unanchored panel's insets do nothing"
665        );
666        assert_eq!(
667            super::panel_rect(area, PanelAnchor::all(), launcher, insets),
668            PhysicalRect::new(116, 44, 1920 - 24, 1040 - 16),
669        );
670    }
671
672    #[test]
673    fn only_a_fresh_escape_press_on_a_grabbing_popup_dismisses_it() {
674        use crcbl_core::KeyCode;
675        use crcbl_core::input::{ButtonState, DeviceId, Keysym, Modifiers, Scancode};
676
677        let window = crate::WindowId::from_bits(1 << 32).expect("a valid handle");
678        let key = |key_code, keysym, state, repeat| ShellEvent::Key {
679            window,
680            device: DeviceId(1),
681            time: crcbl_core::EventTime::ZERO,
682            scancode: Scancode(0),
683            key_code,
684            keysym,
685            state,
686            repeat,
687            modifiers: Modifiers::empty(),
688        };
689        let pressed = ButtonState::Pressed;
690        let escape = key(Some(KeyCode::Escape), Keysym::ESCAPE, pressed, false);
691        let grab = Some(PopupInput::Grab);
692        assert!(super::escape_dismisses(grab, &escape));
693        // The symbol decides: Caps Lock remapped to Escape (`caps:escape`)
694        // dismisses, and wtype's Down on evdev 1, Escape's scancode, does not.
695        let remapped = key(Some(KeyCode::CapsLock), Keysym::ESCAPE, pressed, false);
696        assert!(super::escape_dismisses(grab, &remapped));
697        let wtype_down = key(Some(KeyCode::Escape), Keysym::DOWN, pressed, false);
698        assert!(!super::escape_dismisses(grab, &wtype_down));
699
700        // Not a release, a repeat, another key, or a key with no symbol.
701        let released = key(
702            Some(KeyCode::Escape),
703            Keysym::ESCAPE,
704            ButtonState::Released,
705            false,
706        );
707        let repeated = key(Some(KeyCode::Escape), Keysym::ESCAPE, pressed, true);
708        let enter = key(Some(KeyCode::Enter), Keysym::RETURN, pressed, false);
709        let unnamed = key(None, Keysym::NONE, pressed, false);
710        for other in [&released, &repeated, &enter, &unnamed] {
711            assert!(!super::escape_dismisses(grab, other), "{other:?}");
712        }
713        // And not on a popup that does not hold the keyboard, or on a window
714        // that is no popup at all.
715        for input in [
716            Some(PopupInput::Pointer),
717            Some(PopupInput::Passthrough),
718            None,
719        ] {
720            assert!(!super::escape_dismisses(input, &escape), "{input:?}");
721        }
722        // Only a key event is a keystroke.
723        assert!(!super::escape_dismisses(
724            grab,
725            &ShellEvent::CloseRequested { window }
726        ));
727    }
728
729    fn panel(anchor: PanelAnchor) -> PanelDesc<'static> {
730        PanelDesc {
731            anchor,
732            ..PanelDesc::default()
733        }
734    }
735
736    #[test]
737    fn a_reserved_edge_is_one_edge_alone_or_with_both_neighbours() {
738        let top = PanelAnchor::TOP;
739        let sides = PanelAnchor::LEFT | PanelAnchor::RIGHT;
740        assert_eq!(panel(top).reserved_edge(), Some(top));
741        assert_eq!(panel(top | sides).reserved_edge(), Some(top));
742        let left = PanelAnchor::LEFT;
743        let ends = PanelAnchor::TOP | PanelAnchor::BOTTOM;
744        assert_eq!(panel(left | ends).reserved_edge(), Some(left));
745        assert_eq!(
746            panel(PanelAnchor::BOTTOM).reserved_edge(),
747            Some(PanelAnchor::BOTTOM)
748        );
749        // A corner, a centre and an everything name no single edge.
750        assert_eq!(panel(top | PanelAnchor::LEFT).reserved_edge(), None);
751        assert_eq!(panel(PanelAnchor::empty()).reserved_edge(), None);
752        assert_eq!(panel(PanelAnchor::all()).reserved_edge(), None);
753        assert_eq!(panel(sides).reserved_edge(), None);
754    }
755
756    fn with_role(role: WindowRole<'_>) -> WindowDesc<'_> {
757        WindowDesc {
758            role,
759            ..WindowDesc::default()
760        }
761    }
762
763    fn refused(result: Result<crate::WindowId, ShellError>) -> &'static str {
764        match result {
765            Err(ShellError::Unsupported { what, .. }) => what,
766            other => panic!("expected Unsupported, got {other:?}"),
767        }
768    }
769
770    #[test]
771    fn a_role_or_transparency_the_caps_lack_is_refused_not_downgraded() {
772        let mut shell = HeadlessShell::new().with_caps(
773            ShellCaps::DESKTOP - (ShellCaps::PANEL | ShellCaps::POPUP | ShellCaps::TRANSPARENT),
774        );
775        let parent = shell
776            .create_window(&WindowDesc::default())
777            .expect("a toplevel");
778        assert_eq!(
779            refused(shell.create_window(&with_role(WindowRole::Panel(PanelDesc::default())))),
780            "panel windows"
781        );
782        let popup = WindowRole::Popup(PopupDesc {
783            parent,
784            anchor: LogicalRect::default(),
785            placement: PopupPlacement::Below,
786            input: PopupInput::Grab,
787        });
788        assert_eq!(
789            refused(shell.create_window(&with_role(popup))),
790            "popup windows"
791        );
792        let transparent = WindowDesc {
793            transparent: true,
794            ..WindowDesc::default()
795        };
796        assert_eq!(
797            refused(shell.create_window(&transparent)),
798            "transparent windows"
799        );
800    }
801
802    #[test]
803    fn a_reserved_panel_needs_one_edge_and_a_real_monitor() {
804        let mut shell = HeadlessShell::new();
805        let cornered = PanelDesc {
806            reserve: Reserve::OwnExtent,
807            ..panel(PanelAnchor::TOP | PanelAnchor::LEFT)
808        };
809        assert_eq!(
810            refused(shell.create_window(&with_role(WindowRole::Panel(cornered)))),
811            "a reserved panel that is not held against one edge"
812        );
813        let bar = PanelDesc {
814            reserve: Reserve::OwnExtent,
815            ..panel(PanelAnchor::TOP | PanelAnchor::LEFT | PanelAnchor::RIGHT)
816        };
817        shell
818            .create_window(&with_role(WindowRole::Panel(bar)))
819            .expect("a bar names the top edge");
820        let elsewhere = PanelDesc {
821            monitor: Some(MonitorId(u32::MAX)),
822            ..PanelDesc::default()
823        };
824        assert!(matches!(
825            shell.create_window(&with_role(WindowRole::Panel(elsewhere))),
826            Err(ShellError::NoSuchMonitor(_))
827        ));
828    }
829
830    #[test]
831    fn a_popup_needs_a_live_parent_and_goes_with_it() {
832        let mut shell = HeadlessShell::new();
833        let parent = shell
834            .create_window(&WindowDesc::default())
835            .expect("a toplevel");
836        let popup_of = |parent| {
837            WindowRole::Popup(PopupDesc {
838                parent,
839                anchor: LogicalRect::default(),
840                placement: PopupPlacement::Below,
841                input: PopupInput::Passthrough,
842            })
843        };
844        let popup = shell
845            .create_window(&with_role(popup_of(parent)))
846            .expect("a popup");
847        let submenu = shell
848            .create_window(&with_role(popup_of(popup)))
849            .expect("a popup's popup");
850        shell.pump(&mut |_| {});
851
852        shell.destroy_window(parent).expect("destroy");
853        let mut destroyed = Vec::new();
854        shell.pump(&mut |event| {
855            if let ShellEvent::WindowDestroyed { window } = event {
856                destroyed.push(window);
857            }
858        });
859        assert_eq!(
860            destroyed,
861            [submenu, popup, parent],
862            "children first, each reported"
863        );
864        assert!(
865            shell.create_window(&with_role(popup_of(parent))).is_err(),
866            "a dead parent"
867        );
868    }
869}