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