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