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, and an open that named
306    ///    no backend moves on to D3D12, while one that named Vulkan fails
307    ///    with that refusal. A swapchain built by hand asks for premultiplied
308    ///    itself.
309    /// 3. **The frame.** Premultiplied colour, with alpha below one where the
310    ///    desktop should show. A forward renderer view with
311    ///    `ViewBackground::Transparent` clears to transparent black and writes
312    ///    its coverage as alpha beside straight colour. That is premultiplied
313    ///    output wherever the alpha is zero or one and an empty pixel holds no
314    ///    scattered light (a frame without volumetric fog); where the alpha is
315    ///    partial, such as water's edge fade, it is not.
316    pub transparent: bool,
317}
318
319impl Default for WindowDesc<'_> {
320    fn default() -> Self {
321        Self {
322            title: "Crucible",
323            app_id: "sh.kryptic.crcbl",
324            size: LogicalSize::new(1280.0, 720.0),
325            constraints: SizeConstraints::NONE,
326            mode: DisplayMode::Windowed,
327            resizable: true,
328            visible: true,
329            accept_drops: false,
330            role: crate::WindowRole::Toplevel,
331            transparent: false,
332        }
333    }
334}
335
336/// What the window system has actually told us about a window.
337///
338/// The three facts arrive **together or not at all**, which is why they are one
339/// struct rather than three fields on [`WindowState`]. Every backend delivers
340/// them as a single message — Wayland's `xdg_surface.configure`, Win32's
341/// `WM_DPICHANGED` with its suggested rectangle, AppKit's
342/// `windowDidChangeBackingProperties` — and a consumer that could hold a size
343/// from one configure and a scale from another would compute the wrong logical
344/// size for exactly one frame, which is the kind of bug that gets closed as
345/// "cannot reproduce".
346#[derive(Clone, Copy, Debug, PartialEq)]
347pub struct WindowConfiguration {
348    /// Client area in device pixels — what the swapchain must match.
349    pub size: PhysicalSize,
350    /// Device pixels per logical unit for this window.
351    pub scale_factor: f64,
352    /// The mode actually in effect.
353    ///
354    /// Not necessarily [`WindowState::requested_mode`]: a compositor may refuse
355    /// a fullscreen request, and a tiling window manager makes the whole
356    /// question moot. This is the answer, that is the question.
357    pub mode: DisplayMode,
358}
359
360/// A window's current, observable state.
361///
362/// Returned by [`Shell::window_state`](crate::Shell::window_state) as a snapshot
363/// by value. It exists so a consumer that missed an event — a system that
364/// started late, an editor panel that was just opened — can ask, instead of
365/// having to have watched the whole event stream from the beginning.
366///
367/// It is **not** a substitute for the events: a renderer that polls this every
368/// frame instead of handling
369/// [`ShellEvent::Resized`](crate::ShellEvent::Resized) will recreate its
370/// swapchain a frame late.
371///
372/// # Requested versus effective
373///
374/// The split down the middle of this struct is the seam's answer to "a window
375/// request is not a window command":
376///
377/// * [`requested_mode`](Self::requested_mode) and
378///   [`requested_constraints`](Self::requested_constraints) are what *we asked
379///   for*. They are always known, because we said them.
380/// * [`configuration`](Self::configuration) is what the *window system did*.
381///   It is `None` until the first configure arrives, and its
382///   [`mode`](WindowConfiguration::mode) can disagree with the requested one
383///   forever.
384///
385/// Nothing here reports "effective constraints", because no platform has such a
386/// concept: min/max/aspect are hints whose only observable effect is the size
387/// in [`configuration`](Self::configuration).
388#[derive(Clone, Copy, Debug, PartialEq)]
389pub struct WindowState {
390    /// What the window system has told us, or `None` if it has not yet.
391    ///
392    /// See [`WindowState::size`] for the ordering constraint this creates.
393    pub configuration: Option<WindowConfiguration>,
394    /// The mode most recently asked for through
395    /// [`Shell::set_mode`](crate::Shell::set_mode) or
396    /// [`WindowDesc::mode`].
397    pub requested_mode: DisplayMode,
398    /// The constraints most recently asked for. Hints; see
399    /// [`SizeConstraints`].
400    pub requested_constraints: SizeConstraints,
401    /// Whether this window has keyboard focus.
402    pub focused: bool,
403    /// Whether the window is mapped and not minimized.
404    pub visible: bool,
405    /// Current pointer mode; see [`PointerMode`](crate::PointerMode).
406    pub pointer_mode: crate::PointerMode,
407    /// Whether a [`CloseRequested`](crate::ShellEvent::CloseRequested) is
408    /// awaiting a [`reply_close_request`](crate::Shell::reply_close_request).
409    pub close_pending: bool,
410}
411
412impl WindowState {
413    /// The client area in device pixels, or `None` while the window is
414    /// unconfigured.
415    ///
416    /// # This is the swapchain ordering constraint
417    ///
418    /// A freshly created window **has no size**. On Wayland an `xdg_surface`
419    /// is not sized — and must not be given a buffer — until the compositor
420    /// answers with a `configure`, which is a round trip away. A swapchain
421    /// needs an extent, so the mandatory sequence is:
422    ///
423    /// ```text
424    /// let window = shell.create_window(&desc)?;
425    /// loop {
426    ///     shell.pump(&mut |event| { … });          // ← the first Resized
427    ///     if let Some(size) = shell.window_state(window)?.size() {
428    ///         create_swapchain(size);              // only now
429    ///         break;
430    ///     }
431    /// }
432    /// ```
433    ///
434    /// [`HeadlessShell`](crate::HeadlessShell) deliberately defers the first
435    /// configure by at least one `pump` so that this sequence is exercised
436    /// rather than merely documented — a test double that succeeds immediately
437    /// proves nothing about the code path that matters.
438    #[inline]
439    #[must_use]
440    pub fn size(&self) -> Option<PhysicalSize> {
441        self.configuration.map(|config| config.size)
442    }
443
444    /// Device pixels per logical unit, or `None` while the window is
445    /// unconfigured.
446    #[inline]
447    #[must_use]
448    pub fn scale_factor(&self) -> Option<f64> {
449        self.configuration.map(|config| config.scale_factor)
450    }
451
452    /// The mode actually in effect, or `None` while the window is unconfigured.
453    ///
454    /// Compare [`requested_mode`](Self::requested_mode) — they can differ.
455    #[inline]
456    #[must_use]
457    pub fn effective_mode(&self) -> Option<DisplayMode> {
458        self.configuration.map(|config| config.mode)
459    }
460
461    /// Whether the window system has reported a size yet.
462    #[inline]
463    #[must_use]
464    pub const fn is_configured(&self) -> bool {
465        self.configuration.is_some()
466    }
467
468    /// Whether the window system honoured the last mode request.
469    ///
470    /// `false` while unconfigured (nothing has been honoured yet) and `false`
471    /// when a compositor refused. A UI showing a fullscreen toggle reads this,
472    /// not [`requested_mode`](Self::requested_mode), or the checkbox lies.
473    ///
474    /// The comparison is [`DisplayMode::satisfied_by`] rather than `==`; see
475    /// there for why a request naming no monitor is answered by one that does.
476    #[must_use]
477    pub fn mode_request_honoured(&self) -> bool {
478        self.effective_mode()
479            .is_some_and(|effective| self.requested_mode.satisfied_by(effective))
480    }
481}
482
483/// What to do about a close request.
484///
485/// The reply exists because closing is the one window operation with a
486/// *decision* in it — "you have unsaved changes" — and a shell that closes the
487/// window before asking has already lost the argument. Both Wayland
488/// (`xdg_toplevel.close` is purely advisory) and X11
489/// (`WM_DELETE_WINDOW`) model it exactly this way; Win32's `WM_CLOSE` does too.
490#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
491pub enum CloseReply {
492    /// Destroy the window. A
493    /// [`WindowDestroyed`](crate::ShellEvent::WindowDestroyed) event follows.
494    #[default]
495    Close,
496    /// Keep it open — the application handled the request itself, by showing a
497    /// save prompt or by refusing.
498    Keep,
499}
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504
505    #[test]
506    fn there_is_no_true_fullscreen_variant() {
507        // The assertion is structural: `DisplayMode` has two variants and this
508        // match is exhaustive without a wildcard. Adding exclusive fullscreen
509        // would break this file, which is the intent — see the module docs.
510        let modes = [
511            DisplayMode::Windowed,
512            DisplayMode::Borderless { monitor: None },
513        ];
514        for mode in modes {
515            match mode {
516                DisplayMode::Windowed => assert!(!mode.is_borderless()),
517                DisplayMode::Borderless { .. } => assert!(mode.is_borderless()),
518            }
519        }
520        assert_eq!(DisplayMode::default(), DisplayMode::Windowed);
521        assert_eq!(DisplayMode::Windowed.to_string(), "windowed");
522        assert_eq!(
523            DisplayMode::Borderless { monitor: None }.to_string(),
524            "borderless"
525        );
526        assert_eq!(
527            DisplayMode::Borderless {
528                monitor: Some(MonitorId(3))
529            }
530            .to_string(),
531            "borderless on monitor 3"
532        );
533    }
534
535    #[test]
536    fn constraints_clamp_then_fit() {
537        let constraints = SizeConstraints {
538            min: Some(LogicalSize::new(320.0, 240.0)),
539            max: Some(LogicalSize::new(1920.0, 1080.0)),
540            aspect: None,
541        };
542        assert_eq!(
543            constraints.apply(PhysicalSize::new(100, 100), 1.0),
544            PhysicalSize::new(320, 240)
545        );
546        assert_eq!(
547            constraints.apply(PhysicalSize::new(4000, 4000), 1.0),
548            PhysicalSize::new(1920, 1080)
549        );
550        // Constraints are logical, so they scale with the monitor.
551        assert_eq!(
552            constraints.apply(PhysicalSize::new(100, 100), 2.0),
553            PhysicalSize::new(640, 480)
554        );
555    }
556
557    #[test]
558    fn an_aspect_lock_wins_against_the_minimum() {
559        // The documented tie-break: a wrong-shaped window is visibly broken, a
560        // slightly-too-small one is not.
561        let constraints = SizeConstraints::min(LogicalSize::new(640.0, 640.0))
562            .with_aspect(AspectRatio::WIDESCREEN);
563        let result = constraints.apply(PhysicalSize::new(100, 100), 1.0);
564        assert_eq!(result, PhysicalSize::new(640, 360));
565        assert!(result.height < 640, "the aspect fit took precedence");
566    }
567
568    #[test]
569    fn no_constraints_is_the_identity() {
570        let size = PhysicalSize::new(1234, 567);
571        assert_eq!(SizeConstraints::NONE.apply(size, 1.0), size);
572        assert_eq!(SizeConstraints::default(), SizeConstraints::NONE);
573    }
574
575    #[test]
576    fn an_unconfigured_window_has_no_size_at_all() {
577        let unconfigured = WindowState {
578            configuration: None,
579            requested_mode: DisplayMode::Borderless { monitor: None },
580            requested_constraints: SizeConstraints::NONE,
581            focused: false,
582            visible: true,
583            pointer_mode: crate::PointerMode::Free,
584            close_pending: false,
585        };
586        assert!(!unconfigured.is_configured());
587        assert_eq!(unconfigured.size(), None);
588        assert_eq!(unconfigured.scale_factor(), None);
589        assert_eq!(unconfigured.effective_mode(), None);
590        assert!(
591            !unconfigured.mode_request_honoured(),
592            "nothing has been honoured before the first configure"
593        );
594    }
595
596    #[test]
597    fn a_refused_mode_request_is_visible_as_a_disagreement() {
598        // The compositor said no. `requested_mode` still says what we asked;
599        // the configuration says what happened. A UI toggle must read the
600        // latter.
601        let refused = WindowState {
602            configuration: Some(WindowConfiguration {
603                size: PhysicalSize::new(1280, 720),
604                scale_factor: 1.0,
605                mode: DisplayMode::Windowed,
606            }),
607            requested_mode: DisplayMode::Borderless { monitor: None },
608            requested_constraints: SizeConstraints::NONE,
609            focused: true,
610            visible: true,
611            pointer_mode: crate::PointerMode::Free,
612            close_pending: false,
613        };
614        assert!(refused.is_configured());
615        assert_eq!(refused.size(), Some(PhysicalSize::new(1280, 720)));
616        assert_eq!(refused.effective_mode(), Some(DisplayMode::Windowed));
617        assert!(!refused.mode_request_honoured());
618
619        let honoured = WindowState {
620            requested_mode: DisplayMode::Windowed,
621            ..refused
622        };
623        assert!(honoured.mode_request_honoured());
624    }
625
626    #[test]
627    fn a_request_naming_no_monitor_is_answered_by_one_that_names_a_monitor() {
628        let anywhere = DisplayMode::Borderless { monitor: None };
629        let there = DisplayMode::Borderless {
630            monitor: Some(MonitorId(1)),
631        };
632        let elsewhere = DisplayMode::Borderless {
633            monitor: Some(MonitorId(2)),
634        };
635
636        // The asymmetry, in both directions. A request that did not care is
637        // answered by any monitor; a request that named one is not answered by
638        // a backend that cannot say which.
639        assert!(anywhere.satisfied_by(there));
640        assert!(anywhere.satisfied_by(anywhere));
641        assert!(there.satisfied_by(there));
642        assert!(!there.satisfied_by(anywhere));
643        assert!(!there.satisfied_by(elsewhere));
644
645        // And it is still a comparison, not a rubber stamp.
646        assert!(!anywhere.satisfied_by(DisplayMode::Windowed));
647        assert!(!DisplayMode::Windowed.satisfied_by(there));
648        assert!(DisplayMode::Windowed.satisfied_by(DisplayMode::Windowed));
649
650        // The case the X11 backend produces: fullscreen was granted, and the
651        // backend worked out the monitor from the window's origin.
652        let granted = WindowState {
653            configuration: Some(WindowConfiguration {
654                size: PhysicalSize::new(1920, 1080),
655                scale_factor: 1.0,
656                mode: there,
657            }),
658            requested_mode: anywhere,
659            requested_constraints: SizeConstraints::NONE,
660            focused: true,
661            visible: true,
662            pointer_mode: crate::PointerMode::Free,
663            close_pending: false,
664        };
665        assert!(granted.mode_request_honoured());
666        assert!(
667            !WindowState {
668                requested_mode: elsewhere,
669                ..granted
670            }
671            .mode_request_honoured(),
672            "asking for monitor 2 and being put on monitor 1 is a refusal"
673        );
674    }
675
676    #[test]
677    fn the_default_window_is_a_sane_720p_one() {
678        let desc = WindowDesc::default();
679        assert_eq!(desc.size, LogicalSize::new(1280.0, 720.0));
680        assert_eq!(desc.mode, DisplayMode::Windowed);
681        assert!(desc.resizable && desc.visible);
682        assert!(!desc.accept_drops, "drops are opt-in");
683        assert!(!desc.app_id.is_empty(), "app_id has no useful empty value");
684        assert_eq!(CloseReply::default(), CloseReply::Close);
685    }
686}