Skip to main content

crcbl_shell/
lib.rs

1//! The Crucible windowing seam: one trait, engine-typed vocabulary, and a
2//! complete headless implementation.
3//!
4//! Everything above this crate — the engine loop, the renderer, the samples,
5//! the editor — talks to the window system exclusively through [`Shell`].
6//! Everything below it (the Wayland and X11 backends at P0.5/P0.6, Win32 at
7//! P5C, AppKit next to it, the canvas at P5) implements it. **No platform
8//! type ever appears in a signature here, and a `#[cfg(target_os = …)]` in a
9//! consumer is a regression** — the same discipline `crcbl-hal` applies to the
10//! GPU, applied to the window.
11//!
12//! The one sanctioned leak is [`SurfaceTarget`], produced by
13//! [`Shell::surface_target`] and destructured only by HAL backends. It lives in
14//! `crcbl-core` because this crate and `crcbl-hal` must not depend on each
15//! other; see [`crcbl_core::surface`] for the argument. The dependency
16//! direction across the two seams is:
17//!
18//! ```text
19//! crcbl-shell ──▶ crcbl-core ◀── crcbl-hal ◀── crcbl-vk / crcbl-webgpu / …
20//! ```
21//!
22//! # What is in this crate
23//!
24//! P0.4 added the seam and [`HeadlessShell`]. P0.5a added the **Wayland
25//! backend**'s window lifecycle — connection, registry, `xdg-shell`,
26//! configure/ack, modes, monitors. P0.5b added everything downstream of
27//! `wl_seat`: keyboard and pointer input with XKB keymaps, pointer lock and
28//! confinement, raw relative motion, fractional scale, `xdg_output` monitor
29//! geometry and `xdg-decoration`. P0.5c added `data-device`: the clipboard in
30//! both directions and drag-and-drop *reception*, with the payload transfers
31//! run across pipes without ever blocking the frame loop.
32//!
33//! P0.6 added the **X11 backend**: libxcb through `dlopen`, the window
34//! lifecycle with ICCCM and EWMH properties, RandR monitor enumeration, XKB
35//! keymaps read from the server, XInput 2 raw motion, pointer grabs, and the
36//! selection protocol with `TARGETS` negotiation and `INCR` transfers in both
37//! directions. Drag and drop (XDND) is deliberately *not* in it and
38//! [`ShellCaps::DRAG_DROP`] says so. Both backends are compiled only on Linux,
39//! reached only through [`open`], and built on hand-written `extern "C"`
40//! declarations — for libwayland-client, libxcb and libxkbcommon — with the
41//! Wayland protocol marshalling generated at build time from vendored XML by
42//! `crcbl-wl-scanner`. There is no `wayland-client`, no `x11rb` and no `winit`.
43//!
44//! P5C added the **Win32 backend**: the window class and its procedure,
45//! create/destroy, title, visibility, windowed ↔ borderless on a named monitor,
46//! `WM_GETMINMAXINFO` and `WM_SIZING` size constraints, monitor enumeration with
47//! per-monitor-v2 DPI, the message pump and a blocking `wait_events` — then
48//! keyboard, text, pointer, wheel, raw relative motion, pointer lock and
49//! confinement, cursors and warping. The clipboard and drag-and-drop are the
50//! slice after it, and [`ShellCaps`] is clear on every bit they would set. It is
51//! hand-written `extern "system"` FFI to `user32`, `gdi32`, `shcore` and
52//! `kernel32` — **linked rather than `dlopen`ed**, which is the one place the
53//! native backends deliberately disagree: a Linux box can genuinely lack
54//! libwayland, and a Windows cannot lack `user32.dll`. There is no
55//! `windows-rs`.
56//!
57//! P5C also added the **AppKit backend**: the Objective-C runtime FFI, the
58//! `NSApplication` bootstrap, `NSWindow` and a layer-hosting `NSView` carrying
59//! the `CAMetalLayer` that *is* [`SurfaceTarget::AppKit`], the event pump and a
60//! blocking `wait_events`, windowed ↔ borderless on a named display, `NSScreen`
61//! enumeration with scale and refresh, size constraints and close-request
62//! interception — then keyboard, text through a real `NSTextInputClient`,
63//! pointer, both scroll units, relative motion, pointer lock, cursors and
64//! warping. The pasteboard and drag-and-drop are the slice after it, and
65//! [`ShellCaps`] is clear on every bit they would set. It is hand-written
66//! `objc_msgSend` FFI — **linked rather than `dlopen`ed**, for the same reason
67//! the Win32 backend gives — with no `objc2` and no framework.
68//!
69//! Four macOS facts are worth knowing at this level rather than at that one.
70//! **AppKit is main-thread-only and enforces it by raising an Objective-C
71//! exception**, which is the concrete reason [`Shell`] is not `Send` and is a
72//! stronger rule than Win32's thread affinity — there, any thread may own a
73//! window as long as it is the one pumping. **The Y axis points up**, and macOS
74//! is the only one of the five platforms whose coordinate system disagrees with
75//! this seam's about that. **`backingScaleFactor` is 1.0 or 2.0 and nothing
76//! between**, so [`ShellCaps::FRACTIONAL_SCALE`] is clear on the one desktop
77//! backend a reader would expect it to be set on. And **macOS cannot confine a
78//! pointer at all** — there is no `ClipCursor` and no `confine_to`, only warping
79//! it back after it has already left — so [`ShellCaps::POINTER_CONFINE`] is
80//! clear while [`ShellCaps::POINTER_LOCK`] is set, which is a split no other
81//! desktop backend has.
82//!
83//! Two Win32 facts the other backends do not have are worth knowing at this
84//! level rather than at that one. A window's contents freeze while the user
85//! drags its edge, because Windows runs a **modal message loop** of its own
86//! there and [`pump`](Shell::pump) does not return until the mouse is released.
87//! And a window belongs to the thread that created it, which is the concrete
88//! reason [`Shell`] is not `Send`.
89//!
90//! Two properties of the Wayland clipboard shaped the seam rather than being
91//! worked around in the backend, because they are properties of the *platform*:
92//! a client may only read the selection while one of its windows has keyboard
93//! focus, and may only *claim* it while holding a serial from real user input.
94//! So [`Shell::clipboard_readable`] says whether a read would answer now,
95//! [`Shell::clipboard_request`] **holds** a read it cannot yet answer instead of
96//! reporting an empty clipboard, [`ClipboardContent`] separates "empty" from
97//! "could not be read", and [`ShellError::NeedsUserInteraction`] names the
98//! refusal. [`HeadlessShell`] can simulate all three, so a consumer is tested
99//! against them with no compositor in the room.
100//!
101//! P0.6 checked that prediction against the other platform, and it held with
102//! one correction worth recording: X11 needs none of the *holding* (its
103//! `GetSelectionOwner` answers synchronously and there is no focus gate) and
104//! never returns [`ShellError::NeedsUserInteraction`] (there is no such rule),
105//! but it does **not** get "answered within a bounded time" for free. A stalled
106//! `INCR` transfer has no descriptor to poll and no protocol event for "the
107//! owner gave up", so the X11 backend carries its own deadline. The seam did
108//! not move; one backend obligation turned out to cost more than predicted, and
109//! that is written down where it is paid.
110//!
111//! `HeadlessShell` is not a stub standing in for the real thing: per the
112//! windowing rules in `docs/notes/backends.md` it is a first-class implementation that CI,
113//! `crcbl screenshot` and `crcbl sim` run the *identical* engine loop through.
114//! Where the real compositor turned out to differ from what it models, the
115//! difference is documented in the Wayland backend rather than smoothed over —
116//! the first configure carrying no size at all is the one to know about.
117//!
118//! # Decision: `dyn`-primary, like the HAL
119//!
120//! [`Shell`] is object-safe and [`open`] returns `Box<dyn Shell>`. This is
121//! forced rather than chosen: the windowing rules require Wayland and
122//! X11 to be compiled in together and selected *at runtime*, so the backend
123//! cannot be a type parameter. The consequences are the ones `crcbl-hal`
124//! documents — no generic methods, no `impl Trait` returns, no cross-crate
125//! inlining — and they cost less here: a frame makes a handful of shell calls,
126//! not one per draw. Generic use still works (`fn f<S: Shell + ?Sized>(s: &S)`)
127//! and monomorphises normally.
128//!
129//! The trait requires [`Debug`](core::fmt::Debug) and deliberately **not**
130//! `Send`/`Sync`. Shells are thread-affine and cannot promise otherwise: a
131//! Win32 `HWND` belongs to the thread pumping its message queue, and AppKit is
132//! main-thread-only. [`SurfaceTarget`] reached the same conclusion for the same
133//! reason. Create the shell on the thread that will pump it.
134//!
135//! # Decision: `pump` is a drain, never a loop
136//!
137//! ```text
138//! loop {                                  // native: the outer loop is yours
139//!     shell.pump(&mut |event| …);         // ← drain what arrived
140//!     clock.update(time.elapsed());
141//!     while clock.consume_tick() { tick(clock.tick_dt()); }
142//!     render(clock.alpha());
143//! }
144//! ```
145//!
146//! There is no `Shell::run(closure)` and there never will be, because
147//! the browser requires the engine loop to be "a `fn tick(dt)`
148//! driven by an outer loop, not a `loop {}` that owns the thread" — in a
149//! browser the outer loop is `requestAnimationFrame`, which *calls* the engine
150//! and cannot be called by it. A framework-shaped `run()` would compile on wasm
151//! and then deadlock on the first frame, which is the worst possible failure
152//! schedule. The rAF build is the same body with the `loop {` line deleted:
153//!
154//! ```text
155//! // wasm: the outer loop is the browser's
156//! extern "C" fn crcbl_frame(now_ms: f64) {   // called from rAF by the JS shim
157//!     shell.pump(&mut |event| …);
158//!     clock.update(Duration::from_secs_f64(now_ms / 1000.0));
159//!     while clock.consume_tick() { tick(clock.tick_dt()); }
160//!     render(clock.alpha());
161//! }
162//! ```
163//!
164//! [`Shell::wait_events`] exists for the editor, which idles at zero frames per
165//! second on battery. It is *advisory*: returning immediately is always a
166//! correct implementation, which is what makes it portable to a browser that
167//! cannot block at all.
168//!
169//! # Decision: where the input vocabulary lives
170//!
171//! The shell-boundary rule in `docs/notes/backends.md` puts input normalization in
172//! `crcbl-core::input`, and [`ShellEvent`] is naturally a `crcbl-shell` type.
173//! Both are true, and the split is:
174//!
175//! | Lives in | What | Why |
176//! | --- | --- | --- |
177//! | [`crcbl_core::input`] | [`KeyCode`], [`Keysym`], [`PointerButton`], [`Modifiers`], [`ScrollDelta`], [`DeviceId`] | the P2 action layer, the profile store, the ECS and the UI all need to *name* a key; none of them should link a windowing library to spell `Space` |
178//! | `crcbl-shell` | [`ShellEvent`] | it names [`WindowId`], and only a shell backend ever produces one |
179//!
180//! Dependency direction: `crcbl-shell → crcbl-core`, never the reverse, and
181//! `crcbl-input` (P2) will depend on `crcbl-core` for vocabulary plus
182//! `crcbl-shell` for the event stream. The vocabulary types are re-exported
183//! here so an event consumer needs one `use`, not two.
184//!
185//! # Decision: raw events only
186//!
187//! This crate produces *normalized raw* events. Actions, bindings, contexts and
188//! patterns are `crcbl-input`'s action layer. What
189//! this slice owes that layer is the information it cannot reconstruct later —
190//! per-device ids, and timestamps that are the window system's rather than the
191//! frame's. Both are on every input event; see [`event`].
192//!
193//! # Decision: a window has no size until the window system says so
194//!
195//! [`Shell::create_window`] returns a [`WindowId`] and *nothing else*.
196//! [`WindowState::size`] is `None` until the first
197//! [`ShellEvent::Resized`] arrives, and a swapchain — which needs an extent —
198//! cannot be created before then.
199//!
200//! This is the shape Wayland forces: an `xdg_surface` is unsized and may not be
201//! given a buffer until the compositor answers with a `configure`, a round trip
202//! away. Modelling it in the type rather than in prose is the difference
203//! between every consumer written before P0.5 being correct and all of them
204//! breaking on the same day. [`HeadlessShell`] deliberately defers the first
205//! configure by at least one [`pump`](Shell::pump) — same reasoning as
206//! `crcbl-hal`'s injected readback latency — so the create-then-wait loop is
207//! *exercised* in CI rather than merely documented.
208//!
209//! The same distinction runs through [`WindowState`]: what was **requested**
210//! ([`requested_mode`](WindowState::requested_mode),
211//! [`requested_constraints`](WindowState::requested_constraints)) is always
212//! known because we said it, while what is **effective**
213//! ([`WindowConfiguration`]) only exists once the window system has answered
214//! and may disagree forever. A compositor can refuse a fullscreen request; a
215//! tiling window manager ignores every size hint there is.
216//!
217//! # Stability
218//!
219//! Provisional, like the HAL seam, and with a specific reason to be: it has no
220//! real backend behind it yet. The places most likely to move are stated rather
221//! than glossed — [`HeadlessShell`]'s event ordering is a *model* of a
222//! compositor's, not a recording of one, and its configure delay is a fixed
223//! number of pumps where a compositor's is a round trip.
224//!
225//! # Example
226//!
227//! A complete, deterministic window session with no window system anywhere:
228//!
229//! ```
230//! use crcbl_shell::{CloseReply, HeadlessShell, PhysicalSize, Shell, WindowDesc};
231//!
232//! let mut shell = HeadlessShell::new();
233//! let window = shell.create_window(&WindowDesc {
234//!     title: "sandbox",
235//!     ..WindowDesc::default()
236//! })?;
237//!
238//! // The window has no size yet, so there is no swapchain to create.
239//! assert_eq!(shell.window_state(window)?.size(), None);
240//!
241//! // Wait for the window system to configure it. This loop is mandatory, not
242//! // defensive: on Wayland the answer is a round trip away.
243//! let size = loop {
244//!     shell.pump(&mut |_event| {});
245//!     if let Some(size) = shell.window_state(window)?.size() {
246//!         break size;
247//!     }
248//! };
249//! assert_eq!(size, PhysicalSize::new(1280, 720));
250//!
251//! // Script what a compositor would do next.
252//! shell.resize(window, PhysicalSize::new(1920, 1080))?;
253//! shell.request_close(window)?;
254//!
255//! let mut names = Vec::new();
256//! shell.pump(&mut |event| names.push(event.name()));
257//! assert_eq!(names, ["Resized", "CloseRequested"]);
258//!
259//! // The close request is a question, not a notification.
260//! shell.reply_close_request(window, CloseReply::Keep)?;
261//! assert!(shell.window_state(window).is_ok(), "still open");
262//! # Ok::<(), crcbl_shell::ShellError>(())
263//! ```
264
265pub mod backend;
266pub mod caps;
267pub mod clipboard;
268pub mod cursor;
269pub mod error;
270pub mod event;
271pub mod geom;
272pub mod headless;
273pub mod monitor;
274pub mod role;
275pub mod url;
276pub mod waker;
277pub mod window;
278
279pub(crate) mod frame;
280pub(crate) mod keysym;
281
282#[cfg(any(target_os = "macos", target_os = "windows", test))]
283pub(crate) mod preedit;
284
285#[cfg(desktop_unix)]
286pub(crate) mod linux;
287
288#[cfg(desktop_unix)]
289pub(crate) mod wayland;
290
291#[cfg(desktop_unix)]
292pub(crate) mod x11;
293
294#[cfg(any(target_os = "windows", test))]
295pub(crate) mod win32;
296
297#[cfg(any(target_os = "macos", test))]
298pub(crate) mod appkit;
299
300#[cfg(any(target_arch = "wasm32", test))]
301pub(crate) mod web;
302
303/// Scaffolding for the nested-compositor end-to-end suite, behind the
304/// `wayland-e2e` feature.
305///
306/// Not part of the seam and not for consumers: it exists because a Wayland
307/// surface is only managed by the compositor once it has a *buffer*, and
308/// attaching one is the renderer's job — which does not exist until P1. See
309/// the module's docs for the full finding.
310#[cfg(all(desktop_unix, feature = "wayland-e2e"))]
311pub use wayland::e2e as wayland_test_support;
312
313/// Scaffolding for the Xvfb end-to-end suite, behind the `x11-e2e` feature.
314///
315/// Not part of the seam and not for consumers. Where the Wayland version exists
316/// because a surface needs a buffer, this one exists because on X11 the window
317/// manager, the input devices and the clipboard owner are all *other programs* —
318/// see the module's docs.
319#[cfg(all(desktop_unix, feature = "x11-e2e"))]
320pub use x11::e2e as x11_test_support;
321
322#[cfg(target_os = "macos")]
323pub use appkit::session_support;
324
325/// The accessibility vocabulary [`Shell::update_accessibility`] and the
326/// `Accessibility*` events speak, re-exported so a caller builds its tree with
327/// the version the shell was built against. Behind the `accesskit` feature.
328#[cfg(feature = "accesskit")]
329pub use accesskit;
330
331pub use backend::{BACKEND_ENV_VAR, ShellBackend, open, open_backend};
332pub use caps::ShellCaps;
333pub use clipboard::{
334    ClipboardContent, ClipboardOffer, ClipboardRequestId, MimeType, ReceivedMime, parse_uri_list,
335};
336pub use cursor::{CursorIcon, PointerMode};
337pub use error::ShellError;
338pub use event::ShellEvent;
339pub use geom::{
340    AspectRatio, LogicalPoint, LogicalRect, LogicalSize, PhysicalPoint, PhysicalRect, PhysicalSize,
341};
342pub use headless::HeadlessShell;
343pub use monitor::{MonitorId, MonitorInfo};
344pub use role::{
345    Margins, PanelAnchor, PanelDesc, PanelKeyboard, PanelLayer, PopupDesc, PopupInput,
346    PopupPlacement, Reserve, WindowRole,
347};
348pub use url::{WebUrl, WebUrlError};
349pub use waker::ShellWaker;
350pub use window::{
351    CloseReply, DisplayMode, SizeConstraints, Window, WindowConfiguration, WindowDesc, WindowId,
352    WindowState,
353};
354
355/// Cross-seam vocabulary from `crcbl-core`: the input-event clock, the key
356/// names, and the one sanctioned platform leak.
357pub use crcbl_core::input::TextPurpose;
358/// Input vocabulary, defined in [`crcbl_core::input`] and re-exported so a
359/// consumer of the event stream needs one `use` rather than two.
360///
361/// See the [crate docs](self) for why these live in `crcbl-core`.
362pub use crcbl_core::input::{
363    ButtonState, ContactId, DeviceId, Keysym, Modifiers, PointerButton, Scancode, ScrollDelta,
364    TouchPhase,
365};
366pub use crcbl_core::{EventTime, KeyCode, SurfaceTarget};
367
368use core::time::Duration;
369
370/// The window system, as the engine sees it.
371///
372/// Object-safe by requirement; see the [crate docs](self) for why, and
373/// [`backend`] for how an implementation is selected at runtime.
374///
375/// # The loop is the caller's
376///
377/// There is no `run` that takes the program over: the caller loops on
378/// [`pump`](Self::pump) and [`wait_events`](Self::wait_events) itself, so
379/// ending the program is leaving that loop and dropping the shell. Each
380/// desktop backend's `Drop` destroys the windows it still holds, so nothing has
381/// to be torn down by hand first. Work that arrives
382/// on another thread reaches a sleeping loop through a [`ShellWaker`].
383///
384/// # Implementing this
385///
386/// Six obligations that are not visible in the signatures. The first three are
387/// P0.4's; the last three are P0.5c's, written down because a real compositor
388/// showed that leaving them unstated pushes the work onto every consumer.
389///
390/// 1. **Stale handles fail cleanly.** Every method taking a [`WindowId`] must
391///    return [`ShellError::InvalidWindow`] for a destroyed or foreign one, never
392///    act on whatever window took the slot.
393/// 2. **Timestamps are rebased.** Input events carry the window system's
394///    timestamp converted to the engine epoch — see [`EventTime`], which
395///    documents the conversion each platform needs.
396/// 3. **Capabilities are latched.** [`caps`](Self::caps) must return the same
397///    value for the shell's lifetime, so a consumer that branched on it at
398///    startup is never contradicted mid-session. See [`caps`] for the
399///    Wayland-binds-a-global-late edge this rules out.
400/// 4. **Every accepted clipboard read is answered exactly once, and within a
401///    bounded time.** [`clipboard_request`](Self::clipboard_request) that
402///    returns `Ok` promises exactly one
403///    [`ShellEvent::ClipboardData`] carrying that
404///    [`ClipboardRequestId`]. A backend that cannot complete the read answers
405///    [`ClipboardContent::Unavailable`] rather than dropping the request; a
406///    request with no answer is a UI stuck on "pasting…" forever, which no
407///    consumer can detect.
408/// 5. **A read that cannot be answered *yet* is held, never answered "empty".**
409///    If the backend does not yet know what is on the clipboard — Wayland's
410///    focus-gated, asynchronous `wl_data_device.selection` is the case this
411///    exists for — it holds the request until it does, and bounds that wait.
412///    Reporting [`Empty`](ClipboardContent::Empty) for "I have not looked yet"
413///    is indistinguishable from an empty clipboard, so it makes every consumer
414///    write a retry loop with a timeout it cannot choose correctly. Where the
415///    answer is always immediately available (X11 asks the server who owns the
416///    selection; a headless shell owns it), holding costs nothing and this
417///    obligation is discharged by answering.
418/// 6. **A refusal for want of user interaction is named.** Where the window
419///    system requires a recent input event to claim the clipboard — Wayland
420///    does, the browser does, X11 and Win32 do not —
421///    [`clipboard_offer`](Self::clipboard_offer) returns
422///    [`ShellError::NeedsUserInteraction`] and not a generic backend error, so
423///    a caller can distinguish "ask the user to press the key again" from "this
424///    is broken". A backend on a platform without the rule never returns it.
425pub trait Shell: core::fmt::Debug {
426    /// Which implementation this is. Logs and bug reports only — behaviour
427    /// branches on [`caps`](Self::caps).
428    fn backend(&self) -> ShellBackend;
429
430    /// What this backend can do. Fixed for the shell's lifetime.
431    fn caps(&self) -> ShellCaps;
432
433    /// Creates a window. **The window has no size yet.**
434    ///
435    /// `desc.size` is a request. The authoritative size arrives later, as the
436    /// first [`ShellEvent::Resized`], and until it does
437    /// [`WindowState::size`] is `None`. This is not a headless quirk: a Wayland
438    /// `xdg_surface` is unsized and may not be given a buffer until the
439    /// compositor answers with a `configure`, which is a round trip away.
440    ///
441    /// A swapchain needs an extent, so this is a hard ordering constraint:
442    ///
443    /// ```
444    /// # use crcbl_shell::{HeadlessShell, PhysicalSize, Shell, WindowDesc};
445    /// # let mut shell = HeadlessShell::new();
446    /// let window = shell.create_window(&WindowDesc::default())?;
447    /// let size: PhysicalSize = loop {
448    ///     shell.pump(&mut |_event| { /* … */ });
449    ///     if let Some(size) = shell.window_state(window)?.size() {
450    ///         break size;
451    ///     }
452    /// };
453    /// // Only now is there an extent to create a swapchain at.
454    /// assert_eq!(size, PhysicalSize::new(1280, 720));
455    /// # Ok::<(), crcbl_shell::ShellError>(())
456    /// ```
457    ///
458    /// [`HeadlessShell`] deliberately defers the first configure by at least one
459    /// `pump`, so a consumer that skips the loop fails in CI rather than at
460    /// P0.5.
461    ///
462    /// # Errors
463    ///
464    /// [`ShellError::WindowCreation`] if the window system refused, or
465    /// [`ShellError::Unsupported`] if this would be a second window on a
466    /// backend without [`ShellCaps::MULTI_WINDOW`].
467    fn create_window(&mut self, desc: &WindowDesc<'_>) -> Result<WindowId, ShellError>;
468
469    /// Destroys a window. A [`ShellEvent::WindowDestroyed`] follows.
470    ///
471    /// The [`SurfaceTarget`] previously returned for this window is dead
472    /// afterwards; the HAL surface built from it must be destroyed **first**.
473    ///
474    /// # Errors
475    ///
476    /// [`ShellError::InvalidWindow`] if the handle is stale.
477    fn destroy_window(&mut self, window: WindowId) -> Result<(), ShellError>;
478
479    /// A snapshot of the window's current state.
480    ///
481    /// For consumers that started late; not a substitute for handling events.
482    /// Note that [`WindowState::size`] is `None` until the window system has
483    /// configured the window, and that the *requested* mode and the *effective*
484    /// one are separate fields — see [`WindowState`].
485    ///
486    /// # Errors
487    ///
488    /// [`ShellError::InvalidWindow`] if the handle is stale.
489    fn window_state(&self, window: WindowId) -> Result<WindowState, ShellError>;
490
491    /// Sets the title bar text.
492    ///
493    /// # Errors
494    ///
495    /// [`ShellError::InvalidWindow`] if the handle is stale.
496    fn set_title(&mut self, window: WindowId, title: &str) -> Result<(), ShellError>;
497
498    /// Maps or unmaps the window.
499    ///
500    /// # Errors
501    ///
502    /// [`ShellError::InvalidWindow`] if the handle is stale.
503    fn set_visible(&mut self, window: WindowId, visible: bool) -> Result<(), ShellError>;
504
505    /// Asks for windowed or borderless.
506    ///
507    /// **A request, not a command.** It updates
508    /// [`WindowState::requested_mode`] immediately and nothing else; the mode
509    /// that is actually in effect appears in
510    /// [`WindowConfiguration::mode`] when the window system answers, which it
511    /// may do with something other than what was asked (a compositor can refuse
512    /// fullscreen, and a tiling window manager makes the question moot). Use
513    /// [`WindowState::mode_request_honoured`] rather than reading the request
514    /// back as fact.
515    ///
516    /// The answer usually arrives as a [`ShellEvent::Resized`], preceded by a
517    /// [`ShellEvent::ScaleFactorChanged`] when the target monitor has a
518    /// different scale. Never assume the new size — read the event.
519    ///
520    /// # Errors
521    ///
522    /// [`ShellError::InvalidWindow`] if the handle is stale, or
523    /// [`ShellError::NoSuchMonitor`] if the named monitor is gone.
524    fn set_mode(&mut self, window: WindowId, mode: DisplayMode) -> Result<(), ShellError>;
525
526    /// Asks for resize limits and an aspect lock.
527    ///
528    /// Hints, not guarantees, and a request rather than a command: it updates
529    /// [`WindowState::requested_constraints`] immediately, and the only
530    /// observable effect is whatever size the window system reports next. No
531    /// platform reports back "effective constraints", so neither does this
532    /// seam. See [`SizeConstraints`].
533    ///
534    /// # Errors
535    ///
536    /// [`ShellError::InvalidWindow`] if the handle is stale.
537    fn set_constraints(
538        &mut self,
539        window: WindowId,
540        constraints: SizeConstraints,
541    ) -> Result<(), ShellError>;
542
543    /// Moves a live popup against a new `anchor` in its parent and gives it
544    /// `size`, placed by the [`PopupPlacement`] it was created with: flipped
545    /// or slid to stay on screen, as when it opened.
546    ///
547    /// For a popup whose anchor moved — a bar's clock grew and its calendar
548    /// should follow — or whose content changed size. A request, like the
549    /// rest of a window's geometry: the new size arrives as the next
550    /// [`ShellEvent::Resized`], once the window system has placed it.
551    ///
552    /// Requires [`ShellCaps::POPUP_REPOSITION`]. Without it, close the popup
553    /// and open another.
554    ///
555    /// # Errors
556    ///
557    /// [`ShellError::Unsupported`] without the capability,
558    /// [`ShellError::InvalidWindow`] for a stale handle, and
559    /// [`ShellError::InvalidDescriptor`] for a window that is not a popup.
560    fn reposition_popup(
561        &mut self,
562        popup: WindowId,
563        anchor: LogicalRect,
564        size: LogicalSize,
565    ) -> Result<(), ShellError> {
566        let _ = (popup, anchor, size);
567        Err(ShellError::Unsupported {
568            backend: self.backend(),
569            what: "repositioning a popup",
570        })
571    }
572
573    /// Gives a live panel a new size and a new [`PanelDesc`]: its layer,
574    /// anchor, reserve, margins and keyboard change in place, with the same
575    /// surface and the same swapchain, where destroying it and creating
576    /// another would flicker.
577    ///
578    /// `size` means what [`WindowDesc::size`] means for a panel: ignored along
579    /// an axis anchored to both opposite edges. The new descriptor is checked
580    /// as [`create_window`](Self::create_window) checks one, so a reserving
581    /// panel must still name one edge. A request, like the rest of a window's
582    /// geometry: the size the panel gets arrives as the next
583    /// [`ShellEvent::Resized`], as its first one did, and none arrives when
584    /// the size did not change.
585    ///
586    /// **The namespace and the monitor cannot change.** `wlr-layer-shell`
587    /// fixes both when the surface is made, and every backend refuses a
588    /// change to either alike; close the panel and open another for that.
589    ///
590    /// A visible panel changed to [`PanelKeyboard::Exclusive`] takes the
591    /// keyboard at once on Wayland, where the compositor hands it over; on
592    /// Win32 and AppKit it takes it when it is next shown, as a new one does.
593    ///
594    /// A provided method answering [`ShellError::Unsupported`], as
595    /// [`create_window`](Self::create_window) answers a panel on a backend
596    /// without [`ShellCaps::PANEL`], once the handle has been checked; every
597    /// backend with panels implements it.
598    ///
599    /// # Errors
600    ///
601    /// [`ShellError::InvalidWindow`] for a stale handle,
602    /// [`ShellError::InvalidDescriptor`] for a window that is not a panel or a
603    /// descriptor that changes its namespace or its monitor, and whatever
604    /// [`create_window`](Self::create_window) refuses a panel with:
605    /// [`ShellError::Unsupported`] for what the backend cannot build, and
606    /// [`ShellError::NoSuchMonitor`] if the panel's monitor is gone.
607    fn set_panel(
608        &mut self,
609        window: WindowId,
610        size: LogicalSize,
611        panel: &PanelDesc<'_>,
612    ) -> Result<(), ShellError> {
613        let _ = (size, panel);
614        self.window_state(window)?;
615        Err(ShellError::Unsupported {
616            backend: self.backend(),
617            what: "panel windows",
618        })
619    }
620
621    /// Every connected monitor.
622    ///
623    /// Invalidated by [`ShellEvent::MonitorsChanged`]; call again rather than
624    /// caching across it.
625    fn monitors(&self) -> &[MonitorInfo];
626
627    /// One monitor by id, or `None` if it is not connected.
628    ///
629    /// A provided method rather than a required one: it is a lookup over
630    /// [`monitors`](Self::monitors), and every backend would write the same
631    /// three lines.
632    fn monitor(&self, id: MonitorId) -> Option<&MonitorInfo> {
633        self.monitors().iter().find(|monitor| monitor.id == id)
634    }
635
636    /// Drains everything that has arrived from the window system into `sink`.
637    ///
638    /// Non-blocking and finite: it delivers what is queued and returns. Call it
639    /// once at the top of every frame. See the [crate docs](self) for why there
640    /// is no `run()` and how a `requestAnimationFrame`-driven build uses this.
641    ///
642    /// The sink is `&mut dyn FnMut` rather than a generic parameter because the
643    /// trait must stay object-safe. That is not purely a cost: a closure passed
644    /// here can borrow the caller's state mutably, which an
645    /// `Iterator`-returning shape could not while the shell is also borrowed.
646    fn pump(&mut self, sink: &mut dyn FnMut(ShellEvent));
647
648    /// Lets the window system run, and delivers nothing.
649    ///
650    /// Everything [`pump`](Self::pump) does except hand events to a sink: the
651    /// platform's queue is drained, whatever it asks of the client is answered,
652    /// and what it produced is **kept** in this shell's queue, in order, for the
653    /// next `pump`. Every backend implements `pump` as this followed by the
654    /// delivery, so the two cannot drift apart.
655    ///
656    /// # What it is for: a long load on the thread that owns the window
657    ///
658    /// Desktops mark a client that stops draining its queue as hung. Windows
659    /// ghosts a window whose thread has not asked for a message for about five
660    /// seconds — `IsHungAppWindow`, a greyed "Not Responding" title, clicks
661    /// held — a Wayland compositor pings through `xdg_wm_base` and offers to
662    /// kill a client that does not answer (GNOME's "not responding" dialog),
663    /// and macOS puts the spinning cursor over an application whose main thread
664    /// has stopped taking events. A game that loads its assets between opening
665    /// its window and its first frame calls this every so often — a few times a
666    /// second is plenty — and stays responsive to all three.
667    ///
668    /// **Nothing is dropped**, which is why this is not `pump` with an empty
669    /// sink: a key pressed, a resize or a close request made during the load is
670    /// delivered by the first frame's `pump`, exactly as if the load had been
671    /// one long frame. So a close request stays unanswered until then — the
672    /// window stays open and the load finishes first — and a loader that must
673    /// stop for one has to pump and handle events itself.
674    ///
675    /// Work that can leave the loop thread should, instead: a loop that keeps
676    /// running frames keeps pumping on its own, and can show progress while it
677    /// does. This is for the load that cannot — work on the device the loop
678    /// owns, or on anything else tied to this thread.
679    ///
680    /// Non-blocking and finite, like `pump`, and with the same exception: on
681    /// Win32 and AppKit it does not return while the user is dragging the
682    /// window's border.
683    fn keep_alive(&mut self);
684
685    /// Blocks until an event arrives or `timeout` elapses.
686    ///
687    /// **Advisory.** Returning immediately is always correct, and backends
688    /// without [`ShellCaps::EVENT_WAIT`] — a browser above all — do exactly
689    /// that. A caller must treat this as "sleep if you can" and must not assume
690    /// an event is pending afterwards; [`pump`](Self::pump) is still what
691    /// delivers them.
692    ///
693    /// `timeout` of `None` means "until something happens", which on a backend
694    /// without the capability still means "immediately".
695    fn wait_events(&mut self, timeout: Option<Duration>) {
696        let _ = timeout;
697    }
698
699    /// A handle that wakes [`wait_events`](Self::wait_events) from another
700    /// thread — see [`ShellWaker`].
701    ///
702    /// Every call hands out a handle to the same doorbell, so a program can ask
703    /// once and clone, or ask wherever it needs one. `None` from a backend
704    /// whose wait never blocks, which is the browser: there, nothing sleeps and
705    /// there is nothing to wake.
706    fn waker(&mut self) -> Option<ShellWaker> {
707        None
708    }
709
710    /// Puts input timestamps on the engine's clock.
711    ///
712    /// Call this **once, at startup**, with the reading of the
713    /// [`TimeSource`](crcbl_core::time::TimeSource) that will drive
714    /// [`FrameClock::update`](crcbl_core::FrameClock::update).
715    ///
716    /// [`EventTime`]'s epoch is defined as "the same origin as" that source,
717    /// and no backend can know it: a shell is created some time *after* the
718    /// clock starts, and the window system's own clock has a third origin
719    /// again. Without this call, timestamps are measured from the shell's own
720    /// creation — internally consistent, so durations between two input events
721    /// are already correct, but offset from a
722    /// [`FrameClock`](crcbl_core::FrameClock) reading by however long startup
723    /// took. That offset is exactly what `docs/plan/26-prediction.md`'s input-to-photon
724    /// latency measurement is a subtraction across.
725    ///
726    /// A provided method with a no-op default, because it is only meaningful
727    /// for a backend whose event clock is not already the engine's — the
728    /// browser's `event.timeStamp` needs nothing done to it. The obligation it
729    /// discharges is the "Timestamps are rebased" one in this trait's
730    /// [implementor notes](Self), which previously had no way to be satisfied
731    /// from outside the crate.
732    ///
733    /// ```
734    /// # use core::time::Duration;
735    /// # use crcbl_shell::{HeadlessShell, Shell};
736    /// let mut shell = HeadlessShell::new();
737    /// // The engine clock has been running for 40 ms by the time the window
738    /// // system is up; input stamped "now" should read as 40 ms, not as zero.
739    /// shell.align_event_clock(Duration::from_millis(40));
740    /// ```
741    fn align_event_clock(&mut self, elapsed: Duration) {
742        let _ = elapsed;
743    }
744
745    /// The native handles a HAL backend needs to create a surface for this
746    /// window.
747    ///
748    /// Opaque to everyone except a HAL backend. Re-query it after a
749    /// [`set_mode`](Self::set_mode): some backends recreate the underlying
750    /// surface on a mode switch, and a cached target then names a dead object.
751    /// **A resize is not such an event** — see below.
752    ///
753    /// # Available before the window is configured, and unchanged by a resize
754    ///
755    /// The *handle* exists as soon as the window does — a `wl_surface` is
756    /// created up front and only its size is pending — so this succeeds
757    /// immediately and a HAL surface can be created from it right away. Only
758    /// the **swapchain** has to wait, because only the swapchain needs an
759    /// extent.
760    ///
761    /// **No variant carries a size**, so the value this returns does not change
762    /// when the window is resized, and a HAL surface created before the first
763    /// configure stays correct forever. That is what makes "re-query after a
764    /// mode change" a complete rule. [`SurfaceTarget::Offscreen`] used to carry
765    /// `width`/`height` and was the one exception; P0.7 removed it, because an
766    /// exception in this rule means a surface silently describing a size the
767    /// window no longer has. The extent reaches a backend through
768    /// [`WindowState::size`] and the swapchain descriptor, and only there —
769    /// `crcbl-hal`'s `swapchain` module states that precedence as a backend
770    /// obligation.
771    ///
772    /// # Errors
773    ///
774    /// [`ShellError::InvalidWindow`] if the handle is stale.
775    fn surface_target(&self, window: WindowId) -> Result<SurfaceTarget, ShellError>;
776
777    /// Frees, confines or locks the pointer.
778    ///
779    /// # Errors
780    ///
781    /// [`ShellError::Unsupported`] if [`caps`](Self::caps) lacks
782    /// [`PointerMode::required_cap`], or [`ShellError::InvalidWindow`] for a
783    /// stale handle.
784    fn set_pointer_mode(&mut self, window: WindowId, mode: PointerMode) -> Result<(), ShellError>;
785
786    /// Sets the cursor shape, or hides the cursor with `None`.
787    ///
788    /// Visibility and capture are separate axes; see [`cursor`].
789    ///
790    /// Hiding works on every backend. A shape is seen only where
791    /// [`ShellCaps::CURSOR_SHAPE`] is set; elsewhere it is accepted and
792    /// recorded rather than refused, because a UI calls this on every hover.
793    ///
794    /// # Errors
795    ///
796    /// [`ShellError::InvalidWindow`] if the handle is stale.
797    fn set_cursor(
798        &mut self,
799        window: WindowId,
800        cursor: Option<CursorIcon>,
801    ) -> Result<(), ShellError>;
802
803    /// Moves the pointer to a position in the window.
804    ///
805    /// Requires [`ShellCaps::POINTER_WARP`], which **Wayland does not have and
806    /// will not get**. Anything reaching for this to implement a camera should
807    /// use [`PointerMode::Locked`] instead; warping is for a UI that needs to
808    /// reposition a cursor (a slider snapping to a value), not for input.
809    ///
810    /// # Errors
811    ///
812    /// [`ShellError::Unsupported`] without the capability, or
813    /// [`ShellError::InvalidWindow`] for a stale handle.
814    fn warp_pointer(&mut self, window: WindowId, position: PhysicalPoint)
815    -> Result<(), ShellError>;
816
817    /// Answers a [`ShellEvent::CloseRequested`].
818    ///
819    /// Until this is called the window stays open and the request stays
820    /// outstanding. See [`CloseReply`].
821    ///
822    /// # Errors
823    ///
824    /// [`ShellError::NoPendingCloseRequest`] if nothing was asked, or
825    /// [`ShellError::InvalidWindow`] for a stale handle.
826    fn reply_close_request(
827        &mut self,
828        window: WindowId,
829        reply: CloseReply,
830    ) -> Result<(), ShellError>;
831
832    /// Publishes clipboard content in one or more formats.
833    ///
834    /// Synchronous: every platform's write is "claim the selection", and the
835    /// bytes transfer later, on demand. Offer both [`MimeType::TextUtf8`] and
836    /// [`MimeType::CrcblRon`] for engine data; see [`clipboard`].
837    ///
838    /// An empty `offers` slice releases the clipboard, where the platform can
839    /// express that.
840    ///
841    /// # Errors
842    ///
843    /// [`ShellError::Unsupported`] without [`ShellCaps::CLIPBOARD`],
844    /// [`ShellError::InvalidWindow`] for a stale handle, or
845    /// [`ShellError::NeedsUserInteraction`] where the window system requires a
846    /// recent input event to claim the clipboard and there has not been one —
847    /// implementor obligation 6 above, and **not** a failure a caller should
848    /// retry blindly.
849    fn clipboard_offer(
850        &mut self,
851        window: WindowId,
852        offers: &[ClipboardOffer<'_>],
853    ) -> Result<(), ShellError>;
854
855    /// Asks for the clipboard's content in `mime`.
856    ///
857    /// Asynchronous by necessity — the answer arrives as
858    /// [`ShellEvent::ClipboardData`] carrying the returned id, and **exactly one
859    /// answer arrives for every `Ok` return** (implementor obligations 4 and 5).
860    /// [`clipboard`] has the full argument; briefly, X11 `INCR` transfers,
861    /// Wayland file descriptors and the browser's Promise make the synchronous
862    /// shape unimplementable on three of five backends.
863    ///
864    /// The answer may be several frames away, and on a backend whose clipboard
865    /// is not readable at the moment of asking it is *held* until it is, rather
866    /// than being answered [`Empty`](ClipboardContent::Empty). A caller
867    /// therefore never needs to poll or retry: ask once, handle the event.
868    /// [`clipboard_readable`](Self::clipboard_readable) is the way to find out
869    /// in advance that the answer will not be immediate.
870    ///
871    /// # Errors
872    ///
873    /// [`ShellError::Unsupported`] without [`ShellCaps::CLIPBOARD`], or
874    /// [`ShellError::InvalidWindow`] for a stale handle. An empty clipboard is
875    /// **not** an error — it is an answer.
876    fn clipboard_request(
877        &mut self,
878        window: WindowId,
879        mime: MimeType,
880    ) -> Result<ClipboardRequestId, ShellError>;
881
882    /// Whether a [`clipboard_request`](Self::clipboard_request) issued for
883    /// `window` right now could be answered without waiting.
884    ///
885    /// What a "Paste" menu item greys out on, and the visible form of a
886    /// platform rule that is otherwise invisible: **on Wayland a client is told
887    /// what is on the clipboard only while one of its windows has keyboard
888    /// focus**, and only asynchronously, so a background window genuinely
889    /// cannot read the clipboard. `clipboard_request` still works there — it
890    /// holds the request — but it may take until focus arrives, and a UI that
891    /// would rather not offer the action than offer a slow one asks here first.
892    ///
893    /// `false` for a stale handle, and for a backend without
894    /// [`ShellCaps::CLIPBOARD`].
895    ///
896    /// # A provided method, because most platforms have nothing to say
897    ///
898    /// The default is "readable whenever the capability is present and the
899    /// handle is live", which is exactly right for X11 (any window may
900    /// `ConvertSelection` at any time, focus is not involved), for Win32, and
901    /// for [`HeadlessShell`] in its default configuration. Only a backend that
902    /// really has a gate overrides it — so this is a fact a consumer can rely
903    /// on, not ceremony every backend has to perform.
904    ///
905    /// The staleness half goes through
906    /// [`window_state`](Self::window_state) rather than being left to each
907    /// implementation. P0.6 found the earlier default — which ignored `window`
908    /// entirely — contradicting the "`false` for a stale handle" sentence
909    /// above: both backends that existed had *overridden* it and so had
910    /// covered for it, and the first backend to accept the default inherited
911    /// the bug. A default that cannot keep its own promise is worse than no
912    /// default.
913    fn clipboard_readable(&self, window: WindowId) -> bool {
914        self.caps().contains(ShellCaps::CLIPBOARD) && self.window_state(window).is_ok()
915    }
916
917    /// A token another program can raise its own window with, made from
918    /// `window` and the latest input this program received.
919    ///
920    /// What a notification daemon sends as the notification spec's
921    /// `ActivationToken` before `ActionInvoked`, and what a launcher puts in a
922    /// child's `XDG_ACTIVATION_TOKEN`: the receiver hands it back to its window
923    /// system, which raises the window if it accepts the token. Ask for it in
924    /// answer to the click that warrants it, since the window system judges a
925    /// token by the input it was made from.
926    ///
927    /// `Ok(None)` where the window system has no such thing: Wayland without
928    /// `xdg-activation-v1`, and every other backend, whose default this is.
929    /// Wayland waits for the compositor's answer, a round trip or more.
930    ///
931    /// # Errors
932    ///
933    /// [`ShellError::InvalidWindow`] for a stale handle, or
934    /// [`ShellError::Backend`] where the window system has the mechanism but
935    /// gave no token.
936    fn activation_token(&mut self, window: WindowId) -> Result<Option<String>, ShellError> {
937        self.window_state(window).map(|_| None)
938    }
939
940    /// Tells the input method where text is being entered, so it can put its
941    /// composition window and candidate list there instead of at a corner of the
942    /// screen.
943    ///
944    /// `area` is the text field's caret, or the line it sits on, in the
945    /// window's **client** device pixels; the candidate list opens beside it
946    /// without covering it. `None` hands placement back to the input method's
947    /// default. Call it when a field gains focus and whenever its caret moves.
948    /// A hint: the input method may place its windows elsewhere, and nothing
949    /// reports where they went.
950    ///
951    /// # A provided method, and a no-op where there is nothing to place
952    ///
953    /// Only a backend with an input method that draws its own windows has
954    /// anything to do here — Win32's IMM and TSF, a Wayland compositor's
955    /// `text-input-v3` cursor rectangle, AppKit's `firstRectForCharacterRange:`.
956    /// The default validates the handle and does nothing else, which is exactly
957    /// right for the rest. Win32, Wayland and AppKit override it. Wayland
958    /// sends the area only while [`set_text_input`](Self::set_text_input)
959    /// names a field, since `text-input-v3` has nowhere to put it otherwise;
960    /// AppKit answers `firstRectForCharacterRange:` with it whenever asked.
961    ///
962    /// # Errors
963    ///
964    /// [`ShellError::InvalidWindow`] if the handle is stale.
965    fn set_text_input_area(
966        &mut self,
967        window: WindowId,
968        area: Option<PhysicalRect>,
969    ) -> Result<(), ShellError> {
970        let _ = area;
971        self.window_state(window).map(|_| ())
972    }
973
974    /// Limits where `window` takes the pointer to a union of rectangles, so
975    /// the pointer anywhere else goes to whatever is behind the window.
976    ///
977    /// For a transparent window that draws a shape smaller than itself: a
978    /// launcher's rounded panel, whose transparent corners should let a click
979    /// through to the program beneath. A pixel of alpha zero is otherwise the
980    /// window's on every desktop backend; nothing hit-tests by alpha, because
981    /// that would read the frame back from the GPU on every pointer move.
982    ///
983    /// `region` is in the window's **client** device pixels, the units of
984    /// [`set_text_input_area`](Self::set_text_input_area). Empty rectangles
985    /// are ignored, and parts outside the window take nothing; an empty slice
986    /// leaves the window no pointer input at all. `None` gives the whole window
987    /// back, which is where every window starts. Keyboard focus is not
988    /// affected. The rectangles are the caller's layout, so send them again
989    /// after a [`ShellEvent::Resized`] or [`ShellEvent::ScaleFactorChanged`]
990    /// that moves the shape. [`PhysicalRect::rounded`] builds them for a
991    /// rounded rectangle.
992    ///
993    /// **Give the outer bound of what the window draws, antialiased edge
994    /// included.** On Win32 the region is a window region, which also clips
995    /// the window's drawing to it, so a region inside the edge's blend cuts
996    /// the edge off. Elsewhere a tighter region only loses the pointer over
997    /// the edge.
998    ///
999    /// A [`Passthrough`](PopupInput::Passthrough) popup stays passthrough: a
1000    /// region given to it is validated and has no effect.
1001    ///
1002    /// # A provided method
1003    ///
1004    /// The default validates the handle and does nothing else, which leaves
1005    /// the whole window taking the pointer. Wayland, X11 and Win32 override it,
1006    /// and [`ShellCaps::INPUT_REGION`] is set only where it is acted on; a
1007    /// caller that must have the click-through checks the bit rather than the
1008    /// platform.
1009    ///
1010    /// # Errors
1011    ///
1012    /// [`ShellError::InvalidWindow`] if the handle is stale.
1013    fn set_input_region(
1014        &mut self,
1015        window: WindowId,
1016        region: Option<&[PhysicalRect]>,
1017    ) -> Result<(), ShellError> {
1018        let _ = region;
1019        self.window_state(window).map(|_| ())
1020    }
1021
1022    /// Keeps the display on and the screensaver from starting while `window`
1023    /// asks: `true` to ask, `false` to stop.
1024    ///
1025    /// For a game played on a gamepad, or a cutscene, which nothing the system
1026    /// counts as user activity interrupts. Ask while the game is being played
1027    /// and stop in the menus, as a video player stops at pause; asking for the
1028    /// whole session keeps a desktop awake that its user walked away from.
1029    ///
1030    /// Per window, because Wayland's `zwp_idle_inhibit_manager_v1` inhibits
1031    /// per surface, and its compositor honours that only while the surface is
1032    /// shown; Win32 keeps the display on whether or not the window is. The
1033    /// display is kept on while **any** window of the shell asks. Asking twice
1034    /// is asking once, and stopping a window that never asked does nothing.
1035    /// Destroying a window ends its request, and dropping the shell ends them
1036    /// all.
1037    ///
1038    /// # A provided method
1039    ///
1040    /// The default validates the handle and answers
1041    /// [`ShellError::Unsupported`], since a request nothing acts on would let
1042    /// the display sleep while the caller thinks otherwise.
1043    /// [`ShellCaps::IDLE_INHIBIT`] is set only where it is acted on, and says
1044    /// which backends that is.
1045    ///
1046    /// # Errors
1047    ///
1048    /// [`ShellError::InvalidWindow`] if the handle is stale,
1049    /// [`ShellError::Unsupported`] without [`ShellCaps::IDLE_INHIBIT`], and
1050    /// [`ShellError::Backend`] if the system refused.
1051    fn set_idle_inhibit(&mut self, window: WindowId, inhibit: bool) -> Result<(), ShellError> {
1052        let _ = inhibit;
1053        self.window_state(window)?;
1054        Err(ShellError::Unsupported {
1055            backend: self.backend(),
1056            what: "idle inhibit",
1057        })
1058    }
1059
1060    /// Opens `url` in the user's web browser, for a store page, a community
1061    /// link, or the end-user licence and privacy policy.
1062    ///
1063    /// Only a [`WebUrl`] is taken: an absolute `http` or `https` URL, checked
1064    /// by [`WebUrl::parse`] before any backend sees it. The system's own
1065    /// "open" would also run a program, open a file with whatever is
1066    /// associated with it, or start any registered protocol handler, and a
1067    /// game passing on a link a server sent it must never reach any of that;
1068    /// see the [`url`] module.
1069    ///
1070    /// `window` owns whatever the system shows while it opens the browser,
1071    /// such as Win32's message box when no browser is registered, as Wayland's
1072    /// `org.freedesktop.portal.OpenURI` takes a parent window. Whether the
1073    /// browser opened the page is not reported back: success means the system
1074    /// accepted the request. Leave the browser's window to come forward on its
1075    /// own; a fullscreen game may want to leave fullscreen first.
1076    ///
1077    /// # A provided method
1078    ///
1079    /// The default validates the handle and answers
1080    /// [`ShellError::Unsupported`], since a link that silently does nothing
1081    /// leaves the player clicking it again. [`ShellCaps::OPEN_URL`] is set
1082    /// only where it is acted on, and says which backends that is.
1083    ///
1084    /// # Errors
1085    ///
1086    /// [`ShellError::InvalidWindow`] if the handle is stale,
1087    /// [`ShellError::Unsupported`] without [`ShellCaps::OPEN_URL`], and
1088    /// [`ShellError::Backend`] if the system refused.
1089    fn open_url(&mut self, window: WindowId, url: &WebUrl) -> Result<(), ShellError> {
1090        let _ = url;
1091        self.window_state(window)?;
1092        Err(ShellError::Unsupported {
1093            backend: self.backend(),
1094            what: "opening a URL",
1095        })
1096    }
1097
1098    /// Asks for a [`ShellEvent::FrameReady`] when `window` should present its
1099    /// next frame.
1100    ///
1101    /// For a loop that presents only when told to: a status bar on an output
1102    /// the compositor has turned off is never asked for a frame there, and a
1103    /// FIFO present to it can block the loop for as long as the output stays
1104    /// off. Ask **before** presenting the frame the answer paces, then present
1105    /// the next one once it comes. The first frame needs no answer.
1106    ///
1107    /// ```
1108    /// # use crcbl_shell::{HeadlessShell, Shell, ShellEvent, WindowDesc};
1109    /// # let mut shell = HeadlessShell::new();
1110    /// # let window = shell.create_window(&WindowDesc::default())?;
1111    /// let mut may_present = true;
1112    /// # let mut presented = 0;
1113    /// for _ in 0..3 {
1114    ///     shell.pump(&mut |event| {
1115    ///         if let ShellEvent::FrameReady { window: ready } = event {
1116    ///             may_present |= ready == window;
1117    ///         }
1118    ///     });
1119    ///     if may_present {
1120    ///         shell.request_frame(window)?;
1121    ///         // … record and present …
1122    /// #       presented += 1;
1123    ///         may_present = false;
1124    ///     }
1125    /// }
1126    /// // With no compositor to hold it back, each answer came at the next pump.
1127    /// assert_eq!(presented, 3);
1128    /// # Ok::<(), crcbl_shell::ShellError>(())
1129    /// ```
1130    ///
1131    /// Requests made before the answer comes share it: one `FrameReady`
1132    /// answers them all. A window destroyed with a request outstanding gets no
1133    /// answer.
1134    ///
1135    /// Where [`ShellCaps::FRAME_CALLBACK`] is set — Wayland, as
1136    /// `wl_surface.frame` — the compositor answers. The request is surface
1137    /// state that rides the next commit, which is the one the swapchain's
1138    /// present makes, and this call commits nothing itself, so a window that
1139    /// asks and then never presents may never be answered. Everywhere else
1140    /// the answer comes at the next [`pump`](Self::pump), and
1141    /// [`wait_events`](Self::wait_events) does not sleep while one is due.
1142    ///
1143    /// A required method, unlike its neighbours: a default could validate the
1144    /// handle but not keep the request, and a request nothing answers stalls
1145    /// the loop waiting for it.
1146    ///
1147    /// # Errors
1148    ///
1149    /// [`ShellError::InvalidWindow`] if the handle is stale.
1150    fn request_frame(&mut self, window: WindowId) -> Result<(), ShellError>;
1151
1152    /// Tells the input method what text field in `window` is taking typing:
1153    /// `Some(purpose)` as a field gains the keyboard focus, `None` as it loses
1154    /// it.
1155    ///
1156    /// While a field is taking typing, a platform input method composes what
1157    /// is typed in the window, reporting the composition as
1158    /// [`TextPreedit`](ShellEvent::TextPreedit) and the result as
1159    /// [`TextCommit`](ShellEvent::TextCommit). While none is, the input method
1160    /// keeps out of the way and keys reach the window as keys: one in its
1161    /// native mode would otherwise turn a game's WASD into a candidate list. A
1162    /// window starts with none.
1163    ///
1164    /// The [`TextPurpose`] says what the field is for. A password or a PIN is
1165    /// [sensitive](TextPurpose::is_sensitive): nothing typed into it may be
1166    /// learned, stored or suggested back.
1167    ///
1168    /// # A provided method
1169    ///
1170    /// The default validates the handle and does nothing else. Wayland acts on
1171    /// it with `text-input-v3`'s `enable`, `set_content_type` and `disable`;
1172    /// Win32 associates the window's input context only for a
1173    /// [`Normal`](TextPurpose::Normal) field, keeping it off for a password or
1174    /// a PIN as a Windows password box does. AppKit draws the same line by
1175    /// holding the view's input context to the keyboard layouts for no field
1176    /// and for a sensitive one, and also holds secure event input while a
1177    /// password or PIN field's window has the keyboard.
1178    /// [`ShellCaps::TEXT_IME`] is set only where this is acted on.
1179    ///
1180    /// # Errors
1181    ///
1182    /// [`ShellError::InvalidWindow`] if the handle is stale.
1183    fn set_text_input(
1184        &mut self,
1185        window: WindowId,
1186        field: Option<TextPurpose>,
1187    ) -> Result<(), ShellError> {
1188        let _ = field;
1189        self.window_state(window).map(|_| ())
1190    }
1191
1192    /// Hands `window`'s accessibility tree, or what changed in it, to the
1193    /// platform's assistive technologies.
1194    ///
1195    /// Send a whole tree (with [`TreeUpdate::tree`](accesskit::TreeUpdate::tree)
1196    /// set) after each
1197    /// [`AccessibilityActivated`](ShellEvent::AccessibilityActivated), then
1198    /// only what changed, each frame something did. An update sent while
1199    /// nobody is reading — before the activation, or after
1200    /// [`AccessibilityDeactivated`](ShellEvent::AccessibilityDeactivated) — is
1201    /// dropped, so a program may also send unconditionally and skip tracking
1202    /// the two events.
1203    ///
1204    /// # A provided method
1205    ///
1206    /// The default validates the handle and drops the update, which is right
1207    /// for a backend with no adapter: it never raises the activation either.
1208    /// [`ShellCaps::ACCESSIBILITY`] says whether the trees go anywhere.
1209    ///
1210    /// # Errors
1211    ///
1212    /// [`ShellError::InvalidWindow`] if the handle is stale.
1213    #[cfg(feature = "accesskit")]
1214    fn update_accessibility(
1215        &mut self,
1216        window: WindowId,
1217        update: accesskit::TreeUpdate,
1218    ) -> Result<(), ShellError> {
1219        let _ = update;
1220        self.window_state(window).map(|_| ())
1221    }
1222}