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