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