Skip to main content

crcbl_shell/
headless.rs

1//! [`HeadlessShell`] — a complete [`Shell`] with a virtual monitor, scripted
2//! event injection, and no OS calls anywhere.
3//!
4//! It is the windowing counterpart of `crcbl-hal`'s `NullBackend`, and it has
5//! the same three jobs, in increasing order of usefulness:
6//!
7//! 1. **Prove the seam is implementable without platform types.** If a trait
8//!    method could not be written without naming a compositor, this module
9//!    would not compile.
10//! 2. **Be the substrate CI runs the engine through.**
11//!    The windowing rules in `docs/notes/backends.md` are explicit that this is
12//!    not a stub: "CI and
13//!    `crcbl screenshot`/`sim` run the identical engine loop through it". A
14//!    render test pairs it with [`SurfaceTarget::Offscreen`] — which
15//!    [`surface_target`](Shell::surface_target) returns — so the golden-image
16//!    path exercises the same acquire/present code a real window does.
17//! 3. **Make window behaviour testable.** Resize storms, scale-factor changes
18//!    mid-session, aspect-lock clamping, focus loss: all scriptable, all
19//!    deterministic, none needing a nested compositor.
20//!
21//! # Deterministic by construction
22//!
23//! Nothing here reads a clock, a socket or an environment variable. Time comes
24//! from an internal [`ManualTime`], so a test that drives both the shell and a
25//! [`FrameClock`](crcbl_core::FrameClock) from the same schedule gets a session
26//! that is byte-identical on every machine — the property
27//! `docs/notes/process.md` demands of every nondeterminism source.
28//! [`HeadlessShell::set_time`] exists so the shell's clock can be slaved to a
29//! test's own `ManualTime` rather than drifting alongside it.
30//!
31//! # It is *stricter* than a compositor, on purpose
32//!
33//! The temptation with a test double is to make every call succeed
34//! immediately. That is the shape that proves nothing: a consumer written
35//! against it grows an assumption the real backend then breaks, all at once,
36//! long after the code was written. So where headless can be the harsher of the
37//! two, it is.
38//!
39//! * **A new window has no size.** [`create_window`](Shell::create_window)
40//!   returns a [`WindowId`] and queues *nothing*. The first
41//!   [`Resized`](ShellEvent::Resized) is delivered by a later
42//!   [`pump`](Shell::pump) — by default the second one, and
43//!   [`set_configure_delay`](HeadlessShell::set_configure_delay) makes it
44//!   later still. This models Wayland exactly: an `xdg_surface` is unsized and
45//!   may not be given a buffer until the compositor answers with a `configure`,
46//!   a round trip away. Consumers must loop on
47//!   [`WindowState::size`] before creating a swapchain, and here they are made
48//!   to. (The same trick as `crcbl-hal`'s injected readback latency, for the
49//!   same reason.)
50//! * **`set_mode` and `set_constraints` are requests, not commands.** They
51//!   record the request in [`WindowState::requested_mode`] /
52//!   [`requested_constraints`](WindowState::requested_constraints) and schedule
53//!   a configure; the *effective* mode only appears in
54//!   [`WindowConfiguration::mode`] once that configure is delivered. A consumer
55//!   that reads back its own request as fact fails here.
56//! * **Constraints are enforced on delivered configures.**
57//!   [`SizeConstraints::apply`] runs, so aspect-locked and min/max behaviour is
58//!   exercised. A tiling window manager enforces none of it — which is why
59//!   in-renderer letterboxing is the universal fallback, and why
60//!   [`inject`](HeadlessShell::inject) exists to feed sizes that violate the
61//!   constraints outright.
62//!
63//! Where it is still *easier* than a compositor, and knowingly so: the injection
64//! helpers ([`resize`](HeadlessShell::resize),
65//! [`change_scale_factor`](HeadlessShell::change_scale_factor)) apply at once,
66//! because they *are* the compositor speaking rather than the client asking.
67//! They are also the escape hatch for a test that wants a configured window in
68//! one line.
69//!
70//! # Where the model is now known to be wrong (P0.5a)
71//!
72//! The Wayland backend's end-to-end suite is the first thing that could check
73//! any of this against a real compositor. Three differences came back, and they
74//! are recorded here rather than only in the backend, because this is the file
75//! consumers are written against:
76//!
77//! 1. **A first configure usually dictates no size at all.** Headless always
78//!    delivers one. `xdg-shell` defines `0 × 0` as "you choose", and it is what
79//!    sway sends for a freshly created toplevel — so a real backend supplies
80//!    the fallback and the requested size stands. A consumer is unaffected
81//!    (both shapes are "a size arrived"), but a *backend* author reading only
82//!    this file would not expect it.
83//! 2. **Size arrives before scale.** A window's scale factor comes from
84//!    `wl_surface.enter`, which only fires once the surface is mapped — which
85//!    needs a buffer, which needs the size. So the first configure is at scale
86//!    1.0 and the true scale follows as a separate
87//!    [`ScaleFactorChanged`](ShellEvent::ScaleFactorChanged).
88//!    [`WindowConfiguration`] groups the two because they arrive in *one
89//!    message*; it does not promise the first message is right about both.
90//!    [`change_scale_factor`](HeadlessShell::change_scale_factor) is how a test
91//!    reproduces the sequence.
92//! 3. **Configured is not the same as managed.** A Wayland window that has
93//!    never been given a buffer is answered once and then ignored: no
94//!    compositor-chosen geometry, no fullscreen configure, not even present in
95//!    the window manager's tree. Headless has no such state, and nothing in the
96//!    seam expresses it. It is not a defect in the seam — the sequence a
97//!    consumer must write (configure → swapchain → present → *then* expect real
98//!    geometry) is the same either way — but a test that resizes a headless
99//!    window before its first present is exercising something a compositor
100//!    would not have done.
101//!
102//! # The clipboard's hard cases are settable (P0.5c)
103//!
104//! P0.5c found the same "test double only knows the easy path" problem in the
105//! clipboard that P0.4 had fixed for window sizes: a real compositor's
106//! clipboard is *not* always readable and *not* always claimable, and a shell
107//! that always answered instantly would let a consumer grow exactly the
108//! assumption Wayland then breaks. All three cases are now scriptable, and none
109//! of them needs a compositor:
110//!
111//! | Real behaviour | Modelled by | What a consumer learns |
112//! | --- | --- | --- |
113//! | Wayland delivers `wl_data_device.selection` only to the focused client, asynchronously | [`set_clipboard_readable`](HeadlessShell::set_clipboard_readable) | a read is **held** until it can be answered, and is never answered [`Empty`](ClipboardContent::Empty) for "I have not looked yet" |
114//! | A peer takes the transfer descriptor and never writes | [`set_clipboard_stalled`](HeadlessShell::set_clipboard_stalled) | the read ends as [`Unavailable`](ClipboardContent::Unavailable) — a failure, not an empty clipboard |
115//! | `set_selection` needs a serial from real user input | [`require_user_interaction`](HeadlessShell::require_user_interaction) | [`clipboard_offer`](Shell::clipboard_offer) can fail with [`ShellError::NeedsUserInteraction`], and that is a policy rather than a bug |
116//!
117//! The deadline a held read gives up after is
118//! [`clipboard_deadline`](HeadlessShell::clipboard_deadline), counted in
119//! **pumps** rather than milliseconds so a test asserting the timeout is not a
120//! flake. Defaults are the permissive ones — readable, not stalled, no
121//! interaction required — because most platforms have no such rules and a test
122//! that does not care about them should not have to say so.
123//!
124//! # Capabilities are settable, which is the point
125//!
126//! [`HeadlessShell::new`] reports [`ShellCaps::DESKTOP`] — everything. But
127//! [`with_caps`](HeadlessShell::with_caps) lets a test report a *degraded* set:
128//! a Wayland-shaped shell with no [`POINTER_WARP`](ShellCaps::POINTER_WARP) and
129//! no [`ASPECT_HINT_HONORED`](ShellCaps::ASPECT_HINT_HONORED), say. That is how
130//! the caps system gets tested before a second backend exists — a consumer that
131//! branches on the platform instead of on caps fails against a headless shell
132//! configured to look like the platform it forgot.
133
134use std::collections::VecDeque;
135use std::path::PathBuf;
136use std::sync::{Arc, Condvar, Mutex, PoisonError};
137
138use core::time::Duration;
139
140use crcbl_core::input::{
141    ButtonState, ContactId, DeviceId, Keysym, Modifiers, PointerButton, Scancode, ScrollDelta,
142    TouchPhase,
143};
144use crcbl_core::time::{ManualTime, TimeSource};
145use crcbl_core::{EventTime, Pool, SurfaceTarget};
146
147use crate::frame::FrameRequests;
148use crate::waker::WakeSignal;
149use crate::{
150    ClipboardContent, ClipboardOffer, ClipboardRequestId, CloseReply, CursorIcon, DisplayMode,
151    KeyCode, LogicalRect, LogicalSize, MimeType, MonitorId, MonitorInfo, PhysicalPoint,
152    PhysicalRect, PhysicalSize, PointerMode, ReceivedMime, Shell, ShellBackend, ShellCaps,
153    ShellError, ShellEvent, ShellWaker, SizeConstraints, TextPurpose, WindowConfiguration,
154    WindowDesc, WindowId, WindowState,
155};
156
157/// Resolution of the virtual monitor [`HeadlessShell::new`] creates.
158///
159/// 1920×1080 at scale 1.0: the size golden images are most often blessed at,
160/// and small enough that a readback in a test is cheap.
161pub const DEFAULT_MONITOR_SIZE: PhysicalSize = PhysicalSize::new(1920, 1080);
162
163/// Refresh rate of the virtual monitor, in millihertz — exactly 60 Hz.
164pub const DEFAULT_REFRESH_MILLIHERTZ: u32 = 60_000;
165
166/// Pumps that must complete before a newly requested configure is delivered.
167///
168/// One, not zero: a consumer that creates a window and immediately reads its
169/// size must fail, because that is what it will do on Wayland. See the [module
170/// docs](self).
171pub const DEFAULT_CONFIGURE_DELAY: u32 = 1;
172
173/// The device id every injected input event carries unless a helper says
174/// otherwise.
175///
176/// A real id rather than [`DeviceId::UNKNOWN`], so a consumer that routes by
177/// device is exercised rather than short-circuited.
178pub const VIRTUAL_DEVICE: DeviceId = DeviceId(1);
179
180/// Pumps a held clipboard read waits before giving up.
181///
182/// The modelled form of the deadline every real backend needs: a read that
183/// cannot be answered has to *end* rather than stay outstanding, and
184/// [`ClipboardContent::Unavailable`] is how it ends. Counted in pumps rather
185/// than in milliseconds, because a headless shell has no wall clock and a test
186/// that waited on one would be a flake.
187pub const DEFAULT_CLIPBOARD_DEADLINE: u32 = 4;
188
189/// A clipboard read that has been accepted and not yet answered.
190///
191/// The seam's obligation 5 — "a read that cannot be answered yet is held, never
192/// answered empty" — made into state. `HeadlessShell` models the case because
193/// it is the one a consumer written only against the easy path gets wrong.
194#[derive(Clone, Debug)]
195struct PendingRead {
196    window: WindowId,
197    request: ClipboardRequestId,
198    mime: MimeType,
199    /// Pumps still to go before this is reported unavailable.
200    countdown: u32,
201}
202
203/// A configure the window system has decided on but not yet delivered.
204///
205/// The whole point of the delay; see the [module docs](self).
206#[derive(Clone, Copy, Debug)]
207struct PendingConfigure {
208    size: PhysicalSize,
209    scale_factor: f64,
210    mode: DisplayMode,
211    /// Pumps still to go before this is delivered.
212    countdown: u32,
213}
214
215/// A window inside the headless shell.
216#[derive(Clone, Debug)]
217struct HeadlessWindow {
218    title: String,
219    /// What has actually been reported. `None` until the first configure.
220    configuration: Option<WindowConfiguration>,
221    /// What is on its way, if anything.
222    pending: Option<PendingConfigure>,
223    requested_mode: DisplayMode,
224    requested_constraints: SizeConstraints,
225    focused: bool,
226    visible: bool,
227    pointer_mode: PointerMode,
228    cursor: Option<CursorIcon>,
229    close_pending: bool,
230    accept_drops: bool,
231    /// The window a popup belongs to, which takes it along when destroyed.
232    parent: Option<WindowId>,
233    /// What input a popup takes, which decides whether `Escape` dismisses it.
234    popup_input: Option<crate::PopupInput>,
235    /// What a panel was created with, or last given by [`Shell::set_panel`];
236    /// its monitor's unplugging asks it to close. `None` for every other
237    /// window.
238    panel: Option<crate::role::KeptPanel>,
239    /// [`Shell::set_text_input`]'s last word.
240    text_input: Option<TextPurpose>,
241    /// [`Shell::set_input_region`]'s last word: `None` for the whole window.
242    input_region: Option<Vec<PhysicalRect>>,
243    /// Whether a pretend assistive technology is reading the window, and what
244    /// it has been sent since it started.
245    #[cfg(feature = "accesskit")]
246    accessibility: Option<Vec<accesskit::TreeUpdate>>,
247}
248
249impl HeadlessWindow {
250    /// The client area, or `None` while unconfigured.
251    fn size(&self) -> Option<PhysicalSize> {
252        self.configuration.map(|config| config.size)
253    }
254
255    /// The scale factor to apply to a logical size right now.
256    ///
257    /// Falls back to the pending configure's, then to 1.0 — which is exactly
258    /// what a Wayland client does before its first configure: render at scale 1
259    /// and correct itself when told.
260    fn scale_factor(&self) -> f64 {
261        self.configuration
262            .map(|config| config.scale_factor)
263            .or_else(|| self.pending.map(|pending| pending.scale_factor))
264            .unwrap_or(1.0)
265    }
266
267    /// The mode that is in effect, or — before the first configure — the one
268    /// that is on its way.
269    ///
270    /// Deliberately *not* the pending configure's when the window is already
271    /// configured: a compositor answering a `set_mode` with something else is a
272    /// case the seam exists to express, so an injected
273    /// [`resize`](HeadlessShell::resize) must report the mode currently in
274    /// effect rather than adopting the one that was merely asked for.
275    fn settled_mode(&self) -> DisplayMode {
276        self.configuration
277            .map(|config| config.mode)
278            .or_else(|| self.pending.map(|pending| pending.mode))
279            .unwrap_or(self.requested_mode)
280    }
281
282    /// The size a fresh configure should start from.
283    fn base_size(&self) -> PhysicalSize {
284        self.size()
285            .or_else(|| self.pending.map(|pending| pending.size))
286            .unwrap_or(PhysicalSize::new(1, 1))
287    }
288
289    /// Takes `size` as the configure on its way, or as a new one due in
290    /// `countdown` pumps once the first has been delivered: a window system
291    /// answering a request for a new size.
292    fn configure_at(&mut self, size: LogicalSize, countdown: u32) {
293        if let Some(pending) = self.pending.as_mut() {
294            pending.size = size.to_physical(pending.scale_factor);
295        } else if let Some(config) = self.configuration {
296            self.pending = Some(PendingConfigure {
297                size: size.to_physical(config.scale_factor),
298                scale_factor: config.scale_factor,
299                mode: config.mode,
300                countdown,
301            });
302        }
303    }
304}
305
306/// A [`Shell`] with no window system behind it.
307///
308/// See the [module docs](self) for what it models and where it deliberately
309/// differs from a real compositor.
310#[derive(Debug)]
311pub struct HeadlessShell {
312    caps: ShellCaps,
313    monitors: Vec<MonitorInfo>,
314    next_monitor_id: u32,
315    windows: Pool<HeadlessWindow>,
316    queue: VecDeque<ShellEvent>,
317    clock: ManualTime,
318    modifiers: Modifiers,
319    clipboard: Vec<(ReceivedMime, Vec<u8>)>,
320    next_request_id: u32,
321    /// Reads that have been accepted and not yet answered; see
322    /// [`PendingRead`].
323    reads: Vec<PendingRead>,
324    /// Whether the clipboard can be read at all right now — the modelled form
325    /// of Wayland's focus gate. See
326    /// [`set_clipboard_readable`](HeadlessShell::set_clipboard_readable).
327    clipboard_readable: bool,
328    /// Whether a peer holds the clipboard and never delivers.
329    clipboard_stalled: bool,
330    /// Whether claiming the clipboard requires a recent input event.
331    clipboard_needs_interaction: bool,
332    /// Whether any input has been injected since the last
333    /// [`clear_user_interaction`](HeadlessShell::clear_user_interaction).
334    saw_interaction: bool,
335    /// Pumps a held read waits before it reports
336    /// [`ClipboardContent::Unavailable`].
337    clipboard_deadline: u32,
338    waits: u32,
339    configure_delay: u32,
340    /// The doorbell [`waker`](Shell::waker) hands out, made on the first ask.
341    wake: Option<Arc<HeadlessWake>>,
342    /// [`Shell::request_frame`]s the next pump answers.
343    frames: FrameRequests,
344}
345
346/// The headless shell's doorbell: a flag and the condition variable a wait
347/// sleeps on.
348#[derive(Debug, Default)]
349struct HeadlessWake {
350    rung: Mutex<bool>,
351    bell: Condvar,
352}
353
354impl HeadlessWake {
355    /// Sleeps until rung or until `timeout` passes, then clears the bell. A
356    /// ring that came before the wait ends it at once.
357    fn wait(&self, timeout: Option<Duration>) {
358        let rung = self.rung.lock().unwrap_or_else(PoisonError::into_inner);
359        let mut rung = match timeout {
360            Some(timeout) => {
361                self.bell
362                    .wait_timeout_while(rung, timeout, |rung| !*rung)
363                    .unwrap_or_else(PoisonError::into_inner)
364                    .0
365            }
366            None => self
367                .bell
368                .wait_while(rung, |rung| !*rung)
369                .unwrap_or_else(PoisonError::into_inner),
370        };
371        *rung = false;
372    }
373}
374
375impl WakeSignal for HeadlessWake {
376    fn wake(&self) {
377        *self.rung.lock().unwrap_or_else(PoisonError::into_inner) = true;
378        self.bell.notify_all();
379    }
380}
381
382impl Default for HeadlessShell {
383    fn default() -> Self {
384        Self::new()
385    }
386}
387
388impl HeadlessShell {
389    /// A shell with one 1920×1080 monitor at scale 1.0 and every capability.
390    #[must_use]
391    pub fn new() -> Self {
392        Self {
393            caps: ShellCaps::DESKTOP,
394            monitors: vec![MonitorInfo {
395                id: MonitorId(1),
396                name: "HEADLESS-1".to_string(),
397                bounds: PhysicalRect::new(
398                    0,
399                    0,
400                    DEFAULT_MONITOR_SIZE.width,
401                    DEFAULT_MONITOR_SIZE.height,
402                ),
403                work_area: PhysicalRect::new(
404                    0,
405                    0,
406                    DEFAULT_MONITOR_SIZE.width,
407                    DEFAULT_MONITOR_SIZE.height,
408                ),
409                scale_factor: 1.0,
410                refresh_millihertz: DEFAULT_REFRESH_MILLIHERTZ,
411                is_primary: true,
412            }],
413            next_monitor_id: 2,
414            windows: Pool::new(),
415            queue: VecDeque::new(),
416            clock: ManualTime::new(),
417            modifiers: Modifiers::empty(),
418            clipboard: Vec::new(),
419            next_request_id: 1,
420            reads: Vec::new(),
421            clipboard_readable: true,
422            clipboard_stalled: false,
423            clipboard_needs_interaction: false,
424            saw_interaction: false,
425            clipboard_deadline: DEFAULT_CLIPBOARD_DEADLINE,
426            waits: 0,
427            configure_delay: DEFAULT_CONFIGURE_DELAY,
428            wake: None,
429            frames: FrameRequests::default(),
430        }
431    }
432
433    /// This shell, reporting only `caps`.
434    ///
435    /// The lever that makes the capability system testable; see the [module
436    /// docs](self).
437    #[must_use]
438    pub fn with_caps(mut self, caps: ShellCaps) -> Self {
439        self.caps = caps;
440        self
441    }
442
443    /// This shell, with `monitors` instead of the default one.
444    ///
445    /// # Panics
446    ///
447    /// If `monitors` is empty. A shell with no monitor cannot answer where a
448    /// borderless window goes, and every backend that could report zero
449    /// monitors (a headless X server, a disconnected session) is a case the
450    /// engine treats as "no display".
451    ///
452    /// If two monitors share a [`MonitorId`], or if one uses `u32::MAX`. Ids
453    /// are the key [`Shell::monitor`] looks up and the ones
454    /// [`plug_monitor`](Self::plug_monitor) hands out afterwards, so a
455    /// duplicate makes the lookup ambiguous and `u32::MAX` leaves no id for the
456    /// next hotplug.
457    #[must_use]
458    pub fn with_monitors(mut self, monitors: Vec<MonitorInfo>) -> Self {
459        assert!(!monitors.is_empty(), "a shell needs at least one monitor");
460        for (index, monitor) in monitors.iter().enumerate() {
461            assert!(
462                monitor.id.0 != u32::MAX,
463                "monitor id {} leaves no id for the next hotplug",
464                monitor.id.0
465            );
466            assert!(
467                !monitors[..index].iter().any(|seen| seen.id == monitor.id),
468                "monitor id {} appears twice",
469                monitor.id.0
470            );
471        }
472        self.next_monitor_id = monitors
473            .iter()
474            .map(|monitor| monitor.id.0 + 1)
475            .max()
476            .unwrap_or(1);
477        self.monitors = monitors;
478        self
479    }
480
481    /// This shell, with the primary monitor at `scale_factor`.
482    ///
483    /// Convenience for the common DPI test, which otherwise has to build a
484    /// whole [`MonitorInfo`].
485    #[must_use]
486    pub fn with_scale_factor(mut self, scale_factor: f64) -> Self {
487        assert!(scale_factor > 0.0, "scale factor must be positive");
488        self.monitors[0].scale_factor = scale_factor;
489        self
490    }
491
492    /// This shell, delivering the first configure after `pumps` calls to
493    /// [`pump`](Shell::pump).
494    ///
495    /// See [`set_configure_delay`](Self::set_configure_delay).
496    #[must_use]
497    pub const fn with_configure_delay(mut self, pumps: u32) -> Self {
498        self.configure_delay = pumps;
499        self
500    }
501
502    /// How many pumps a configure waits before it is delivered.
503    ///
504    /// Defaults to [`DEFAULT_CONFIGURE_DELAY`]. Raise it to prove that a
505    /// consumer's create-then-wait loop is a real loop rather than an
506    /// off-by-one that happens to work; drop it to `0` when a test is about
507    /// something else entirely and wants a window configured on the first
508    /// pump.
509    ///
510    /// Applies to configures scheduled *after* this call.
511    pub const fn set_configure_delay(&mut self, pumps: u32) {
512        self.configure_delay = pumps;
513    }
514
515    /// Replaces the reported capabilities mid-session.
516    ///
517    /// Real backends must not do this — [`Shell::caps`] is latched — but a test
518    /// building one shell per capability set is noise. Setting it before the
519    /// first [`create_window`](Shell::create_window) is the intended use.
520    pub fn set_caps(&mut self, caps: ShellCaps) {
521        self.caps = caps;
522    }
523
524    /// The shell's current time, which every injected event is stamped with.
525    #[must_use]
526    pub fn now(&self) -> EventTime {
527        EventTime::from_duration(self.clock.elapsed())
528    }
529
530    /// Moves the shell's clock forward.
531    ///
532    /// Injected events are stamped with the time *after* the advance, so
533    /// `advance(200ms)` between a press and a release produces a 200 ms hold —
534    /// which is how the P2 pattern evaluator's tap/hold table gets tested.
535    pub fn advance(&mut self, delta: Duration) {
536        self.clock.advance(delta);
537    }
538
539    /// Slaves the shell's clock to an absolute timestamp.
540    ///
541    /// Pass `time.elapsed()` from the test's own
542    /// [`ManualTime`] each frame and the shell's
543    /// event timestamps land on the same timeline as the
544    /// [`FrameClock`](crcbl_core::FrameClock)'s, which is what
545    /// [`EventTime`]'s epoch contract requires of a real backend.
546    pub fn set_time(&mut self, now: Duration) {
547        self.clock.set(now);
548    }
549
550    /// The modifier state stamped onto subsequent injected events.
551    ///
552    /// Sticky, like a real backend's tracked state — see [`event`](crate::event)
553    /// for why modifiers ride on each event rather than arriving separately.
554    pub fn set_modifiers(&mut self, modifiers: Modifiers) {
555        self.modifiers = modifiers;
556    }
557
558    /// How many events are queued and not yet pumped.
559    #[must_use]
560    pub fn pending_events(&self) -> usize {
561        self.queue.len()
562    }
563
564    /// How many times [`Shell::wait_events`] has been called.
565    ///
566    /// A test for an editor-style idle loop asserts that it actually waits
567    /// instead of spinning, and there is nothing else observable about a
568    /// headless wait.
569    #[must_use]
570    pub const fn wait_count(&self) -> u32 {
571        self.waits
572    }
573
574    /// Queues an event verbatim, with no state update and no validation.
575    ///
576    /// The escape hatch: use it to inject sequences a well-behaved compositor
577    /// would not produce — a resize that violates the constraints, events for a
578    /// destroyed window, a scale change with no resize — because those are
579    /// exactly the cases a consumer is most likely to get wrong. The typed
580    /// helpers below keep the shell's state consistent; this one does not.
581    pub fn inject(&mut self, event: ShellEvent) {
582        self.queue.push_back(event);
583    }
584
585    /// Delivers a configure at `size` immediately, as a compositor answering a
586    /// resize would.
587    ///
588    /// This is the *compositor* speaking, not the client asking, so it takes
589    /// effect at once and supersedes any pending configure. It is therefore
590    /// also the one-line way to get a configured window in a test that is about
591    /// something other than the configure handshake.
592    ///
593    /// The window's constraints are applied, because a compositor that honours
594    /// hints would have applied them. Use [`inject`](Self::inject) to feed a
595    /// size that violates them, which is what a tiling window manager does.
596    ///
597    /// # Errors
598    ///
599    /// [`ShellError::InvalidWindow`] if the handle is stale.
600    pub fn resize(&mut self, window: WindowId, size: PhysicalSize) -> Result<(), ShellError> {
601        let state = self.window_mut(window)?;
602        let scale_factor = state.scale_factor();
603        let mode = state.settled_mode();
604        let constrained = state.requested_constraints.apply(size, scale_factor);
605        state.pending = None;
606        state.configuration = Some(WindowConfiguration {
607            size: constrained,
608            scale_factor,
609            mode,
610        });
611        self.queue.push_back(ShellEvent::Resized {
612            window,
613            size: constrained,
614            scale_factor,
615        });
616        Ok(())
617    }
618
619    /// Changes a window's scale factor, as dragging it to another monitor
620    /// would.
621    ///
622    /// The window's *logical* size is preserved and the physical size
623    /// recomputed, which is what both Wayland (`wl_surface.set_buffer_scale`
624    /// against an unchanged window geometry) and Win32 (`WM_DPICHANGED`'s
625    /// suggested rectangle) do. Queues a
626    /// [`ScaleFactorChanged`](ShellEvent::ScaleFactorChanged) carrying both
627    /// facts, so a consumer cannot hold a mismatched size/scale pair, followed
628    /// by the [`Resized`](ShellEvent::Resized) it implies — which is the pair
629    /// both Linux backends emit for a scale change, and the pair
630    /// `deliver_due_configures` emits for a
631    /// scale change that arrives with a configure.
632    ///
633    /// # Errors
634    ///
635    /// [`ShellError::InvalidWindow`] if the handle is stale.
636    ///
637    /// # Panics
638    ///
639    /// If `scale_factor` is not positive.
640    pub fn change_scale_factor(
641        &mut self,
642        window: WindowId,
643        scale_factor: f64,
644    ) -> Result<(), ShellError> {
645        assert!(scale_factor > 0.0, "scale factor must be positive");
646        let state = self.window_mut(window)?;
647        let logical = state.base_size().to_logical(state.scale_factor());
648        let mode = state.settled_mode();
649        let size = state
650            .requested_constraints
651            .apply(logical.to_physical(scale_factor), scale_factor);
652        state.pending = None;
653        state.configuration = Some(WindowConfiguration {
654            size,
655            scale_factor,
656            mode,
657        });
658        self.queue.push_back(ShellEvent::ScaleFactorChanged {
659            window,
660            scale_factor,
661            size,
662        });
663        self.queue.push_back(ShellEvent::Resized {
664            window,
665            size,
666            scale_factor,
667        });
668        Ok(())
669    }
670
671    /// Gives or takes keyboard focus.
672    ///
673    /// # Errors
674    ///
675    /// [`ShellError::InvalidWindow`] if the handle is stale.
676    pub fn set_focus(&mut self, window: WindowId, focused: bool) -> Result<(), ShellError> {
677        self.window_mut(window)?.focused = focused;
678        self.queue.push_back(ShellEvent::Focus { window, focused });
679        Ok(())
680    }
681
682    /// Asks the application to close the window, as a title-bar button would.
683    ///
684    /// The window stays open until
685    /// [`reply_close_request`](Shell::reply_close_request).
686    ///
687    /// # Errors
688    ///
689    /// [`ShellError::InvalidWindow`] if the handle is stale.
690    pub fn request_close(&mut self, window: WindowId) -> Result<(), ShellError> {
691        self.window_mut(window)?.close_pending = true;
692        self.queue.push_back(ShellEvent::CloseRequested { window });
693        Ok(())
694    }
695
696    /// Injects a key press or release.
697    ///
698    /// The scancode and keysym are synthesized deterministically from `key`:
699    /// the scancode is the key's index in [`KeyCode::ALL`] offset by 8 (evdev's
700    /// convention, so the numbers look plausible in a log), and the keysym is
701    /// the US-layout symbol where there is one. Neither is a claim about any
702    /// real keyboard; they exist so a consumer reading those fields is
703    /// exercised rather than reading zero.
704    ///
705    /// # Errors
706    ///
707    /// [`ShellError::InvalidWindow`] if the handle is stale.
708    pub fn key(
709        &mut self,
710        window: WindowId,
711        key: KeyCode,
712        state: ButtonState,
713    ) -> Result<(), ShellError> {
714        self.key_with(window, key, state, false)
715    }
716
717    /// Injects a key press.
718    ///
719    /// # Errors
720    ///
721    /// [`ShellError::InvalidWindow`] if the handle is stale.
722    pub fn key_press(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
723        self.key(window, key, ButtonState::Pressed)
724    }
725
726    /// Injects a key release.
727    ///
728    /// # Errors
729    ///
730    /// [`ShellError::InvalidWindow`] if the handle is stale.
731    pub fn key_release(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
732        self.key(window, key, ButtonState::Released)
733    }
734
735    /// Injects an auto-repeat press, the kind a text field wants and a jump
736    /// button must ignore.
737    ///
738    /// # Errors
739    ///
740    /// [`ShellError::InvalidWindow`] if the handle is stale.
741    pub fn key_repeat(&mut self, window: WindowId, key: KeyCode) -> Result<(), ShellError> {
742        self.key_with(window, key, ButtonState::Pressed, true)
743    }
744
745    fn key_with(
746        &mut self,
747        window: WindowId,
748        key: KeyCode,
749        state: ButtonState,
750        repeat: bool,
751    ) -> Result<(), ShellError> {
752        let popup_input = self.window(window)?.popup_input;
753        self.saw_interaction = true;
754        let event = ShellEvent::Key {
755            window,
756            device: VIRTUAL_DEVICE,
757            time: self.now(),
758            scancode: synthetic_scancode(key),
759            key_code: Some(key),
760            keysym: synthetic_keysym(key),
761            state,
762            repeat,
763            modifiers: self.modifiers,
764        };
765        let dismissed = crate::role::escape_dismisses(popup_input, &event);
766        self.queue.push_back(event);
767        // As the real backends do: one request until it is answered.
768        if dismissed && !self.window(window)?.close_pending {
769            self.request_close(window)?;
770        }
771        Ok(())
772    }
773
774    /// Moves the pointer to an absolute position, reporting the implied
775    /// relative delta as well.
776    ///
777    /// Under [`PointerMode::Locked`] the absolute position is dropped, matching
778    /// what the mode documents — which is what makes a camera that wrongly
779    /// reads `abs` fail here rather than in a playtest.
780    ///
781    /// # Errors
782    ///
783    /// [`ShellError::InvalidWindow`] if the handle is stale.
784    pub fn move_pointer(
785        &mut self,
786        window: WindowId,
787        position: PhysicalPoint,
788        raw_delta: (f64, f64),
789    ) -> Result<(), ShellError> {
790        let locked = self.window(window)?.pointer_mode == PointerMode::Locked;
791        let raw = if self.caps.contains(ShellCaps::RAW_POINTER_MOTION) {
792            Some(raw_delta)
793        } else {
794            None
795        };
796        self.saw_interaction = true;
797        let event = ShellEvent::PointerMotion {
798            window,
799            device: VIRTUAL_DEVICE,
800            time: self.now(),
801            abs: if locked { None } else { Some(position) },
802            raw_delta: raw,
803        };
804        self.queue.push_back(event);
805        Ok(())
806    }
807
808    /// Moves the pointer into or out of the window.
809    ///
810    /// # Errors
811    ///
812    /// [`ShellError::InvalidWindow`] if the handle is stale.
813    pub fn set_pointer_focus(
814        &mut self,
815        window: WindowId,
816        entered: bool,
817        position: Option<PhysicalPoint>,
818    ) -> Result<(), ShellError> {
819        self.check_window(window)?;
820        let event = ShellEvent::PointerFocus {
821            window,
822            device: VIRTUAL_DEVICE,
823            time: self.now(),
824            entered,
825            position: if entered { position } else { None },
826        };
827        self.queue.push_back(event);
828        Ok(())
829    }
830
831    /// Injects a pointer button press or release.
832    ///
833    /// # Errors
834    ///
835    /// [`ShellError::InvalidWindow`] if the handle is stale.
836    pub fn button(
837        &mut self,
838        window: WindowId,
839        button: PointerButton,
840        state: ButtonState,
841        position: Option<PhysicalPoint>,
842    ) -> Result<(), ShellError> {
843        self.check_window(window)?;
844        self.saw_interaction = true;
845        let event = ShellEvent::Button {
846            window,
847            device: VIRTUAL_DEVICE,
848            time: self.now(),
849            button,
850            state,
851            position,
852            modifiers: self.modifiers,
853        };
854        self.queue.push_back(event);
855        Ok(())
856    }
857
858    /// Injects a scroll.
859    ///
860    /// # Errors
861    ///
862    /// [`ShellError::InvalidWindow`] if the handle is stale.
863    pub fn scroll(
864        &mut self,
865        window: WindowId,
866        delta: ScrollDelta,
867        position: Option<PhysicalPoint>,
868    ) -> Result<(), ShellError> {
869        self.check_window(window)?;
870        self.saw_interaction = true;
871        let event = ShellEvent::Wheel {
872            window,
873            device: VIRTUAL_DEVICE,
874            time: self.now(),
875            delta,
876            position,
877            modifiers: self.modifiers,
878        };
879        self.queue.push_back(event);
880        Ok(())
881    }
882
883    /// Injects one contact's landing, move, lift or cancellation.
884    ///
885    /// **One call is one contact**, so a scripted two-finger gesture is two
886    /// interleaved sequences of these — which is the point, and is why there is
887    /// no "set the whole contact list" call that would let a script express a
888    /// state a real touchscreen never reports.
889    ///
890    /// Nothing here checks that a [`Moved`](TouchPhase::Moved) follows a
891    /// [`Began`](TouchPhase::Began), or that an id is not reused while it is
892    /// still down: a test double that refused to produce a malformed stream
893    /// could not be used to check what a consumer does with one.
894    ///
895    /// The emulated pointer events a real touch backend owes for its primary
896    /// contact are **not** synthesized here either — [`move_pointer`] and
897    /// [`button`] are how a script states those, and a test that wants the two
898    /// halves together says so rather than getting a second event it did not
899    /// ask for.
900    ///
901    /// [`move_pointer`]: Self::move_pointer
902    /// [`button`]: Self::button
903    ///
904    /// # Errors
905    ///
906    /// [`ShellError::InvalidWindow`] if the handle is stale, or
907    /// [`ShellError::Unsupported`] without [`ShellCaps::TOUCH`] — a shell
908    /// configured as a touchless backend must not be able to script a finger,
909    /// or a test would prove something about a platform that cannot happen.
910    pub fn touch(
911        &mut self,
912        window: WindowId,
913        contact: ContactId,
914        phase: TouchPhase,
915        position: PhysicalPoint,
916    ) -> Result<(), ShellError> {
917        self.check_window(window)?;
918        if !self.caps.contains(ShellCaps::TOUCH) {
919            return Err(self.unsupported("touch"));
920        }
921        self.saw_interaction = true;
922        let event = ShellEvent::Touch {
923            window,
924            device: VIRTUAL_DEVICE,
925            time: self.now(),
926            contact,
927            phase,
928            position,
929        };
930        self.queue.push_back(event);
931        Ok(())
932    }
933
934    /// Injects committed text, as an input method would.
935    ///
936    /// # Errors
937    ///
938    /// [`ShellError::InvalidWindow`] if the handle is stale.
939    ///
940    /// # Panics
941    ///
942    /// If `text` is empty — [`ShellEvent::TextCommit`] documents that it never
943    /// is, and a shell that could produce one would make that a lie.
944    pub fn commit_text(&mut self, window: WindowId, text: &str) -> Result<(), ShellError> {
945        assert!(!text.is_empty(), "a text commit is never empty");
946        self.check_window(window)?;
947        let event = ShellEvent::TextCommit {
948            window,
949            time: self.now(),
950            text: text.to_string(),
951        };
952        self.queue.push_back(event);
953        Ok(())
954    }
955
956    /// Drops a file on the window.
957    ///
958    /// # Errors
959    ///
960    /// [`ShellError::InvalidWindow`] if the handle is stale, or
961    /// [`ShellError::Unsupported`] if the window did not opt into drops or the
962    /// shell lacks [`ShellCaps::DRAG_DROP`].
963    pub fn drop_file(
964        &mut self,
965        window: WindowId,
966        path: impl Into<PathBuf>,
967        position: Option<PhysicalPoint>,
968    ) -> Result<(), ShellError> {
969        if !self.caps.contains(ShellCaps::DRAG_DROP) {
970            return Err(self.unsupported("drag and drop"));
971        }
972        if !self.window(window)?.accept_drops {
973            return Err(ShellError::Unsupported {
974                backend: ShellBackend::Headless,
975                what: "drops on a window created without accept_drops",
976            });
977        }
978        let event = ShellEvent::DroppedFile {
979            window,
980            time: self.now(),
981            path: path.into(),
982            position,
983        };
984        self.queue.push_back(event);
985        Ok(())
986    }
987
988    /// Adds a monitor and queues a
989    /// [`MonitorsChanged`](ShellEvent::MonitorsChanged).
990    ///
991    /// The id is assigned by the shell and never reuses one that has been
992    /// handed out, which is the obligation
993    /// [`monitor`](crate::monitor) places on every backend.
994    pub fn plug_monitor(&mut self, mut info: MonitorInfo) -> MonitorId {
995        let id = MonitorId(self.next_monitor_id);
996        self.next_monitor_id += 1;
997        info.id = id;
998        self.monitors.push(info);
999        self.queue.push_back(ShellEvent::MonitorsChanged);
1000        id
1001    }
1002
1003    /// Removes a monitor: queues a
1004    /// [`MonitorRemoved`](ShellEvent::MonitorRemoved), a
1005    /// [`CloseRequested`](ShellEvent::CloseRequested) for every panel created
1006    /// for it that has none outstanding, and a
1007    /// [`MonitorsChanged`](ShellEvent::MonitorsChanged).
1008    ///
1009    /// # Errors
1010    ///
1011    /// [`ShellError::NoSuchMonitor`] if it is not connected.
1012    ///
1013    /// # Panics
1014    ///
1015    /// If it is the last monitor; see [`with_monitors`](Self::with_monitors).
1016    pub fn unplug_monitor(&mut self, id: MonitorId) -> Result<(), ShellError> {
1017        let index = self
1018            .monitors
1019            .iter()
1020            .position(|monitor| monitor.id == id)
1021            .ok_or(ShellError::NoSuchMonitor(id.0))?;
1022        assert!(
1023            self.monitors.len() > 1,
1024            "a shell needs at least one monitor"
1025        );
1026        let previous = self.monitors.clone();
1027        self.monitors.remove(index);
1028        crate::monitor::queue_removed(&previous, &self.monitors, &mut self.queue);
1029        let panels: Vec<WindowId> = self
1030            .windows
1031            .iter()
1032            .filter(|(_, state)| {
1033                state.panel.as_ref().and_then(|panel| panel.desc().monitor) == Some(id)
1034                    && !state.close_pending
1035            })
1036            .map(|(window, _)| window.cast())
1037            .collect();
1038        for panel in panels {
1039            self.request_close(panel)?;
1040        }
1041        self.queue.push_back(ShellEvent::MonitorsChanged);
1042        Ok(())
1043    }
1044
1045    /// The window's title, which no [`Shell`] method exposes.
1046    ///
1047    /// Titles are write-only across the seam — no backend can read one back
1048    /// reliably, since a window manager may decorate or truncate it — so this
1049    /// is a headless-only accessor for tests that assert
1050    /// [`set_title`](Shell::set_title) did something.
1051    ///
1052    /// # Errors
1053    ///
1054    /// [`ShellError::InvalidWindow`] if the handle is stale.
1055    pub fn title(&self, window: WindowId) -> Result<&str, ShellError> {
1056        Ok(self.window(window)?.title.as_str())
1057    }
1058
1059    /// The window's current cursor, or `None` if it is hidden.
1060    ///
1061    /// Headless-only, for the same reason as [`title`](Self::title).
1062    ///
1063    /// # Errors
1064    ///
1065    /// [`ShellError::InvalidWindow`] if the handle is stale.
1066    pub fn cursor(&self, window: WindowId) -> Result<Option<CursorIcon>, ShellError> {
1067        Ok(self.window(window)?.cursor)
1068    }
1069
1070    /// The text field the window last said is taking typing, with
1071    /// [`Shell::set_text_input`]; `None` while none is.
1072    ///
1073    /// Headless-only, for the same reason as [`title`](Self::title).
1074    ///
1075    /// # Errors
1076    ///
1077    /// [`ShellError::InvalidWindow`] if the handle is stale.
1078    pub fn text_input(&self, window: WindowId) -> Result<Option<TextPurpose>, ShellError> {
1079        Ok(self.window(window)?.text_input)
1080    }
1081
1082    /// What the panel was created with, or last given by
1083    /// [`Shell::set_panel`]; `None` for a window that is not a panel.
1084    ///
1085    /// Headless-only, for the same reason as [`title`](Self::title).
1086    ///
1087    /// # Errors
1088    ///
1089    /// [`ShellError::InvalidWindow`] if the handle is stale.
1090    pub fn panel(&self, window: WindowId) -> Result<Option<crate::PanelDesc<'_>>, ShellError> {
1091        Ok(self
1092            .window(window)?
1093            .panel
1094            .as_ref()
1095            .map(crate::role::KeptPanel::desc))
1096    }
1097
1098    /// The rectangles the window last limited its pointer input to, with
1099    /// [`Shell::set_input_region`]; `None` while it takes the pointer
1100    /// everywhere.
1101    ///
1102    /// Headless-only, for the same reason as [`title`](Self::title). Kept as
1103    /// given, empty rectangles included, so a test reads back what it sent.
1104    ///
1105    /// # Errors
1106    ///
1107    /// [`ShellError::InvalidWindow`] if the handle is stale.
1108    pub fn input_region(&self, window: WindowId) -> Result<Option<&[PhysicalRect]>, ShellError> {
1109        Ok(self.window(window)?.input_region.as_deref())
1110    }
1111
1112    /// Whether the window has asked for a frame, with
1113    /// [`Shell::request_frame`], that the next pump has yet to answer.
1114    ///
1115    /// Headless-only, for the same reason as [`title`](Self::title): a test
1116    /// of a loop that paces itself reads whether the loop asked.
1117    ///
1118    /// # Errors
1119    ///
1120    /// [`ShellError::InvalidWindow`] if the handle is stale.
1121    pub fn frame_requested(&self, window: WindowId) -> Result<bool, ShellError> {
1122        self.window(window)?;
1123        Ok(self.frames.contains(window))
1124    }
1125
1126    /// Starts a pretend assistive technology reading `window`, raising
1127    /// [`ShellEvent::AccessibilityActivated`]. Does nothing if one already is.
1128    ///
1129    /// Headless-only.
1130    ///
1131    /// # Errors
1132    ///
1133    /// [`ShellError::InvalidWindow`] if the handle is stale.
1134    #[cfg(feature = "accesskit")]
1135    pub fn activate_accessibility(&mut self, window: WindowId) -> Result<(), ShellError> {
1136        let state = &mut self.window_mut(window)?.accessibility;
1137        if state.is_none() {
1138            *state = Some(Vec::new());
1139            self.queue
1140                .push_back(ShellEvent::AccessibilityActivated { window });
1141        }
1142        Ok(())
1143    }
1144
1145    /// Stops the pretend assistive technology, raising
1146    /// [`ShellEvent::AccessibilityDeactivated`] and forgetting what it was
1147    /// sent. Does nothing if none was reading.
1148    ///
1149    /// Headless-only.
1150    ///
1151    /// # Errors
1152    ///
1153    /// [`ShellError::InvalidWindow`] if the handle is stale.
1154    #[cfg(feature = "accesskit")]
1155    pub fn deactivate_accessibility(&mut self, window: WindowId) -> Result<(), ShellError> {
1156        if self.window_mut(window)?.accessibility.take().is_some() {
1157            self.queue
1158                .push_back(ShellEvent::AccessibilityDeactivated { window });
1159        }
1160        Ok(())
1161    }
1162
1163    /// Has the pretend assistive technology ask for `request`, raising
1164    /// [`ShellEvent::AccessibilityAction`].
1165    ///
1166    /// Headless-only.
1167    ///
1168    /// # Errors
1169    ///
1170    /// [`ShellError::InvalidWindow`] if the handle is stale.
1171    ///
1172    /// # Panics
1173    ///
1174    /// If nothing is reading `window`: an assistive technology that never
1175    /// started has no tree to act on.
1176    #[cfg(feature = "accesskit")]
1177    pub fn request_accessibility_action(
1178        &mut self,
1179        window: WindowId,
1180        request: accesskit::ActionRequest,
1181    ) -> Result<(), ShellError> {
1182        assert!(
1183            self.window(window)?.accessibility.is_some(),
1184            "no assistive technology is reading the window"
1185        );
1186        self.queue
1187            .push_back(ShellEvent::AccessibilityAction { window, request });
1188        Ok(())
1189    }
1190
1191    /// The tree updates the pretend assistive technology has been sent since
1192    /// it started, oldest first; `None` while none is reading.
1193    ///
1194    /// Headless-only.
1195    ///
1196    /// # Errors
1197    ///
1198    /// [`ShellError::InvalidWindow`] if the handle is stale.
1199    #[cfg(feature = "accesskit")]
1200    pub fn accessibility_updates(
1201        &self,
1202        window: WindowId,
1203    ) -> Result<Option<&[accesskit::TreeUpdate]>, ShellError> {
1204        Ok(self.window(window)?.accessibility.as_deref())
1205    }
1206
1207    /// The bytes currently offered on the in-process clipboard for `mime`.
1208    ///
1209    /// Headless-only.
1210    #[must_use]
1211    pub fn clipboard_bytes(&self, mime: MimeType) -> Option<&[u8]> {
1212        self.clipboard
1213            .iter()
1214            .find(|(offered, _)| offered.matches(mime))
1215            .map(|(_, bytes)| bytes.as_slice())
1216    }
1217
1218    /// Whether the clipboard can be read at all right now.
1219    ///
1220    /// **The focus gate, modelled.** On Wayland a client learns what is on the
1221    /// clipboard from `wl_data_device.selection`, which arrives only while one
1222    /// of its windows has keyboard focus — so a background window cannot read
1223    /// the clipboard, and a read issued a moment before focus arrives has
1224    /// nothing to answer from. `HeadlessShell` is readable by default, because
1225    /// most platforms have no such gate; set this to `false` to make a test
1226    /// walk the path a Wayland consumer will walk.
1227    ///
1228    /// A read issued while this is `false` is **held**, not answered: it is
1229    /// answered as soon as this becomes `true` again, or reported
1230    /// [`ClipboardContent::Unavailable`] once
1231    /// [`clipboard_deadline`](Self::clipboard_deadline) pumps have gone by.
1232    /// That is the seam's obligation 5, and this is how a consumer is tested
1233    /// against it.
1234    pub fn set_clipboard_readable(&mut self, readable: bool) {
1235        self.clipboard_readable = readable;
1236    }
1237
1238    /// Whether a peer holds the clipboard and never hands the data over.
1239    ///
1240    /// The other way a read can fail to complete, and the one that has no
1241    /// protocol event behind it: on Wayland the transfer is a pipe another
1242    /// application writes, and an application that accepts the descriptor and
1243    /// then hangs produces exactly this. The read is held and then reported
1244    /// [`ClipboardContent::Unavailable`] — never `Empty`, because there really
1245    /// is something on the clipboard.
1246    pub fn set_clipboard_stalled(&mut self, stalled: bool) {
1247        self.clipboard_stalled = stalled;
1248    }
1249
1250    /// Whether claiming the clipboard requires a recent user interaction.
1251    ///
1252    /// **The serial requirement, modelled.** Off by default, because X11 and
1253    /// Win32 have no such rule; on, [`clipboard_offer`](Shell::clipboard_offer)
1254    /// fails with [`ShellError::NeedsUserInteraction`] until an input event has
1255    /// been injected — a key, a pointer button, a scroll or a motion. That is
1256    /// what Wayland's `set_selection` serial and the browser's user-gesture gate
1257    /// both amount to, and it is a *feature*: it is what stops a background
1258    /// process taking the clipboard.
1259    ///
1260    /// ```
1261    /// use crcbl_shell::{ClipboardOffer, HeadlessShell, KeyCode, Shell, ShellError, WindowDesc};
1262    ///
1263    /// let mut shell = HeadlessShell::new();
1264    /// let window = shell.create_window(&WindowDesc::default())?;
1265    /// shell.require_user_interaction(true);
1266    ///
1267    /// // Nobody has touched anything, so the clipboard is not ours to take.
1268    /// let refused = shell.clipboard_offer(window, &[ClipboardOffer::text("x")]);
1269    /// assert!(matches!(refused, Err(ShellError::NeedsUserInteraction { .. })));
1270    ///
1271    /// // The copy the user actually asked for goes through.
1272    /// shell.key_press(window, KeyCode::KeyC)?;
1273    /// shell.clipboard_offer(window, &[ClipboardOffer::text("x")])?;
1274    /// # Ok::<(), ShellError>(())
1275    /// ```
1276    pub fn require_user_interaction(&mut self, required: bool) {
1277        self.clipboard_needs_interaction = required;
1278    }
1279
1280    /// Forgets that any input has been injected.
1281    ///
1282    /// For a test that wants a *second* refusal after a successful copy — a
1283    /// real serial goes stale the same way.
1284    pub fn clear_user_interaction(&mut self) {
1285        self.saw_interaction = false;
1286    }
1287
1288    /// How many pumps a held read waits before it reports
1289    /// [`ClipboardContent::Unavailable`].
1290    ///
1291    /// Defaults to [`DEFAULT_CLIPBOARD_DEADLINE`]. Zero means the very next
1292    /// pump gives up, which is the shortest way to write the timeout case.
1293    pub fn clipboard_deadline(&mut self, pumps: u32) {
1294        self.clipboard_deadline = pumps;
1295    }
1296
1297    /// How many reads have been accepted and not yet answered.
1298    ///
1299    /// Headless-only, so a test can assert that a request really is being
1300    /// *held* rather than having been answered and missed.
1301    #[must_use]
1302    pub fn held_clipboard_reads(&self) -> usize {
1303        self.reads.len()
1304    }
1305
1306    /// Replaces the clipboard with content spelled the way *another*
1307    /// application would spell it.
1308    ///
1309    /// [`clipboard_offer`](Shell::clipboard_offer) can only publish the mimes
1310    /// this engine names, so it cannot produce the case that matters: a paste
1311    /// from a foreign application whose mime string is neither `'static` nor
1312    /// one of ours. This can.
1313    pub fn set_foreign_clipboard(&mut self, offers: Vec<(ReceivedMime, Vec<u8>)>) {
1314        self.clipboard = offers;
1315    }
1316
1317    /// Whether a read issued now could be answered from what this shell knows.
1318    ///
1319    /// Two ways to say no, and they model different real things: the clipboard
1320    /// is not readable at all (Wayland's focus gate), or a peer owns it and
1321    /// never delivers (a hung application on the other end of the pipe).
1322    fn answerable(&self) -> bool {
1323        self.clipboard_readable && !self.clipboard_stalled
1324    }
1325
1326    /// The answer to one read, from the current clipboard.
1327    fn answer(&self, window: WindowId, request: ClipboardRequestId, mime: MimeType) -> ShellEvent {
1328        // Match on format, not on spelling: a peer offering `text/plain`
1329        // satisfies a request for `text/plain;charset=utf-8`. The answer
1330        // reports the peer's spelling, because that is what a real backend has
1331        // in hand and what an editor may need to echo back.
1332        let found = self
1333            .clipboard
1334            .iter()
1335            .find(|(offered, _)| offered.matches(mime));
1336        let (mime, content) = match found {
1337            Some((offered, bytes)) => (offered.clone(), ClipboardContent::Bytes(bytes.clone())),
1338            // Nothing on the clipboard, or nothing in this format — one case,
1339            // and emphatically not the same as "could not read it".
1340            None => (ReceivedMime::from(mime), ClipboardContent::Empty),
1341        };
1342        ShellEvent::ClipboardData {
1343            window,
1344            request,
1345            mime,
1346            content,
1347        }
1348    }
1349
1350    /// Answers every held read that has become answerable, and gives up on the
1351    /// ones that have run out of pumps.
1352    ///
1353    /// Runs at the top of [`pump`](Shell::pump), beside the configure delivery,
1354    /// because a read is the same kind of thing: something the window system
1355    /// will get to, not something that has already happened.
1356    fn resolve_reads(&mut self) {
1357        if self.reads.is_empty() {
1358            return;
1359        }
1360        let answerable = self.answerable();
1361        let mut held = Vec::with_capacity(self.reads.len());
1362        for mut read in core::mem::take(&mut self.reads) {
1363            // A window destroyed while its read was outstanding: the answer
1364            // would name a stale handle, which the seam forbids. Obligation 4
1365            // is about *accepted* requests, and this one's window is gone.
1366            if self.window(read.window).is_err() {
1367                continue;
1368            }
1369            if answerable {
1370                let answer = self.answer(read.window, read.request, read.mime);
1371                self.queue.push_back(answer);
1372                continue;
1373            }
1374            if read.countdown > 0 {
1375                read.countdown -= 1;
1376                held.push(read);
1377                continue;
1378            }
1379            // Out of time. Ending the request is the obligation; ending it with
1380            // `Empty` would be the lie this whole shape exists to prevent.
1381            self.queue.push_back(ShellEvent::ClipboardData {
1382                window: read.window,
1383                request: read.request,
1384                mime: ReceivedMime::from(read.mime),
1385                content: ClipboardContent::Unavailable,
1386            });
1387        }
1388        self.reads = held;
1389    }
1390
1391    /// Delivers every configure whose countdown has run out, and ticks the
1392    /// rest.
1393    ///
1394    /// Runs at the top of [`pump`](Shell::pump), so a configure scheduled
1395    /// during frame *n* is visible to the consumer no earlier than frame
1396    /// *n + 1 + delay*.
1397    fn deliver_due_configures(&mut self) {
1398        // Two phases: the window pool is borrowed mutably while deciding, and
1399        // the event queue is a separate field that cannot be touched until that
1400        // borrow ends.
1401        let mut due: Vec<(WindowId, PendingConfigure, Option<f64>)> = Vec::new();
1402        for (handle, state) in self.windows.iter_mut() {
1403            let Some(pending) = state.pending.as_mut() else {
1404                continue;
1405            };
1406            if pending.countdown > 0 {
1407                pending.countdown -= 1;
1408                continue;
1409            }
1410            let pending = state.pending.take().expect("checked just above");
1411            let previous_scale = state.configuration.map(|config| config.scale_factor);
1412            state.configuration = Some(WindowConfiguration {
1413                size: pending.size,
1414                scale_factor: pending.scale_factor,
1415                mode: pending.mode,
1416            });
1417            due.push((handle.cast(), pending, previous_scale));
1418        }
1419
1420        for (window, pending, previous_scale) in due {
1421            // A *change* of scale needs its own event; the first configure does
1422            // not, because `Resized` already carries the scale and there was
1423            // nothing to change from.
1424            if previous_scale.is_some_and(|previous| previous != pending.scale_factor) {
1425                self.queue.push_back(ShellEvent::ScaleFactorChanged {
1426                    window,
1427                    scale_factor: pending.scale_factor,
1428                    size: pending.size,
1429                });
1430            }
1431            self.queue.push_back(ShellEvent::Resized {
1432                window,
1433                size: pending.size,
1434                scale_factor: pending.scale_factor,
1435            });
1436        }
1437    }
1438
1439    fn window(&self, window: WindowId) -> Result<&HeadlessWindow, ShellError> {
1440        self.windows
1441            .get(window.cast())
1442            .ok_or_else(|| ShellError::invalid_window(window))
1443    }
1444
1445    fn window_mut(&mut self, window: WindowId) -> Result<&mut HeadlessWindow, ShellError> {
1446        self.windows
1447            .get_mut(window.cast())
1448            .ok_or_else(|| ShellError::invalid_window(window))
1449    }
1450
1451    fn check_window(&self, window: WindowId) -> Result<(), ShellError> {
1452        self.window(window).map(|_| ())
1453    }
1454
1455    fn unsupported(&self, what: &'static str) -> ShellError {
1456        ShellError::Unsupported {
1457            backend: ShellBackend::Headless,
1458            what,
1459        }
1460    }
1461}
1462
1463/// A stable, plausible-looking scancode for a key.
1464///
1465/// Not a claim about any real keyboard; see [`HeadlessShell::key`].
1466fn synthetic_scancode(key: KeyCode) -> Scancode {
1467    let index = KeyCode::ALL
1468        .iter()
1469        .position(|candidate| *candidate == key)
1470        .unwrap_or(0);
1471    // evdev codes reach the client offset by 8 on both Wayland and X11, so
1472    // starting there keeps the numbers in a range that looks right in a log.
1473    Scancode(index as u32 + 8)
1474}
1475
1476/// The US-layout symbol a key produces, where it produces one.
1477fn synthetic_keysym(key: KeyCode) -> Keysym {
1478    let name = key.as_str();
1479    if let Some(letter) = name.strip_prefix("Key") {
1480        // Unshifted, as XKB reports for a key with no modifiers.
1481        return Keysym::from_char(letter.to_ascii_lowercase().chars().next().unwrap_or('?'));
1482    }
1483    if let Some(digit) = name.strip_prefix("Digit") {
1484        return Keysym::from_char(digit.chars().next().unwrap_or('?'));
1485    }
1486    match key {
1487        KeyCode::Space => Keysym::from_char(' '),
1488        KeyCode::Minus => Keysym::from_char('-'),
1489        KeyCode::Equal => Keysym::from_char('='),
1490        KeyCode::Comma => Keysym::from_char(','),
1491        KeyCode::Period => Keysym::from_char('.'),
1492        KeyCode::Slash => Keysym::from_char('/'),
1493        KeyCode::Semicolon => Keysym::from_char(';'),
1494        KeyCode::Quote => Keysym::from_char('\''),
1495        KeyCode::Backslash => Keysym::from_char('\\'),
1496        KeyCode::BracketLeft => Keysym::from_char('['),
1497        KeyCode::BracketRight => Keysym::from_char(']'),
1498        KeyCode::Backquote => Keysym::from_char('`'),
1499        // Function keys, modifiers and navigation produce no character, and
1500        // report the named keysym every real backend reports for them, so a
1501        // consumer that reads Enter or an arrow from the keysym can be driven.
1502        _ => crate::keysym::named_keysym(key).unwrap_or(Keysym::NONE),
1503    }
1504}
1505
1506impl Shell for HeadlessShell {
1507    fn backend(&self) -> ShellBackend {
1508        ShellBackend::Headless
1509    }
1510
1511    fn caps(&self) -> ShellCaps {
1512        self.caps
1513    }
1514
1515    fn create_window(&mut self, desc: &WindowDesc<'_>) -> Result<WindowId, ShellError> {
1516        crate::role::check(desc, self.caps(), self.backend(), |monitor| {
1517            self.monitor(monitor).is_some()
1518        })?;
1519        if !self.windows.is_empty() && !self.caps.contains(ShellCaps::MULTI_WINDOW) {
1520            return Err(self.unsupported("a second window"));
1521        }
1522        if let DisplayMode::Borderless {
1523            monitor: Some(monitor),
1524        } = desc.mode
1525            && self.monitor(monitor).is_none()
1526        {
1527            return Err(ShellError::NoSuchMonitor(monitor.0));
1528        }
1529
1530        // `desc.accept_drops` without `ShellCaps::DRAG_DROP` is deliberately not
1531        // an error: it is a hint, and on a backend with no drag-and-drop it is
1532        // simply a hint nobody reads — which is what X11 does with it too. The
1533        // request is inert rather than ignored, because
1534        // [`drop_file`](Self::drop_file) refuses without the capability.
1535        //
1536        // The scale factor comes from the monitor the window lands on, not from
1537        // the primary one: a borderless window on a HiDPI secondary monitor is
1538        // exactly the case the windowing rules' DPI matrix is looking
1539        // for, and reporting the primary's scale there produces a swapchain at
1540        // the wrong size. `set_mode` takes the same pair from the same place.
1541        let (size, scale_factor) = match desc.mode {
1542            DisplayMode::Windowed => {
1543                let scale_factor = self.monitors[0].scale_factor;
1544                (
1545                    desc.constraints
1546                        .apply(desc.size.to_physical(scale_factor), scale_factor),
1547                    scale_factor,
1548                )
1549            }
1550            DisplayMode::Borderless { monitor } => {
1551                let monitor = monitor
1552                    .and_then(|id| self.monitor(id))
1553                    .unwrap_or(&self.monitors[0]);
1554                (monitor.size(), monitor.scale_factor)
1555            }
1556        };
1557
1558        let (parent, popup_input, panel) = match desc.role {
1559            crate::WindowRole::Popup(popup) => {
1560                self.window(popup.parent)?;
1561                (Some(popup.parent), Some(popup.input), None)
1562            }
1563            crate::WindowRole::Panel(panel) => {
1564                (None, None, Some(crate::role::KeptPanel::new(&panel)))
1565            }
1566            crate::WindowRole::Toplevel => (None, None, None),
1567        };
1568        let countdown = self.configure_delay;
1569        let window: WindowId = self
1570            .windows
1571            .insert(HeadlessWindow {
1572                title: desc.title.to_string(),
1573                // Unsized until the compositor says otherwise. This is the
1574                // whole point; see the module docs.
1575                configuration: None,
1576                pending: Some(PendingConfigure {
1577                    size,
1578                    scale_factor,
1579                    mode: desc.mode,
1580                    countdown,
1581                }),
1582                requested_mode: desc.mode,
1583                requested_constraints: desc.constraints,
1584                focused: false,
1585                visible: desc.visible,
1586                pointer_mode: PointerMode::Free,
1587                cursor: Some(CursorIcon::Default),
1588                close_pending: false,
1589                accept_drops: desc.accept_drops,
1590                parent,
1591                popup_input,
1592                panel,
1593                text_input: None,
1594                input_region: None,
1595                #[cfg(feature = "accesskit")]
1596                accessibility: None,
1597            })
1598            .cast();
1599        Ok(window)
1600    }
1601
1602    /// Destroys the window and, first, every popup that belongs to it — each
1603    /// with its own [`ShellEvent::WindowDestroyed`] — as every window system
1604    /// destroys a popup with its parent.
1605    fn destroy_window(&mut self, window: WindowId) -> Result<(), ShellError> {
1606        self.window(window)?;
1607        let popups: Vec<WindowId> = self
1608            .windows
1609            .iter()
1610            .filter(|(_, state)| state.parent == Some(window))
1611            .map(|(id, _)| id.cast())
1612            .collect();
1613        for popup in popups {
1614            self.destroy_window(popup)?;
1615        }
1616        self.windows
1617            .remove(window.cast())
1618            .ok_or_else(|| ShellError::invalid_window(window))?;
1619        self.queue.push_back(ShellEvent::WindowDestroyed { window });
1620        Ok(())
1621    }
1622
1623    fn window_state(&self, window: WindowId) -> Result<WindowState, ShellError> {
1624        let state = self.window(window)?;
1625        Ok(WindowState {
1626            configuration: state.configuration,
1627            requested_mode: state.requested_mode,
1628            requested_constraints: state.requested_constraints,
1629            focused: state.focused,
1630            visible: state.visible,
1631            pointer_mode: state.pointer_mode,
1632            close_pending: state.close_pending,
1633        })
1634    }
1635
1636    fn set_title(&mut self, window: WindowId, title: &str) -> Result<(), ShellError> {
1637        self.window_mut(window)?.title = title.to_string();
1638        Ok(())
1639    }
1640
1641    fn set_visible(&mut self, window: WindowId, visible: bool) -> Result<(), ShellError> {
1642        self.window_mut(window)?.visible = visible;
1643        Ok(())
1644    }
1645
1646    fn set_mode(&mut self, window: WindowId, mode: DisplayMode) -> Result<(), ShellError> {
1647        let target = match mode {
1648            DisplayMode::Windowed => None,
1649            DisplayMode::Borderless { monitor } => {
1650                let monitor = match monitor {
1651                    Some(id) => self
1652                        .monitor(id)
1653                        .ok_or(ShellError::NoSuchMonitor(id.0))?
1654                        .clone(),
1655                    None => self.monitors[0].clone(),
1656                };
1657                Some(monitor)
1658            }
1659        };
1660        let countdown = self.configure_delay;
1661
1662        let state = self.window_mut(window)?;
1663        // The request is recorded immediately — we said it, so it is a fact —
1664        // and the *effect* is scheduled. Nothing about the window's reported
1665        // configuration changes until the configure is delivered, so a consumer
1666        // that reads back its own request as fact is wrong here exactly as it
1667        // would be on Wayland.
1668        state.requested_mode = mode;
1669        let (size, scale_factor) = match &target {
1670            Some(monitor) => (monitor.size(), monitor.scale_factor),
1671            // Leaving borderless restores nothing in particular — a real
1672            // compositor restores the pre-fullscreen geometry, and a test that
1673            // cares injects the resize it expects.
1674            None => (state.base_size(), state.scale_factor()),
1675        };
1676        state.pending = Some(PendingConfigure {
1677            size,
1678            scale_factor,
1679            mode,
1680            countdown,
1681        });
1682        Ok(())
1683    }
1684
1685    fn set_constraints(
1686        &mut self,
1687        window: WindowId,
1688        constraints: SizeConstraints,
1689    ) -> Result<(), ShellError> {
1690        let countdown = self.configure_delay;
1691        let state = self.window_mut(window)?;
1692        state.requested_constraints = constraints;
1693
1694        // A configure already on its way is the authoritative statement of what
1695        // is coming, whether or not the window has been configured before, so
1696        // the hints fold into it rather than scheduling a second one. Rebuilding
1697        // it from the *effective* configuration instead would silently discard
1698        // whatever `set_mode` had put there — leaving `requested_mode` set,
1699        // `effective_mode` unchanged and `mode_request_honoured` false forever.
1700        if let Some(pending) = state.pending.as_mut() {
1701            pending.size = constraints.apply(pending.size, pending.scale_factor);
1702            return Ok(());
1703        }
1704
1705        // Nothing in flight: the window system already told us a size, and if
1706        // the new hints disagree with it, a compositor that honours them answers
1707        // with a fresh configure — later, not now.
1708        if let Some(config) = state.configuration {
1709            let constrained = constraints.apply(config.size, config.scale_factor);
1710            if constrained != config.size {
1711                state.pending = Some(PendingConfigure {
1712                    size: constrained,
1713                    scale_factor: config.scale_factor,
1714                    mode: config.mode,
1715                    countdown,
1716                });
1717            }
1718        }
1719        Ok(())
1720    }
1721
1722    /// Takes the new size as a pending configure, like a window system that
1723    /// answers a move with a resize. The anchor places nothing: there is no
1724    /// screen to place against.
1725    fn reposition_popup(
1726        &mut self,
1727        popup: WindowId,
1728        anchor: LogicalRect,
1729        size: LogicalSize,
1730    ) -> Result<(), ShellError> {
1731        let _ = anchor;
1732        let countdown = self.configure_delay;
1733        let state = self.window_mut(popup)?;
1734        if state.parent.is_none() {
1735            return Err(crate::role::not_a_popup(popup));
1736        }
1737        state.configure_at(size, countdown);
1738        Ok(())
1739    }
1740
1741    /// Records the new descriptor, which [`panel`](Self::panel) reads back,
1742    /// and takes the new size as a pending configure, as
1743    /// [`create_window`](Shell::create_window) takes a panel's first.
1744    fn set_panel(
1745        &mut self,
1746        window: WindowId,
1747        size: LogicalSize,
1748        panel: &crate::PanelDesc<'_>,
1749    ) -> Result<(), ShellError> {
1750        let was = self.window(window)?.panel.as_ref().map(|kept| kept.desc());
1751        crate::role::check_set_panel(window, was, panel, self.caps, self.backend(), |monitor| {
1752            self.monitor(monitor).is_some()
1753        })?;
1754        let countdown = self.configure_delay;
1755        let state = self.window_mut(window)?;
1756        state.panel = Some(crate::role::KeptPanel::new(panel));
1757        state.configure_at(size, countdown);
1758        Ok(())
1759    }
1760
1761    fn monitors(&self) -> &[MonitorInfo] {
1762        &self.monitors
1763    }
1764
1765    fn pump(&mut self, sink: &mut dyn FnMut(ShellEvent)) {
1766        self.keep_alive();
1767        let windows = &self.windows;
1768        self.queue.extend(
1769            self.frames
1770                .take_due(|window| windows.contains(window.cast())),
1771        );
1772        // Drain by count rather than `while let`: a sink that injects further
1773        // events (a UI that opens a window on a click) must not be able to spin
1774        // this loop forever, and the events it queued belong to the next frame
1775        // anyway — which is exactly what a real backend's socket read does.
1776        for _ in 0..self.queue.len() {
1777            let Some(event) = self.queue.pop_front() else {
1778                break;
1779            };
1780            sink(event);
1781        }
1782    }
1783
1784    /// Lets the configures and the held reads come due, and keeps what they
1785    /// produce.
1786    ///
1787    /// **This counts as a pump** for both of this shell's pump-counted delays,
1788    /// [`set_configure_delay`](Self::set_configure_delay) and
1789    /// [`clipboard_deadline`](Self::clipboard_deadline): each counts the window
1790    /// system's turns, and delivery is the only part of a turn this leaves out.
1791    fn keep_alive(&mut self) {
1792        self.deliver_due_configures();
1793        self.resolve_reads();
1794    }
1795
1796    /// Sleeps as far as a shell with no window system behind it can.
1797    ///
1798    /// This shell reports [`ShellCaps::EVENT_WAIT`] by default, and that bit
1799    /// says `wait_events` *genuinely blocks* — so it does: with the capability
1800    /// set, nothing already queued and no frame due, this really sleeps for
1801    /// `timeout`, and an editor-style idle loop tested here idles rather than
1802    /// spinning. Without
1803    /// the capability it returns immediately, like every backend that cannot
1804    /// block.
1805    ///
1806    /// A [`waker`](Shell::waker) ends the sleep early, from any thread, and so
1807    /// does a wake rung before the wait began.
1808    ///
1809    /// `None` — "until something happens" — returns immediately while no waker
1810    /// has been handed out, because then the only thing that can make
1811    /// something happen in a headless shell is the caller, on this thread, and
1812    /// blocking forever would be a deadlock by construction rather than a
1813    /// wait. Once a waker exists, another thread can, so `None` sleeps until it
1814    /// rings.
1815    fn wait_events(&mut self, timeout: Option<Duration>) {
1816        self.waits += 1;
1817        if !self.caps.contains(ShellCaps::EVENT_WAIT)
1818            || !self.queue.is_empty()
1819            || self.frames.is_pending()
1820        {
1821            return;
1822        }
1823        match (&self.wake, timeout) {
1824            (Some(wake), timeout) => wake.wait(timeout),
1825            (None, Some(timeout)) => std::thread::sleep(timeout),
1826            (None, None) => {}
1827        }
1828    }
1829
1830    /// The headless doorbell: a condition variable [`wait_events`](Shell::wait_events)
1831    /// sleeps on, rung from any thread.
1832    fn waker(&mut self) -> Option<ShellWaker> {
1833        let wake = self.wake.get_or_insert_with(Arc::default);
1834        Some(ShellWaker::new(Arc::clone(wake) as Arc<dyn WakeSignal>))
1835    }
1836
1837    /// The seam-level spelling of [`set_time`](Self::set_time).
1838    ///
1839    /// Both exist because a test drives this shell through the concrete type
1840    /// and calls `set_time` every frame, while a consumer written against
1841    /// `dyn Shell` can only reach the trait — and the trait method is the one
1842    /// a real backend has to implement, so it must be exercised here too.
1843    fn align_event_clock(&mut self, elapsed: Duration) {
1844        self.set_time(elapsed);
1845    }
1846
1847    fn surface_target(&self, window: WindowId) -> Result<SurfaceTarget, ShellError> {
1848        // Available from creation — the *handle* exists as soon as the window
1849        // does — and constant thereafter. It deliberately carries no size: the
1850        // extent travels through `WindowState::size` and the swapchain
1851        // descriptor, so nothing here goes stale on a resize. The only thing
1852        // this call can fail on is a dead handle.
1853        self.window(window)?;
1854        Ok(SurfaceTarget::Offscreen)
1855    }
1856
1857    fn set_pointer_mode(&mut self, window: WindowId, mode: PointerMode) -> Result<(), ShellError> {
1858        if !self.caps.contains(mode.required_cap()) {
1859            return Err(self.unsupported(mode.as_str()));
1860        }
1861        self.window_mut(window)?.pointer_mode = mode;
1862        Ok(())
1863    }
1864
1865    /// Records the request, which
1866    /// [`text_input`](HeadlessShell::text_input) reads back; there is no
1867    /// input method to tell.
1868    fn set_text_input(
1869        &mut self,
1870        window: WindowId,
1871        field: Option<TextPurpose>,
1872    ) -> Result<(), ShellError> {
1873        self.window_mut(window)?.text_input = field;
1874        Ok(())
1875    }
1876
1877    /// Records the region, which
1878    /// [`input_region`](HeadlessShell::input_region) reads back; there is no
1879    /// pointer to route. A passthrough popup records it too, though no
1880    /// backend acts on it there.
1881    fn set_input_region(
1882        &mut self,
1883        window: WindowId,
1884        region: Option<&[PhysicalRect]>,
1885    ) -> Result<(), ShellError> {
1886        self.window_mut(window)?.input_region = region.map(<[PhysicalRect]>::to_vec);
1887        Ok(())
1888    }
1889
1890    /// Answered at the next pump, as every backend without
1891    /// [`ShellCaps::FRAME_CALLBACK`] answers; there is no compositor to hold
1892    /// it back. [`frame_requested`](HeadlessShell::frame_requested) reads
1893    /// whether one is outstanding.
1894    fn request_frame(&mut self, window: WindowId) -> Result<(), ShellError> {
1895        self.window(window)?;
1896        self.frames.request(window);
1897        Ok(())
1898    }
1899
1900    #[cfg(feature = "accesskit")]
1901    fn update_accessibility(
1902        &mut self,
1903        window: WindowId,
1904        update: accesskit::TreeUpdate,
1905    ) -> Result<(), ShellError> {
1906        if let Some(updates) = &mut self.window_mut(window)?.accessibility {
1907            updates.push(update);
1908        }
1909        Ok(())
1910    }
1911
1912    fn set_cursor(
1913        &mut self,
1914        window: WindowId,
1915        cursor: Option<CursorIcon>,
1916    ) -> Result<(), ShellError> {
1917        self.window_mut(window)?.cursor = cursor;
1918        Ok(())
1919    }
1920
1921    fn warp_pointer(
1922        &mut self,
1923        window: WindowId,
1924        position: PhysicalPoint,
1925    ) -> Result<(), ShellError> {
1926        if !self.caps.contains(ShellCaps::POINTER_WARP) {
1927            return Err(self.unsupported("pointer warp"));
1928        }
1929        self.check_window(window)?;
1930        // A warp produces motion, as it does on every platform that has one —
1931        // which is why code that warps in response to motion oscillates.
1932        let event = ShellEvent::PointerMotion {
1933            window,
1934            device: VIRTUAL_DEVICE,
1935            time: self.now(),
1936            abs: Some(position),
1937            raw_delta: None,
1938        };
1939        self.queue.push_back(event);
1940        Ok(())
1941    }
1942
1943    fn reply_close_request(
1944        &mut self,
1945        window: WindowId,
1946        reply: CloseReply,
1947    ) -> Result<(), ShellError> {
1948        let state = self.window_mut(window)?;
1949        if !state.close_pending {
1950            return Err(ShellError::NoPendingCloseRequest { window });
1951        }
1952        state.close_pending = false;
1953        match reply {
1954            CloseReply::Close => self.destroy_window(window),
1955            CloseReply::Keep => Ok(()),
1956        }
1957    }
1958
1959    /// Claims the in-process clipboard.
1960    ///
1961    /// # Errors
1962    ///
1963    /// [`ShellError::Unsupported`] without [`ShellCaps::CLIPBOARD`],
1964    /// [`ShellError::InvalidWindow`] for a stale handle, or
1965    /// [`ShellError::NeedsUserInteraction`] when
1966    /// [`require_user_interaction`](Self::require_user_interaction) is on and
1967    /// no input has been injected since — the modelled form of Wayland's serial
1968    /// requirement and the browser's user-gesture gate.
1969    fn clipboard_offer(
1970        &mut self,
1971        window: WindowId,
1972        offers: &[ClipboardOffer<'_>],
1973    ) -> Result<(), ShellError> {
1974        if !self.caps.contains(ShellCaps::CLIPBOARD) {
1975            return Err(self.unsupported("clipboard"));
1976        }
1977        self.check_window(window)?;
1978        if self.clipboard_needs_interaction && !self.saw_interaction {
1979            return Err(ShellError::NeedsUserInteraction {
1980                backend: ShellBackend::Headless,
1981                what: "claiming the clipboard",
1982            });
1983        }
1984        self.clipboard = offers
1985            .iter()
1986            .map(|offer| (ReceivedMime::from(offer.mime), offer.bytes.to_vec()))
1987            .collect();
1988        Ok(())
1989    }
1990
1991    /// Accepts a read, answering it now if the clipboard is readable and
1992    /// **holding** it if it is not.
1993    ///
1994    /// The held path is the seam's obligation 5, modelled: with
1995    /// [`set_clipboard_readable(false)`](Self::set_clipboard_readable) the
1996    /// answer waits, exactly as it does on Wayland while another client has
1997    /// focus, and a consumer that assumed an immediate empty answer sees the
1998    /// difference here rather than on someone's desktop.
1999    ///
2000    /// # Errors
2001    ///
2002    /// [`ShellError::Unsupported`] without [`ShellCaps::CLIPBOARD`], or
2003    /// [`ShellError::InvalidWindow`] for a stale handle.
2004    fn clipboard_request(
2005        &mut self,
2006        window: WindowId,
2007        mime: MimeType,
2008    ) -> Result<ClipboardRequestId, ShellError> {
2009        if !self.caps.contains(ShellCaps::CLIPBOARD) {
2010            return Err(self.unsupported("clipboard"));
2011        }
2012        self.check_window(window)?;
2013        let request = ClipboardRequestId(self.next_request_id);
2014        self.next_request_id += 1;
2015        if self.answerable() {
2016            let answer = self.answer(window, request, mime);
2017            // Queued rather than returned: the whole point of the asynchronous
2018            // shape is that a consumer written against it also works on X11,
2019            // where the answer is several round trips away.
2020            self.queue.push_back(answer);
2021        } else {
2022            self.reads.push(PendingRead {
2023                window,
2024                request,
2025                mime,
2026                countdown: self.clipboard_deadline,
2027            });
2028        }
2029        Ok(request)
2030    }
2031
2032    /// Whether a read would be answered without waiting.
2033    ///
2034    /// The default would be "yes, always"; this one can be told otherwise, so
2035    /// that a consumer can be tested against a backend that has a focus gate
2036    /// without there being a compositor in the room.
2037    fn clipboard_readable(&self, window: WindowId) -> bool {
2038        self.caps.contains(ShellCaps::CLIPBOARD) && self.window(window).is_ok() && self.answerable()
2039    }
2040}
2041
2042#[cfg(test)]
2043mod tests {
2044    use super::*;
2045    use crate::{AspectRatio, LogicalSize};
2046
2047    /// A shell plus an *unconfigured* window — the state every window starts in.
2048    fn shell_with_window() -> (HeadlessShell, WindowId) {
2049        let mut shell = HeadlessShell::new();
2050        let window = shell
2051            .create_window(&WindowDesc::default())
2052            .expect("headless window creation never fails");
2053        (shell, window)
2054    }
2055
2056    /// A shell plus a window that has been through the configure handshake.
2057    fn shell_with_configured_window() -> (HeadlessShell, WindowId) {
2058        let (mut shell, window) = shell_with_window();
2059        settle(&mut shell);
2060        assert!(shell.window_state(window).expect("state").is_configured());
2061        (shell, window)
2062    }
2063
2064    /// Pumps until nothing new arrives, discarding events.
2065    fn settle(shell: &mut HeadlessShell) {
2066        for _ in 0..8 {
2067            let mut seen = 0;
2068            shell.pump(&mut |_| seen += 1);
2069            if seen == 0 && shell.windows.iter().all(|(_, w)| w.pending.is_none()) {
2070                return;
2071            }
2072        }
2073        panic!("the shell never settled");
2074    }
2075
2076    fn drain(shell: &mut HeadlessShell) -> Vec<ShellEvent> {
2077        let mut events = Vec::new();
2078        shell.pump(&mut |event| events.push(event));
2079        events
2080    }
2081
2082    fn names(events: &[ShellEvent]) -> Vec<&'static str> {
2083        events.iter().map(ShellEvent::name).collect()
2084    }
2085
2086    /// The shape a P1 swapchain-creating consumer has: it needs an extent, and
2087    /// there is no extent before the first configure.
2088    fn create_swapchain(state: &WindowState) -> Result<PhysicalSize, &'static str> {
2089        let size = state.size().ok_or("window is not configured yet")?;
2090        if size.is_empty() {
2091            return Err("zero-extent swapchain");
2092        }
2093        Ok(size)
2094    }
2095
2096    // ------------------------------------------------------------ accessibility
2097
2098    /// A tree reaches the pretend reader only while it reads, and each start
2099    /// and stop is one event: what a program that sends every frame relies on.
2100    #[cfg(feature = "accesskit")]
2101    #[test]
2102    fn a_tree_reaches_the_reader_only_while_it_reads() {
2103        use accesskit::{Action, ActionRequest, Node, NodeId, Role, TreeId, TreeInfo, TreeUpdate};
2104
2105        let (mut shell, window) = shell_with_configured_window();
2106        let update = |label: &str| TreeUpdate {
2107            nodes: vec![(NodeId(1), {
2108                let mut node = Node::new(Role::Window);
2109                node.set_label(label);
2110                node
2111            })],
2112            tree: Some(TreeInfo::new(NodeId(1))),
2113            tree_id: TreeId::ROOT,
2114            focus: NodeId(1),
2115        };
2116
2117        shell
2118            .update_accessibility(window, update("unread"))
2119            .expect("update");
2120        assert_eq!(shell.accessibility_updates(window).expect("updates"), None);
2121
2122        shell.activate_accessibility(window).expect("activate");
2123        shell
2124            .activate_accessibility(window)
2125            .expect("activate twice");
2126        shell
2127            .update_accessibility(window, update("read"))
2128            .expect("update");
2129        let click = ActionRequest {
2130            action: Action::Click,
2131            target_tree: TreeId::ROOT,
2132            target_node: NodeId(1),
2133            data: None,
2134        };
2135        shell
2136            .request_accessibility_action(window, click.clone())
2137            .expect("action");
2138        assert_eq!(
2139            shell.accessibility_updates(window).expect("updates"),
2140            Some(&[update("read")][..])
2141        );
2142        let events = drain(&mut shell);
2143        assert_eq!(
2144            names(&events),
2145            ["AccessibilityActivated", "AccessibilityAction"]
2146        );
2147        assert_eq!(
2148            events[1],
2149            ShellEvent::AccessibilityAction {
2150                window,
2151                request: click
2152            }
2153        );
2154
2155        shell.deactivate_accessibility(window).expect("deactivate");
2156        shell
2157            .deactivate_accessibility(window)
2158            .expect("deactivate twice");
2159        shell
2160            .update_accessibility(window, update("gone"))
2161            .expect("update");
2162        assert_eq!(shell.accessibility_updates(window).expect("updates"), None);
2163        assert_eq!(names(&drain(&mut shell)), ["AccessibilityDeactivated"]);
2164    }
2165
2166    // ---------------------------------------------------------------- configure
2167
2168    #[test]
2169    fn a_new_window_has_no_size_until_a_later_pump() {
2170        let (mut shell, window) = shell_with_window();
2171
2172        // Nothing has been reported. Not "0x0", not "the requested size" —
2173        // nothing.
2174        let state = shell.window_state(window).expect("state");
2175        assert!(!state.is_configured());
2176        assert_eq!(state.size(), None);
2177        assert_eq!(state.scale_factor(), None);
2178        assert_eq!(state.effective_mode(), None);
2179        assert_eq!(state.requested_mode, DisplayMode::Windowed);
2180
2181        // The default delay is one pump: the first delivers nothing.
2182        assert_eq!(drain(&mut shell), Vec::new());
2183        assert!(!shell.window_state(window).expect("state").is_configured());
2184
2185        // The second delivers the configure.
2186        let events = drain(&mut shell);
2187        assert_eq!(
2188            events,
2189            vec![ShellEvent::Resized {
2190                window,
2191                size: PhysicalSize::new(1280, 720),
2192                scale_factor: 1.0,
2193            }]
2194        );
2195        let state = shell.window_state(window).expect("state");
2196        assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2197        assert_eq!(state.scale_factor(), Some(1.0));
2198        assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2199        assert!(state.mode_request_honoured());
2200    }
2201
2202    /// A loader's `keep_alive` is a turn of the window system that keeps what
2203    /// the turn produced: the configure comes due, and it and an event that was
2204    /// already waiting are delivered by the next pump, in order, once each.
2205    #[test]
2206    fn keep_alive_takes_a_turn_and_keeps_everything_for_the_next_pump() {
2207        let (mut shell, window) = shell_with_window();
2208        shell.inject(ShellEvent::CloseRequested { window });
2209
2210        // The configure waits this many turns and comes due on the next one.
2211        for _ in 0..=DEFAULT_CONFIGURE_DELAY {
2212            shell.keep_alive();
2213        }
2214        assert!(
2215            shell.window_state(window).expect("state").is_configured(),
2216            "every turn the configure waits for was taken by keep_alive"
2217        );
2218
2219        assert_eq!(
2220            drain(&mut shell),
2221            vec![
2222                ShellEvent::CloseRequested { window },
2223                ShellEvent::Resized {
2224                    window,
2225                    size: PhysicalSize::new(1280, 720),
2226                    scale_factor: 1.0,
2227                },
2228            ],
2229            "nothing keep_alive saw was delivered or dropped"
2230        );
2231        assert_eq!(drain(&mut shell), Vec::new(), "and each arrived once");
2232    }
2233
2234    #[test]
2235    fn a_consumer_that_skips_the_wait_gets_a_clean_error_not_a_wrong_extent() {
2236        let (mut shell, window) = shell_with_window();
2237
2238        // The failure mode this whole design exists to force.
2239        assert_eq!(
2240            create_swapchain(&shell.window_state(window).expect("state")),
2241            Err("window is not configured yet")
2242        );
2243
2244        // And the surface handle *is* available meanwhile — only the extent is
2245        // pending. The target carries no size at all, which is the stronger
2246        // version of the same guarantee: there is no plausible-looking number
2247        // in it for a consumer to mistake for the size the window is about to
2248        // have.
2249        let target = shell.surface_target(window).expect("the handle exists");
2250        assert_eq!(target, SurfaceTarget::Offscreen);
2251
2252        // The loop a consumer must write.
2253        let size = loop {
2254            shell.pump(&mut |_| {});
2255            if let Ok(size) = create_swapchain(&shell.window_state(window).expect("state")) {
2256                break size;
2257            }
2258        };
2259        assert_eq!(size, PhysicalSize::new(1280, 720));
2260        // Configuring the window did not change the target: a HAL surface made
2261        // from the pre-configure value is still describing the right object.
2262        assert_eq!(
2263            shell.surface_target(window).expect("target"),
2264            target,
2265            "the target is invariant under a configure"
2266        );
2267    }
2268
2269    #[test]
2270    fn the_configure_delay_is_a_knob() {
2271        let mut shell = HeadlessShell::new().with_configure_delay(3);
2272        let window = shell.create_window(&WindowDesc::default()).expect("window");
2273        for pump in 0..3 {
2274            assert_eq!(drain(&mut shell), Vec::new(), "pump {pump}");
2275            assert!(!shell.window_state(window).expect("state").is_configured());
2276        }
2277        assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2278        assert!(shell.window_state(window).expect("state").is_configured());
2279
2280        // Zero means "configured on the first pump", for tests about something
2281        // else.
2282        let mut shell = HeadlessShell::new().with_configure_delay(0);
2283        let window = shell.create_window(&WindowDesc::default()).expect("window");
2284        assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2285        assert!(shell.window_state(window).expect("state").is_configured());
2286
2287        shell.set_configure_delay(2);
2288        shell
2289            .set_mode(window, DisplayMode::Borderless { monitor: None })
2290            .expect("request");
2291        assert_eq!(drain(&mut shell), Vec::new());
2292        assert_eq!(drain(&mut shell), Vec::new());
2293        assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2294    }
2295
2296    #[test]
2297    fn set_mode_is_a_request_whose_effect_arrives_later() {
2298        let (mut shell, window) = shell_with_configured_window();
2299        shell
2300            .set_mode(window, DisplayMode::Borderless { monitor: None })
2301            .expect("request");
2302
2303        // The request is a fact; the effect is not.
2304        let state = shell.window_state(window).expect("state");
2305        assert_eq!(
2306            state.requested_mode,
2307            DisplayMode::Borderless { monitor: None }
2308        );
2309        assert_eq!(
2310            state.effective_mode(),
2311            Some(DisplayMode::Windowed),
2312            "still windowed until the window system says otherwise"
2313        );
2314        assert!(
2315            !state.mode_request_honoured(),
2316            "a UI toggle reading the request instead of the effect would lie here"
2317        );
2318        assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2319
2320        settle(&mut shell);
2321        let state = shell.window_state(window).expect("state");
2322        assert_eq!(
2323            state.effective_mode(),
2324            Some(DisplayMode::Borderless { monitor: None })
2325        );
2326        assert!(state.mode_request_honoured());
2327        assert_eq!(state.size(), Some(PhysicalSize::new(1920, 1080)));
2328    }
2329
2330    #[test]
2331    fn a_refused_mode_is_expressible_through_injection() {
2332        // What a compositor that says no looks like: the request stands, and
2333        // the configure that arrives says something else.
2334        let (mut shell, window) = shell_with_configured_window();
2335        shell
2336            .set_mode(window, DisplayMode::Borderless { monitor: None })
2337            .expect("request");
2338        // The compositor answers with a windowed configure instead.
2339        shell
2340            .resize(window, PhysicalSize::new(1280, 720))
2341            .expect("answer");
2342        let state = shell.window_state(window).expect("state");
2343        assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2344        assert!(!state.mode_request_honoured());
2345    }
2346
2347    /// A reconfigured panel keeps its new descriptor and is configured at its
2348    /// new size, reported by a `Resized` as its first size was; a change of
2349    /// namespace or monitor, a window that is no panel, a descriptor a new
2350    /// panel could not have and a stale handle are each refused, and a
2351    /// refusal changes nothing.
2352    #[test]
2353    fn a_reconfigured_panel_is_configured_at_its_new_size() {
2354        let mut shell = HeadlessShell::new();
2355        let monitor = shell.monitors()[0].id;
2356        let bar = crate::PanelDesc {
2357            namespace: "bar",
2358            monitor: Some(monitor),
2359            anchor: crate::PanelAnchor::TOP | crate::PanelAnchor::LEFT | crate::PanelAnchor::RIGHT,
2360            reserve: crate::Reserve::OwnExtent,
2361            ..crate::PanelDesc::default()
2362        };
2363        let window = shell
2364            .create_window(&WindowDesc {
2365                size: LogicalSize::new(400.0, 30.0),
2366                role: crate::WindowRole::Panel(bar),
2367                ..WindowDesc::default()
2368            })
2369            .expect("a panel");
2370        settle(&mut shell);
2371        let scale = shell
2372            .window_state(window)
2373            .expect("live")
2374            .scale_factor()
2375            .expect("configured");
2376
2377        let taller = crate::PanelDesc {
2378            layer: crate::PanelLayer::Overlay,
2379            margins: crate::Margins {
2380                top: 4.0,
2381                ..crate::Margins::default()
2382            },
2383            keyboard: crate::PanelKeyboard::OnDemand,
2384            ..bar
2385        };
2386        let size = LogicalSize::new(400.0, 50.0);
2387        shell.set_panel(window, size, &taller).expect("set_panel");
2388        assert_eq!(shell.panel(window).expect("live"), Some(taller));
2389        assert_eq!(
2390            shell.window_state(window).expect("live").size(),
2391            Some(LogicalSize::new(400.0, 30.0).to_physical(scale)),
2392            "a request: nothing changes before the window system answers"
2393        );
2394        let mut resized = Vec::new();
2395        for _ in 0..8 {
2396            resized.extend(
2397                drain(&mut shell)
2398                    .into_iter()
2399                    .filter_map(|event| match event {
2400                        ShellEvent::Resized {
2401                            window: of, size, ..
2402                        } if of == window => Some(size),
2403                        _ => None,
2404                    }),
2405            );
2406        }
2407        assert_eq!(resized, [size.to_physical(scale)]);
2408
2409        let refused = |shell: &mut HeadlessShell, panel: &crate::PanelDesc<'_>| match shell
2410            .set_panel(window, LogicalSize::new(1.0, 1.0), panel)
2411        {
2412            Err(ShellError::InvalidDescriptor(message)) => message,
2413            other => panic!("expected InvalidDescriptor, got {other:?}"),
2414        };
2415        let renamed = crate::PanelDesc {
2416            namespace: "dock",
2417            ..taller
2418        };
2419        assert!(refused(&mut shell, &renamed).contains("namespace"));
2420        let anywhere = crate::PanelDesc {
2421            monitor: None,
2422            ..taller
2423        };
2424        assert!(refused(&mut shell, &anywhere).contains("monitor"));
2425        let toplevel = shell
2426            .create_window(&WindowDesc::default())
2427            .expect("a toplevel");
2428        assert!(matches!(
2429            shell.set_panel(toplevel, size, &taller),
2430            Err(ShellError::InvalidDescriptor(message)) if message.contains("not a panel")
2431        ));
2432        let cornered = crate::PanelDesc {
2433            anchor: crate::PanelAnchor::TOP | crate::PanelAnchor::LEFT,
2434            ..taller
2435        };
2436        assert!(matches!(
2437            shell.set_panel(window, size, &cornered),
2438            Err(ShellError::Unsupported { .. })
2439        ));
2440        assert_eq!(
2441            shell.panel(window).expect("live"),
2442            Some(taller),
2443            "a refused descriptor is not recorded"
2444        );
2445        assert_eq!(shell.panel(toplevel).expect("live"), None);
2446
2447        shell.destroy_window(window).expect("destroy");
2448        assert!(matches!(
2449            shell.set_panel(window, size, &taller),
2450            Err(ShellError::InvalidWindow { .. })
2451        ));
2452    }
2453
2454    /// A repositioned popup is configured at its new size, as a window system
2455    /// answers a move; a window that is not a popup is refused.
2456    #[test]
2457    fn a_repositioned_popup_is_configured_at_its_new_size() {
2458        let (mut shell, window) = shell_with_configured_window();
2459        let anchor = crate::LogicalRect::new(
2460            crate::LogicalPoint::new(10.0, 10.0),
2461            LogicalSize::new(100.0, 20.0),
2462        );
2463        let popup = shell
2464            .create_window(&WindowDesc {
2465                size: LogicalSize::new(200.0, 150.0),
2466                role: crate::WindowRole::Popup(crate::PopupDesc {
2467                    parent: window,
2468                    anchor,
2469                    placement: crate::PopupPlacement::Below,
2470                    input: crate::PopupInput::Pointer,
2471                }),
2472                ..WindowDesc::default()
2473            })
2474            .expect("a popup");
2475        settle(&mut shell);
2476        let scale = shell
2477            .window_state(popup)
2478            .expect("live")
2479            .scale_factor()
2480            .expect("configured");
2481
2482        let moved = LogicalSize::new(240.0, 90.0);
2483        shell
2484            .reposition_popup(popup, anchor, moved)
2485            .expect("reposition");
2486        settle(&mut shell);
2487        assert_eq!(
2488            shell.window_state(popup).expect("live").size(),
2489            Some(moved.to_physical(scale))
2490        );
2491        assert!(matches!(
2492            shell.reposition_popup(window, anchor, moved),
2493            Err(ShellError::InvalidDescriptor(_))
2494        ));
2495    }
2496
2497    /// Escape on a grabbing popup is delivered and then asks the popup to
2498    /// close, once until answered; a popup that does not hold the keyboard
2499    /// only hears the key.
2500    #[test]
2501    fn escape_on_a_grabbing_popup_is_delivered_then_asks_it_to_close() {
2502        let (mut shell, window) = shell_with_configured_window();
2503        let popup_of = |shell: &mut HeadlessShell, input| {
2504            shell
2505                .create_window(&WindowDesc {
2506                    role: crate::WindowRole::Popup(crate::PopupDesc {
2507                        parent: window,
2508                        anchor: crate::LogicalRect::default(),
2509                        placement: crate::PopupPlacement::Below,
2510                        input,
2511                    }),
2512                    ..WindowDesc::default()
2513                })
2514                .expect("a popup")
2515        };
2516        let menu = popup_of(&mut shell, crate::PopupInput::Grab);
2517        let tooltip = popup_of(&mut shell, crate::PopupInput::Pointer);
2518        settle(&mut shell);
2519        let is_close = |event: &ShellEvent| matches!(event, ShellEvent::CloseRequested { .. });
2520
2521        shell.key_press(menu, KeyCode::Escape).expect("press");
2522        let events = drain(&mut shell);
2523        assert!(
2524            matches!(
2525                events.as_slice(),
2526                [
2527                    ShellEvent::Key {
2528                        key_code: Some(KeyCode::Escape),
2529                        ..
2530                    },
2531                    ShellEvent::CloseRequested { window: closed },
2532                ] if *closed == menu
2533            ),
2534            "the key, then the request: {events:?}"
2535        );
2536        // Unanswered, a second press and a repeat ask nothing more.
2537        shell.key_press(menu, KeyCode::Escape).expect("press");
2538        shell.key_repeat(menu, KeyCode::Escape).expect("repeat");
2539        assert!(!drain(&mut shell).iter().any(is_close));
2540
2541        // Kept open — backing out of a submenu — the next press asks again.
2542        shell
2543            .reply_close_request(menu, CloseReply::Keep)
2544            .expect("keep");
2545        shell.key_release(menu, KeyCode::Escape).expect("release");
2546        shell.key_press(menu, KeyCode::Enter).expect("press");
2547        assert!(!drain(&mut shell).iter().any(is_close), "not these");
2548        shell.key_press(menu, KeyCode::Escape).expect("press");
2549        assert!(drain(&mut shell).iter().any(is_close));
2550
2551        shell.key_press(tooltip, KeyCode::Escape).expect("press");
2552        shell.key_press(window, KeyCode::Escape).expect("press");
2553        assert!(!drain(&mut shell).iter().any(is_close));
2554    }
2555
2556    #[test]
2557    fn set_constraints_is_a_request_too() {
2558        let (mut shell, window) = shell_with_configured_window();
2559        let constraints = SizeConstraints::min(LogicalSize::new(1600.0, 900.0));
2560        shell.set_constraints(window, constraints).expect("request");
2561
2562        let state = shell.window_state(window).expect("state");
2563        assert_eq!(state.requested_constraints, constraints);
2564        assert_eq!(
2565            state.size(),
2566            Some(PhysicalSize::new(1280, 720)),
2567            "the size only changes when the window system says so"
2568        );
2569
2570        settle(&mut shell);
2571        assert_eq!(
2572            shell.window_state(window).expect("state").size(),
2573            Some(PhysicalSize::new(1600, 900))
2574        );
2575    }
2576
2577    #[test]
2578    fn constraints_do_not_cancel_a_mode_request_that_has_not_landed() {
2579        // Both are requests, and they are independent. Rebuilding the pending
2580        // configure from the *effective* configuration threw the mode away:
2581        // `requested_mode` stayed `Borderless`, `effective_mode` stayed
2582        // `Windowed`, and `mode_request_honoured` was false forever.
2583        let (mut shell, window) = shell_with_configured_window();
2584        shell
2585            .set_mode(window, DisplayMode::Borderless { monitor: None })
2586            .expect("mode request");
2587        shell
2588            .set_constraints(window, SizeConstraints::min(LogicalSize::new(320.0, 180.0)))
2589            .expect("constraint request");
2590        settle(&mut shell);
2591
2592        let state = shell.window_state(window).expect("state");
2593        assert_eq!(
2594            state.effective_mode(),
2595            Some(DisplayMode::Borderless { monitor: None })
2596        );
2597        assert!(state.mode_request_honoured());
2598    }
2599
2600    #[test]
2601    fn a_borderless_window_reports_its_own_monitors_scale() {
2602        // The scale used to be hoisted from `monitors[0]` above the match, so a
2603        // borderless window on a HiDPI secondary monitor took that monitor's
2604        // *size* and the primary's *scale* — a swapchain sized for one and a UI
2605        // laid out for the other.
2606        let mut shell = HeadlessShell::new();
2607        let hidpi = shell.plug_monitor(MonitorInfo {
2608            id: MonitorId(0),
2609            name: "HEADLESS-2".to_string(),
2610            bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2611            work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2612            scale_factor: 2.0,
2613            refresh_millihertz: 143_998,
2614            is_primary: false,
2615        });
2616        let window = shell
2617            .create_window(&WindowDesc {
2618                mode: DisplayMode::Borderless {
2619                    monitor: Some(hidpi),
2620                },
2621                ..WindowDesc::default()
2622            })
2623            .expect("window");
2624        settle(&mut shell);
2625
2626        let state = shell.window_state(window).expect("state");
2627        assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2628        assert_eq!(
2629            state.scale_factor(),
2630            Some(2.0),
2631            "the scale is the borderless monitor's, not the primary's"
2632        );
2633        // And `set_mode` onto the same monitor agrees, which is the pair that
2634        // used to disagree.
2635        let other = shell
2636            .create_window(&WindowDesc::default())
2637            .expect("second window");
2638        settle(&mut shell);
2639        shell
2640            .set_mode(
2641                other,
2642                DisplayMode::Borderless {
2643                    monitor: Some(hidpi),
2644                },
2645            )
2646            .expect("request");
2647        settle(&mut shell);
2648        assert_eq!(
2649            shell.window_state(other).expect("state").scale_factor(),
2650            shell.window_state(window).expect("state").scale_factor()
2651        );
2652    }
2653
2654    #[test]
2655    fn constraints_set_before_the_first_configure_fold_into_it() {
2656        let mut shell = HeadlessShell::new();
2657        let window = shell.create_window(&WindowDesc::default()).expect("window");
2658        shell
2659            .set_constraints(
2660                window,
2661                SizeConstraints::NONE.with_aspect(AspectRatio::SQUARE),
2662            )
2663            .expect("request");
2664        settle(&mut shell);
2665        // One configure, already square — not a 1280x720 configure followed by
2666        // a correction.
2667        assert_eq!(
2668            shell.window_state(window).expect("state").size(),
2669            Some(PhysicalSize::new(720, 720))
2670        );
2671    }
2672
2673    #[test]
2674    fn creation_uses_the_requested_size_and_the_monitors_scale() {
2675        let mut shell = HeadlessShell::new();
2676        let window = shell
2677            .create_window(&WindowDesc {
2678                size: LogicalSize::new(800.0, 600.0),
2679                ..WindowDesc::default()
2680            })
2681            .expect("window");
2682        settle(&mut shell);
2683        assert_eq!(
2684            shell.window_state(window).expect("state").size(),
2685            Some(PhysicalSize::new(800, 600))
2686        );
2687
2688        let mut shell = HeadlessShell::new().with_scale_factor(2.0);
2689        let window = shell
2690            .create_window(&WindowDesc {
2691                size: LogicalSize::new(640.0, 360.0),
2692                ..WindowDesc::default()
2693            })
2694            .expect("window");
2695        settle(&mut shell);
2696        let state = shell.window_state(window).expect("state");
2697        assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2698        assert_eq!(state.scale_factor(), Some(2.0));
2699    }
2700
2701    // ------------------------------------------------------------ compositor side
2702
2703    #[test]
2704    fn scale_changes_preserve_logical_size_and_report_both_facts() {
2705        let (mut shell, window) = shell_with_configured_window();
2706        shell
2707            .change_scale_factor(window, 1.5)
2708            .expect("scale change");
2709        let events = drain(&mut shell);
2710        assert_eq!(
2711            events,
2712            vec![
2713                ShellEvent::ScaleFactorChanged {
2714                    window,
2715                    scale_factor: 1.5,
2716                    size: PhysicalSize::new(1920, 1080),
2717                },
2718                // The resize the scale change implies, in the same order both
2719                // Linux backends report it.
2720                ShellEvent::Resized {
2721                    window,
2722                    size: PhysicalSize::new(1920, 1080),
2723                    scale_factor: 1.5,
2724                },
2725            ]
2726        );
2727        // 1280x720 logical at 1.5 is 1920x1080 physical — the swapchain must be
2728        // recreated at the new size, which is the leak the DPI matrix test in
2729        // the windowing rules (`docs/notes/backends.md`) is looking for.
2730        assert_eq!(
2731            shell.window_state(window).expect("state").size(),
2732            Some(PhysicalSize::new(1920, 1080))
2733        );
2734    }
2735
2736    #[test]
2737    fn a_scale_change_scheduled_by_a_mode_switch_arrives_before_the_resize() {
2738        let mut shell = HeadlessShell::new();
2739        let hidpi = shell.plug_monitor(MonitorInfo {
2740            id: MonitorId(0),
2741            name: "HEADLESS-2".to_string(),
2742            bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2743            work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2744            scale_factor: 2.0,
2745            refresh_millihertz: 143_998,
2746            is_primary: false,
2747        });
2748        assert_eq!(hidpi, MonitorId(2), "ids are never reused");
2749        let window = shell.create_window(&WindowDesc::default()).expect("window");
2750        settle(&mut shell);
2751
2752        shell
2753            .set_mode(
2754                window,
2755                DisplayMode::Borderless {
2756                    monitor: Some(hidpi),
2757                },
2758            )
2759            .expect("request");
2760        assert_eq!(drain(&mut shell), Vec::new(), "still only a request");
2761        let events = drain(&mut shell);
2762        assert_eq!(names(&events), ["ScaleFactorChanged", "Resized"]);
2763
2764        let state = shell.window_state(window).expect("state");
2765        assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2766        assert_eq!(state.scale_factor(), Some(2.0));
2767        assert!(state.effective_mode().expect("configured").is_borderless());
2768        // A mode change is the one event the seam says can invalidate a target.
2769        // Here it does not, because this backend's target names nothing that a
2770        // mode switch recreates — and, crucially, it does not encode the new
2771        // size either.
2772        assert_eq!(
2773            shell.surface_target(window).expect("target"),
2774            SurfaceTarget::Offscreen
2775        );
2776
2777        assert!(matches!(
2778            shell.set_mode(
2779                window,
2780                DisplayMode::Borderless {
2781                    monitor: Some(MonitorId(99)),
2782                },
2783            ),
2784            Err(ShellError::NoSuchMonitor(99))
2785        ));
2786    }
2787
2788    #[test]
2789    fn resizes_honour_constraints_and_inject_bypasses_them() {
2790        let (mut shell, window) = shell_with_configured_window();
2791        shell
2792            .set_constraints(
2793                window,
2794                SizeConstraints::min(LogicalSize::new(640.0, 360.0))
2795                    .with_aspect(AspectRatio::WIDESCREEN),
2796            )
2797            .expect("request");
2798        settle(&mut shell);
2799
2800        shell
2801            .resize(window, PhysicalSize::new(1000, 1000))
2802            .expect("resize");
2803        assert_eq!(
2804            drain(&mut shell),
2805            vec![ShellEvent::Resized {
2806                window,
2807                size: PhysicalSize::new(1000, 562),
2808                scale_factor: 1.0,
2809            }]
2810        );
2811
2812        // A tiling window manager ignores every hint, and `inject` is how that
2813        // gets tested.
2814        shell.inject(ShellEvent::Resized {
2815            window,
2816            size: PhysicalSize::new(300, 900),
2817            scale_factor: 1.0,
2818        });
2819        assert_eq!(
2820            drain(&mut shell)[0],
2821            ShellEvent::Resized {
2822                window,
2823                size: PhysicalSize::new(300, 900),
2824                scale_factor: 1.0,
2825            }
2826        );
2827    }
2828
2829    #[test]
2830    fn a_compositor_configure_supersedes_a_pending_one() {
2831        let (mut shell, window) = shell_with_configured_window();
2832        shell
2833            .set_mode(window, DisplayMode::Borderless { monitor: None })
2834            .expect("request");
2835        // The compositor answers before the scheduled configure lands.
2836        shell
2837            .resize(window, PhysicalSize::new(640, 480))
2838            .expect("answer");
2839        assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2840        assert_eq!(
2841            shell.window_state(window).expect("state").size(),
2842            Some(PhysicalSize::new(640, 480))
2843        );
2844        // And nothing else arrives later.
2845        assert_eq!(drain(&mut shell), Vec::new());
2846    }
2847
2848    // ------------------------------------------------------------------- input
2849
2850    #[test]
2851    fn timestamps_come_from_the_shells_clock_not_from_the_pump() {
2852        let (mut shell, window) = shell_with_configured_window();
2853        shell.set_time(Duration::from_secs(2));
2854        shell.key_press(window, KeyCode::Space).expect("press");
2855        shell.advance(Duration::from_millis(180));
2856        shell.key_release(window, KeyCode::Space).expect("release");
2857
2858        let events = drain(&mut shell);
2859        let held = events[1]
2860            .time()
2861            .expect("input")
2862            .saturating_since(events[0].time().expect("input"));
2863        assert_eq!(held, Duration::from_millis(180));
2864        assert_eq!(events[0].time(), Some(EventTime::from_millis(2_000)));
2865    }
2866
2867    #[test]
2868    fn synthesized_key_fields_are_stable_and_plausible() {
2869        let (mut shell, window) = shell_with_configured_window();
2870        shell.set_modifiers(Modifiers::CTRL);
2871        shell.key_press(window, KeyCode::KeyS).expect("press");
2872        let events = drain(&mut shell);
2873        let ShellEvent::Key {
2874            scancode,
2875            key_code,
2876            keysym,
2877            modifiers,
2878            repeat,
2879            device,
2880            ..
2881        } = events[0].clone()
2882        else {
2883            panic!("wrong variant");
2884        };
2885        assert_eq!(key_code, Some(KeyCode::KeyS));
2886        assert_eq!(keysym, Keysym::from_char('s'));
2887        assert_eq!(modifiers, Modifiers::CTRL);
2888        assert!(!repeat);
2889        assert_eq!(device, VIRTUAL_DEVICE);
2890        assert!(scancode.0 >= 8, "evdev-shaped scancodes start at 8");
2891        assert_eq!(synthetic_keysym(KeyCode::Digit7), Keysym::from_char('7'));
2892        assert_eq!(synthetic_keysym(KeyCode::F5), Keysym(0xFFC2));
2893        assert_eq!(synthetic_keysym(KeyCode::Enter), Keysym(0xFF0D));
2894        assert_eq!(synthetic_keysym(KeyCode::Escape), Keysym(0xFF1B));
2895        assert_eq!(synthetic_keysym(KeyCode::ArrowLeft), Keysym(0xFF51));
2896        assert_eq!(synthetic_keysym(KeyCode::Slash), Keysym::from_char('/'));
2897
2898        shell.key_repeat(window, KeyCode::KeyS).expect("repeat");
2899        let events = drain(&mut shell);
2900        let ShellEvent::Key { repeat, .. } = events[0] else {
2901            panic!("wrong variant");
2902        };
2903        assert!(repeat);
2904    }
2905
2906    #[test]
2907    fn a_locked_pointer_reports_no_absolute_position() {
2908        let (mut shell, window) = shell_with_configured_window();
2909        shell
2910            .set_pointer_mode(window, PointerMode::Locked)
2911            .expect("lock");
2912        shell
2913            .move_pointer(window, PhysicalPoint::new(10.0, 10.0), (3.0, -4.0))
2914            .expect("motion");
2915        let events = drain(&mut shell);
2916        let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
2917            panic!("wrong variant");
2918        };
2919        assert_eq!(abs, None);
2920        assert_eq!(raw_delta, Some((3.0, -4.0)));
2921    }
2922
2923    #[test]
2924    fn a_backend_without_raw_motion_reports_none() {
2925        let mut shell = HeadlessShell::new()
2926            .with_caps(ShellCaps::DESKTOP - ShellCaps::RAW_POINTER_MOTION)
2927            .with_configure_delay(0);
2928        let window = shell.create_window(&WindowDesc::default()).expect("window");
2929        drain(&mut shell);
2930        shell
2931            .move_pointer(window, PhysicalPoint::new(1.0, 2.0), (5.0, 5.0))
2932            .expect("motion");
2933        let events = drain(&mut shell);
2934        let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
2935            panic!("wrong variant");
2936        };
2937        assert_eq!(abs, Some(PhysicalPoint::new(1.0, 2.0)));
2938        assert_eq!(raw_delta, None, "no relative-motion source");
2939        assert!(!shell.caps().has_mouselook());
2940    }
2941
2942    #[test]
2943    fn pointer_focus_and_scroll_and_text_are_injectable() {
2944        let (mut shell, window) = shell_with_configured_window();
2945        shell
2946            .set_pointer_focus(window, true, Some(PhysicalPoint::new(1.0, 1.0)))
2947            .expect("enter");
2948        shell
2949            .scroll(window, ScrollDelta::Lines { x: 0.0, y: -1.0 }, None)
2950            .expect("scroll");
2951        shell
2952            .button(
2953                window,
2954                PointerButton::Left,
2955                ButtonState::Pressed,
2956                Some(PhysicalPoint::new(1.0, 1.0)),
2957            )
2958            .expect("click");
2959        shell.commit_text(window, "ありがとう").expect("text");
2960        shell
2961            .set_pointer_focus(window, false, Some(PhysicalPoint::new(1.0, 1.0)))
2962            .expect("leave");
2963
2964        let events = drain(&mut shell);
2965        assert_eq!(
2966            names(&events),
2967            [
2968                "PointerFocus",
2969                "Wheel",
2970                "Button",
2971                "TextCommit",
2972                "PointerFocus"
2973            ]
2974        );
2975        let ShellEvent::PointerFocus {
2976            entered, position, ..
2977        } = &events[4]
2978        else {
2979            panic!("wrong variant");
2980        };
2981        assert!(!entered);
2982        assert_eq!(*position, None, "a leave has no position");
2983        assert!(events.iter().all(ShellEvent::is_input));
2984    }
2985
2986    /// Two fingers script as two contacts, and a touchless shell has none.
2987    ///
2988    /// The second half is the one worth having: a backend with the capability
2989    /// clear is a platform where a finger *cannot arrive*, and a script that
2990    /// could inject one anyway would let a test assert a game works on a
2991    /// touchscreen the shell does not have.
2992    #[test]
2993    fn contacts_are_injectable_and_a_touchless_shell_refuses_them() {
2994        let (mut shell, window) = shell_with_configured_window();
2995        assert!(shell.caps().contains(ShellCaps::TOUCH));
2996        for (contact, phase, x) in [
2997            (1, TouchPhase::Began, 10.0),
2998            (2, TouchPhase::Began, 200.0),
2999            (2, TouchPhase::Moved, 220.0),
3000            (1, TouchPhase::Ended, 10.0),
3001            (2, TouchPhase::Cancelled, 220.0),
3002        ] {
3003            shell
3004                .touch(
3005                    window,
3006                    ContactId(contact),
3007                    phase,
3008                    PhysicalPoint::new(x, 5.0),
3009                )
3010                .expect("a contact");
3011        }
3012        let events = drain(&mut shell);
3013        assert_eq!(names(&events), ["Touch"; 5]);
3014        let seen: Vec<(ContactId, TouchPhase, f64)> = events
3015            .iter()
3016            .map(|event| {
3017                let ShellEvent::Touch {
3018                    contact,
3019                    phase,
3020                    position,
3021                    ..
3022                } = event
3023                else {
3024                    panic!("wrong variant");
3025                };
3026                (*contact, *phase, position.x)
3027            })
3028            .collect();
3029        assert_eq!(
3030            seen,
3031            vec![
3032                (ContactId(1), TouchPhase::Began, 10.0),
3033                (ContactId(2), TouchPhase::Began, 200.0),
3034                (ContactId(2), TouchPhase::Moved, 220.0),
3035                (ContactId(1), TouchPhase::Ended, 10.0),
3036                (ContactId(2), TouchPhase::Cancelled, 220.0),
3037            ]
3038        );
3039        assert!(events.iter().all(ShellEvent::is_input));
3040
3041        let mut touchless = HeadlessShell::new()
3042            .with_caps(ShellCaps::DESKTOP - ShellCaps::TOUCH)
3043            .with_configure_delay(0);
3044        let window = touchless
3045            .create_window(&WindowDesc::default())
3046            .expect("window");
3047        drain(&mut touchless);
3048        assert!(matches!(
3049            touchless.touch(
3050                window,
3051                ContactId(1),
3052                TouchPhase::Began,
3053                PhysicalPoint::ORIGIN
3054            ),
3055            Err(ShellError::Unsupported { .. })
3056        ));
3057        assert!(drain(&mut touchless).is_empty(), "and queued nothing");
3058    }
3059
3060    #[test]
3061    fn warping_produces_motion_like_every_platform_that_has_it() {
3062        let (mut shell, window) = shell_with_configured_window();
3063        shell
3064            .warp_pointer(window, PhysicalPoint::new(320.0, 180.0))
3065            .expect("warp");
3066        let events = drain(&mut shell);
3067        let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
3068            panic!("wrong variant");
3069        };
3070        assert_eq!(abs, Some(PhysicalPoint::new(320.0, 180.0)));
3071        assert_eq!(raw_delta, None, "a warp is not physical motion");
3072    }
3073
3074    // ------------------------------------------------------------- lifecycle
3075
3076    #[test]
3077    fn capabilities_gate_the_operations_that_need_them() {
3078        let mut shell = HeadlessShell::new().with_caps(ShellCaps::empty());
3079        let window = shell.create_window(&WindowDesc::default()).expect("window");
3080
3081        assert!(matches!(
3082            shell.warp_pointer(window, PhysicalPoint::ORIGIN),
3083            Err(ShellError::Unsupported { .. })
3084        ));
3085        assert!(matches!(
3086            shell.set_pointer_mode(window, PointerMode::Locked),
3087            Err(ShellError::Unsupported { .. })
3088        ));
3089        assert!(matches!(
3090            shell.clipboard_request(window, MimeType::TextUtf8),
3091            Err(ShellError::Unsupported { .. })
3092        ));
3093        // Free needs no capability, so it always works.
3094        shell
3095            .set_pointer_mode(window, PointerMode::Free)
3096            .expect("Free is always available");
3097        // And a second window is refused without MULTI_WINDOW.
3098        assert!(matches!(
3099            shell.create_window(&WindowDesc::default()),
3100            Err(ShellError::Unsupported { .. })
3101        ));
3102    }
3103
3104    #[test]
3105    fn stale_handles_fail_cleanly_on_every_accessor() {
3106        let (mut shell, window) = shell_with_configured_window();
3107        shell.destroy_window(window).expect("destroy");
3108        assert_eq!(
3109            drain(&mut shell),
3110            vec![ShellEvent::WindowDestroyed { window }]
3111        );
3112
3113        assert!(matches!(
3114            shell.window_state(window),
3115            Err(ShellError::InvalidWindow { .. })
3116        ));
3117        assert!(matches!(
3118            shell.surface_target(window),
3119            Err(ShellError::InvalidWindow { .. })
3120        ));
3121        assert!(matches!(
3122            shell.resize(window, PhysicalSize::new(1, 1)),
3123            Err(ShellError::InvalidWindow { .. })
3124        ));
3125        assert!(matches!(
3126            shell.set_title(window, "gone"),
3127            Err(ShellError::InvalidWindow { .. })
3128        ));
3129        assert!(matches!(
3130            shell.destroy_window(window),
3131            Err(ShellError::InvalidWindow { .. })
3132        ));
3133        assert!(matches!(
3134            shell.key_press(window, KeyCode::KeyA),
3135            Err(ShellError::InvalidWindow { .. })
3136        ));
3137    }
3138
3139    #[test]
3140    fn destroying_a_window_cancels_its_pending_configure() {
3141        // The bug a naive delayed-configure implementation has: an event for a
3142        // window that no longer exists.
3143        let (mut shell, window) = shell_with_window();
3144        shell.destroy_window(window).expect("destroy");
3145        assert_eq!(names(&drain(&mut shell)), ["WindowDestroyed"]);
3146        assert_eq!(drain(&mut shell), Vec::new());
3147        assert_eq!(drain(&mut shell), Vec::new());
3148    }
3149
3150    #[test]
3151    fn close_requests_are_questions() {
3152        let (mut shell, window) = shell_with_configured_window();
3153        assert!(matches!(
3154            shell.reply_close_request(window, CloseReply::Close),
3155            Err(ShellError::NoPendingCloseRequest { .. })
3156        ));
3157
3158        shell.request_close(window).expect("request");
3159        assert!(shell.window_state(window).expect("state").close_pending);
3160        shell
3161            .reply_close_request(window, CloseReply::Keep)
3162            .expect("keep");
3163        assert!(!shell.window_state(window).expect("state").close_pending);
3164        assert!(shell.window_state(window).is_ok(), "still open");
3165
3166        shell.request_close(window).expect("request again");
3167        shell
3168            .reply_close_request(window, CloseReply::Close)
3169            .expect("close");
3170        assert!(matches!(
3171            shell.window_state(window),
3172            Err(ShellError::InvalidWindow { .. })
3173        ));
3174    }
3175
3176    #[test]
3177    fn monitor_hotplug_reports_a_change_and_keeps_ids_unique() {
3178        let mut shell = HeadlessShell::new();
3179        let extra = shell.plug_monitor(MonitorInfo {
3180            id: MonitorId(0),
3181            name: "HEADLESS-2".to_string(),
3182            bounds: PhysicalRect::new(1920, 0, 1920, 1080),
3183            work_area: PhysicalRect::new(1920, 0, 1920, 1080),
3184            scale_factor: 1.0,
3185            refresh_millihertz: 60_000,
3186            is_primary: false,
3187        });
3188        assert_eq!(shell.monitors().len(), 2);
3189        assert_eq!(drain(&mut shell), vec![ShellEvent::MonitorsChanged]);
3190
3191        shell.unplug_monitor(extra).expect("unplug");
3192        assert_eq!(shell.monitors().len(), 1);
3193        assert_eq!(
3194            drain(&mut shell),
3195            vec![
3196                ShellEvent::MonitorRemoved { monitor: extra },
3197                ShellEvent::MonitorsChanged
3198            ]
3199        );
3200        assert!(matches!(
3201            shell.unplug_monitor(extra),
3202            Err(ShellError::NoSuchMonitor(_))
3203        ));
3204        assert_eq!(
3205            shell.monitor(MonitorId(1)).map(|m| m.name.as_str()),
3206            Some("HEADLESS-1")
3207        );
3208        assert!(shell.monitor(extra).is_none());
3209    }
3210
3211    #[test]
3212    fn unplugging_a_monitor_asks_its_panels_and_only_its_panels_to_close() {
3213        let mut shell = HeadlessShell::new();
3214        let second = shell.plug_monitor(MonitorInfo {
3215            id: MonitorId(0),
3216            name: "HEADLESS-2".to_string(),
3217            bounds: PhysicalRect::new(1920, 0, 1920, 1080),
3218            work_area: PhysicalRect::new(1920, 0, 1920, 1080),
3219            scale_factor: 1.0,
3220            refresh_millihertz: 60_000,
3221            is_primary: false,
3222        });
3223        let first = shell.monitors()[0].id;
3224        let panel_on = |shell: &mut HeadlessShell, monitor| {
3225            shell
3226                .create_window(&WindowDesc {
3227                    role: crate::WindowRole::Panel(crate::PanelDesc {
3228                        monitor,
3229                        ..crate::PanelDesc::default()
3230                    }),
3231                    ..WindowDesc::default()
3232                })
3233                .expect("panel")
3234        };
3235        let doomed = panel_on(&mut shell, Some(second));
3236        let already_asked = panel_on(&mut shell, Some(second));
3237        let elsewhere = panel_on(&mut shell, Some(first));
3238        let anywhere = panel_on(&mut shell, None);
3239        let toplevel = shell.create_window(&WindowDesc::default()).expect("window");
3240        shell.request_close(already_asked).expect("close");
3241        settle(&mut shell);
3242        drain(&mut shell);
3243
3244        shell.unplug_monitor(second).expect("unplug");
3245        assert_eq!(
3246            drain(&mut shell),
3247            vec![
3248                ShellEvent::MonitorRemoved { monitor: second },
3249                ShellEvent::CloseRequested { window: doomed },
3250                ShellEvent::MonitorsChanged,
3251            ]
3252        );
3253        for window in [elsewhere, anywhere, toplevel] {
3254            assert!(!shell.window_state(window).expect("open").close_pending);
3255        }
3256    }
3257
3258    #[test]
3259    fn drops_require_both_the_capability_and_the_opt_in() {
3260        let mut shell = HeadlessShell::new();
3261        let plain = shell.create_window(&WindowDesc::default()).expect("window");
3262        assert!(matches!(
3263            shell.drop_file(plain, "/tmp/a.gltf", None),
3264            Err(ShellError::Unsupported { .. })
3265        ));
3266
3267        let dropper = shell
3268            .create_window(&WindowDesc {
3269                accept_drops: true,
3270                ..WindowDesc::default()
3271            })
3272            .expect("window");
3273        settle(&mut shell);
3274        shell
3275            .drop_file(dropper, "/tmp/a.gltf", Some(PhysicalPoint::new(4.0, 5.0)))
3276            .expect("drop");
3277        assert_eq!(names(&drain(&mut shell))[0], "DroppedFile");
3278    }
3279
3280    #[test]
3281    fn a_sink_that_queues_more_events_does_not_spin_the_pump() {
3282        let (mut shell, window) = shell_with_configured_window();
3283        shell.inject(ShellEvent::MonitorsChanged);
3284        let mut extra = Vec::new();
3285        shell.pump(&mut |event| extra.push(event));
3286        assert_eq!(extra.len(), 1);
3287        shell.inject(ShellEvent::CloseRequested { window });
3288        assert_eq!(shell.pending_events(), 1);
3289    }
3290
3291    #[test]
3292    fn accessors_that_no_real_backend_can_provide_are_headless_only() {
3293        let (mut shell, window) = shell_with_configured_window();
3294        shell.set_title(window, "renamed").expect("title");
3295        assert_eq!(shell.title(window).expect("title"), "renamed");
3296        shell.set_cursor(window, None).expect("hide");
3297        assert_eq!(shell.cursor(window).expect("cursor"), None);
3298        shell
3299            .set_cursor(window, Some(CursorIcon::Crosshair))
3300            .expect("shape");
3301        assert_eq!(
3302            shell.cursor(window).expect("cursor"),
3303            Some(CursorIcon::Crosshair)
3304        );
3305        shell.set_visible(window, false).expect("hide window");
3306        assert!(!shell.window_state(window).expect("state").visible);
3307    }
3308
3309    #[test]
3310    fn an_input_region_is_recorded_replaced_and_given_back() {
3311        let (mut shell, window) = shell_with_configured_window();
3312        assert_eq!(shell.input_region(window).expect("region"), None);
3313        let panel = PhysicalRect::new(0, 0, 400, 300).rounded(12);
3314        shell
3315            .set_input_region(window, Some(&panel))
3316            .expect("a region");
3317        assert_eq!(
3318            shell.input_region(window).expect("region"),
3319            Some(panel.as_slice())
3320        );
3321        shell.set_input_region(window, Some(&[])).expect("nothing");
3322        assert_eq!(shell.input_region(window).expect("region"), Some(&[][..]));
3323        shell.set_input_region(window, None).expect("whole window");
3324        assert_eq!(shell.input_region(window).expect("region"), None);
3325        shell.destroy_window(window).expect("destroy");
3326        assert!(matches!(
3327            shell.set_input_region(window, None),
3328            Err(ShellError::InvalidWindow { .. })
3329        ));
3330        assert!(shell.input_region(window).is_err());
3331    }
3332
3333    fn frames_for(events: &[ShellEvent], window: WindowId) -> usize {
3334        events
3335            .iter()
3336            .filter(|event| matches!(event, ShellEvent::FrameReady { window: w } if *w == window))
3337            .count()
3338    }
3339
3340    #[test]
3341    fn requests_for_a_frame_share_one_answer_at_the_next_pump() {
3342        let (mut shell, window) = shell_with_configured_window();
3343        assert!(!shell.frame_requested(window).expect("live"));
3344        shell.request_frame(window).expect("a frame");
3345        shell.request_frame(window).expect("the same frame");
3346        assert!(shell.frame_requested(window).expect("live"));
3347        let events = drain(&mut shell);
3348        assert_eq!(frames_for(&events, window), 1, "{:?}", names(&events));
3349        assert!(!shell.frame_requested(window).expect("live"));
3350        assert_eq!(
3351            frames_for(&drain(&mut shell), window),
3352            0,
3353            "nothing asked again"
3354        );
3355        shell.request_frame(window).expect("the next frame");
3356        assert_eq!(frames_for(&drain(&mut shell), window), 1);
3357    }
3358
3359    #[test]
3360    fn a_window_destroyed_with_a_frame_requested_is_not_answered() {
3361        let (mut shell, doomed) = shell_with_configured_window();
3362        let kept = shell
3363            .create_window(&WindowDesc::default())
3364            .expect("a second window");
3365        settle(&mut shell);
3366        shell.request_frame(doomed).expect("a frame");
3367        shell.request_frame(kept).expect("a frame");
3368        shell.destroy_window(doomed).expect("destroy");
3369        let events = drain(&mut shell);
3370        assert_eq!(frames_for(&events, doomed), 0, "{:?}", names(&events));
3371        assert_eq!(frames_for(&events, kept), 1, "the other window still asked");
3372        assert!(matches!(
3373            shell.request_frame(doomed),
3374            Err(ShellError::InvalidWindow { .. })
3375        ));
3376        assert!(shell.frame_requested(doomed).is_err());
3377    }
3378
3379    #[test]
3380    fn a_wait_does_not_sleep_through_a_frame_that_is_due() {
3381        let (mut shell, window) = shell_with_configured_window();
3382        shell.request_frame(window).expect("a frame");
3383        let start = std::time::Instant::now();
3384        shell.wait_events(Some(LONG_WAIT));
3385        assert!(
3386            start.elapsed() < LONG_WAIT / 2,
3387            "slept {:?} with a frame due",
3388            start.elapsed()
3389        );
3390        assert_eq!(frames_for(&drain(&mut shell), window), 1);
3391    }
3392
3393    #[test]
3394    fn waiting_is_observable_and_focus_transitions_are_events() {
3395        let (mut shell, window) = shell_with_configured_window();
3396        assert_eq!(shell.wait_count(), 0);
3397        shell.wait_events(Some(Duration::from_millis(5)));
3398        shell.wait_events(None);
3399        assert_eq!(shell.wait_count(), 2);
3400
3401        shell.set_focus(window, true).expect("focus");
3402        assert!(shell.window_state(window).expect("state").focused);
3403        shell.set_focus(window, false).expect("unfocus");
3404        assert_eq!(names(&drain(&mut shell)), ["Focus", "Focus"]);
3405    }
3406
3407    // ----------------------------------------------------------------- waker
3408
3409    /// Longer than any of these tests should take: a wait that runs this long
3410    /// was not woken.
3411    const LONG_WAIT: Duration = Duration::from_secs(60);
3412    /// How long the waking thread lets the wait settle before it rings.
3413    const RING_AFTER: Duration = Duration::from_millis(50);
3414
3415    /// Rings `waker` from another thread after [`RING_AFTER`].
3416    fn ring_later(waker: ShellWaker) -> std::thread::JoinHandle<()> {
3417        std::thread::spawn(move || {
3418            std::thread::sleep(RING_AFTER);
3419            waker.wake();
3420        })
3421    }
3422
3423    #[test]
3424    fn a_wake_from_another_thread_ends_a_long_wait() {
3425        let mut shell = HeadlessShell::new();
3426        let ringer = ring_later(shell.waker().expect("headless has a waker"));
3427        let start = std::time::Instant::now();
3428        shell.wait_events(Some(LONG_WAIT));
3429        let slept = start.elapsed();
3430        ringer.join().expect("the ringer");
3431        assert!(slept < LONG_WAIT / 2, "the wait ran {slept:?}");
3432    }
3433
3434    #[test]
3435    fn a_wake_before_the_wait_ends_it_at_once_and_wakes_coalesce() {
3436        let mut shell = HeadlessShell::new();
3437        let waker = shell.waker().expect("headless has a waker");
3438        for _ in 0..3 {
3439            waker.wake();
3440        }
3441        let start = std::time::Instant::now();
3442        shell.wait_events(Some(LONG_WAIT));
3443        assert!(
3444            start.elapsed() < LONG_WAIT / 2,
3445            "a rung bell must end the wait"
3446        );
3447
3448        // Three rings were one: the next wait sleeps out its timeout.
3449        let timeout = Duration::from_millis(100);
3450        let start = std::time::Instant::now();
3451        shell.wait_events(Some(timeout));
3452        assert!(
3453            start.elapsed() >= timeout / 2,
3454            "the bell was cleared, so this wait must sleep, but it took {:?}",
3455            start.elapsed()
3456        );
3457    }
3458
3459    #[test]
3460    fn with_a_waker_out_an_untimed_wait_sleeps_until_it_rings() {
3461        let mut shell = HeadlessShell::new();
3462        let ringer = ring_later(shell.waker().expect("headless has a waker"));
3463        let start = std::time::Instant::now();
3464        shell.wait_events(None);
3465        let slept = start.elapsed();
3466        ringer.join().expect("the ringer");
3467        assert!(
3468            slept >= RING_AFTER / 2,
3469            "an untimed wait returned after {slept:?}, before anything rang"
3470        );
3471    }
3472
3473    // ------------------------------------------------------------- clipboard
3474
3475    #[test]
3476    fn the_clipboard_round_trips_through_an_event() {
3477        let (mut shell, window) = shell_with_configured_window();
3478        shell
3479            .clipboard_offer(
3480                window,
3481                &[
3482                    ClipboardOffer::text("node"),
3483                    ClipboardOffer::ron("(kind:\"node\")"),
3484                ],
3485            )
3486            .expect("offer");
3487        assert_eq!(
3488            shell.clipboard_bytes(MimeType::TextUtf8),
3489            Some(&b"node"[..])
3490        );
3491
3492        let request = shell
3493            .clipboard_request(window, MimeType::CrcblRon)
3494            .expect("request");
3495        assert_eq!(
3496            drain(&mut shell)[0],
3497            ShellEvent::ClipboardData {
3498                window,
3499                request,
3500                mime: ReceivedMime::from(MimeType::CrcblRon),
3501                content: ClipboardContent::Bytes(b"(kind:\"node\")".to_vec()),
3502            }
3503        );
3504
3505        // A format nobody offered comes back `Empty` — an answer, and a
3506        // different one from "the read failed".
3507        let request = shell
3508            .clipboard_request(window, MimeType::UriList)
3509            .expect("request");
3510        assert_eq!(
3511            drain(&mut shell)[0],
3512            ShellEvent::ClipboardData {
3513                window,
3514                request,
3515                mime: ReceivedMime::from(MimeType::UriList),
3516                content: ClipboardContent::Empty,
3517            }
3518        );
3519
3520        // A published payload of zero bytes is a *successful* read of nothing,
3521        // which the old `Option<Vec<u8>>` shape could not say.
3522        shell
3523            .clipboard_offer(window, &[ClipboardOffer::text("")])
3524            .expect("offer");
3525        let request = shell
3526            .clipboard_request(window, MimeType::TextUtf8)
3527            .expect("request");
3528        assert_eq!(
3529            drain(&mut shell)[0],
3530            ShellEvent::ClipboardData {
3531                window,
3532                request,
3533                mime: ReceivedMime::from(MimeType::TextUtf8),
3534                content: ClipboardContent::Bytes(Vec::new()),
3535            }
3536        );
3537    }
3538
3539    #[test]
3540    fn a_read_taken_while_the_clipboard_is_unreadable_is_held_not_answered_empty() {
3541        // The Wayland focus gate, modelled. A consumer written only against a
3542        // clipboard that answers instantly cannot tell "empty" from "I have
3543        // not looked yet" — which is exactly what the seam's obligation 5
3544        // exists to stop backends doing, and what this shell can now reproduce
3545        // with no compositor in the room.
3546        let (mut shell, window) = shell_with_configured_window();
3547        shell
3548            .clipboard_offer(window, &[ClipboardOffer::text("published")])
3549            .expect("offer");
3550        shell.set_clipboard_readable(false);
3551        assert!(!shell.clipboard_readable(window));
3552
3553        let request = shell
3554            .clipboard_request(window, MimeType::TextUtf8)
3555            .expect("a read is accepted even when it cannot be answered yet");
3556        assert_eq!(shell.held_clipboard_reads(), 1);
3557        assert!(
3558            drain(&mut shell).is_empty(),
3559            "a held read produces no event at all — least of all an empty one"
3560        );
3561
3562        // Focus arrives. The *same* request answers; the caller asked once.
3563        shell.set_clipboard_readable(true);
3564        let events = drain(&mut shell);
3565        assert_eq!(events.len(), 1);
3566        assert_eq!(
3567            events[0],
3568            ShellEvent::ClipboardData {
3569                window,
3570                request,
3571                mime: ReceivedMime::from(MimeType::TextUtf8),
3572                content: ClipboardContent::Bytes(b"published".to_vec()),
3573            }
3574        );
3575        assert_eq!(shell.held_clipboard_reads(), 0);
3576    }
3577
3578    #[test]
3579    fn a_read_that_never_becomes_answerable_ends_as_unavailable() {
3580        // Obligation 4: every accepted request is answered, within a bounded
3581        // time. A request that stayed outstanding forever is a UI stuck on
3582        // "pasting…" that no consumer can detect.
3583        let (mut shell, window) = shell_with_configured_window();
3584        shell
3585            .clipboard_offer(window, &[ClipboardOffer::text("unreachable")])
3586            .expect("offer");
3587        shell.set_clipboard_readable(false);
3588        shell.clipboard_deadline(2);
3589
3590        let request = shell
3591            .clipboard_request(window, MimeType::TextUtf8)
3592            .expect("request");
3593        assert!(drain(&mut shell).is_empty(), "pump 1: still held");
3594        assert!(drain(&mut shell).is_empty(), "pump 2: still held");
3595        let events = drain(&mut shell);
3596        assert_eq!(
3597            events[0],
3598            ShellEvent::ClipboardData {
3599                window,
3600                request,
3601                mime: ReceivedMime::from(MimeType::TextUtf8),
3602                // Not `Empty`: there is something on the clipboard, and this
3603                // shell simply could not get at it.
3604                content: ClipboardContent::Unavailable,
3605            }
3606        );
3607        assert_eq!(shell.held_clipboard_reads(), 0);
3608    }
3609
3610    #[test]
3611    fn a_peer_that_holds_the_clipboard_and_never_delivers_is_unavailable() {
3612        // The other failure with no protocol event behind it: on Wayland the
3613        // transfer is a pipe another application writes, and an application
3614        // that takes the descriptor and hangs produces exactly this.
3615        let (mut shell, window) = shell_with_configured_window();
3616        shell.set_foreign_clipboard(vec![(
3617            ReceivedMime::new("text/plain"),
3618            b"never sent".to_vec(),
3619        )]);
3620        shell.set_clipboard_stalled(true);
3621        shell.clipboard_deadline(1);
3622        assert!(
3623            !shell.clipboard_readable(window),
3624            "a clipboard whose owner will not answer is not readable"
3625        );
3626
3627        let request = shell
3628            .clipboard_request(window, MimeType::TextUtf8)
3629            .expect("request");
3630        assert!(drain(&mut shell).is_empty());
3631        let events = drain(&mut shell);
3632        let ShellEvent::ClipboardData {
3633            request: answered,
3634            content,
3635            ..
3636        } = &events[0]
3637        else {
3638            panic!("wrong variant");
3639        };
3640        assert_eq!(*answered, request);
3641        assert_eq!(*content, ClipboardContent::Unavailable);
3642
3643        // The peer wakes up; a fresh read now succeeds.
3644        shell.set_clipboard_stalled(false);
3645        let request = shell
3646            .clipboard_request(window, MimeType::TextUtf8)
3647            .expect("request");
3648        assert_eq!(
3649            drain(&mut shell)[0],
3650            ShellEvent::ClipboardData {
3651                window,
3652                request,
3653                mime: ReceivedMime::new("text/plain"),
3654                content: ClipboardContent::Bytes(b"never sent".to_vec()),
3655            }
3656        );
3657    }
3658
3659    #[test]
3660    fn claiming_the_clipboard_can_require_a_user_interaction() {
3661        // Wayland's `set_selection` serial and the browser's user-gesture gate,
3662        // modelled as one thing — because they are one thing: a background
3663        // process must not be able to take the clipboard.
3664        let (mut shell, window) = shell_with_configured_window();
3665        shell.require_user_interaction(true);
3666
3667        let refused = shell.clipboard_offer(window, &[ClipboardOffer::text("stolen")]);
3668        assert!(
3669            matches!(
3670                refused,
3671                Err(ShellError::NeedsUserInteraction {
3672                    backend: ShellBackend::Headless,
3673                    ..
3674                })
3675            ),
3676            "{refused:?}"
3677        );
3678        assert!(
3679            shell.clipboard_bytes(MimeType::TextUtf8).is_none(),
3680            "a refused claim must not have taken the clipboard anyway"
3681        );
3682
3683        // The copy the user actually asked for. Any injected input counts, as
3684        // any real input serial would.
3685        shell.key_press(window, KeyCode::KeyC).expect("key");
3686        shell
3687            .clipboard_offer(window, &[ClipboardOffer::text("copied")])
3688            .expect("with a serial in hand, the claim goes through");
3689        assert_eq!(
3690            shell.clipboard_bytes(MimeType::TextUtf8),
3691            Some(&b"copied"[..])
3692        );
3693
3694        // And the permission goes stale, exactly as a serial does.
3695        shell.clear_user_interaction();
3696        assert!(matches!(
3697            shell.clipboard_offer(window, &[ClipboardOffer::text("later")]),
3698            Err(ShellError::NeedsUserInteraction { .. })
3699        ));
3700    }
3701
3702    #[test]
3703    fn a_held_read_for_a_destroyed_window_is_dropped_rather_than_answered() {
3704        // Obligation 1 outranks obligation 4: an answer naming a window that no
3705        // longer exists is a stale handle in a consumer's hands.
3706        let (mut shell, window) = shell_with_configured_window();
3707        shell.set_clipboard_readable(false);
3708        let _request = shell
3709            .clipboard_request(window, MimeType::TextUtf8)
3710            .expect("request");
3711        assert_eq!(shell.held_clipboard_reads(), 1);
3712
3713        shell.destroy_window(window).expect("destroy");
3714        let events = drain(&mut shell);
3715        assert_eq!(shell.held_clipboard_reads(), 0);
3716        assert!(
3717            !events
3718                .iter()
3719                .any(|event| matches!(event, ShellEvent::ClipboardData { .. })),
3720            "no clipboard answer may name a destroyed window: {events:?}"
3721        );
3722    }
3723
3724    #[test]
3725    fn a_paste_from_a_foreign_application_keeps_the_foreign_spelling() {
3726        let (mut shell, window) = shell_with_configured_window();
3727        shell.set_foreign_clipboard(vec![
3728            // Exactly what a GTK application offers, and a string no
3729            // `&'static str` in this crate could have held.
3730            (ReceivedMime::new("text/plain"), b"from gedit".to_vec()),
3731            (
3732                ReceivedMime::new("application/vnd.some-editor.scene+json"),
3733                b"{}".to_vec(),
3734            ),
3735        ]);
3736
3737        // Asking for our canonical spelling still finds it …
3738        let request = shell
3739            .clipboard_request(window, MimeType::TextUtf8)
3740            .expect("request");
3741        let events = drain(&mut shell);
3742        let ShellEvent::ClipboardData {
3743            request: answered,
3744            mime,
3745            content,
3746            ..
3747        } = &events[0]
3748        else {
3749            panic!("wrong variant");
3750        };
3751        assert_eq!(*answered, request);
3752        assert_eq!(content.bytes(), Some(&b"from gedit"[..]));
3753        // … and the answer reports what the peer actually called it.
3754        assert_eq!(mime.as_str(), "text/plain");
3755        assert!(mime.matches(MimeType::TextUtf8));
3756        assert_ne!(*mime, ReceivedMime::from(MimeType::TextUtf8));
3757
3758        // A format the engine has no name for is still reachable and still
3759        // reported verbatim.
3760        assert_eq!(
3761            shell.clipboard_bytes(MimeType::Other("application/vnd.some-editor.scene+json")),
3762            Some(&b"{}"[..])
3763        );
3764    }
3765
3766    // ------------------------------------------------------------------ panics
3767
3768    #[test]
3769    #[should_panic(expected = "a shell needs at least one monitor")]
3770    fn a_shell_with_no_monitors_is_rejected() {
3771        let _ = HeadlessShell::new().with_monitors(Vec::new());
3772    }
3773
3774    #[test]
3775    #[should_panic(expected = "a text commit is never empty")]
3776    fn an_empty_text_commit_is_rejected() {
3777        let (mut shell, window) = shell_with_configured_window();
3778        let _ = shell.commit_text(window, "");
3779    }
3780}