Skip to main content

crcbl_shell/
headless.rs

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