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};
25use crate::{ShellBackend, ShellCaps, ShellError};
26
27/// What kind of window to create — see the [module docs](self).
28#[derive(Clone, Copy, Debug, Default, PartialEq)]
29#[non_exhaustive]
30pub enum WindowRole<'a> {
31 /// An ordinary application window: decorated, in the taskbar, moved and
32 /// sized by the user and the window manager.
33 #[default]
34 Toplevel,
35 /// Desktop furniture placed against a monitor's edges — a bar, a dock, a
36 /// launcher, an overlay. See [`PanelDesc`].
37 Panel(PanelDesc<'a>),
38 /// A window anchored to a rectangle of another, above it — a menu, a
39 /// tooltip, a calendar opened from a bar. See [`PopupDesc`].
40 Popup(PopupDesc),
41}
42
43/// Where a [panel](WindowRole::Panel) goes and how it behaves.
44///
45/// Laid out the way `wlr-layer-shell` describes a surface, because that
46/// protocol is the most complete statement of what a panel is: the other
47/// window systems implement a subset, and the backend docs say which.
48///
49/// **Its size is [`WindowDesc::size`], with one
50/// exception: along an axis where it is anchored to both opposite edges, that
51/// size is ignored and the panel stretches between them.** A bar anchored
52/// `TOP | LEFT | RIGHT` with a size of `(0, 50)` is 50 high and as wide as the
53/// monitor. Either way the size it actually gets arrives as the first
54/// [`Resized`](crate::ShellEvent::Resized), as for any window, and is the one
55/// to draw at.
56#[derive(Clone, Copy, Debug, Default, PartialEq)]
57pub struct PanelDesc<'a> {
58 /// The name window-system rules match the panel by: the layer-shell
59 /// namespace on Wayland, which Sway's and Hyprland's per-layer rules key
60 /// on. Empty means [`WindowDesc::app_id`].
61 pub namespace: &'a str,
62 /// The monitor it is placed on, or `None` for the system's choice, which
63 /// is **where the user is** rather than the primary monitor: the
64 /// compositor's pick on Wayland (the focused output on Sway, Hyprland and
65 /// niri), and the monitor under the pointer on Win32 and AppKit. A launcher
66 /// opening on a screen the user is not looking at is the failure this
67 /// avoids.
68 pub monitor: Option<MonitorId>,
69 /// Which band of the stack it lives in.
70 pub layer: PanelLayer,
71 /// The edges it is held against. A panel held against two opposite edges
72 /// stretches between them; one held against none is centred.
73 pub anchor: PanelAnchor,
74 /// Whether, and how, it keeps other windows out of its space.
75 pub reserve: Reserve,
76 /// Space between the panel and the edges it is anchored to, in logical
77 /// units. A margin on an edge it is not anchored to does nothing.
78 pub margins: Margins,
79 /// Whether it takes the keyboard.
80 pub keyboard: PanelKeyboard,
81}
82
83/// The band of the stack a [panel](PanelDesc) lives in, bottom to top.
84#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
85pub enum PanelLayer {
86 /// Below every application window: a wallpaper, a desktop widget.
87 Background,
88 /// Above the background, below application windows.
89 Bottom,
90 /// Above application windows, below full-screen ones: a status bar.
91 #[default]
92 Top,
93 /// Above everything, full-screen windows included: a launcher, a lock
94 /// screen, a notification.
95 ///
96 /// Win32 has one topmost band, which `Top` uses too, so there an overlay
97 /// is above application windows and shares the band with full-screen
98 /// ones that are topmost themselves.
99 Overlay,
100}
101
102bitflags::bitflags! {
103 /// The monitor edges a [panel](PanelDesc) is held against.
104 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
105 pub struct PanelAnchor: u8 {
106 /// The top edge.
107 const TOP = 1 << 0;
108 /// The bottom edge.
109 const BOTTOM = 1 << 1;
110 /// The left edge.
111 const LEFT = 1 << 2;
112 /// The right edge.
113 const RIGHT = 1 << 3;
114 }
115}
116
117/// Whether a [panel](PanelDesc) keeps other windows out of its space.
118///
119/// **Reserving is a request a window system may be unable to grant**, which is
120/// different from being unable to build the panel at all:
121/// [`ShellCaps::PANEL_RESERVE`] says whether
122/// this backend can. Where it cannot — macOS, which lets nothing but the menu
123/// bar and the Dock reserve screen space — the panel is still created and
124/// simply overlaps what is behind it.
125#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
126pub enum Reserve {
127 /// It reserves nothing, and keeps clear of the space other panels have
128 /// reserved — a launcher placed below a bar rather than over it.
129 /// Layer-shell's exclusive zone 0.
130 #[default]
131 None,
132 /// The edge it is anchored to gives up the panel's own extent: maximised
133 /// windows stop at it. A bar is this. Layer-shell's exclusive zone equal
134 /// to the panel's extent across that edge.
135 ///
136 /// **The panel must name one edge**: anchored to exactly one, or to one and
137 /// both of the edges beside it — `TOP`, or `TOP | LEFT | RIGHT`, reserves
138 /// the top. Any other anchoring has no edge to reserve, and creating it is
139 /// [`ShellError::Unsupported`] on every
140 /// backend; see [`PanelDesc::reserved_edge`].
141 OwnExtent,
142 /// It reserves nothing and is placed as if no other panel reserved
143 /// anything — at the very edge, over other bars. Layer-shell's exclusive
144 /// zone -1.
145 IgnoreOthers,
146}
147
148impl PanelDesc<'_> {
149 /// The edge [`Reserve::OwnExtent`] would reserve: the one anchored edge,
150 /// alone or with both of its neighbours. `None` for any other anchoring,
151 /// which has no single edge to give up.
152 #[must_use]
153 pub fn reserved_edge(&self) -> Option<PanelAnchor> {
154 let vertical = PanelAnchor::LEFT | PanelAnchor::RIGHT;
155 let horizontal = PanelAnchor::TOP | PanelAnchor::BOTTOM;
156 [
157 (PanelAnchor::TOP, vertical),
158 (PanelAnchor::BOTTOM, vertical),
159 (PanelAnchor::LEFT, horizontal),
160 (PanelAnchor::RIGHT, horizontal),
161 ]
162 .into_iter()
163 .find(|&(edge, beside)| self.anchor == edge || self.anchor == edge | beside)
164 .map(|(edge, _)| edge)
165 }
166}
167
168/// Space around a [panel](PanelDesc), in logical units.
169#[derive(Clone, Copy, Debug, Default, PartialEq)]
170pub struct Margins {
171 /// From the top edge.
172 pub top: f64,
173 /// From the right edge.
174 pub right: f64,
175 /// From the bottom edge.
176 pub bottom: f64,
177 /// From the left edge.
178 pub left: f64,
179}
180
181/// Whether a [panel](PanelDesc) takes the keyboard.
182#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
183pub enum PanelKeyboard {
184 /// Never: a click leaves focus where it was. A bar is this.
185 #[default]
186 None,
187 /// When clicked, like an application window.
188 OnDemand,
189 /// Takes it on mapping: a launcher, a lock screen. Keys typed the moment
190 /// it appears are its, or
191 /// [`FocusRefused`](crate::ShellEvent::FocusRefused) says the window
192 /// system would not hand it over.
193 ///
194 /// Held while mapped only where
195 /// [`ShellCaps::PANEL_EXCLUSIVE_KEYBOARD`](crate::ShellCaps::PANEL_EXCLUSIVE_KEYBOARD)
196 /// is set. Elsewhere the user can click another window and the panel
197 /// hears [`Focus`](crate::ShellEvent::Focus) with `focused: false`;
198 /// nothing takes the keyboard back, since that would fight the user and
199 /// the system's foreground rules.
200 Exclusive,
201}
202
203/// Where a [popup](WindowRole::Popup) goes.
204#[derive(Clone, Copy, Debug, PartialEq)]
205pub struct PopupDesc {
206 /// The window it belongs to and moves with. Destroying the parent
207 /// destroys the popup.
208 pub parent: WindowId,
209 /// The rectangle it is placed against, in the parent's logical
210 /// coordinates — the module or button that opened it.
211 pub anchor: LogicalRect,
212 /// Which side of the anchor it opens on. The system flips it to the other
213 /// side, or slides it along this one, rather than let it leave the screen.
214 pub placement: PopupPlacement,
215 /// What input it takes while open.
216 pub input: PopupInput,
217}
218
219/// What input a [popup](PopupDesc) takes.
220#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
221pub enum PopupInput {
222 /// None at all: the pointer passes through it to whatever is beneath, and
223 /// it never activates. A tooltip is this, so hovering stays on the bar
224 /// that showed it.
225 #[default]
226 Passthrough,
227 /// The pointer over it, as any window has; never the keyboard, and a click
228 /// elsewhere leaves it open.
229 Pointer,
230 /// The pointer and the keyboard while it is open. A click outside it or
231 /// `Escape` dismisses it, which arrives as a
232 /// [`CloseRequested`](crate::ShellEvent::CloseRequested) for it. A menu or
233 /// a calendar is this.
234 ///
235 /// `Escape` is still delivered: the popup hears the
236 /// [`Key`](crate::ShellEvent::Key) press first and the `CloseRequested`
237 /// after it, so a consumer can tell a dismissal by the keyboard from one by
238 /// a click, and can answer [`CloseReply::Keep`](crate::CloseReply::Keep)
239 /// to back out of a submenu instead of closing the whole menu. Only a
240 /// fresh press dismisses — an auto-repeat or a release does not.
241 Grab,
242}
243
244/// Whether `key`, delivered to a window whose popup input is `popup`, asks
245/// for that window to close: a fresh press of `Escape` on a
246/// [`PopupInput::Grab`] popup, as that variant documents. `popup` is `None`
247/// for a window that is not a popup.
248///
249/// Every backend that builds popups calls this after queueing the key, and on
250/// `true` queues the window's
251/// [`CloseRequested`](crate::ShellEvent::CloseRequested) through its own
252/// close-request path, so a request already pending is not repeated.
253pub(crate) fn escape_dismisses(popup: Option<PopupInput>, key: &crate::ShellEvent) -> bool {
254 popup == Some(PopupInput::Grab)
255 && matches!(
256 key,
257 crate::ShellEvent::Key {
258 key_code: Some(crcbl_core::KeyCode::Escape),
259 state: crcbl_core::input::ButtonState::Pressed,
260 repeat: false,
261 ..
262 }
263 )
264}
265
266/// What every backend checks before it builds a window: that its caps cover
267/// the role and the transparency asked for, that a reserved panel names one
268/// edge, and that a panel's monitor exists.
269///
270/// The popup's parent is the backend's to check, since only it knows its
271/// windows.
272pub(crate) fn check(
273 desc: &WindowDesc<'_>,
274 caps: ShellCaps,
275 backend: ShellBackend,
276 has_monitor: impl Fn(MonitorId) -> bool,
277) -> Result<(), ShellError> {
278 let unsupported = |what| Err(ShellError::Unsupported { backend, what });
279 if desc.transparent && !caps.contains(ShellCaps::TRANSPARENT) {
280 return unsupported("transparent windows");
281 }
282 match desc.role {
283 WindowRole::Toplevel => Ok(()),
284 WindowRole::Panel(panel) => check_panel(&panel, caps, backend, has_monitor),
285 WindowRole::Popup(_) if !caps.contains(ShellCaps::POPUP) => unsupported("popup windows"),
286 WindowRole::Popup(_) => Ok(()),
287 }
288}
289
290/// The panel half of [`check`], which
291/// [`Shell::set_panel`](crate::Shell::set_panel) applies to a panel's new
292/// descriptor too, so a panel cannot be reconfigured into one it could not
293/// have been created as.
294fn check_panel(
295 panel: &PanelDesc<'_>,
296 caps: ShellCaps,
297 backend: ShellBackend,
298 has_monitor: impl Fn(MonitorId) -> bool,
299) -> Result<(), ShellError> {
300 let unsupported = |what| Err(ShellError::Unsupported { backend, what });
301 if !caps.contains(ShellCaps::PANEL) {
302 return unsupported("panel windows");
303 }
304 if panel.reserve == Reserve::OwnExtent && panel.reserved_edge().is_none() {
305 return unsupported("a reserved panel that is not held against one edge");
306 }
307 match panel.monitor {
308 Some(monitor) if !has_monitor(monitor) => Err(ShellError::NoSuchMonitor(monitor.0)),
309 _ => Ok(()),
310 }
311}
312
313/// What every backend checks before it reconfigures a panel with
314/// [`Shell::set_panel`](crate::Shell::set_panel): that the window is a panel,
315/// whose descriptor is `was`, that the new descriptor keeps its namespace and
316/// its monitor, and that it passes the checks a new panel would.
317///
318/// The namespace and the monitor are fixed by `wlr-layer-shell`, which names
319/// both in `get_layer_surface` and has no request to change either. Refusing
320/// a change on every backend, rather than only where the protocol does, keeps
321/// a panel that works on one system from failing on another.
322pub(crate) fn check_set_panel(
323 window: WindowId,
324 was: Option<PanelDesc<'_>>,
325 panel: &PanelDesc<'_>,
326 caps: ShellCaps,
327 backend: ShellBackend,
328 has_monitor: impl Fn(MonitorId) -> bool,
329) -> Result<(), ShellError> {
330 let Some(was) = was else {
331 return Err(not_a_panel(window));
332 };
333 if panel.namespace != was.namespace {
334 return Err(ShellError::InvalidDescriptor(
335 "set_panel: a panel's namespace is fixed when it is created".to_string(),
336 ));
337 }
338 if panel.monitor != was.monitor {
339 return Err(ShellError::InvalidDescriptor(
340 "set_panel: a panel's monitor is fixed when it is created".to_string(),
341 ));
342 }
343 check_panel(panel, caps, backend, has_monitor)
344}
345
346/// What [`Shell::set_panel`](crate::Shell::set_panel) answers for a window
347/// that is not a panel, in every backend's words alike.
348pub(crate) fn not_a_panel(window: WindowId) -> ShellError {
349 ShellError::InvalidDescriptor(format!("set_panel: {window:?} is not a panel"))
350}
351
352/// A [`PanelDesc`] kept past the call that gave it, for the backends that
353/// have no panel record of their own: the namespace owned, the rest as given.
354#[derive(Clone, Debug, PartialEq)]
355pub(crate) struct KeptPanel {
356 namespace: String,
357 /// The descriptor with an empty namespace, which [`desc`](Self::desc)
358 /// puts back.
359 desc: PanelDesc<'static>,
360}
361
362impl KeptPanel {
363 pub(crate) fn new(desc: &PanelDesc<'_>) -> Self {
364 Self {
365 namespace: desc.namespace.to_string(),
366 desc: PanelDesc {
367 namespace: "",
368 ..*desc
369 },
370 }
371 }
372
373 /// The descriptor as it was given.
374 pub(crate) fn desc(&self) -> PanelDesc<'_> {
375 PanelDesc {
376 namespace: &self.namespace,
377 ..self.desc
378 }
379 }
380}
381
382/// Which side of its anchor a [popup](PopupDesc) opens on.
383#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
384pub enum PopupPlacement {
385 /// Below the anchor, left edges aligned.
386 #[default]
387 Below,
388 /// Above the anchor, left edges aligned.
389 Above,
390 /// To the left of the anchor, top edges aligned.
391 Left,
392 /// To the right of the anchor, top edges aligned: a submenu.
393 Right,
394}
395
396/// A rectangle in a backend's own Y-down space — desktop pixels on Win32,
397/// flipped screen points on AppKit — for [`place_popup`].
398///
399/// Compiled where its callers are: the two backends that place popups
400/// themselves.
401#[cfg(any(target_os = "windows", target_os = "macos", test))]
402#[derive(Clone, Copy, Debug, PartialEq)]
403pub(crate) struct Placed {
404 pub x: f64,
405 pub y: f64,
406 pub width: f64,
407 pub height: f64,
408}
409
410/// Where a popup of `width × height` goes against `anchor`: on the
411/// `placement` side of it, flipped to the other side when it does not fit in
412/// `area` and the other side does, and slid along the side it is on to stay
413/// in `area`.
414///
415/// The placement `xdg_positioner` is asked for on Wayland, worked out here
416/// for the window systems that have no one to ask. Y grows downwards.
417/// What [`Shell::reposition_popup`](crate::Shell::reposition_popup) answers
418/// for a window that is not a popup, in every backend's words alike.
419pub(crate) fn not_a_popup(window: WindowId) -> ShellError {
420 ShellError::InvalidDescriptor(format!("reposition_popup: {window:?} is not a popup"))
421}
422
423#[cfg(any(target_os = "windows", target_os = "macos", test))]
424pub(crate) fn place_popup(
425 anchor: Placed,
426 (width, height): (f64, f64),
427 placement: PopupPlacement,
428 area: Placed,
429) -> (f64, f64) {
430 let (left, top) = (anchor.x, anchor.y);
431 let (right, bottom) = (left + anchor.width, top + anchor.height);
432 let (area_right, area_bottom) = (area.x + area.width, area.y + area.height);
433 // The near start on the placement's axis, or the flip when only that fits.
434 let side = |near: f64, far: f64, extent: f64, low: f64, high: f64| {
435 if near >= low && near + extent <= high {
436 near
437 } else if far >= low && far + extent <= high {
438 far
439 } else {
440 near
441 }
442 };
443 let slide = |start: f64, extent: f64, low: f64, high: f64| start.min(high - extent).max(low);
444 match placement {
445 PopupPlacement::Below => (
446 slide(left, width, area.x, area_right),
447 side(bottom, top - height, height, area.y, area_bottom),
448 ),
449 PopupPlacement::Above => (
450 slide(left, width, area.x, area_right),
451 side(top - height, bottom, height, area.y, area_bottom),
452 ),
453 PopupPlacement::Left => (
454 side(left - width, right, width, area.x, area_right),
455 slide(top, height, area.y, area_bottom),
456 ),
457 PopupPlacement::Right => (
458 side(right, left - width, width, area.x, area_right),
459 slide(top, height, area.y, area_bottom),
460 ),
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::{
467 PanelAnchor, PanelDesc, PopupDesc, PopupInput, PopupPlacement, Reserve, WindowRole,
468 };
469 use crate::{
470 HeadlessShell, LogicalRect, MonitorId, Shell, ShellCaps, ShellError, ShellEvent, WindowDesc,
471 };
472
473 #[test]
474 fn only_a_fresh_escape_press_on_a_grabbing_popup_dismisses_it() {
475 use crcbl_core::KeyCode;
476 use crcbl_core::input::{ButtonState, DeviceId, Keysym, Modifiers, Scancode};
477
478 let window = crate::WindowId::from_bits(1 << 32).expect("a valid handle");
479 let key = |key_code, state, repeat| ShellEvent::Key {
480 window,
481 device: DeviceId(1),
482 time: crcbl_core::EventTime::ZERO,
483 scancode: Scancode(0),
484 key_code,
485 keysym: Keysym::NONE,
486 state,
487 repeat,
488 modifiers: Modifiers::empty(),
489 };
490 let escape = key(Some(KeyCode::Escape), ButtonState::Pressed, false);
491 let grab = Some(PopupInput::Grab);
492 assert!(super::escape_dismisses(grab, &escape));
493
494 // Not a release, a repeat, another key, or a key with no name.
495 let released = key(Some(KeyCode::Escape), ButtonState::Released, false);
496 let repeated = key(Some(KeyCode::Escape), ButtonState::Pressed, true);
497 let enter = key(Some(KeyCode::Enter), ButtonState::Pressed, false);
498 let unnamed = key(None, ButtonState::Pressed, false);
499 for other in [&released, &repeated, &enter, &unnamed] {
500 assert!(!super::escape_dismisses(grab, other), "{other:?}");
501 }
502 // And not on a popup that does not hold the keyboard, or on a window
503 // that is no popup at all.
504 for input in [
505 Some(PopupInput::Pointer),
506 Some(PopupInput::Passthrough),
507 None,
508 ] {
509 assert!(!super::escape_dismisses(input, &escape), "{input:?}");
510 }
511 // Only a key event is a keystroke.
512 assert!(!super::escape_dismisses(
513 grab,
514 &ShellEvent::CloseRequested { window }
515 ));
516 }
517
518 fn panel(anchor: PanelAnchor) -> PanelDesc<'static> {
519 PanelDesc {
520 anchor,
521 ..PanelDesc::default()
522 }
523 }
524
525 #[test]
526 fn a_reserved_edge_is_one_edge_alone_or_with_both_neighbours() {
527 let top = PanelAnchor::TOP;
528 let sides = PanelAnchor::LEFT | PanelAnchor::RIGHT;
529 assert_eq!(panel(top).reserved_edge(), Some(top));
530 assert_eq!(panel(top | sides).reserved_edge(), Some(top));
531 let left = PanelAnchor::LEFT;
532 let ends = PanelAnchor::TOP | PanelAnchor::BOTTOM;
533 assert_eq!(panel(left | ends).reserved_edge(), Some(left));
534 assert_eq!(
535 panel(PanelAnchor::BOTTOM).reserved_edge(),
536 Some(PanelAnchor::BOTTOM)
537 );
538 // A corner, a centre and an everything name no single edge.
539 assert_eq!(panel(top | PanelAnchor::LEFT).reserved_edge(), None);
540 assert_eq!(panel(PanelAnchor::empty()).reserved_edge(), None);
541 assert_eq!(panel(PanelAnchor::all()).reserved_edge(), None);
542 assert_eq!(panel(sides).reserved_edge(), None);
543 }
544
545 fn with_role(role: WindowRole<'_>) -> WindowDesc<'_> {
546 WindowDesc {
547 role,
548 ..WindowDesc::default()
549 }
550 }
551
552 fn refused(result: Result<crate::WindowId, ShellError>) -> &'static str {
553 match result {
554 Err(ShellError::Unsupported { what, .. }) => what,
555 other => panic!("expected Unsupported, got {other:?}"),
556 }
557 }
558
559 #[test]
560 fn a_role_or_transparency_the_caps_lack_is_refused_not_downgraded() {
561 let mut shell = HeadlessShell::new().with_caps(
562 ShellCaps::DESKTOP - (ShellCaps::PANEL | ShellCaps::POPUP | ShellCaps::TRANSPARENT),
563 );
564 let parent = shell
565 .create_window(&WindowDesc::default())
566 .expect("a toplevel");
567 assert_eq!(
568 refused(shell.create_window(&with_role(WindowRole::Panel(PanelDesc::default())))),
569 "panel windows"
570 );
571 let popup = WindowRole::Popup(PopupDesc {
572 parent,
573 anchor: LogicalRect::default(),
574 placement: PopupPlacement::Below,
575 input: PopupInput::Grab,
576 });
577 assert_eq!(
578 refused(shell.create_window(&with_role(popup))),
579 "popup windows"
580 );
581 let transparent = WindowDesc {
582 transparent: true,
583 ..WindowDesc::default()
584 };
585 assert_eq!(
586 refused(shell.create_window(&transparent)),
587 "transparent windows"
588 );
589 }
590
591 #[test]
592 fn a_reserved_panel_needs_one_edge_and_a_real_monitor() {
593 let mut shell = HeadlessShell::new();
594 let cornered = PanelDesc {
595 reserve: Reserve::OwnExtent,
596 ..panel(PanelAnchor::TOP | PanelAnchor::LEFT)
597 };
598 assert_eq!(
599 refused(shell.create_window(&with_role(WindowRole::Panel(cornered)))),
600 "a reserved panel that is not held against one edge"
601 );
602 let bar = PanelDesc {
603 reserve: Reserve::OwnExtent,
604 ..panel(PanelAnchor::TOP | PanelAnchor::LEFT | PanelAnchor::RIGHT)
605 };
606 shell
607 .create_window(&with_role(WindowRole::Panel(bar)))
608 .expect("a bar names the top edge");
609 let elsewhere = PanelDesc {
610 monitor: Some(MonitorId(u32::MAX)),
611 ..PanelDesc::default()
612 };
613 assert!(matches!(
614 shell.create_window(&with_role(WindowRole::Panel(elsewhere))),
615 Err(ShellError::NoSuchMonitor(_))
616 ));
617 }
618
619 #[test]
620 fn a_popup_needs_a_live_parent_and_goes_with_it() {
621 let mut shell = HeadlessShell::new();
622 let parent = shell
623 .create_window(&WindowDesc::default())
624 .expect("a toplevel");
625 let popup_of = |parent| {
626 WindowRole::Popup(PopupDesc {
627 parent,
628 anchor: LogicalRect::default(),
629 placement: PopupPlacement::Below,
630 input: PopupInput::Passthrough,
631 })
632 };
633 let popup = shell
634 .create_window(&with_role(popup_of(parent)))
635 .expect("a popup");
636 let submenu = shell
637 .create_window(&with_role(popup_of(popup)))
638 .expect("a popup's popup");
639 shell.pump(&mut |_| {});
640
641 shell.destroy_window(parent).expect("destroy");
642 let mut destroyed = Vec::new();
643 shell.pump(&mut |event| {
644 if let ShellEvent::WindowDestroyed { window } = event {
645 destroyed.push(window);
646 }
647 });
648 assert_eq!(
649 destroyed,
650 [submenu, popup, parent],
651 "children first, each reported"
652 );
653 assert!(
654 shell.create_window(&with_role(popup_of(parent))).is_err(),
655 "a dead parent"
656 );
657 }
658}