Skip to main content

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}