Skip to main content

crcbl_shell/
headless.rs

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