1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
//! [`ShellCaps`] — what a backend can actually do, asked as a question about
//! capability rather than about identity.
//!
//! # The regression this exists to prevent
//!
//! ```ignore
//! if cfg!(target_os = "linux") && std::env::var("WAYLAND_DISPLAY").is_ok() {
//! // …use the viewport path
//! }
//! ```
//!
//! That code is wrong three ways and all three are invisible until someone
//! files a bug: it is wrong on a Wayland compositor that does not implement
//! `wp_viewporter`, wrong under XWayland, and wrong the day the shell grows a
//! backend nobody thought about. The windowing rules in `docs/notes/backends.md`
//! are explicit — "the
//! renderer picks blit-vs-viewport and windowed-aspect behavior from caps, never
//! from *am I on Wayland*" — and this module is the mechanism. It mirrors
//! `crcbl-hal`'s `Features`, where the renderer asks for `DRAW_INDIRECT_COUNT`
//! rather than for Vulkan.
//!
//! # Caps are per-shell, not per-window
//!
//! A capability here is a property of the connection and the protocols the
//! server advertised, so it is fixed for the shell's lifetime and reported by
//! [`Shell::caps`](crate::Shell::caps) rather than per window. The one thing
//! that genuinely varies per window — its current size and scale — is
//! [`WindowState`](crate::WindowState), not a capability.
//!
//! There is a real edge here, and it is stated rather than hidden: a Wayland
//! compositor can bind a global *after* startup, so a protocol could in
//! principle appear mid-session. Backends must not flip caps bits when that
//! happens; a renderer that picked the blit path at init cannot be told
//! halfway through a frame that it should have picked the viewport path.
//! Capabilities are latched at connection time, and a missing protocol stays
//! missing until the next run.
bitflags::bitflags! {
/// What a shell backend supports.
///
/// Each flag names a *behaviour a consumer would otherwise have to guess
/// from the platform*. A flag that no consumer would branch on does not
/// belong here.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct ShellCaps: u32 {
/// The compositor can scale a smaller buffer to the window's size in
/// display hardware, so borderless render-scale costs no shader work.
///
/// Wayland's `wp_viewporter`; the browser's CSS-sized canvas. Absent on
/// X11, where the renderer must do an upscale blit instead. This is the
/// exact decision the windowing rules call out as the reason caps
/// exist. Nothing asks for it: the engine does its own upscale on every
/// platform (`docs/backlog.md`, _The engine owns scaling_).
const HW_UPSCALE = 1 << 0;
/// The window system will enforce an aspect ratio during an interactive
/// resize.
///
/// X11 `WM_NORMAL_HINTS`, Win32 `WM_SIZING`, macOS
/// `setContentAspectRatio`. **Wayland has no aspect hint**, so a
/// Wayland backend clears this bit and the renderer letterboxes.
/// Letterboxing must work regardless — a tiling window manager ignores
/// the hint on every backend that has one — so this flag chooses
/// between "good" and "correct", never between "works" and "broken".
const ASPECT_HINT_HONORED = 1 << 1;
/// [`Shell::warp_pointer`](crate::Shell::warp_pointer) moves the
/// pointer.
///
/// X11 `XWarpPointer`, Win32 `SetCursorPos`. Wayland deliberately
/// forbids it (a client that can move the cursor can spoof clicks); the
/// engine's answer there is pointer lock plus relative motion, which is
/// what a camera wanted anyway.
const POINTER_WARP = 1 << 2;
/// [`PointerMode::Locked`](crate::PointerMode) is available: the
/// pointer can be hidden and pinned while relative motion keeps
/// flowing.
///
/// Wayland `pointer-constraints` + `relative-pointer`, X11 pointer
/// grab, Win32 clip-and-recentre, the browser's Pointer Lock API.
///
/// The browser is the one that cannot take the lock when asked: it
/// grants `requestPointerLock` only inside a user gesture, so
/// [`set_pointer_mode`](crate::Shell::set_pointer_mode) there records
/// the request and the player's next click completes it. The bit still
/// means what it says — the mode is available — and a consumer that
/// needs to know whether the pointer is pinned *now* reads the shape of
/// the motion events, as
/// [`PointerMode::Locked`](crate::PointerMode::Locked) describes.
const POINTER_LOCK = 1 << 3;
/// [`PointerMode::Confined`](crate::PointerMode) is available: the
/// pointer stays inside the window but keeps moving visibly.
///
/// A separate bit from [`POINTER_LOCK`](Self::POINTER_LOCK) because
/// Wayland's `pointer-constraints` advertises lock and confine
/// independently, and a strategy game wants confine specifically.
const POINTER_CONFINE = 1 << 4;
/// Relative pointer motion is delivered separately from absolute
/// position, unaccelerated and unclamped by the screen edge.
///
/// This is what a first-person camera must read;
/// the windowing rules make raw motion a P0 feature, not an
/// afterthought, on the grounds that an esports-grade engine cannot
/// derive aim from clamped absolute positions. Without this bit,
/// [`ShellEvent::PointerMotion`](crate::ShellEvent::PointerMotion)
/// carries `raw_delta: None` and the camera has to difference absolute
/// positions — which is wrong at the screen edge and wrong under
/// pointer acceleration.
///
/// # The one platform that can decline the bypass
///
/// On the desktop backends the unaccelerated stream is a separate
/// device interface and either exists or does not. In a browser it is
/// an *option* on the lock — `requestPointerLock({ unadjustedMovement:
/// true })` — and a browser that does not implement it hands back the
/// OS-adjusted deltas instead, either by ignoring the key or by
/// rejecting with `NotSupportedError` and being asked again without it.
/// The web backend sets this bit because it reads a relative stream and
/// asks for it unadjusted, which is the decision a camera branches on:
/// differencing absolute positions is wrong there whatever the browser
/// answered. It is not a promise that every browser granted the bypass;
/// the `web` backend's module docs name which browsers do.
const RAW_POINTER_MOTION = 1 << 5;
/// The backend is wired to the platform's input method, the framework
/// CJK and other complex-script input runs through, end to end.
///
/// For a window that named a field with
/// [`Shell::set_text_input`](crate::Shell::set_text_input), the
/// composition in progress arrives as
/// [`ShellEvent::TextPreedit`](crate::ShellEvent::TextPreedit), the
/// conversion the user picks as
/// [`ShellEvent::TextCommit`](crate::ShellEvent::TextCommit), and
/// [`Shell::set_text_input_area`](crate::Shell::set_text_input_area)
/// is passed on so the input method can place its candidate window.
/// Whether the user runs an input method is not the shell's to know;
/// the bit says one would reach the engine.
///
/// Wayland sets it when the compositor offers `text-input-v3`, Win32
/// through `imm32`, and AppKit through `NSTextInputClient`. Clear on
/// X11, which has no XIM, IBus or Fcitx client, and on the web. Dead keys and Compose sequences are
/// [`TEXT_COMPOSE`](Self::TEXT_COMPOSE), which a backend can have
/// without this.
const TEXT_IME = 1 << 6;
/// Clipboard get/set works. See
/// [`clipboard`](crate::clipboard).
///
/// Explicitly a capability because the browser gates clipboard reads
/// behind a permission prompt and a user gesture, which
/// the windowing rules call out by name: the editor UI has to
/// degrade gracefully in-browser rather than offering a paste button
/// that cannot work.
const CLIPBOARD = 1 << 7;
/// Files dragged onto a window arrive as
/// [`ShellEvent::DroppedFile`](crate::ShellEvent::DroppedFile).
const DRAG_DROP = 1 << 8;
/// The shell can create more than one window.
///
/// The MVP editor is single-window, and a canvas backend has exactly
/// one canvas, so this is not universal. A consumer that wants a
/// tear-off panel checks this rather than assuming.
const MULTI_WINDOW = 1 << 9;
/// Window position in desktop coordinates can be read and set.
///
/// **Wayland cannot do either** — a client is not told where it is and
/// cannot ask to be moved. Anything that wants to restore a window's
/// position across sessions (the editor, topic 14's settings store)
/// must check this bit and degrade to "the compositor decides".
const WINDOW_POSITION = 1 << 10;
/// The window system draws the title bar and borders.
///
/// Wayland with `xdg-decoration` and a compositor that opts in, and
/// every other desktop platform. Without it the UI layer has to draw
/// its own decorations, which is a layout decision, not a rendering
/// detail — so it belongs in caps rather than being discovered by
/// noticing the window looks wrong.
const SERVER_DECORATIONS = 1 << 11;
/// Scale factors other than whole integers are reported.
///
/// Wayland `fractional-scale-v1` (in 1/120ths), Win32 per-monitor-v2,
/// browser `devicePixelRatio`. Without it the shell reports integer
/// scales only, and a 125% desktop shows up as 1.0 or 2.0 — which the
/// UI needs to know before it decides whether to snap its layout to a
/// pixel grid.
const FRACTIONAL_SCALE = 1 << 12;
/// [`Shell::wait_events`](crate::Shell::wait_events) genuinely blocks.
///
/// An editor idling at zero frames per second on battery depends on
/// it; a game never calls it. Backends that cannot block — the browser
/// above all, where blocking the main thread is not a thing that
/// happens — leave this clear and their `wait_events` returns
/// immediately, which is always a correct implementation.
const EVENT_WAIT = 1 << 13;
/// Fingers arrive as [`ShellEvent::Touch`](crate::ShellEvent::Touch),
/// with a contact id and a phase, so more than one can be tracked at a
/// time.
///
/// **Win32 is the one desktop backend that sets it** (`WM_POINTER*`),
/// beside the browser. Clear on the others, and that is a statement
/// about the backends rather than about the platforms: a Linux tablet
/// has a touchscreen, and X11 (XInput2 `XI_TouchBegin`), Wayland
/// (`wl_touch`) and AppKit (`NSTouch`) all have a way to deliver one.
/// None of those paths is written, so a game on them sees no contacts
/// at all — an input that is *absent*, which is a different thing from
/// one that silently fails.
///
/// A backend that does set it owes the emulated pointer stream for the
/// primary contact as well; [`ShellEvent::Touch`](crate::ShellEvent::Touch)
/// says why, and that obligation is what stops a backend turning this on
/// from breaking every game that binds only the pointer.
const TOUCH = 1 << 14;
/// [`WindowRole::Panel`](crate::WindowRole::Panel) can be created:
/// desktop furniture held against a monitor's edges.
const PANEL = 1 << 15;
/// A panel's [`Reserve::OwnExtent`](crate::Reserve::OwnExtent) is
/// honoured: maximised windows stop at the panel.
///
/// Separate from [`PANEL`](Self::PANEL) because a window system can
/// have panels and still give nothing but its own furniture reserved
/// space — macOS, where the panel is created and overlaps what is
/// behind it.
const PANEL_RESERVE = 1 << 16;
/// [`WindowRole::Popup`](crate::WindowRole::Popup) can be created:
/// a window anchored to another's rectangle.
const POPUP = 1 << 17;
/// [`WindowDesc::transparent`](crate::WindowDesc::transparent) is
/// honoured: where the frame's alpha is below one, the desktop shows
/// through.
///
/// The window system's half. The frame's half is a swapchain that
/// carries alpha, premultiplied — which the HAL's surface caps say
/// whether the driver offers, and which `crcbl::engine::GpuContext`
/// asks for when the window's surface target says it is transparent
/// — and a renderer that clears to alpha zero. See
/// [`WindowDesc::transparent`](crate::WindowDesc::transparent) for the
/// whole recipe.
const TRANSPARENT = 1 << 18;
/// [`Shell::reposition_popup`](crate::Shell::reposition_popup) moves
/// and resizes a live popup.
///
/// Separate from [`POPUP`](Self::POPUP) because Wayland's
/// `xdg_popup.reposition` arrived in `xdg_wm_base` 3, and a compositor
/// offering only 1 or 2 has popups that stay where they opened.
const POPUP_REPOSITION = 1 << 19;
/// `Shell::update_accessibility` reaches the platform's assistive
/// technologies, and the `Accessibility*` events are raised.
///
/// Set only by a backend built with the `accesskit` feature, which is
/// where that method and those events are, and an adapter for its
/// platform.
const ACCESSIBILITY = 1 << 20;
/// Dead keys and Compose sequences compose: a dead accent and the next
/// key, or a Compose-key sequence, arrive as one
/// [`ShellEvent::TextCommit`](crate::ShellEvent::TextCommit) carrying
/// the composed text.
///
/// libxkbcommon's compose module on Wayland and X11, with the user's
/// locale's table; `TranslateMessage` on Win32; `interpretKeyEvents:`
/// on AppKit. Without it a backend may still commit plain typing, but
/// an accent typed through a dead key never lands on its letter. It
/// says nothing about an input method, which is
/// [`TEXT_IME`](Self::TEXT_IME): X11 composes and has none.
const TEXT_COMPOSE = 1 << 21;
/// A panel with
/// [`PanelKeyboard::Exclusive`](crate::PanelKeyboard::Exclusive)
/// keeps the keyboard while it is mapped: the window system will not
/// give it to another window, whatever the user clicks.
///
/// Wayland's layer-shell enforces it in the compositor, on the
/// [`Top`](crate::PanelLayer::Top) and
/// [`Overlay`](crate::PanelLayer::Overlay) layers; the protocol lets a
/// compositor use ordinary focus on the lower two. Clear on Win32 and
/// AppKit, where an exclusive panel takes the keyboard when it is
/// shown (or hears
/// [`FocusRefused`](crate::ShellEvent::FocusRefused)) and the user
/// can click it away, which arrives as
/// [`Focus`](crate::ShellEvent::Focus) with `focused: false`. A
/// launcher that should close when it loses the keyboard closes on
/// that event; a lock screen cannot be built without this bit.
const PANEL_EXCLUSIVE_KEYBOARD = 1 << 22;
/// [`Shell::set_cursor`](crate::Shell::set_cursor) with a shape
/// changes the cursor the user sees, to the platform's own themed
/// cursor for that [`CursorIcon`](crate::CursorIcon).
///
/// Win32's stock `user32` cursors, AppKit's `NSCursor`, and the
/// canvas's CSS `cursor` on the web; on Wayland, `cursor-shape-v1`
/// where the compositor offers it, which hands the compositor the
/// shape's name and lets it draw the user's theme; on X11, the user's
/// XCursor theme loaded through libxcb-cursor, where that library is
/// installed. Without it, `set_cursor` still
/// accepts and records a shape — a UI calls it on every hover, and an
/// error there would be one every caller had to handle — and hiding
/// with `None` works everywhere; only the shape goes unseen.
const CURSOR_SHAPE = 1 << 23;
/// [`Shell::set_input_region`](crate::Shell::set_input_region) is
/// acted on: the pointer outside the region goes to whatever is behind
/// the window, another program's window included.
///
/// Wayland's `wl_surface.set_input_region`; X11's SHAPE input region,
/// where libxcb-shape is installed and the server has SHAPE 1.1; a
/// window region on Win32, which also clips what the window draws.
/// Clear on AppKit, where an `NSWindow` has no input region, and on the
/// web, whose one canvas has nothing behind it to reach. Without it the
/// region is accepted and the whole window keeps the pointer, so a
/// launcher's transparent corners stay its own.
const INPUT_REGION = 1 << 24;
}
}
impl ShellCaps {
/// The set a full-featured desktop backend is expected to report.
///
/// Not a promise about any particular platform — it is the reference point
/// tests and [`HeadlessShell`](crate::HeadlessShell) start from, and the
/// value a doc example can name without pretending to know what the machine
/// running it supports.
pub const DESKTOP: Self = Self::all();
/// Whether pointer capture in either form is available.
#[inline]
#[must_use]
pub const fn has_pointer_capture(self) -> bool {
self.intersects(Self::POINTER_LOCK.union(Self::POINTER_CONFINE))
}
/// Whether a first-person camera can get trustworthy aim input: the pointer
/// can be locked *and* relative motion is delivered.
///
/// Both halves are required, and this helper exists because forgetting the
/// second one produces a camera that works until the pointer reaches the
/// edge of the screen.
#[inline]
#[must_use]
pub const fn has_mouselook(self) -> bool {
self.contains(Self::POINTER_LOCK.union(Self::RAW_POINTER_MOTION))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mouselook_needs_both_halves() {
assert!(ShellCaps::DESKTOP.has_mouselook());
// Pointer lock alone is the trap: aim breaks at the screen edge.
assert!(!ShellCaps::POINTER_LOCK.has_mouselook());
assert!(!ShellCaps::RAW_POINTER_MOTION.has_mouselook());
assert!((ShellCaps::POINTER_LOCK | ShellCaps::RAW_POINTER_MOTION).has_mouselook());
}
#[test]
fn pointer_capture_is_either_form() {
assert!(ShellCaps::POINTER_LOCK.has_pointer_capture());
assert!(ShellCaps::POINTER_CONFINE.has_pointer_capture());
assert!(!ShellCaps::POINTER_WARP.has_pointer_capture());
assert!(!ShellCaps::empty().has_pointer_capture());
}
#[test]
fn a_wayland_shaped_cap_set_is_expressible() {
// The point of the type: the exact combination Wayland produces — hardware
// upscale yes, aspect hint no, pointer warp no — is a value, not a
// `#[cfg]`.
let wayland = ShellCaps::HW_UPSCALE
| ShellCaps::POINTER_LOCK
| ShellCaps::POINTER_CONFINE
| ShellCaps::RAW_POINTER_MOTION
| ShellCaps::TEXT_IME
| ShellCaps::TEXT_COMPOSE
| ShellCaps::CLIPBOARD
| ShellCaps::DRAG_DROP
| ShellCaps::MULTI_WINDOW
| ShellCaps::SERVER_DECORATIONS
| ShellCaps::FRACTIONAL_SCALE
| ShellCaps::EVENT_WAIT
| ShellCaps::CURSOR_SHAPE;
assert!(wayland.has_mouselook());
assert!(!wayland.contains(ShellCaps::POINTER_WARP));
assert!(!wayland.contains(ShellCaps::ASPECT_HINT_HONORED));
assert!(!wayland.contains(ShellCaps::WINDOW_POSITION));
// `wl_touch` exists and this backend does not bind it, which is the
// whole difference between a platform and a backend.
assert!(!wayland.contains(ShellCaps::TOUCH));
assert!(ShellCaps::DESKTOP.contains(wayland));
assert_eq!(ShellCaps::default(), ShellCaps::empty());
}
}