crcbl_shell/window.rs
1//! Windows: how one is asked for, what it can be told to do, and what it
2//! currently is.
3//!
4//! # Two display modes, and no third
5//!
6//! [`DisplayMode`] has exactly two variants because
7//! the windowing rules in `docs/notes/backends.md` lock it at two. Exclusive/true fullscreen — the
8//! mode that changes the display's video timing — is **dropped by decision**,
9//! not deferred:
10//!
11//! * Wayland cannot modeset from a client, by design.
12//! * macOS fights it.
13//! * On Windows, borderless plus the DXGI flip model gets independent-flip and
14//! MPO latency close enough to exclusive that the remaining difference does
15//! not buy back the alt-tab disasters, the mode-restore-on-crash problem, or
16//! the multi-monitor breakage.
17//!
18//! Resolution control lives one layer up instead: in
19//! [`Borderless`](DisplayMode::Borderless) the renderer draws to an offscreen
20//! target at whatever internal resolution it wants and upscales to the native
21//! surface, either with a blit or — where
22//! [`ShellCaps::HW_UPSCALE`](crate::ShellCaps::HW_UPSCALE) is set — by handing
23//! the compositor the smaller buffer. **The shell only ever presents
24//! native-size surfaces.** A `DisplayMode` that carried a resolution would be a
25//! promise this crate cannot keep.
26//!
27//! If a "true fullscreen" request ever arrives, the answer is that it is this
28//! enum's absence of a variant, not a `ShellError::Unsupported`.
29//!
30//! # Aspect ratio is a constraint, not a mode
31//!
32//! The windowing rules describe windowed mode as "freeform **or
33//! aspect-locked**", which reads like two modes. It is one mode with a
34//! constraint, and the constraint lives in [`SizeConstraints`] alongside
35//! min/max — where the windowing plan's trait sketch,
36//! `set_constraints(win, c) // min/max/aspect`, already put it. Modelling it in both places would create a question with no
37//! good answer: if `DisplayMode::Windowed { aspect: Some(16:9) }` and
38//! `SizeConstraints { aspect: Some(4:3) }` disagree, which wins? Keeping one
39//! home also means an aspect lock survives a round trip through borderless and
40//! back, which is what a player toggling modes expects.
41
42use core::fmt;
43
44use crate::{AspectRatio, LogicalSize, MonitorId, PhysicalSize};
45
46/// Marker type for the [`Handle`](crcbl_core::Handle) that names a window.
47///
48/// Uninhabited: it exists only to give [`WindowId`] a distinct type, exactly as
49/// `crcbl-hal`'s resource markers do. There is no `Window` struct to hold — the
50/// real object lives inside the backend.
51#[derive(Debug)]
52pub enum Window {}
53
54/// Identifier of a window created by a [`Shell`](crate::Shell).
55///
56/// A generational [`Handle`](crcbl_core::Handle), so using one after
57/// [`destroy_window`](crate::Shell::destroy_window) is a clean
58/// [`ShellError::InvalidWindow`](crate::ShellError::InvalidWindow) rather than
59/// an operation applied to whatever window took the slot.
60pub type WindowId = crcbl_core::Handle<Window>;
61
62/// How a window is presented.
63///
64/// Two variants, deliberately. See the [module docs](self).
65#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
66pub enum DisplayMode {
67 /// A decorated window the user can move and resize.
68 ///
69 /// Render resolution is the client area, 1:1; a resize recreates the
70 /// swapchain. Whether the decorations are drawn by the window system or by
71 /// the engine's UI depends on
72 /// [`ShellCaps::SERVER_DECORATIONS`](crate::ShellCaps::SERVER_DECORATIONS).
73 #[default]
74 Windowed,
75
76 /// A frameless window covering a monitor, with the desktop's video mode
77 /// untouched.
78 Borderless {
79 /// Which monitor to cover, or `None` for "wherever the window already
80 /// is".
81 ///
82 /// `None` is not a lazy default — it is the only thing a Wayland
83 /// backend can honour, since a client cannot position its own surface
84 /// and can only ask `xdg_toplevel.set_fullscreen` with an output hint
85 /// the compositor may ignore. A caller that genuinely needs a specific
86 /// monitor checks
87 /// [`ShellCaps::WINDOW_POSITION`](crate::ShellCaps::WINDOW_POSITION)
88 /// first.
89 monitor: Option<MonitorId>,
90 },
91}
92
93impl DisplayMode {
94 /// Whether this is [`DisplayMode::Borderless`].
95 #[inline]
96 #[must_use]
97 pub const fn is_borderless(self) -> bool {
98 matches!(self, Self::Borderless { .. })
99 }
100
101 /// Whether `effective` is an acceptable answer to `self` as a request.
102 ///
103 /// Not equality, because [`Borderless::monitor`](Self::Borderless) is not
104 /// symmetric: `None` in a *request* means "wherever the window already is",
105 /// so any monitor answers it. `None` in an *answer* means the backend
106 /// cannot say which monitor — which answers a request that did not care,
107 /// and does not answer one that named a monitor, because nothing has
108 /// confirmed the window is on it.
109 ///
110 /// Comparing the two with `==` made every granted fullscreen on a backend
111 /// that *can* name the monitor read as refused: X11 fills in
112 /// `effective_monitor` from the window's origin, so a request for
113 /// `Borderless { monitor: None }` was answered `Borderless { monitor:
114 /// Some(..) }` and a UI toggle would have shown "off" over a fullscreen
115 /// window.
116 #[must_use]
117 pub const fn satisfied_by(self, effective: Self) -> bool {
118 match (self, effective) {
119 (Self::Windowed, Self::Windowed) => true,
120 (Self::Borderless { monitor: None }, Self::Borderless { .. }) => true,
121 (
122 Self::Borderless {
123 monitor: Some(wanted),
124 },
125 Self::Borderless { monitor: Some(got) },
126 ) => wanted.0 == got.0,
127 _ => false,
128 }
129 }
130}
131
132impl fmt::Display for DisplayMode {
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 match self {
135 Self::Windowed => f.write_str("windowed"),
136 Self::Borderless { monitor: None } => f.write_str("borderless"),
137 Self::Borderless {
138 monitor: Some(monitor),
139 } => write!(f, "borderless on {monitor}"),
140 }
141 }
142}
143
144/// Limits the window system should enforce on an interactive resize.
145///
146/// All three are hints in the strict sense: a tiling window manager honours
147/// none of them, and Wayland has no aspect hint at all. Consumers must stay
148/// correct when they are ignored — which for aspect means letterboxing
149/// in-renderer with [`AspectRatio::fit`], the universal fallback.
150#[derive(Clone, Copy, Debug, Default, PartialEq)]
151pub struct SizeConstraints {
152 /// Smallest client area the user may resize to.
153 pub min: Option<LogicalSize>,
154 /// Largest client area the user may resize to.
155 pub max: Option<LogicalSize>,
156 /// Ratio the client area should keep during a resize.
157 ///
158 /// Honoured natively only where
159 /// [`ShellCaps::ASPECT_HINT_HONORED`](crate::ShellCaps::ASPECT_HINT_HONORED)
160 /// is set. Where it is not, a backend picks the nearest aspect-correct size
161 /// it is allowed to pick and the renderer letterboxes the remainder.
162 pub aspect: Option<AspectRatio>,
163}
164
165impl SizeConstraints {
166 /// No constraints at all.
167 pub const NONE: Self = Self {
168 min: None,
169 max: None,
170 aspect: None,
171 };
172
173 /// Constraints with a minimum size and nothing else.
174 #[must_use]
175 pub const fn min(size: LogicalSize) -> Self {
176 Self {
177 min: Some(size),
178 max: None,
179 aspect: None,
180 }
181 }
182
183 /// These constraints with an aspect lock added.
184 #[must_use]
185 pub const fn with_aspect(mut self, aspect: AspectRatio) -> Self {
186 self.aspect = Some(aspect);
187 self
188 }
189
190 /// Applies the constraints to a physical size, as a backend that cannot
191 /// delegate to the window system would.
192 ///
193 /// Clamping happens before the aspect fit, so the result never exceeds
194 /// `max`. It can fall below `min` when an aspect lock demands it: a size
195 /// that violates the aspect ratio is visibly wrong, while one a few pixels
196 /// under the minimum is not, and something has to give when the two
197 /// disagree.
198 ///
199 /// [`HeadlessShell`](crate::HeadlessShell) applies this, which is what
200 /// makes constraint behaviour testable without a window manager.
201 #[must_use]
202 pub fn apply(&self, size: PhysicalSize, scale_factor: f64) -> PhysicalSize {
203 let mut result = size;
204 if let Some(min) = self.min {
205 let min = min.to_physical(scale_factor);
206 result.width = result.width.max(min.width);
207 result.height = result.height.max(min.height);
208 }
209 if let Some(max) = self.max {
210 let max = max.to_physical(scale_factor);
211 result.width = result.width.min(max.width);
212 result.height = result.height.min(max.height);
213 }
214 if let Some(aspect) = self.aspect {
215 result = aspect.fit(result);
216 }
217 result
218 }
219}
220
221/// Everything needed to create a window.
222///
223/// POD apart from the borrowed title, and `Copy`-free only because of it. The
224/// lifetime is deliberate: a shell never needs to own the string past the call,
225/// and forcing a `String` on every caller would allocate on a path that runs
226/// once.
227#[derive(Clone, Copy, Debug)]
228pub struct WindowDesc<'a> {
229 /// Title bar text. Also what the taskbar and window switcher show.
230 pub title: &'a str,
231
232 /// Application identifier, e.g. `"sh.kryptic.crcbl.sandbox"`.
233 ///
234 /// Wayland's `xdg_toplevel.set_app_id` and X11's `WM_CLASS`. This is how a
235 /// desktop environment matches a window to its `.desktop` file for the icon
236 /// and the taskbar grouping, and getting it wrong is why some applications
237 /// show a generic gear icon. There is no useful default, so it is
238 /// required.
239 pub app_id: &'a str,
240
241 /// Requested client-area size, in logical units.
242 ///
243 /// A request, not a demand — a tiling window manager will size the window
244 /// however it likes and the first
245 /// [`ShellEvent::Resized`](crate::ShellEvent::Resized) is the authority.
246 /// Never size a swapchain from this field.
247 pub size: LogicalSize,
248
249 /// Resize limits and aspect lock; see [`SizeConstraints`].
250 pub constraints: SizeConstraints,
251
252 /// Windowed or borderless at creation.
253 ///
254 /// Setting it here rather than creating windowed and immediately switching
255 /// avoids a visible flash of a decorated window, which is what every engine
256 /// that gets this wrong looks like on startup.
257 pub mode: DisplayMode,
258
259 /// Whether the window is resizable at all.
260 ///
261 /// Distinct from `min == max` in [`SizeConstraints`]: this also removes the
262 /// maximize button and the resize handles, which is a different visual
263 /// result even where the enforced size is identical.
264 pub resizable: bool,
265
266 /// Whether to map the window immediately.
267 ///
268 /// `false` creates it hidden, so the first frame can be rendered before
269 /// anything is shown — the fix for the black-window flash on startup.
270 /// Reveal it with
271 /// [`set_visible`](crate::Shell::set_visible) once there is something to
272 /// look at.
273 pub visible: bool,
274
275 /// Whether files dropped on the window should produce
276 /// [`ShellEvent::DroppedFile`](crate::ShellEvent::DroppedFile).
277 ///
278 /// Off by default because accepting a drop means advertising it to the
279 /// window system, and a game that ignores drops should not show a
280 /// drop cursor.
281 pub accept_drops: bool,
282
283 /// What kind of window: an application window, a panel or a popup. See
284 /// [`WindowRole`](crate::WindowRole).
285 pub role: crate::WindowRole<'a>,
286
287 /// Whether the desktop shows through where the frame's alpha is below one:
288 /// a bar with no background, a launcher with rounded corners.
289 ///
290 /// Asked for at creation because every window system decides it then —
291 /// the visual, the swapchain, the window's own background. A backend that
292 /// cannot do it refuses the window
293 /// ([`ShellCaps::TRANSPARENT`](crate::ShellCaps::TRANSPARENT)) rather
294 /// than draw it opaque. The frame's alpha is premultiplied.
295 ///
296 /// # End to end
297 ///
298 /// 1. **The window.** This flag, on a shell with `TRANSPARENT`. The shell
299 /// hands it on in the window's surface target
300 /// ([`SurfaceTarget::transparent`](crcbl_core::SurfaceTarget::transparent)).
301 /// 2. **The swapchain.** `crcbl::engine::GpuContext` reads the target and
302 /// asks for `CompositeAlpha::PreMultiplied`, and refuses the open
303 /// (`GpuError::Unusable`) when no adapter's surface offers it rather
304 /// than drawing the window opaque. On Windows only the D3D12 backend
305 /// can present it; Vulkan refuses the surface. A swapchain built by
306 /// hand asks for premultiplied itself.
307 /// 3. **The frame.** Premultiplied colour, with alpha below one where the
308 /// desktop should show. A forward renderer view with
309 /// `ViewBackground::Transparent` clears to transparent black and writes
310 /// its coverage as alpha beside straight colour. That is premultiplied
311 /// output wherever the alpha is zero or one and an empty pixel holds no
312 /// scattered light (a frame without volumetric fog); where the alpha is
313 /// partial, such as water's edge fade, it is not.
314 pub transparent: bool,
315}
316
317impl Default for WindowDesc<'_> {
318 fn default() -> Self {
319 Self {
320 title: "Crucible",
321 app_id: "sh.kryptic.crcbl",
322 size: LogicalSize::new(1280.0, 720.0),
323 constraints: SizeConstraints::NONE,
324 mode: DisplayMode::Windowed,
325 resizable: true,
326 visible: true,
327 accept_drops: false,
328 role: crate::WindowRole::Toplevel,
329 transparent: false,
330 }
331 }
332}
333
334/// What the window system has actually told us about a window.
335///
336/// The three facts arrive **together or not at all**, which is why they are one
337/// struct rather than three fields on [`WindowState`]. Every backend delivers
338/// them as a single message — Wayland's `xdg_surface.configure`, Win32's
339/// `WM_DPICHANGED` with its suggested rectangle, AppKit's
340/// `windowDidChangeBackingProperties` — and a consumer that could hold a size
341/// from one configure and a scale from another would compute the wrong logical
342/// size for exactly one frame, which is the kind of bug that gets closed as
343/// "cannot reproduce".
344#[derive(Clone, Copy, Debug, PartialEq)]
345pub struct WindowConfiguration {
346 /// Client area in device pixels — what the swapchain must match.
347 pub size: PhysicalSize,
348 /// Device pixels per logical unit for this window.
349 pub scale_factor: f64,
350 /// The mode actually in effect.
351 ///
352 /// Not necessarily [`WindowState::requested_mode`]: a compositor may refuse
353 /// a fullscreen request, and a tiling window manager makes the whole
354 /// question moot. This is the answer, that is the question.
355 pub mode: DisplayMode,
356}
357
358/// A window's current, observable state.
359///
360/// Returned by [`Shell::window_state`](crate::Shell::window_state) as a snapshot
361/// by value. It exists so a consumer that missed an event — a system that
362/// started late, an editor panel that was just opened — can ask, instead of
363/// having to have watched the whole event stream from the beginning.
364///
365/// It is **not** a substitute for the events: a renderer that polls this every
366/// frame instead of handling
367/// [`ShellEvent::Resized`](crate::ShellEvent::Resized) will recreate its
368/// swapchain a frame late.
369///
370/// # Requested versus effective
371///
372/// The split down the middle of this struct is the seam's answer to "a window
373/// request is not a window command":
374///
375/// * [`requested_mode`](Self::requested_mode) and
376/// [`requested_constraints`](Self::requested_constraints) are what *we asked
377/// for*. They are always known, because we said them.
378/// * [`configuration`](Self::configuration) is what the *window system did*.
379/// It is `None` until the first configure arrives, and its
380/// [`mode`](WindowConfiguration::mode) can disagree with the requested one
381/// forever.
382///
383/// Nothing here reports "effective constraints", because no platform has such a
384/// concept: min/max/aspect are hints whose only observable effect is the size
385/// in [`configuration`](Self::configuration).
386#[derive(Clone, Copy, Debug, PartialEq)]
387pub struct WindowState {
388 /// What the window system has told us, or `None` if it has not yet.
389 ///
390 /// See [`WindowState::size`] for the ordering constraint this creates.
391 pub configuration: Option<WindowConfiguration>,
392 /// The mode most recently asked for through
393 /// [`Shell::set_mode`](crate::Shell::set_mode) or
394 /// [`WindowDesc::mode`].
395 pub requested_mode: DisplayMode,
396 /// The constraints most recently asked for. Hints; see
397 /// [`SizeConstraints`].
398 pub requested_constraints: SizeConstraints,
399 /// Whether this window has keyboard focus.
400 pub focused: bool,
401 /// Whether the window is mapped and not minimized.
402 pub visible: bool,
403 /// Current pointer mode; see [`PointerMode`](crate::PointerMode).
404 pub pointer_mode: crate::PointerMode,
405 /// Whether a [`CloseRequested`](crate::ShellEvent::CloseRequested) is
406 /// awaiting a [`reply_close_request`](crate::Shell::reply_close_request).
407 pub close_pending: bool,
408}
409
410impl WindowState {
411 /// The client area in device pixels, or `None` while the window is
412 /// unconfigured.
413 ///
414 /// # This is the swapchain ordering constraint
415 ///
416 /// A freshly created window **has no size**. On Wayland an `xdg_surface`
417 /// is not sized — and must not be given a buffer — until the compositor
418 /// answers with a `configure`, which is a round trip away. A swapchain
419 /// needs an extent, so the mandatory sequence is:
420 ///
421 /// ```text
422 /// let window = shell.create_window(&desc)?;
423 /// loop {
424 /// shell.pump(&mut |event| { … }); // ← the first Resized
425 /// if let Some(size) = shell.window_state(window)?.size() {
426 /// create_swapchain(size); // only now
427 /// break;
428 /// }
429 /// }
430 /// ```
431 ///
432 /// [`HeadlessShell`](crate::HeadlessShell) deliberately defers the first
433 /// configure by at least one `pump` so that this sequence is exercised
434 /// rather than merely documented — a test double that succeeds immediately
435 /// proves nothing about the code path that matters.
436 #[inline]
437 #[must_use]
438 pub fn size(&self) -> Option<PhysicalSize> {
439 self.configuration.map(|config| config.size)
440 }
441
442 /// Device pixels per logical unit, or `None` while the window is
443 /// unconfigured.
444 #[inline]
445 #[must_use]
446 pub fn scale_factor(&self) -> Option<f64> {
447 self.configuration.map(|config| config.scale_factor)
448 }
449
450 /// The mode actually in effect, or `None` while the window is unconfigured.
451 ///
452 /// Compare [`requested_mode`](Self::requested_mode) — they can differ.
453 #[inline]
454 #[must_use]
455 pub fn effective_mode(&self) -> Option<DisplayMode> {
456 self.configuration.map(|config| config.mode)
457 }
458
459 /// Whether the window system has reported a size yet.
460 #[inline]
461 #[must_use]
462 pub const fn is_configured(&self) -> bool {
463 self.configuration.is_some()
464 }
465
466 /// Whether the window system honoured the last mode request.
467 ///
468 /// `false` while unconfigured (nothing has been honoured yet) and `false`
469 /// when a compositor refused. A UI showing a fullscreen toggle reads this,
470 /// not [`requested_mode`](Self::requested_mode), or the checkbox lies.
471 ///
472 /// The comparison is [`DisplayMode::satisfied_by`] rather than `==`; see
473 /// there for why a request naming no monitor is answered by one that does.
474 #[must_use]
475 pub fn mode_request_honoured(&self) -> bool {
476 self.effective_mode()
477 .is_some_and(|effective| self.requested_mode.satisfied_by(effective))
478 }
479}
480
481/// What to do about a close request.
482///
483/// The reply exists because closing is the one window operation with a
484/// *decision* in it — "you have unsaved changes" — and a shell that closes the
485/// window before asking has already lost the argument. Both Wayland
486/// (`xdg_toplevel.close` is purely advisory) and X11
487/// (`WM_DELETE_WINDOW`) model it exactly this way; Win32's `WM_CLOSE` does too.
488#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
489pub enum CloseReply {
490 /// Destroy the window. A
491 /// [`WindowDestroyed`](crate::ShellEvent::WindowDestroyed) event follows.
492 #[default]
493 Close,
494 /// Keep it open — the application handled the request itself, by showing a
495 /// save prompt or by refusing.
496 Keep,
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502
503 #[test]
504 fn there_is_no_true_fullscreen_variant() {
505 // The assertion is structural: `DisplayMode` has two variants and this
506 // match is exhaustive without a wildcard. Adding exclusive fullscreen
507 // would break this file, which is the intent — see the module docs.
508 let modes = [
509 DisplayMode::Windowed,
510 DisplayMode::Borderless { monitor: None },
511 ];
512 for mode in modes {
513 match mode {
514 DisplayMode::Windowed => assert!(!mode.is_borderless()),
515 DisplayMode::Borderless { .. } => assert!(mode.is_borderless()),
516 }
517 }
518 assert_eq!(DisplayMode::default(), DisplayMode::Windowed);
519 assert_eq!(DisplayMode::Windowed.to_string(), "windowed");
520 assert_eq!(
521 DisplayMode::Borderless { monitor: None }.to_string(),
522 "borderless"
523 );
524 assert_eq!(
525 DisplayMode::Borderless {
526 monitor: Some(MonitorId(3))
527 }
528 .to_string(),
529 "borderless on monitor 3"
530 );
531 }
532
533 #[test]
534 fn constraints_clamp_then_fit() {
535 let constraints = SizeConstraints {
536 min: Some(LogicalSize::new(320.0, 240.0)),
537 max: Some(LogicalSize::new(1920.0, 1080.0)),
538 aspect: None,
539 };
540 assert_eq!(
541 constraints.apply(PhysicalSize::new(100, 100), 1.0),
542 PhysicalSize::new(320, 240)
543 );
544 assert_eq!(
545 constraints.apply(PhysicalSize::new(4000, 4000), 1.0),
546 PhysicalSize::new(1920, 1080)
547 );
548 // Constraints are logical, so they scale with the monitor.
549 assert_eq!(
550 constraints.apply(PhysicalSize::new(100, 100), 2.0),
551 PhysicalSize::new(640, 480)
552 );
553 }
554
555 #[test]
556 fn an_aspect_lock_wins_against_the_minimum() {
557 // The documented tie-break: a wrong-shaped window is visibly broken, a
558 // slightly-too-small one is not.
559 let constraints = SizeConstraints::min(LogicalSize::new(640.0, 640.0))
560 .with_aspect(AspectRatio::WIDESCREEN);
561 let result = constraints.apply(PhysicalSize::new(100, 100), 1.0);
562 assert_eq!(result, PhysicalSize::new(640, 360));
563 assert!(result.height < 640, "the aspect fit took precedence");
564 }
565
566 #[test]
567 fn no_constraints_is_the_identity() {
568 let size = PhysicalSize::new(1234, 567);
569 assert_eq!(SizeConstraints::NONE.apply(size, 1.0), size);
570 assert_eq!(SizeConstraints::default(), SizeConstraints::NONE);
571 }
572
573 #[test]
574 fn an_unconfigured_window_has_no_size_at_all() {
575 let unconfigured = WindowState {
576 configuration: None,
577 requested_mode: DisplayMode::Borderless { monitor: None },
578 requested_constraints: SizeConstraints::NONE,
579 focused: false,
580 visible: true,
581 pointer_mode: crate::PointerMode::Free,
582 close_pending: false,
583 };
584 assert!(!unconfigured.is_configured());
585 assert_eq!(unconfigured.size(), None);
586 assert_eq!(unconfigured.scale_factor(), None);
587 assert_eq!(unconfigured.effective_mode(), None);
588 assert!(
589 !unconfigured.mode_request_honoured(),
590 "nothing has been honoured before the first configure"
591 );
592 }
593
594 #[test]
595 fn a_refused_mode_request_is_visible_as_a_disagreement() {
596 // The compositor said no. `requested_mode` still says what we asked;
597 // the configuration says what happened. A UI toggle must read the
598 // latter.
599 let refused = WindowState {
600 configuration: Some(WindowConfiguration {
601 size: PhysicalSize::new(1280, 720),
602 scale_factor: 1.0,
603 mode: DisplayMode::Windowed,
604 }),
605 requested_mode: DisplayMode::Borderless { monitor: None },
606 requested_constraints: SizeConstraints::NONE,
607 focused: true,
608 visible: true,
609 pointer_mode: crate::PointerMode::Free,
610 close_pending: false,
611 };
612 assert!(refused.is_configured());
613 assert_eq!(refused.size(), Some(PhysicalSize::new(1280, 720)));
614 assert_eq!(refused.effective_mode(), Some(DisplayMode::Windowed));
615 assert!(!refused.mode_request_honoured());
616
617 let honoured = WindowState {
618 requested_mode: DisplayMode::Windowed,
619 ..refused
620 };
621 assert!(honoured.mode_request_honoured());
622 }
623
624 #[test]
625 fn a_request_naming_no_monitor_is_answered_by_one_that_names_a_monitor() {
626 let anywhere = DisplayMode::Borderless { monitor: None };
627 let there = DisplayMode::Borderless {
628 monitor: Some(MonitorId(1)),
629 };
630 let elsewhere = DisplayMode::Borderless {
631 monitor: Some(MonitorId(2)),
632 };
633
634 // The asymmetry, in both directions. A request that did not care is
635 // answered by any monitor; a request that named one is not answered by
636 // a backend that cannot say which.
637 assert!(anywhere.satisfied_by(there));
638 assert!(anywhere.satisfied_by(anywhere));
639 assert!(there.satisfied_by(there));
640 assert!(!there.satisfied_by(anywhere));
641 assert!(!there.satisfied_by(elsewhere));
642
643 // And it is still a comparison, not a rubber stamp.
644 assert!(!anywhere.satisfied_by(DisplayMode::Windowed));
645 assert!(!DisplayMode::Windowed.satisfied_by(there));
646 assert!(DisplayMode::Windowed.satisfied_by(DisplayMode::Windowed));
647
648 // The case the X11 backend produces: fullscreen was granted, and the
649 // backend worked out the monitor from the window's origin.
650 let granted = WindowState {
651 configuration: Some(WindowConfiguration {
652 size: PhysicalSize::new(1920, 1080),
653 scale_factor: 1.0,
654 mode: there,
655 }),
656 requested_mode: anywhere,
657 requested_constraints: SizeConstraints::NONE,
658 focused: true,
659 visible: true,
660 pointer_mode: crate::PointerMode::Free,
661 close_pending: false,
662 };
663 assert!(granted.mode_request_honoured());
664 assert!(
665 !WindowState {
666 requested_mode: elsewhere,
667 ..granted
668 }
669 .mode_request_honoured(),
670 "asking for monitor 2 and being put on monitor 1 is a refusal"
671 );
672 }
673
674 #[test]
675 fn the_default_window_is_a_sane_720p_one() {
676 let desc = WindowDesc::default();
677 assert_eq!(desc.size, LogicalSize::new(1280.0, 720.0));
678 assert_eq!(desc.mode, DisplayMode::Windowed);
679 assert!(desc.resizable && desc.visible);
680 assert!(!desc.accept_drops, "drops are opt-in");
681 assert!(!desc.app_id.is_empty(), "app_id has no useful empty value");
682 assert_eq!(CloseReply::default(), CloseReply::Close);
683 }
684}