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