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