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    // ------------------------------------------------------------- activation
2250
2251    /// A backend with no activation tokens answers `None` for a live window
2252    /// and refuses a stale one, which is the trait's default.
2253    #[test]
2254    fn a_backend_without_activation_answers_none() {
2255        let (mut shell, window) = shell_with_window();
2256        assert_eq!(shell.activation_token(window).expect("a live window"), None);
2257        shell.destroy_window(window).expect("destroy");
2258        assert!(matches!(
2259            shell.activation_token(window),
2260            Err(ShellError::InvalidWindow { .. })
2261        ));
2262    }
2263
2264    // ------------------------------------------------------------ accessibility
2265
2266    /// A tree reaches the pretend reader only while it reads, and each start
2267    /// and stop is one event: what a program that sends every frame relies on.
2268    #[cfg(feature = "accesskit")]
2269    #[test]
2270    fn a_tree_reaches_the_reader_only_while_it_reads() {
2271        use accesskit::{Action, ActionRequest, Node, NodeId, Role, TreeId, TreeInfo, TreeUpdate};
2272
2273        let (mut shell, window) = shell_with_configured_window();
2274        let update = |label: &str| TreeUpdate {
2275            nodes: vec![(NodeId(1), {
2276                let mut node = Node::new(Role::Window);
2277                node.set_label(label);
2278                node
2279            })],
2280            tree: Some(TreeInfo::new(NodeId(1))),
2281            tree_id: TreeId::ROOT,
2282            focus: NodeId(1),
2283        };
2284
2285        shell
2286            .update_accessibility(window, update("unread"))
2287            .expect("update");
2288        assert_eq!(shell.accessibility_updates(window).expect("updates"), None);
2289
2290        shell.activate_accessibility(window).expect("activate");
2291        shell
2292            .activate_accessibility(window)
2293            .expect("activate twice");
2294        shell
2295            .update_accessibility(window, update("read"))
2296            .expect("update");
2297        let click = ActionRequest {
2298            action: Action::Click,
2299            target_tree: TreeId::ROOT,
2300            target_node: NodeId(1),
2301            data: None,
2302        };
2303        shell
2304            .request_accessibility_action(window, click.clone())
2305            .expect("action");
2306        assert_eq!(
2307            shell.accessibility_updates(window).expect("updates"),
2308            Some(&[update("read")][..])
2309        );
2310        let events = drain(&mut shell);
2311        assert_eq!(
2312            names(&events),
2313            ["AccessibilityActivated", "AccessibilityAction"]
2314        );
2315        assert_eq!(
2316            events[1],
2317            ShellEvent::AccessibilityAction {
2318                window,
2319                request: click
2320            }
2321        );
2322
2323        shell.deactivate_accessibility(window).expect("deactivate");
2324        shell
2325            .deactivate_accessibility(window)
2326            .expect("deactivate twice");
2327        shell
2328            .update_accessibility(window, update("gone"))
2329            .expect("update");
2330        assert_eq!(shell.accessibility_updates(window).expect("updates"), None);
2331        assert_eq!(names(&drain(&mut shell)), ["AccessibilityDeactivated"]);
2332    }
2333
2334    // ---------------------------------------------------------------- configure
2335
2336    #[test]
2337    fn a_new_window_has_no_size_until_a_later_pump() {
2338        let (mut shell, window) = shell_with_window();
2339
2340        // Nothing has been reported. Not "0x0", not "the requested size" —
2341        // nothing.
2342        let state = shell.window_state(window).expect("state");
2343        assert!(!state.is_configured());
2344        assert_eq!(state.size(), None);
2345        assert_eq!(state.scale_factor(), None);
2346        assert_eq!(state.effective_mode(), None);
2347        assert_eq!(state.requested_mode, DisplayMode::Windowed);
2348
2349        // The default delay is one pump: the first delivers nothing.
2350        assert_eq!(drain(&mut shell), Vec::new());
2351        assert!(!shell.window_state(window).expect("state").is_configured());
2352
2353        // The second delivers the configure.
2354        let events = drain(&mut shell);
2355        assert_eq!(
2356            events,
2357            vec![ShellEvent::Resized {
2358                window,
2359                size: PhysicalSize::new(1280, 720),
2360                scale_factor: 1.0,
2361            }]
2362        );
2363        let state = shell.window_state(window).expect("state");
2364        assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2365        assert_eq!(state.scale_factor(), Some(1.0));
2366        assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2367        assert!(state.mode_request_honoured());
2368    }
2369
2370    /// A loader's `keep_alive` is a turn of the window system that keeps what
2371    /// the turn produced: the configure comes due, and it and an event that was
2372    /// already waiting are delivered by the next pump, in order, once each.
2373    #[test]
2374    fn keep_alive_takes_a_turn_and_keeps_everything_for_the_next_pump() {
2375        let (mut shell, window) = shell_with_window();
2376        shell.inject(ShellEvent::CloseRequested { window });
2377
2378        // The configure waits this many turns and comes due on the next one.
2379        for _ in 0..=DEFAULT_CONFIGURE_DELAY {
2380            shell.keep_alive();
2381        }
2382        assert!(
2383            shell.window_state(window).expect("state").is_configured(),
2384            "every turn the configure waits for was taken by keep_alive"
2385        );
2386
2387        assert_eq!(
2388            drain(&mut shell),
2389            vec![
2390                ShellEvent::CloseRequested { window },
2391                ShellEvent::Resized {
2392                    window,
2393                    size: PhysicalSize::new(1280, 720),
2394                    scale_factor: 1.0,
2395                },
2396            ],
2397            "nothing keep_alive saw was delivered or dropped"
2398        );
2399        assert_eq!(drain(&mut shell), Vec::new(), "and each arrived once");
2400    }
2401
2402    #[test]
2403    fn a_consumer_that_skips_the_wait_gets_a_clean_error_not_a_wrong_extent() {
2404        let (mut shell, window) = shell_with_window();
2405
2406        // The failure mode this whole design exists to force.
2407        assert_eq!(
2408            create_swapchain(&shell.window_state(window).expect("state")),
2409            Err("window is not configured yet")
2410        );
2411
2412        // And the surface handle *is* available meanwhile — only the extent is
2413        // pending. The target carries no size at all, which is the stronger
2414        // version of the same guarantee: there is no plausible-looking number
2415        // in it for a consumer to mistake for the size the window is about to
2416        // have.
2417        let target = shell.surface_target(window).expect("the handle exists");
2418        assert_eq!(target, SurfaceTarget::Offscreen);
2419
2420        // The loop a consumer must write.
2421        let size = loop {
2422            shell.pump(&mut |_| {});
2423            if let Ok(size) = create_swapchain(&shell.window_state(window).expect("state")) {
2424                break size;
2425            }
2426        };
2427        assert_eq!(size, PhysicalSize::new(1280, 720));
2428        // Configuring the window did not change the target: a HAL surface made
2429        // from the pre-configure value is still describing the right object.
2430        assert_eq!(
2431            shell.surface_target(window).expect("target"),
2432            target,
2433            "the target is invariant under a configure"
2434        );
2435    }
2436
2437    #[test]
2438    fn the_configure_delay_is_a_knob() {
2439        let mut shell = HeadlessShell::new().with_configure_delay(3);
2440        let window = shell.create_window(&WindowDesc::default()).expect("window");
2441        for pump in 0..3 {
2442            assert_eq!(drain(&mut shell), Vec::new(), "pump {pump}");
2443            assert!(!shell.window_state(window).expect("state").is_configured());
2444        }
2445        assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2446        assert!(shell.window_state(window).expect("state").is_configured());
2447
2448        // Zero means "configured on the first pump", for tests about something
2449        // else.
2450        let mut shell = HeadlessShell::new().with_configure_delay(0);
2451        let window = shell.create_window(&WindowDesc::default()).expect("window");
2452        assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2453        assert!(shell.window_state(window).expect("state").is_configured());
2454
2455        shell.set_configure_delay(2);
2456        shell
2457            .set_mode(window, DisplayMode::Borderless { monitor: None })
2458            .expect("request");
2459        assert_eq!(drain(&mut shell), Vec::new());
2460        assert_eq!(drain(&mut shell), Vec::new());
2461        assert_eq!(names(&drain(&mut shell)), ["Resized"]);
2462    }
2463
2464    #[test]
2465    fn set_mode_is_a_request_whose_effect_arrives_later() {
2466        let (mut shell, window) = shell_with_configured_window();
2467        shell
2468            .set_mode(window, DisplayMode::Borderless { monitor: None })
2469            .expect("request");
2470
2471        // The request is a fact; the effect is not.
2472        let state = shell.window_state(window).expect("state");
2473        assert_eq!(
2474            state.requested_mode,
2475            DisplayMode::Borderless { monitor: None }
2476        );
2477        assert_eq!(
2478            state.effective_mode(),
2479            Some(DisplayMode::Windowed),
2480            "still windowed until the window system says otherwise"
2481        );
2482        assert!(
2483            !state.mode_request_honoured(),
2484            "a UI toggle reading the request instead of the effect would lie here"
2485        );
2486        assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2487
2488        settle(&mut shell);
2489        let state = shell.window_state(window).expect("state");
2490        assert_eq!(
2491            state.effective_mode(),
2492            Some(DisplayMode::Borderless { monitor: None })
2493        );
2494        assert!(state.mode_request_honoured());
2495        assert_eq!(state.size(), Some(PhysicalSize::new(1920, 1080)));
2496    }
2497
2498    #[test]
2499    fn a_refused_mode_is_expressible_through_injection() {
2500        // What a compositor that says no looks like: the request stands, and
2501        // the configure that arrives says something else.
2502        let (mut shell, window) = shell_with_configured_window();
2503        shell
2504            .set_mode(window, DisplayMode::Borderless { monitor: None })
2505            .expect("request");
2506        // The compositor answers with a windowed configure instead.
2507        shell
2508            .resize(window, PhysicalSize::new(1280, 720))
2509            .expect("answer");
2510        let state = shell.window_state(window).expect("state");
2511        assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
2512        assert!(!state.mode_request_honoured());
2513    }
2514
2515    /// A reconfigured panel keeps its new descriptor and is configured at its
2516    /// new size, reported by a `Resized` as its first size was; a change of
2517    /// namespace or monitor, a window that is no panel, a descriptor a new
2518    /// panel could not have and a stale handle are each refused, and a
2519    /// refusal changes nothing.
2520    #[test]
2521    fn a_reconfigured_panel_is_configured_at_its_new_size() {
2522        let mut shell = HeadlessShell::new();
2523        let monitor = shell.monitors()[0].id;
2524        let bar = crate::PanelDesc {
2525            namespace: "bar",
2526            monitor: Some(monitor),
2527            anchor: crate::PanelAnchor::TOP | crate::PanelAnchor::LEFT | crate::PanelAnchor::RIGHT,
2528            reserve: crate::Reserve::OwnExtent,
2529            ..crate::PanelDesc::default()
2530        };
2531        let window = shell
2532            .create_window(&WindowDesc {
2533                size: LogicalSize::new(400.0, 30.0),
2534                role: crate::WindowRole::Panel(bar),
2535                ..WindowDesc::default()
2536            })
2537            .expect("a panel");
2538        settle(&mut shell);
2539        let scale = shell
2540            .window_state(window)
2541            .expect("live")
2542            .scale_factor()
2543            .expect("configured");
2544
2545        let taller = crate::PanelDesc {
2546            layer: crate::PanelLayer::Overlay,
2547            margins: crate::Margins {
2548                top: 4.0,
2549                ..crate::Margins::default()
2550            },
2551            keyboard: crate::PanelKeyboard::OnDemand,
2552            ..bar
2553        };
2554        let size = LogicalSize::new(400.0, 50.0);
2555        shell.set_panel(window, size, &taller).expect("set_panel");
2556        assert_eq!(shell.panel(window).expect("live"), Some(taller));
2557        assert_eq!(
2558            shell.window_state(window).expect("live").size(),
2559            Some(LogicalSize::new(400.0, 30.0).to_physical(scale)),
2560            "a request: nothing changes before the window system answers"
2561        );
2562        let mut resized = Vec::new();
2563        for _ in 0..8 {
2564            resized.extend(
2565                drain(&mut shell)
2566                    .into_iter()
2567                    .filter_map(|event| match event {
2568                        ShellEvent::Resized {
2569                            window: of, size, ..
2570                        } if of == window => Some(size),
2571                        _ => None,
2572                    }),
2573            );
2574        }
2575        assert_eq!(resized, [size.to_physical(scale)]);
2576
2577        let refused = |shell: &mut HeadlessShell, panel: &crate::PanelDesc<'_>| match shell
2578            .set_panel(window, LogicalSize::new(1.0, 1.0), panel)
2579        {
2580            Err(ShellError::InvalidDescriptor(message)) => message,
2581            other => panic!("expected InvalidDescriptor, got {other:?}"),
2582        };
2583        let renamed = crate::PanelDesc {
2584            namespace: "dock",
2585            ..taller
2586        };
2587        assert!(refused(&mut shell, &renamed).contains("namespace"));
2588        let anywhere = crate::PanelDesc {
2589            monitor: None,
2590            ..taller
2591        };
2592        assert!(refused(&mut shell, &anywhere).contains("monitor"));
2593        let toplevel = shell
2594            .create_window(&WindowDesc::default())
2595            .expect("a toplevel");
2596        assert!(matches!(
2597            shell.set_panel(toplevel, size, &taller),
2598            Err(ShellError::InvalidDescriptor(message)) if message.contains("not a panel")
2599        ));
2600        let cornered = crate::PanelDesc {
2601            anchor: crate::PanelAnchor::TOP | crate::PanelAnchor::LEFT,
2602            ..taller
2603        };
2604        assert!(matches!(
2605            shell.set_panel(window, size, &cornered),
2606            Err(ShellError::Unsupported { .. })
2607        ));
2608        assert_eq!(
2609            shell.panel(window).expect("live"),
2610            Some(taller),
2611            "a refused descriptor is not recorded"
2612        );
2613        assert_eq!(shell.panel(toplevel).expect("live"), None);
2614
2615        shell.destroy_window(window).expect("destroy");
2616        assert!(matches!(
2617            shell.set_panel(window, size, &taller),
2618            Err(ShellError::InvalidWindow { .. })
2619        ));
2620    }
2621
2622    /// A repositioned popup is configured at its new size, as a window system
2623    /// answers a move; a window that is not a popup is refused.
2624    #[test]
2625    fn a_repositioned_popup_is_configured_at_its_new_size() {
2626        let (mut shell, window) = shell_with_configured_window();
2627        let anchor = crate::LogicalRect::new(
2628            crate::LogicalPoint::new(10.0, 10.0),
2629            LogicalSize::new(100.0, 20.0),
2630        );
2631        let popup = shell
2632            .create_window(&WindowDesc {
2633                size: LogicalSize::new(200.0, 150.0),
2634                role: crate::WindowRole::Popup(crate::PopupDesc {
2635                    parent: window,
2636                    anchor,
2637                    placement: crate::PopupPlacement::Below,
2638                    input: crate::PopupInput::Pointer,
2639                }),
2640                ..WindowDesc::default()
2641            })
2642            .expect("a popup");
2643        settle(&mut shell);
2644        let scale = shell
2645            .window_state(popup)
2646            .expect("live")
2647            .scale_factor()
2648            .expect("configured");
2649
2650        let moved = LogicalSize::new(240.0, 90.0);
2651        shell
2652            .reposition_popup(popup, anchor, moved)
2653            .expect("reposition");
2654        settle(&mut shell);
2655        assert_eq!(
2656            shell.window_state(popup).expect("live").size(),
2657            Some(moved.to_physical(scale))
2658        );
2659        assert!(matches!(
2660            shell.reposition_popup(window, anchor, moved),
2661            Err(ShellError::InvalidDescriptor(_))
2662        ));
2663    }
2664
2665    /// Escape on a grabbing popup is delivered and then asks the popup to
2666    /// close, once until answered; a popup that does not hold the keyboard
2667    /// only hears the key.
2668    #[test]
2669    fn escape_on_a_grabbing_popup_is_delivered_then_asks_it_to_close() {
2670        let (mut shell, window) = shell_with_configured_window();
2671        let popup_of = |shell: &mut HeadlessShell, input| {
2672            shell
2673                .create_window(&WindowDesc {
2674                    role: crate::WindowRole::Popup(crate::PopupDesc {
2675                        parent: window,
2676                        anchor: crate::LogicalRect::default(),
2677                        placement: crate::PopupPlacement::Below,
2678                        input,
2679                    }),
2680                    ..WindowDesc::default()
2681                })
2682                .expect("a popup")
2683        };
2684        let menu = popup_of(&mut shell, crate::PopupInput::Grab);
2685        let tooltip = popup_of(&mut shell, crate::PopupInput::Pointer);
2686        settle(&mut shell);
2687        let is_close = |event: &ShellEvent| matches!(event, ShellEvent::CloseRequested { .. });
2688
2689        shell.key_press(menu, KeyCode::Escape).expect("press");
2690        let events = drain(&mut shell);
2691        assert!(
2692            matches!(
2693                events.as_slice(),
2694                [
2695                    ShellEvent::Key {
2696                        key_code: Some(KeyCode::Escape),
2697                        ..
2698                    },
2699                    ShellEvent::CloseRequested { window: closed },
2700                ] if *closed == menu
2701            ),
2702            "the key, then the request: {events:?}"
2703        );
2704        // Unanswered, a second press and a repeat ask nothing more.
2705        shell.key_press(menu, KeyCode::Escape).expect("press");
2706        shell.key_repeat(menu, KeyCode::Escape).expect("repeat");
2707        assert!(!drain(&mut shell).iter().any(is_close));
2708
2709        // Kept open — backing out of a submenu — the next press asks again.
2710        shell
2711            .reply_close_request(menu, CloseReply::Keep)
2712            .expect("keep");
2713        shell.key_release(menu, KeyCode::Escape).expect("release");
2714        shell.key_press(menu, KeyCode::Enter).expect("press");
2715        assert!(!drain(&mut shell).iter().any(is_close), "not these");
2716        shell.key_press(menu, KeyCode::Escape).expect("press");
2717        assert!(drain(&mut shell).iter().any(is_close));
2718
2719        shell.key_press(tooltip, KeyCode::Escape).expect("press");
2720        shell.key_press(window, KeyCode::Escape).expect("press");
2721        assert!(!drain(&mut shell).iter().any(is_close));
2722    }
2723
2724    #[test]
2725    fn set_constraints_is_a_request_too() {
2726        let (mut shell, window) = shell_with_configured_window();
2727        let constraints = SizeConstraints::min(LogicalSize::new(1600.0, 900.0));
2728        shell.set_constraints(window, constraints).expect("request");
2729
2730        let state = shell.window_state(window).expect("state");
2731        assert_eq!(state.requested_constraints, constraints);
2732        assert_eq!(
2733            state.size(),
2734            Some(PhysicalSize::new(1280, 720)),
2735            "the size only changes when the window system says so"
2736        );
2737
2738        settle(&mut shell);
2739        assert_eq!(
2740            shell.window_state(window).expect("state").size(),
2741            Some(PhysicalSize::new(1600, 900))
2742        );
2743    }
2744
2745    #[test]
2746    fn constraints_do_not_cancel_a_mode_request_that_has_not_landed() {
2747        // Both are requests, and they are independent. Rebuilding the pending
2748        // configure from the *effective* configuration threw the mode away:
2749        // `requested_mode` stayed `Borderless`, `effective_mode` stayed
2750        // `Windowed`, and `mode_request_honoured` was false forever.
2751        let (mut shell, window) = shell_with_configured_window();
2752        shell
2753            .set_mode(window, DisplayMode::Borderless { monitor: None })
2754            .expect("mode request");
2755        shell
2756            .set_constraints(window, SizeConstraints::min(LogicalSize::new(320.0, 180.0)))
2757            .expect("constraint request");
2758        settle(&mut shell);
2759
2760        let state = shell.window_state(window).expect("state");
2761        assert_eq!(
2762            state.effective_mode(),
2763            Some(DisplayMode::Borderless { monitor: None })
2764        );
2765        assert!(state.mode_request_honoured());
2766    }
2767
2768    #[test]
2769    fn a_borderless_window_reports_its_own_monitors_scale() {
2770        // The scale used to be hoisted from `monitors[0]` above the match, so a
2771        // borderless window on a HiDPI secondary monitor took that monitor's
2772        // *size* and the primary's *scale* — a swapchain sized for one and a UI
2773        // laid out for the other.
2774        let mut shell = HeadlessShell::new();
2775        let hidpi = shell.plug_monitor(MonitorInfo {
2776            id: MonitorId(0),
2777            name: "HEADLESS-2".to_string(),
2778            bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2779            work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2780            scale_factor: 2.0,
2781            refresh_millihertz: 143_998,
2782            is_primary: false,
2783        });
2784        let window = shell
2785            .create_window(&WindowDesc {
2786                mode: DisplayMode::Borderless {
2787                    monitor: Some(hidpi),
2788                },
2789                ..WindowDesc::default()
2790            })
2791            .expect("window");
2792        settle(&mut shell);
2793
2794        let state = shell.window_state(window).expect("state");
2795        assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2796        assert_eq!(
2797            state.scale_factor(),
2798            Some(2.0),
2799            "the scale is the borderless monitor's, not the primary's"
2800        );
2801        // And `set_mode` onto the same monitor agrees, which is the pair that
2802        // used to disagree.
2803        let other = shell
2804            .create_window(&WindowDesc::default())
2805            .expect("second window");
2806        settle(&mut shell);
2807        shell
2808            .set_mode(
2809                other,
2810                DisplayMode::Borderless {
2811                    monitor: Some(hidpi),
2812                },
2813            )
2814            .expect("request");
2815        settle(&mut shell);
2816        assert_eq!(
2817            shell.window_state(other).expect("state").scale_factor(),
2818            shell.window_state(window).expect("state").scale_factor()
2819        );
2820    }
2821
2822    #[test]
2823    fn constraints_set_before_the_first_configure_fold_into_it() {
2824        let mut shell = HeadlessShell::new();
2825        let window = shell.create_window(&WindowDesc::default()).expect("window");
2826        shell
2827            .set_constraints(
2828                window,
2829                SizeConstraints::NONE.with_aspect(AspectRatio::SQUARE),
2830            )
2831            .expect("request");
2832        settle(&mut shell);
2833        // One configure, already square — not a 1280x720 configure followed by
2834        // a correction.
2835        assert_eq!(
2836            shell.window_state(window).expect("state").size(),
2837            Some(PhysicalSize::new(720, 720))
2838        );
2839    }
2840
2841    #[test]
2842    fn creation_uses_the_requested_size_and_the_monitors_scale() {
2843        let mut shell = HeadlessShell::new();
2844        let window = shell
2845            .create_window(&WindowDesc {
2846                size: LogicalSize::new(800.0, 600.0),
2847                ..WindowDesc::default()
2848            })
2849            .expect("window");
2850        settle(&mut shell);
2851        assert_eq!(
2852            shell.window_state(window).expect("state").size(),
2853            Some(PhysicalSize::new(800, 600))
2854        );
2855
2856        let mut shell = HeadlessShell::new().with_scale_factor(2.0);
2857        let window = shell
2858            .create_window(&WindowDesc {
2859                size: LogicalSize::new(640.0, 360.0),
2860                ..WindowDesc::default()
2861            })
2862            .expect("window");
2863        settle(&mut shell);
2864        let state = shell.window_state(window).expect("state");
2865        assert_eq!(state.size(), Some(PhysicalSize::new(1280, 720)));
2866        assert_eq!(state.scale_factor(), Some(2.0));
2867    }
2868
2869    // ------------------------------------------------------------ compositor side
2870
2871    #[test]
2872    fn scale_changes_preserve_logical_size_and_report_both_facts() {
2873        let (mut shell, window) = shell_with_configured_window();
2874        shell
2875            .change_scale_factor(window, 1.5)
2876            .expect("scale change");
2877        let events = drain(&mut shell);
2878        assert_eq!(
2879            events,
2880            vec![
2881                ShellEvent::ScaleFactorChanged {
2882                    window,
2883                    scale_factor: 1.5,
2884                    size: PhysicalSize::new(1920, 1080),
2885                },
2886                // The resize the scale change implies, in the same order both
2887                // Linux backends report it.
2888                ShellEvent::Resized {
2889                    window,
2890                    size: PhysicalSize::new(1920, 1080),
2891                    scale_factor: 1.5,
2892                },
2893            ]
2894        );
2895        // 1280x720 logical at 1.5 is 1920x1080 physical — the swapchain must be
2896        // recreated at the new size, which is the leak the DPI matrix test in
2897        // the windowing rules (`docs/notes/backends.md`) is looking for.
2898        assert_eq!(
2899            shell.window_state(window).expect("state").size(),
2900            Some(PhysicalSize::new(1920, 1080))
2901        );
2902    }
2903
2904    #[test]
2905    fn a_scale_change_scheduled_by_a_mode_switch_arrives_before_the_resize() {
2906        let mut shell = HeadlessShell::new();
2907        let hidpi = shell.plug_monitor(MonitorInfo {
2908            id: MonitorId(0),
2909            name: "HEADLESS-2".to_string(),
2910            bounds: PhysicalRect::new(1920, 0, 2560, 1440),
2911            work_area: PhysicalRect::new(1920, 0, 2560, 1440),
2912            scale_factor: 2.0,
2913            refresh_millihertz: 143_998,
2914            is_primary: false,
2915        });
2916        assert_eq!(hidpi, MonitorId(2), "ids are never reused");
2917        let window = shell.create_window(&WindowDesc::default()).expect("window");
2918        settle(&mut shell);
2919
2920        shell
2921            .set_mode(
2922                window,
2923                DisplayMode::Borderless {
2924                    monitor: Some(hidpi),
2925                },
2926            )
2927            .expect("request");
2928        assert_eq!(drain(&mut shell), Vec::new(), "still only a request");
2929        let events = drain(&mut shell);
2930        assert_eq!(names(&events), ["ScaleFactorChanged", "Resized"]);
2931
2932        let state = shell.window_state(window).expect("state");
2933        assert_eq!(state.size(), Some(PhysicalSize::new(2560, 1440)));
2934        assert_eq!(state.scale_factor(), Some(2.0));
2935        assert!(state.effective_mode().expect("configured").is_borderless());
2936        // A mode change is the one event the seam says can invalidate a target.
2937        // Here it does not, because this backend's target names nothing that a
2938        // mode switch recreates — and, crucially, it does not encode the new
2939        // size either.
2940        assert_eq!(
2941            shell.surface_target(window).expect("target"),
2942            SurfaceTarget::Offscreen
2943        );
2944
2945        assert!(matches!(
2946            shell.set_mode(
2947                window,
2948                DisplayMode::Borderless {
2949                    monitor: Some(MonitorId(99)),
2950                },
2951            ),
2952            Err(ShellError::NoSuchMonitor(99))
2953        ));
2954    }
2955
2956    #[test]
2957    fn resizes_honour_constraints_and_inject_bypasses_them() {
2958        let (mut shell, window) = shell_with_configured_window();
2959        shell
2960            .set_constraints(
2961                window,
2962                SizeConstraints::min(LogicalSize::new(640.0, 360.0))
2963                    .with_aspect(AspectRatio::WIDESCREEN),
2964            )
2965            .expect("request");
2966        settle(&mut shell);
2967
2968        shell
2969            .resize(window, PhysicalSize::new(1000, 1000))
2970            .expect("resize");
2971        assert_eq!(
2972            drain(&mut shell),
2973            vec![ShellEvent::Resized {
2974                window,
2975                size: PhysicalSize::new(1000, 562),
2976                scale_factor: 1.0,
2977            }]
2978        );
2979
2980        // A tiling window manager ignores every hint, and `inject` is how that
2981        // gets tested.
2982        shell.inject(ShellEvent::Resized {
2983            window,
2984            size: PhysicalSize::new(300, 900),
2985            scale_factor: 1.0,
2986        });
2987        assert_eq!(
2988            drain(&mut shell)[0],
2989            ShellEvent::Resized {
2990                window,
2991                size: PhysicalSize::new(300, 900),
2992                scale_factor: 1.0,
2993            }
2994        );
2995    }
2996
2997    #[test]
2998    fn a_compositor_configure_supersedes_a_pending_one() {
2999        let (mut shell, window) = shell_with_configured_window();
3000        shell
3001            .set_mode(window, DisplayMode::Borderless { monitor: None })
3002            .expect("request");
3003        // The compositor answers before the scheduled configure lands.
3004        shell
3005            .resize(window, PhysicalSize::new(640, 480))
3006            .expect("answer");
3007        assert_eq!(names(&drain(&mut shell)), ["Resized"]);
3008        assert_eq!(
3009            shell.window_state(window).expect("state").size(),
3010            Some(PhysicalSize::new(640, 480))
3011        );
3012        // And nothing else arrives later.
3013        assert_eq!(drain(&mut shell), Vec::new());
3014    }
3015
3016    // ------------------------------------------------------------------- input
3017
3018    #[test]
3019    fn timestamps_come_from_the_shells_clock_not_from_the_pump() {
3020        let (mut shell, window) = shell_with_configured_window();
3021        shell.set_time(Duration::from_secs(2));
3022        shell.key_press(window, KeyCode::Space).expect("press");
3023        shell.advance(Duration::from_millis(180));
3024        shell.key_release(window, KeyCode::Space).expect("release");
3025
3026        let events = drain(&mut shell);
3027        let held = events[1]
3028            .time()
3029            .expect("input")
3030            .saturating_since(events[0].time().expect("input"));
3031        assert_eq!(held, Duration::from_millis(180));
3032        assert_eq!(events[0].time(), Some(EventTime::from_millis(2_000)));
3033    }
3034
3035    /// A scheduled script runs at its pump, not before, and what it injects
3036    /// is stamped with the clock as it stands then.
3037    #[test]
3038    fn a_scheduled_script_injects_at_its_pump_with_that_pumps_time() {
3039        let (mut shell, window) = shell_with_configured_window();
3040        shell.schedule(2, move |shell| {
3041            shell.key_press(window, KeyCode::Space).expect("press");
3042        });
3043
3044        for pump in 0..2 {
3045            shell.advance(Duration::from_millis(16));
3046            assert_eq!(drain(&mut shell), Vec::new(), "pump {pump} came early");
3047        }
3048        shell.set_time(Duration::from_secs(3));
3049        let events = drain(&mut shell);
3050        assert_eq!(names(&events), ["Key"]);
3051        assert_eq!(events[0].time(), Some(EventTime::from_millis(3_000)));
3052        assert_eq!(drain(&mut shell), Vec::new(), "and it ran once");
3053    }
3054
3055    /// Scripts due together run in the order scheduled; one a script
3056    /// schedules at zero waits for the pump after.
3057    #[test]
3058    fn scheduled_scripts_run_in_order_and_a_nested_one_waits_a_pump() {
3059        let (mut shell, window) = shell_with_configured_window();
3060        shell.schedule(0, move |shell| {
3061            shell.key_press(window, KeyCode::KeyA).expect("press");
3062            shell.schedule(0, move |shell| {
3063                shell.key_press(window, KeyCode::KeyC).expect("press");
3064            });
3065        });
3066        shell.schedule(0, move |shell| {
3067            shell.key_press(window, KeyCode::KeyB).expect("press");
3068        });
3069
3070        let keys = |events: Vec<ShellEvent>| -> Vec<Option<KeyCode>> {
3071            events
3072                .into_iter()
3073                .map(|event| match event {
3074                    ShellEvent::Key { key_code, .. } => key_code,
3075                    other => panic!("not a key: {other:?}"),
3076                })
3077                .collect()
3078        };
3079        assert_eq!(
3080            keys(drain(&mut shell)),
3081            [Some(KeyCode::KeyA), Some(KeyCode::KeyB)]
3082        );
3083        assert_eq!(keys(drain(&mut shell)), [Some(KeyCode::KeyC)]);
3084    }
3085
3086    /// `keep_alive` is a turn for a script as for a configure: it runs
3087    /// there, and the next pump delivers what it injected.
3088    #[test]
3089    fn keep_alive_runs_a_due_script_and_keeps_its_events() {
3090        let (mut shell, window) = shell_with_configured_window();
3091        shell.schedule(1, move |shell| {
3092            shell.key_press(window, KeyCode::Enter).expect("press");
3093        });
3094        shell.keep_alive();
3095        shell.keep_alive();
3096        assert_eq!(shell.pending_events(), 1, "the script ran in keep_alive");
3097        assert_eq!(names(&drain(&mut shell)), ["Key"]);
3098    }
3099
3100    /// A wait while a script is scheduled returns at once: only a pump brings
3101    /// the script closer, so sleeping would only delay it, and an untimed
3102    /// wait with a waker out would never end.
3103    #[test]
3104    fn a_wait_does_not_sleep_while_a_script_is_scheduled() {
3105        let (mut shell, window) = shell_with_configured_window();
3106        shell.schedule(1, move |shell| {
3107            shell.key_press(window, KeyCode::Space).expect("press");
3108        });
3109        let start = std::time::Instant::now();
3110        shell.wait_events(Some(LONG_WAIT));
3111        assert!(
3112            start.elapsed() < LONG_WAIT / 2,
3113            "the wait slept {:?}",
3114            start.elapsed()
3115        );
3116    }
3117
3118    #[test]
3119    fn synthesized_key_fields_are_stable_and_plausible() {
3120        let (mut shell, window) = shell_with_configured_window();
3121        shell.set_modifiers(Modifiers::CTRL);
3122        shell.key_press(window, KeyCode::KeyS).expect("press");
3123        let events = drain(&mut shell);
3124        let ShellEvent::Key {
3125            scancode,
3126            key_code,
3127            keysym,
3128            modifiers,
3129            repeat,
3130            device,
3131            ..
3132        } = events[0].clone()
3133        else {
3134            panic!("wrong variant");
3135        };
3136        assert_eq!(key_code, Some(KeyCode::KeyS));
3137        assert_eq!(keysym, Keysym::from_char('s'));
3138        assert_eq!(modifiers, Modifiers::CTRL);
3139        assert!(!repeat);
3140        assert_eq!(device, VIRTUAL_DEVICE);
3141        assert!(scancode.0 >= 8, "evdev-shaped scancodes start at 8");
3142        assert_eq!(synthetic_keysym(KeyCode::Digit7), Keysym::from_char('7'));
3143        assert_eq!(synthetic_keysym(KeyCode::F5), Keysym(0xFFC2));
3144        assert_eq!(synthetic_keysym(KeyCode::Enter), Keysym(0xFF0D));
3145        assert_eq!(synthetic_keysym(KeyCode::Escape), Keysym(0xFF1B));
3146        assert_eq!(synthetic_keysym(KeyCode::ArrowLeft), Keysym(0xFF51));
3147        assert_eq!(synthetic_keysym(KeyCode::Slash), Keysym::from_char('/'));
3148
3149        shell.key_repeat(window, KeyCode::KeyS).expect("repeat");
3150        let events = drain(&mut shell);
3151        let ShellEvent::Key { repeat, .. } = events[0] else {
3152            panic!("wrong variant");
3153        };
3154        assert!(repeat);
3155    }
3156
3157    #[test]
3158    fn a_locked_pointer_reports_no_absolute_position() {
3159        let (mut shell, window) = shell_with_configured_window();
3160        shell
3161            .set_pointer_mode(window, PointerMode::Locked)
3162            .expect("lock");
3163        shell
3164            .move_pointer(window, PhysicalPoint::new(10.0, 10.0), (3.0, -4.0))
3165            .expect("motion");
3166        let events = drain(&mut shell);
3167        let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
3168            panic!("wrong variant");
3169        };
3170        assert_eq!(abs, None);
3171        assert_eq!(raw_delta, Some((3.0, -4.0)));
3172    }
3173
3174    #[test]
3175    fn a_backend_without_raw_motion_reports_none() {
3176        let mut shell = HeadlessShell::new()
3177            .with_caps(ShellCaps::DESKTOP - ShellCaps::RAW_POINTER_MOTION)
3178            .with_configure_delay(0);
3179        let window = shell.create_window(&WindowDesc::default()).expect("window");
3180        drain(&mut shell);
3181        shell
3182            .move_pointer(window, PhysicalPoint::new(1.0, 2.0), (5.0, 5.0))
3183            .expect("motion");
3184        let events = drain(&mut shell);
3185        let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
3186            panic!("wrong variant");
3187        };
3188        assert_eq!(abs, Some(PhysicalPoint::new(1.0, 2.0)));
3189        assert_eq!(raw_delta, None, "no relative-motion source");
3190        assert!(!shell.caps().has_mouselook());
3191    }
3192
3193    #[test]
3194    fn pointer_focus_and_scroll_and_text_are_injectable() {
3195        let (mut shell, window) = shell_with_configured_window();
3196        shell
3197            .set_pointer_focus(window, true, Some(PhysicalPoint::new(1.0, 1.0)))
3198            .expect("enter");
3199        shell
3200            .scroll(window, ScrollDelta::Lines { x: 0.0, y: -1.0 }, None)
3201            .expect("scroll");
3202        shell
3203            .button(
3204                window,
3205                PointerButton::Left,
3206                ButtonState::Pressed,
3207                Some(PhysicalPoint::new(1.0, 1.0)),
3208            )
3209            .expect("click");
3210        shell.commit_text(window, "ありがとう").expect("text");
3211        shell
3212            .set_pointer_focus(window, false, Some(PhysicalPoint::new(1.0, 1.0)))
3213            .expect("leave");
3214
3215        let events = drain(&mut shell);
3216        assert_eq!(
3217            names(&events),
3218            [
3219                "PointerFocus",
3220                "Wheel",
3221                "Button",
3222                "TextCommit",
3223                "PointerFocus"
3224            ]
3225        );
3226        let ShellEvent::PointerFocus {
3227            entered, position, ..
3228        } = &events[4]
3229        else {
3230            panic!("wrong variant");
3231        };
3232        assert!(!entered);
3233        assert_eq!(*position, None, "a leave has no position");
3234        assert!(events.iter().all(ShellEvent::is_input));
3235    }
3236
3237    /// Two fingers script as two contacts, and a touchless shell has none.
3238    ///
3239    /// The second half is the one worth having: a backend with the capability
3240    /// clear is a platform where a finger *cannot arrive*, and a script that
3241    /// could inject one anyway would let a test assert a game works on a
3242    /// touchscreen the shell does not have.
3243    #[test]
3244    fn contacts_are_injectable_and_a_touchless_shell_refuses_them() {
3245        let (mut shell, window) = shell_with_configured_window();
3246        assert!(shell.caps().contains(ShellCaps::TOUCH));
3247        for (contact, phase, x) in [
3248            (1, TouchPhase::Began, 10.0),
3249            (2, TouchPhase::Began, 200.0),
3250            (2, TouchPhase::Moved, 220.0),
3251            (1, TouchPhase::Ended, 10.0),
3252            (2, TouchPhase::Cancelled, 220.0),
3253        ] {
3254            shell
3255                .touch(
3256                    window,
3257                    ContactId(contact),
3258                    phase,
3259                    PhysicalPoint::new(x, 5.0),
3260                )
3261                .expect("a contact");
3262        }
3263        let events = drain(&mut shell);
3264        assert_eq!(names(&events), ["Touch"; 5]);
3265        let seen: Vec<(ContactId, TouchPhase, f64)> = events
3266            .iter()
3267            .map(|event| {
3268                let ShellEvent::Touch {
3269                    contact,
3270                    phase,
3271                    position,
3272                    ..
3273                } = event
3274                else {
3275                    panic!("wrong variant");
3276                };
3277                (*contact, *phase, position.x)
3278            })
3279            .collect();
3280        assert_eq!(
3281            seen,
3282            vec![
3283                (ContactId(1), TouchPhase::Began, 10.0),
3284                (ContactId(2), TouchPhase::Began, 200.0),
3285                (ContactId(2), TouchPhase::Moved, 220.0),
3286                (ContactId(1), TouchPhase::Ended, 10.0),
3287                (ContactId(2), TouchPhase::Cancelled, 220.0),
3288            ]
3289        );
3290        assert!(events.iter().all(ShellEvent::is_input));
3291
3292        let mut touchless = HeadlessShell::new()
3293            .with_caps(ShellCaps::DESKTOP - ShellCaps::TOUCH)
3294            .with_configure_delay(0);
3295        let window = touchless
3296            .create_window(&WindowDesc::default())
3297            .expect("window");
3298        drain(&mut touchless);
3299        assert!(matches!(
3300            touchless.touch(
3301                window,
3302                ContactId(1),
3303                TouchPhase::Began,
3304                PhysicalPoint::ORIGIN
3305            ),
3306            Err(ShellError::Unsupported { .. })
3307        ));
3308        assert!(drain(&mut touchless).is_empty(), "and queued nothing");
3309    }
3310
3311    #[test]
3312    fn warping_produces_motion_like_every_platform_that_has_it() {
3313        let (mut shell, window) = shell_with_configured_window();
3314        shell
3315            .warp_pointer(window, PhysicalPoint::new(320.0, 180.0))
3316            .expect("warp");
3317        let events = drain(&mut shell);
3318        let ShellEvent::PointerMotion { abs, raw_delta, .. } = events[0] else {
3319            panic!("wrong variant");
3320        };
3321        assert_eq!(abs, Some(PhysicalPoint::new(320.0, 180.0)));
3322        assert_eq!(raw_delta, None, "a warp is not physical motion");
3323    }
3324
3325    // ------------------------------------------------------------- lifecycle
3326
3327    #[test]
3328    fn capabilities_gate_the_operations_that_need_them() {
3329        let mut shell = HeadlessShell::new().with_caps(ShellCaps::empty());
3330        let window = shell.create_window(&WindowDesc::default()).expect("window");
3331
3332        assert!(matches!(
3333            shell.warp_pointer(window, PhysicalPoint::ORIGIN),
3334            Err(ShellError::Unsupported { .. })
3335        ));
3336        assert!(matches!(
3337            shell.set_pointer_mode(window, PointerMode::Locked),
3338            Err(ShellError::Unsupported { .. })
3339        ));
3340        assert!(matches!(
3341            shell.clipboard_request(window, MimeType::TextUtf8),
3342            Err(ShellError::Unsupported { .. })
3343        ));
3344        // Free needs no capability, so it always works.
3345        shell
3346            .set_pointer_mode(window, PointerMode::Free)
3347            .expect("Free is always available");
3348        // And a second window is refused without MULTI_WINDOW.
3349        assert!(matches!(
3350            shell.create_window(&WindowDesc::default()),
3351            Err(ShellError::Unsupported { .. })
3352        ));
3353    }
3354
3355    #[test]
3356    fn stale_handles_fail_cleanly_on_every_accessor() {
3357        let (mut shell, window) = shell_with_configured_window();
3358        shell.destroy_window(window).expect("destroy");
3359        assert_eq!(
3360            drain(&mut shell),
3361            vec![ShellEvent::WindowDestroyed { window }]
3362        );
3363
3364        assert!(matches!(
3365            shell.window_state(window),
3366            Err(ShellError::InvalidWindow { .. })
3367        ));
3368        assert!(matches!(
3369            shell.surface_target(window),
3370            Err(ShellError::InvalidWindow { .. })
3371        ));
3372        assert!(matches!(
3373            shell.resize(window, PhysicalSize::new(1, 1)),
3374            Err(ShellError::InvalidWindow { .. })
3375        ));
3376        assert!(matches!(
3377            shell.set_title(window, "gone"),
3378            Err(ShellError::InvalidWindow { .. })
3379        ));
3380        assert!(matches!(
3381            shell.destroy_window(window),
3382            Err(ShellError::InvalidWindow { .. })
3383        ));
3384        assert!(matches!(
3385            shell.key_press(window, KeyCode::KeyA),
3386            Err(ShellError::InvalidWindow { .. })
3387        ));
3388    }
3389
3390    #[test]
3391    fn destroying_a_window_cancels_its_pending_configure() {
3392        // The bug a naive delayed-configure implementation has: an event for a
3393        // window that no longer exists.
3394        let (mut shell, window) = shell_with_window();
3395        shell.destroy_window(window).expect("destroy");
3396        assert_eq!(names(&drain(&mut shell)), ["WindowDestroyed"]);
3397        assert_eq!(drain(&mut shell), Vec::new());
3398        assert_eq!(drain(&mut shell), Vec::new());
3399    }
3400
3401    #[test]
3402    fn close_requests_are_questions() {
3403        let (mut shell, window) = shell_with_configured_window();
3404        assert!(matches!(
3405            shell.reply_close_request(window, CloseReply::Close),
3406            Err(ShellError::NoPendingCloseRequest { .. })
3407        ));
3408
3409        shell.request_close(window).expect("request");
3410        assert!(shell.window_state(window).expect("state").close_pending);
3411        shell
3412            .reply_close_request(window, CloseReply::Keep)
3413            .expect("keep");
3414        assert!(!shell.window_state(window).expect("state").close_pending);
3415        assert!(shell.window_state(window).is_ok(), "still open");
3416
3417        shell.request_close(window).expect("request again");
3418        shell
3419            .reply_close_request(window, CloseReply::Close)
3420            .expect("close");
3421        assert!(matches!(
3422            shell.window_state(window),
3423            Err(ShellError::InvalidWindow { .. })
3424        ));
3425    }
3426
3427    #[test]
3428    fn monitor_hotplug_reports_a_change_and_keeps_ids_unique() {
3429        let mut shell = HeadlessShell::new();
3430        let extra = shell.plug_monitor(MonitorInfo {
3431            id: MonitorId(0),
3432            name: "HEADLESS-2".to_string(),
3433            bounds: PhysicalRect::new(1920, 0, 1920, 1080),
3434            work_area: PhysicalRect::new(1920, 0, 1920, 1080),
3435            scale_factor: 1.0,
3436            refresh_millihertz: 60_000,
3437            is_primary: false,
3438        });
3439        assert_eq!(shell.monitors().len(), 2);
3440        assert_eq!(drain(&mut shell), vec![ShellEvent::MonitorsChanged]);
3441
3442        shell.unplug_monitor(extra).expect("unplug");
3443        assert_eq!(shell.monitors().len(), 1);
3444        assert_eq!(
3445            drain(&mut shell),
3446            vec![
3447                ShellEvent::MonitorRemoved { monitor: extra },
3448                ShellEvent::MonitorsChanged
3449            ]
3450        );
3451        assert!(matches!(
3452            shell.unplug_monitor(extra),
3453            Err(ShellError::NoSuchMonitor(_))
3454        ));
3455        assert_eq!(
3456            shell.monitor(MonitorId(1)).map(|m| m.name.as_str()),
3457            Some("HEADLESS-1")
3458        );
3459        assert!(shell.monitor(extra).is_none());
3460    }
3461
3462    #[test]
3463    fn unplugging_a_monitor_asks_its_panels_and_only_its_panels_to_close() {
3464        let mut shell = HeadlessShell::new();
3465        let second = shell.plug_monitor(MonitorInfo {
3466            id: MonitorId(0),
3467            name: "HEADLESS-2".to_string(),
3468            bounds: PhysicalRect::new(1920, 0, 1920, 1080),
3469            work_area: PhysicalRect::new(1920, 0, 1920, 1080),
3470            scale_factor: 1.0,
3471            refresh_millihertz: 60_000,
3472            is_primary: false,
3473        });
3474        let first = shell.monitors()[0].id;
3475        let panel_on = |shell: &mut HeadlessShell, monitor| {
3476            shell
3477                .create_window(&WindowDesc {
3478                    role: crate::WindowRole::Panel(crate::PanelDesc {
3479                        monitor,
3480                        ..crate::PanelDesc::default()
3481                    }),
3482                    ..WindowDesc::default()
3483                })
3484                .expect("panel")
3485        };
3486        let doomed = panel_on(&mut shell, Some(second));
3487        let already_asked = panel_on(&mut shell, Some(second));
3488        let elsewhere = panel_on(&mut shell, Some(first));
3489        let anywhere = panel_on(&mut shell, None);
3490        let toplevel = shell.create_window(&WindowDesc::default()).expect("window");
3491        shell.request_close(already_asked).expect("close");
3492        settle(&mut shell);
3493        drain(&mut shell);
3494
3495        shell.unplug_monitor(second).expect("unplug");
3496        assert_eq!(
3497            drain(&mut shell),
3498            vec![
3499                ShellEvent::MonitorRemoved { monitor: second },
3500                ShellEvent::CloseRequested { window: doomed },
3501                ShellEvent::MonitorsChanged,
3502            ]
3503        );
3504        for window in [elsewhere, anywhere, toplevel] {
3505            assert!(!shell.window_state(window).expect("open").close_pending);
3506        }
3507    }
3508
3509    #[test]
3510    fn drops_require_both_the_capability_and_the_opt_in() {
3511        let mut shell = HeadlessShell::new();
3512        let plain = shell.create_window(&WindowDesc::default()).expect("window");
3513        assert!(matches!(
3514            shell.drop_file(plain, "/tmp/a.gltf", None),
3515            Err(ShellError::Unsupported { .. })
3516        ));
3517
3518        let dropper = shell
3519            .create_window(&WindowDesc {
3520                accept_drops: true,
3521                ..WindowDesc::default()
3522            })
3523            .expect("window");
3524        settle(&mut shell);
3525        shell
3526            .drop_file(dropper, "/tmp/a.gltf", Some(PhysicalPoint::new(4.0, 5.0)))
3527            .expect("drop");
3528        assert_eq!(names(&drain(&mut shell))[0], "DroppedFile");
3529    }
3530
3531    #[test]
3532    fn a_sink_that_queues_more_events_does_not_spin_the_pump() {
3533        let (mut shell, window) = shell_with_configured_window();
3534        shell.inject(ShellEvent::MonitorsChanged);
3535        let mut extra = Vec::new();
3536        shell.pump(&mut |event| extra.push(event));
3537        assert_eq!(extra.len(), 1);
3538        shell.inject(ShellEvent::CloseRequested { window });
3539        assert_eq!(shell.pending_events(), 1);
3540    }
3541
3542    #[test]
3543    fn accessors_that_no_real_backend_can_provide_are_headless_only() {
3544        let (mut shell, window) = shell_with_configured_window();
3545        shell.set_title(window, "renamed").expect("title");
3546        assert_eq!(shell.title(window).expect("title"), "renamed");
3547        shell.set_cursor(window, None).expect("hide");
3548        assert_eq!(shell.cursor(window).expect("cursor"), None);
3549        shell
3550            .set_cursor(window, Some(CursorIcon::Crosshair))
3551            .expect("shape");
3552        assert_eq!(
3553            shell.cursor(window).expect("cursor"),
3554            Some(CursorIcon::Crosshair)
3555        );
3556        shell.set_visible(window, false).expect("hide window");
3557        assert!(!shell.window_state(window).expect("state").visible);
3558    }
3559
3560    #[test]
3561    fn an_input_region_is_recorded_replaced_and_given_back() {
3562        let (mut shell, window) = shell_with_configured_window();
3563        assert_eq!(shell.input_region(window).expect("region"), None);
3564        let panel = PhysicalRect::new(0, 0, 400, 300).rounded(12);
3565        shell
3566            .set_input_region(window, Some(&panel))
3567            .expect("a region");
3568        assert_eq!(
3569            shell.input_region(window).expect("region"),
3570            Some(panel.as_slice())
3571        );
3572        shell.set_input_region(window, Some(&[])).expect("nothing");
3573        assert_eq!(shell.input_region(window).expect("region"), Some(&[][..]));
3574        shell.set_input_region(window, None).expect("whole window");
3575        assert_eq!(shell.input_region(window).expect("region"), None);
3576        shell.destroy_window(window).expect("destroy");
3577        assert!(matches!(
3578            shell.set_input_region(window, None),
3579            Err(ShellError::InvalidWindow { .. })
3580        ));
3581        assert!(shell.input_region(window).is_err());
3582    }
3583
3584    fn frames_for(events: &[ShellEvent], window: WindowId) -> usize {
3585        events
3586            .iter()
3587            .filter(|event| matches!(event, ShellEvent::FrameReady { window: w } if *w == window))
3588            .count()
3589    }
3590
3591    #[test]
3592    fn requests_for_a_frame_share_one_answer_at_the_next_pump() {
3593        let (mut shell, window) = shell_with_configured_window();
3594        assert!(!shell.frame_requested(window).expect("live"));
3595        shell.request_frame(window).expect("a frame");
3596        shell.request_frame(window).expect("the same frame");
3597        assert!(shell.frame_requested(window).expect("live"));
3598        let events = drain(&mut shell);
3599        assert_eq!(frames_for(&events, window), 1, "{:?}", names(&events));
3600        assert!(!shell.frame_requested(window).expect("live"));
3601        assert_eq!(
3602            frames_for(&drain(&mut shell), window),
3603            0,
3604            "nothing asked again"
3605        );
3606        shell.request_frame(window).expect("the next frame");
3607        assert_eq!(frames_for(&drain(&mut shell), window), 1);
3608    }
3609
3610    #[test]
3611    fn a_window_destroyed_with_a_frame_requested_is_not_answered() {
3612        let (mut shell, doomed) = shell_with_configured_window();
3613        let kept = shell
3614            .create_window(&WindowDesc::default())
3615            .expect("a second window");
3616        settle(&mut shell);
3617        shell.request_frame(doomed).expect("a frame");
3618        shell.request_frame(kept).expect("a frame");
3619        shell.destroy_window(doomed).expect("destroy");
3620        let events = drain(&mut shell);
3621        assert_eq!(frames_for(&events, doomed), 0, "{:?}", names(&events));
3622        assert_eq!(frames_for(&events, kept), 1, "the other window still asked");
3623        assert!(matches!(
3624            shell.request_frame(doomed),
3625            Err(ShellError::InvalidWindow { .. })
3626        ));
3627        assert!(shell.frame_requested(doomed).is_err());
3628    }
3629
3630    #[test]
3631    fn a_wait_does_not_sleep_through_a_frame_that_is_due() {
3632        let (mut shell, window) = shell_with_configured_window();
3633        shell.request_frame(window).expect("a frame");
3634        let start = std::time::Instant::now();
3635        shell.wait_events(Some(LONG_WAIT));
3636        assert!(
3637            start.elapsed() < LONG_WAIT / 2,
3638            "slept {:?} with a frame due",
3639            start.elapsed()
3640        );
3641        assert_eq!(frames_for(&drain(&mut shell), window), 1);
3642    }
3643
3644    #[test]
3645    fn an_output_that_is_off_holds_frame_requests_until_it_is_on() {
3646        let (mut shell, window) = shell_with_configured_window();
3647        shell.set_outputs_powered(false);
3648        shell.request_frame(window).expect("a frame");
3649        assert_eq!(frames_for(&drain(&mut shell), window), 0, "held");
3650        assert!(shell.frame_requested(window).expect("live"), "still owed");
3651
3652        let wait = Duration::from_millis(20);
3653        let start = std::time::Instant::now();
3654        shell.wait_events(Some(wait));
3655        assert!(
3656            start.elapsed() >= wait,
3657            "a held request cut the wait short after {:?}",
3658            start.elapsed()
3659        );
3660
3661        shell.set_outputs_powered(true);
3662        assert_eq!(
3663            frames_for(&drain(&mut shell), window),
3664            1,
3665            "answered once on"
3666        );
3667        assert!(!shell.frame_requested(window).expect("live"));
3668    }
3669
3670    #[test]
3671    fn waiting_is_observable_and_focus_transitions_are_events() {
3672        let (mut shell, window) = shell_with_configured_window();
3673        assert_eq!(shell.wait_count(), 0);
3674        shell.wait_events(Some(Duration::from_millis(5)));
3675        shell.wait_events(None);
3676        assert_eq!(shell.wait_count(), 2);
3677
3678        shell.set_focus(window, true).expect("focus");
3679        assert!(shell.window_state(window).expect("state").focused);
3680        shell.set_focus(window, false).expect("unfocus");
3681        assert_eq!(names(&drain(&mut shell)), ["Focus", "Focus"]);
3682    }
3683
3684    // ----------------------------------------------------------------- waker
3685
3686    /// Longer than any of these tests should take: a wait that runs this long
3687    /// was not woken.
3688    const LONG_WAIT: Duration = Duration::from_secs(60);
3689    /// How long the waking thread lets the wait settle before it rings.
3690    const RING_AFTER: Duration = Duration::from_millis(50);
3691
3692    /// Rings `waker` from another thread after [`RING_AFTER`].
3693    fn ring_later(waker: ShellWaker) -> std::thread::JoinHandle<()> {
3694        std::thread::spawn(move || {
3695            std::thread::sleep(RING_AFTER);
3696            waker.wake();
3697        })
3698    }
3699
3700    #[test]
3701    fn a_wake_from_another_thread_ends_a_long_wait() {
3702        let mut shell = HeadlessShell::new();
3703        let ringer = ring_later(shell.waker().expect("headless has a waker"));
3704        let start = std::time::Instant::now();
3705        shell.wait_events(Some(LONG_WAIT));
3706        let slept = start.elapsed();
3707        ringer.join().expect("the ringer");
3708        assert!(slept < LONG_WAIT / 2, "the wait ran {slept:?}");
3709    }
3710
3711    #[test]
3712    fn a_wake_before_the_wait_ends_it_at_once_and_wakes_coalesce() {
3713        let mut shell = HeadlessShell::new();
3714        let waker = shell.waker().expect("headless has a waker");
3715        for _ in 0..3 {
3716            waker.wake();
3717        }
3718        let start = std::time::Instant::now();
3719        shell.wait_events(Some(LONG_WAIT));
3720        assert!(
3721            start.elapsed() < LONG_WAIT / 2,
3722            "a rung bell must end the wait"
3723        );
3724
3725        // Three rings were one: the next wait sleeps out its timeout.
3726        let timeout = Duration::from_millis(100);
3727        let start = std::time::Instant::now();
3728        shell.wait_events(Some(timeout));
3729        assert!(
3730            start.elapsed() >= timeout / 2,
3731            "the bell was cleared, so this wait must sleep, but it took {:?}",
3732            start.elapsed()
3733        );
3734    }
3735
3736    #[test]
3737    fn with_a_waker_out_an_untimed_wait_sleeps_until_it_rings() {
3738        let mut shell = HeadlessShell::new();
3739        let ringer = ring_later(shell.waker().expect("headless has a waker"));
3740        let start = std::time::Instant::now();
3741        shell.wait_events(None);
3742        let slept = start.elapsed();
3743        ringer.join().expect("the ringer");
3744        assert!(
3745            slept >= RING_AFTER / 2,
3746            "an untimed wait returned after {slept:?}, before anything rang"
3747        );
3748    }
3749
3750    // ------------------------------------------------------------- clipboard
3751
3752    #[test]
3753    fn the_clipboard_round_trips_through_an_event() {
3754        let (mut shell, window) = shell_with_configured_window();
3755        shell
3756            .clipboard_offer(
3757                window,
3758                &[
3759                    ClipboardOffer::text("node"),
3760                    ClipboardOffer::ron("(kind:\"node\")"),
3761                ],
3762            )
3763            .expect("offer");
3764        assert_eq!(
3765            shell.clipboard_bytes(MimeType::TextUtf8),
3766            Some(&b"node"[..])
3767        );
3768
3769        let request = shell
3770            .clipboard_request(window, MimeType::CrcblRon)
3771            .expect("request");
3772        assert_eq!(
3773            drain(&mut shell)[0],
3774            ShellEvent::ClipboardData {
3775                window,
3776                request,
3777                mime: ReceivedMime::from(MimeType::CrcblRon),
3778                content: ClipboardContent::Bytes(b"(kind:\"node\")".to_vec()),
3779            }
3780        );
3781
3782        // A format nobody offered comes back `Empty` — an answer, and a
3783        // different one from "the read failed".
3784        let request = shell
3785            .clipboard_request(window, MimeType::UriList)
3786            .expect("request");
3787        assert_eq!(
3788            drain(&mut shell)[0],
3789            ShellEvent::ClipboardData {
3790                window,
3791                request,
3792                mime: ReceivedMime::from(MimeType::UriList),
3793                content: ClipboardContent::Empty,
3794            }
3795        );
3796
3797        // A published payload of zero bytes is a *successful* read of nothing,
3798        // which the old `Option<Vec<u8>>` shape could not say.
3799        shell
3800            .clipboard_offer(window, &[ClipboardOffer::text("")])
3801            .expect("offer");
3802        let request = shell
3803            .clipboard_request(window, MimeType::TextUtf8)
3804            .expect("request");
3805        assert_eq!(
3806            drain(&mut shell)[0],
3807            ShellEvent::ClipboardData {
3808                window,
3809                request,
3810                mime: ReceivedMime::from(MimeType::TextUtf8),
3811                content: ClipboardContent::Bytes(Vec::new()),
3812            }
3813        );
3814    }
3815
3816    #[test]
3817    fn a_read_taken_while_the_clipboard_is_unreadable_is_held_not_answered_empty() {
3818        // The Wayland focus gate, modelled. A consumer written only against a
3819        // clipboard that answers instantly cannot tell "empty" from "I have
3820        // not looked yet" — which is exactly what the seam's obligation 5
3821        // exists to stop backends doing, and what this shell can now reproduce
3822        // with no compositor in the room.
3823        let (mut shell, window) = shell_with_configured_window();
3824        shell
3825            .clipboard_offer(window, &[ClipboardOffer::text("published")])
3826            .expect("offer");
3827        shell.set_clipboard_readable(false);
3828        assert!(!shell.clipboard_readable(window));
3829
3830        let request = shell
3831            .clipboard_request(window, MimeType::TextUtf8)
3832            .expect("a read is accepted even when it cannot be answered yet");
3833        assert_eq!(shell.held_clipboard_reads(), 1);
3834        assert!(
3835            drain(&mut shell).is_empty(),
3836            "a held read produces no event at all — least of all an empty one"
3837        );
3838
3839        // Focus arrives. The *same* request answers; the caller asked once.
3840        shell.set_clipboard_readable(true);
3841        let events = drain(&mut shell);
3842        assert_eq!(events.len(), 1);
3843        assert_eq!(
3844            events[0],
3845            ShellEvent::ClipboardData {
3846                window,
3847                request,
3848                mime: ReceivedMime::from(MimeType::TextUtf8),
3849                content: ClipboardContent::Bytes(b"published".to_vec()),
3850            }
3851        );
3852        assert_eq!(shell.held_clipboard_reads(), 0);
3853    }
3854
3855    #[test]
3856    fn a_read_that_never_becomes_answerable_ends_as_unavailable() {
3857        // Obligation 4: every accepted request is answered, within a bounded
3858        // time. A request that stayed outstanding forever is a UI stuck on
3859        // "pasting…" that no consumer can detect.
3860        let (mut shell, window) = shell_with_configured_window();
3861        shell
3862            .clipboard_offer(window, &[ClipboardOffer::text("unreachable")])
3863            .expect("offer");
3864        shell.set_clipboard_readable(false);
3865        shell.clipboard_deadline(2);
3866
3867        let request = shell
3868            .clipboard_request(window, MimeType::TextUtf8)
3869            .expect("request");
3870        assert!(drain(&mut shell).is_empty(), "pump 1: still held");
3871        assert!(drain(&mut shell).is_empty(), "pump 2: still held");
3872        let events = drain(&mut shell);
3873        assert_eq!(
3874            events[0],
3875            ShellEvent::ClipboardData {
3876                window,
3877                request,
3878                mime: ReceivedMime::from(MimeType::TextUtf8),
3879                // Not `Empty`: there is something on the clipboard, and this
3880                // shell simply could not get at it.
3881                content: ClipboardContent::Unavailable,
3882            }
3883        );
3884        assert_eq!(shell.held_clipboard_reads(), 0);
3885    }
3886
3887    #[test]
3888    fn a_peer_that_holds_the_clipboard_and_never_delivers_is_unavailable() {
3889        // The other failure with no protocol event behind it: on Wayland the
3890        // transfer is a pipe another application writes, and an application
3891        // that takes the descriptor and hangs produces exactly this.
3892        let (mut shell, window) = shell_with_configured_window();
3893        shell.set_foreign_clipboard(vec![(
3894            ReceivedMime::new("text/plain"),
3895            b"never sent".to_vec(),
3896        )]);
3897        shell.set_clipboard_stalled(true);
3898        shell.clipboard_deadline(1);
3899        assert!(
3900            !shell.clipboard_readable(window),
3901            "a clipboard whose owner will not answer is not readable"
3902        );
3903
3904        let request = shell
3905            .clipboard_request(window, MimeType::TextUtf8)
3906            .expect("request");
3907        assert!(drain(&mut shell).is_empty());
3908        let events = drain(&mut shell);
3909        let ShellEvent::ClipboardData {
3910            request: answered,
3911            content,
3912            ..
3913        } = &events[0]
3914        else {
3915            panic!("wrong variant");
3916        };
3917        assert_eq!(*answered, request);
3918        assert_eq!(*content, ClipboardContent::Unavailable);
3919
3920        // The peer wakes up; a fresh read now succeeds.
3921        shell.set_clipboard_stalled(false);
3922        let request = shell
3923            .clipboard_request(window, MimeType::TextUtf8)
3924            .expect("request");
3925        assert_eq!(
3926            drain(&mut shell)[0],
3927            ShellEvent::ClipboardData {
3928                window,
3929                request,
3930                mime: ReceivedMime::new("text/plain"),
3931                content: ClipboardContent::Bytes(b"never sent".to_vec()),
3932            }
3933        );
3934    }
3935
3936    #[test]
3937    fn claiming_the_clipboard_can_require_a_user_interaction() {
3938        // Wayland's `set_selection` serial and the browser's user-gesture gate,
3939        // modelled as one thing — because they are one thing: a background
3940        // process must not be able to take the clipboard.
3941        let (mut shell, window) = shell_with_configured_window();
3942        shell.require_user_interaction(true);
3943
3944        let refused = shell.clipboard_offer(window, &[ClipboardOffer::text("stolen")]);
3945        assert!(
3946            matches!(
3947                refused,
3948                Err(ShellError::NeedsUserInteraction {
3949                    backend: ShellBackend::Headless,
3950                    ..
3951                })
3952            ),
3953            "{refused:?}"
3954        );
3955        assert!(
3956            shell.clipboard_bytes(MimeType::TextUtf8).is_none(),
3957            "a refused claim must not have taken the clipboard anyway"
3958        );
3959
3960        // The copy the user actually asked for. Any injected input counts, as
3961        // any real input serial would.
3962        shell.key_press(window, KeyCode::KeyC).expect("key");
3963        shell
3964            .clipboard_offer(window, &[ClipboardOffer::text("copied")])
3965            .expect("with a serial in hand, the claim goes through");
3966        assert_eq!(
3967            shell.clipboard_bytes(MimeType::TextUtf8),
3968            Some(&b"copied"[..])
3969        );
3970
3971        // And the permission goes stale, exactly as a serial does.
3972        shell.clear_user_interaction();
3973        assert!(matches!(
3974            shell.clipboard_offer(window, &[ClipboardOffer::text("later")]),
3975            Err(ShellError::NeedsUserInteraction { .. })
3976        ));
3977    }
3978
3979    #[test]
3980    fn a_held_read_for_a_destroyed_window_is_dropped_rather_than_answered() {
3981        // Obligation 1 outranks obligation 4: an answer naming a window that no
3982        // longer exists is a stale handle in a consumer's hands.
3983        let (mut shell, window) = shell_with_configured_window();
3984        shell.set_clipboard_readable(false);
3985        let _request = shell
3986            .clipboard_request(window, MimeType::TextUtf8)
3987            .expect("request");
3988        assert_eq!(shell.held_clipboard_reads(), 1);
3989
3990        shell.destroy_window(window).expect("destroy");
3991        let events = drain(&mut shell);
3992        assert_eq!(shell.held_clipboard_reads(), 0);
3993        assert!(
3994            !events
3995                .iter()
3996                .any(|event| matches!(event, ShellEvent::ClipboardData { .. })),
3997            "no clipboard answer may name a destroyed window: {events:?}"
3998        );
3999    }
4000
4001    #[test]
4002    fn a_paste_from_a_foreign_application_keeps_the_foreign_spelling() {
4003        let (mut shell, window) = shell_with_configured_window();
4004        shell.set_foreign_clipboard(vec![
4005            // Exactly what a GTK application offers, and a string no
4006            // `&'static str` in this crate could have held.
4007            (ReceivedMime::new("text/plain"), b"from gedit".to_vec()),
4008            (
4009                ReceivedMime::new("application/vnd.some-editor.scene+json"),
4010                b"{}".to_vec(),
4011            ),
4012        ]);
4013
4014        // Asking for our canonical spelling still finds it …
4015        let request = shell
4016            .clipboard_request(window, MimeType::TextUtf8)
4017            .expect("request");
4018        let events = drain(&mut shell);
4019        let ShellEvent::ClipboardData {
4020            request: answered,
4021            mime,
4022            content,
4023            ..
4024        } = &events[0]
4025        else {
4026            panic!("wrong variant");
4027        };
4028        assert_eq!(*answered, request);
4029        assert_eq!(content.bytes(), Some(&b"from gedit"[..]));
4030        // … and the answer reports what the peer actually called it.
4031        assert_eq!(mime.as_str(), "text/plain");
4032        assert!(mime.matches(MimeType::TextUtf8));
4033        assert_ne!(*mime, ReceivedMime::from(MimeType::TextUtf8));
4034
4035        // A format the engine has no name for is still reachable and still
4036        // reported verbatim.
4037        assert_eq!(
4038            shell.clipboard_bytes(MimeType::Other("application/vnd.some-editor.scene+json")),
4039            Some(&b"{}"[..])
4040        );
4041    }
4042
4043    // ------------------------------------------------------------------ panics
4044
4045    #[test]
4046    #[should_panic(expected = "a shell needs at least one monitor")]
4047    fn a_shell_with_no_monitors_is_rejected() {
4048        let _ = HeadlessShell::new().with_monitors(Vec::new());
4049    }
4050
4051    #[test]
4052    #[should_panic(expected = "a text commit is never empty")]
4053    fn an_empty_text_commit_is_rejected() {
4054        let (mut shell, window) = shell_with_configured_window();
4055        let _ = shell.commit_text(window, "");
4056    }
4057}