crcbl-shell 0.2.0

Crucible windowing shell: own Wayland, X11, Web/canvas and headless backends
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
//! The arithmetic half of the AppKit backend: the Y flip, points versus backing
//! pixels, style masks and window placement.
//!
//! Everything in here is a **pure function of numbers AppKit handed us**, and
//! that is why it is a module of its own rather than a scattering of helpers
//! through [`window`](super::window) and [`monitors`](super::monitors). The
//! Win32 backend splits `win32::geometry` out for two reasons and the second is
//! the load-bearing one; both apply here and the second applies harder:
//!
//! 1. A delegate callback runs inside `[NSApp sendEvent:]`, which runs inside
//!    this crate's own `pump`. Keeping the arithmetic it needs in functions that
//!    touch no object makes that property visible rather than hoped for.
//! 2. **It is the part of this backend that can be tested without a Mac.** This
//!    module compiles and its tests run on every host, so a mistake in the
//!    coordinate flip or in the points-to-pixels conversion is caught by
//!    `cargo test -p crcbl-shell` on the Linux machine this engine is developed
//!    on, rather than by a CI runner nobody is watching. That matters more here
//!    than it did for Windows, because more of this backend is a conversion:
//!    macOS is the only platform of the five whose coordinate system disagrees
//!    with the seam's on the direction of the Y axis.
//!
//! # Three spaces, and the flip between two of them
//!
//! * **AppKit screen space** — points, origin at the **bottom-left** of the
//!   primary display, **Y increasing upwards**. `NSScreen.frame`,
//!   `NSWindow.frame` and everything `setFrame:` takes are in it.
//! * **Desktop space** — the seam's [`PhysicalRect`], origin at the *top-left*,
//!   Y increasing downwards, matching Win32's virtual screen and X11's screen.
//! * **Backing pixels** — points multiplied by the window's or screen's
//!   `backingScaleFactor`, which is the seam's [`PhysicalSize`] and the extent a
//!   swapchain is created at.
//!
//! [`Flip`] is the first conversion and [`physical_size`] the second, and
//! **they are separate steps on purpose**: the flip has to happen in points,
//! because the global coordinate space *is* points. A desktop whose displays run
//! at different scales has no single pixel space at all — the caveat
//! [`MonitorInfo::bounds`](crate::MonitorInfo::bounds) already states for
//! Wayland — so scaling before flipping would compute a Y offset in one screen's
//! pixels against another screen's height.
//!
//! # Logical units *are* points, exactly
//!
//! The seam's [`LogicalSize`] is "device pixels divided by the scale factor",
//! and an AppKit point is "backing pixel divided by `backingScaleFactor`". They
//! are the same unit, so [`SizeConstraints`] converts to `setContentMinSize:`
//! with no arithmetic at all. Win32 has to multiply by the DPI and X11 by
//! `Xft.dpi`; this is the one platform whose own geometry is already in the
//! seam's request space, and [`content_limits`] is where that shows.

use crate::{
    AspectRatio, DisplayMode, LogicalSize, Margins, PanelAnchor, PanelLayer, PhysicalRect,
    PhysicalSize, PopupPlacement, SizeConstraints,
};

use super::ffi::{
    NSInteger, NSPoint, NSRect, NSSize, NSUInteger, collection, level_key, style, value,
};

/// The conversion between AppKit's Y-up screen space and the seam's Y-down
/// desktop space.
///
/// One number: the **top edge of the primary display**, in AppKit points. Every
/// rectangle in either space is a reflection about it.
///
/// # Why the primary display and not the whole desktop
///
/// AppKit's origin is the bottom-left of the primary display, so its `y = 0` is
/// that display's bottom edge — not the bottom of the union of all displays. A
/// second display placed above the primary one therefore has a *positive* origin
/// in AppKit and a *negative* one in desktop space, which is exactly what Win32
/// and X11 report for the same arrangement. Reflecting about the union's height
/// instead would put every window on a multi-display desktop at the wrong Y and
/// would be right on a single-display one, which is what a CI runner is.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Flip {
    /// `primary.frame.origin.y + primary.frame.size.height`, in points.
    top: f64,
}

impl Flip {
    /// The flip implied by the primary display's frame.
    #[must_use]
    pub const fn about_primary(primary: NSRect) -> Self {
        Self {
            top: primary.max_y(),
        }
    }

    /// A rectangle moved between the two spaces.
    ///
    /// **The same function both ways**, because a reflection is its own inverse:
    /// the top edge of a rectangle in one space is the bottom edge of the same
    /// rectangle in the other, and `top − (y + height)` computes each from the
    /// other. Having one function rather than a `to_`/`from_` pair is not
    /// tidiness — two of them would be two chances to write the formula down
    /// differently, and the round trip is the property that catches it.
    #[must_use]
    pub const fn rect(self, rect: NSRect) -> NSRect {
        NSRect::new(
            rect.origin.x,
            self.top - rect.max_y(),
            rect.size.width,
            rect.size.height,
        )
    }

    /// A **point** moved between the two spaces.
    ///
    /// The same reflection with no height to subtract, which is what makes it
    /// the simpler of the pair rather than a special case of it: a rectangle's
    /// origin is its *bottom* edge in one space and its *top* edge in the other,
    /// so [`rect`](Self::rect) has to reflect `y + height` while a point
    /// reflects `y`. Writing `rect` in terms of this one is the identity
    /// `rect.origin.y ↦ top − (y + height)`, and the test below asserts it so
    /// the two can never disagree about where the edge is.
    ///
    /// This is the space `CGWarpMouseCursorPosition` takes and the space
    /// `CGEventGetLocation` answers in — CoreGraphics puts its origin at the
    /// top-left of the main display, which is the seam's convention and the
    /// opposite of AppKit's. So this function is the whole of the warp's second
    /// reflection; see [`pointer::warp_source`](super::pointer::warp_source) for
    /// the first.
    ///
    /// Its own inverse, for the reason [`rect`](Self::rect) gives.
    #[must_use]
    pub const fn point(self, point: NSPoint) -> NSPoint {
        NSPoint {
            x: point.x,
            y: self.top - point.y,
        }
    }
}

/// A rectangle of points as the seam's desktop rectangle, at `scale`.
///
/// The rectangle must already have been through [`Flip::rect`]; this only
/// scales. Rounding is to nearest, so a fractional point origin — which a
/// display arrangement can produce — does not truncate a window one pixel off
/// its screen.
#[must_use]
pub fn physical_rect(flipped_points: NSRect, scale: f64) -> PhysicalRect {
    let scale = usable_scale(scale);
    PhysicalRect::new(
        round_to_i32(flipped_points.origin.x * scale),
        round_to_i32(flipped_points.origin.y * scale),
        round_to_u32(flipped_points.size.width * scale),
        round_to_u32(flipped_points.size.height * scale),
    )
}

/// A size in points as the seam's size in backing pixels.
///
/// # The conversion this whole module exists to not get wrong
///
/// `NSWindow.frame` and `NSView.bounds` are in **points**; a swapchain is
/// created in **pixels**. On a 1× display they are the same number, which is why
/// mixing them up produces something that looks perfectly correct on a CI runner
/// and is half the size it should be on every Retina Mac — that is to say, on
/// every Mac. The tests for this function are written at 2× deliberately.
#[must_use]
pub fn physical_size(points: NSSize, scale: f64) -> PhysicalSize {
    let scale = usable_scale(scale);
    PhysicalSize::new(
        round_to_u32(points.width * scale).max(1),
        round_to_u32(points.height * scale).max(1),
    )
}

/// The seam's logical size as AppKit points.
///
/// The identity, in a different type. See the module docs for why that is a
/// fact about macOS rather than a shortcut here.
#[must_use]
pub const fn points(size: LogicalSize) -> NSSize {
    NSSize::new(size.width, size.height)
}

/// A `backingScaleFactor` as the seam's scale factor.
///
/// AppKit reports 1.0 or 2.0 and nothing between: a display running a "scaled"
/// HiDPI mode still answers 2.0 and changes the *point* resolution instead.
/// That is why [`FRACTIONAL_SCALE`](crate::ShellCaps::FRACTIONAL_SCALE) is clear
/// on this backend while it is set on Win32 and on Wayland — see
/// [`caps`](super::AppKitShell::caps).
///
/// A zero or negative factor means the object was `nil`; 1.0 is the answer that
/// leaves a window the size it asked for rather than one collapsed to a point.
#[must_use]
pub fn usable_scale(backing: f64) -> f64 {
    if backing.is_finite() && backing > 0.0 {
        backing
    } else {
        1.0
    }
}

/// The `NSWindowStyleMask` for a display mode.
///
/// # Borderless is a style mask, not a mode the system has
///
/// The windowing rules (`docs/notes/backends.md`) drop exclusive fullscreen and
/// keep two modes,
/// and neither of them is macOS's *Spaces* fullscreen — `toggleFullScreen:`,
/// which moves the window to a space of its own with an animation, a menu bar
/// that slides, and a set of failure modes that are entirely its own. Borderless
/// here is the same recipe as on Windows: remember the mask and the frame, drop
/// to `NSWindowStyleMaskBorderless`, and set the frame to the target screen's.
///
/// A non-resizable window loses `NSWindowStyleMaskResizable` rather than being
/// clamped by a constraint —
/// [`WindowDesc::resizable`](crate::WindowDesc::resizable) documents that the
/// difference from `min == max` is exactly this: the resize handles and the
/// zoom button go away.
#[must_use]
pub fn style_mask(mode: DisplayMode, resizable: bool) -> usize {
    match mode {
        DisplayMode::Windowed => {
            let mut mask = style::TITLED | style::CLOSABLE | style::MINIATURIZABLE;
            if resizable {
                mask |= style::RESIZABLE;
            }
            mask
        }
        // Not `TITLED | RESIZABLE` with a hidden title bar, which is the other
        // recipe people reach for: that leaves the window with a frame to
        // account for, and the whole point is that the content rectangle and the
        // frame rectangle become the same thing.
        DisplayMode::Borderless { .. } => style::BORDERLESS,
    }
}

/// The style mask of a [panel](crate::WindowRole::Panel): no frame, and
/// non-activating, so a panel that takes the keyboard does so without
/// bringing the application forward — what a launcher does.
pub const PANEL_STYLE_MASK: NSUInteger = style::BORDERLESS | style::NONACTIVATING_PANEL;

/// The window level of a panel layer, as the `CGWindowLevelKey` to ask
/// `CGWindowLevelForKey` for and how far above that level to sit.
///
/// `Top` is above the menu bar, as a status bar has to be, and `Overlay` is
/// above every application window. `Background` is the desktop picture's own
/// level and `Bottom` sits just above the desktop icons.
#[must_use]
pub const fn panel_level(layer: PanelLayer) -> (i32, NSInteger) {
    match layer {
        PanelLayer::Background => (level_key::DESKTOP, 0),
        PanelLayer::Bottom => (level_key::DESKTOP_ICON, 1),
        PanelLayer::Top => (level_key::STATUS, 0),
        PanelLayer::Overlay => (level_key::POP_UP_MENU, 0),
    }
}

/// How a panel takes part in spaces: on every space and left in place by
/// Mission Control, out of window cycling, and — on the overlay layer, the
/// only one above full-screen windows — allowed into a full-screen window's
/// space.
#[must_use]
pub const fn panel_collection(layer: PanelLayer) -> NSUInteger {
    let common =
        collection::CAN_JOIN_ALL_SPACES | collection::STATIONARY | collection::IGNORES_CYCLE;
    match layer {
        PanelLayer::Overlay => common | collection::FULL_SCREEN_AUXILIARY,
        PanelLayer::Background | PanelLayer::Bottom | PanelLayer::Top => common,
    }
}

/// Where a panel of `size` goes in `area`, all in AppKit points with Y up:
/// `margins` away from each edge it is anchored to, stretched between two
/// opposite anchored edges, and centred on an axis anchored to neither.
///
/// `wlr-layer-shell`'s placement, which [`PanelDesc`](crate::PanelDesc) is
/// written in, with the vertical axis turned over: `TOP` is the high end of Y.
#[must_use]
pub fn panel_frame(area: NSRect, anchor: PanelAnchor, size: NSSize, margins: Margins) -> NSRect {
    let (x, width) = place_on_axis(
        (area.origin.x, area.size.width),
        (
            anchor.contains(PanelAnchor::LEFT),
            anchor.contains(PanelAnchor::RIGHT),
        ),
        (margins.left, margins.right),
        size.width,
    );
    let (y, height) = place_on_axis(
        (area.origin.y, area.size.height),
        (
            anchor.contains(PanelAnchor::BOTTOM),
            anchor.contains(PanelAnchor::TOP),
        ),
        (margins.bottom, margins.top),
        size.height,
    );
    NSRect::new(x, y, width, height)
}

/// One axis of [`panel_frame`]: where a panel of `length` starts along an
/// area's `(start, length)`, and how long it is, given which ends it is
/// anchored to and its margins from them.
///
/// **The start is a whole point.** Centring an even length in an odd one
/// lands on a half point, and AppKit snaps a frame to whole pixels by growing
/// it, so a 300-point panel came out 301 points tall.
fn place_on_axis(
    (area_start, area_length): (f64, f64),
    (at_start, at_end): (bool, bool),
    (margin_start, margin_end): (f64, f64),
    length: f64,
) -> (f64, f64) {
    let (start, length) = match (at_start, at_end) {
        (true, true) => (
            area_start + margin_start,
            (area_length - margin_start - margin_end).max(1.0),
        ),
        (true, false) => (area_start + margin_start, length),
        (false, true) => (area_start + area_length - margin_end - length, length),
        (false, false) => (area_start + (area_length - length) / 2.0, length),
    };
    (start.round(), length)
}

/// Where a popup of `size` goes against `anchor`, all in AppKit points with Y
/// up: [`role::place_popup`](crate::role::place_popup)'s placement, flipped
/// or slid to stay in `visible`, with its origin on a whole point for the
/// reason [`panel_frame`] gives.
///
/// The placement is worked in Y-down, so each rectangle is reflected about
/// `y = 0` on the way in and back on the way out: a reflection is its own
/// inverse, and which line it is about does not matter so long as both ways
/// use the same one.
#[must_use]
pub fn popup_frame(
    anchor: NSRect,
    size: NSSize,
    placement: PopupPlacement,
    visible: NSRect,
) -> NSRect {
    let down = |rect: NSRect| crate::role::Placed {
        x: rect.origin.x,
        y: -rect.max_y(),
        width: rect.size.width,
        height: rect.size.height,
    };
    let (x, y_down) = crate::role::place_popup(
        down(anchor),
        (size.width, size.height),
        placement,
        down(visible),
    );
    NSRect::new(
        x.round(),
        (-y_down - size.height).round(),
        size.width,
        size.height,
    )
}

/// The presentation options a mode wants from `NSApplication`.
///
/// Process-wide state rather than a window property — there is one menu bar —
/// so [`AppKitShell`](super::AppKitShell) owns restoring it, exactly as the
/// Win32 backend owns giving the cursor clip back. See
/// [`PRESENTATION_BORDERLESS`](value::PRESENTATION_BORDERLESS) for why the two
/// bits are never set apart.
#[must_use]
pub const fn presentation_options(mode: DisplayMode) -> usize {
    match mode {
        DisplayMode::Windowed => value::PRESENTATION_DEFAULT,
        DisplayMode::Borderless { .. } => value::PRESENTATION_BORDERLESS,
    }
}

/// [`SizeConstraints`] as the two `NSSize`s `setContentMinSize:` and
/// `setContentMaxSize:` take.
///
/// No scale factor, and no frame to add: AppKit's limits are on the **content**
/// rectangle and are in points, which is the seam's own request space. Both of
/// the other desktop backends have to do arithmetic here and both can get it
/// wrong — Win32's has to add the caption height or the minimum is tens of
/// pixels off.
///
/// `None` becomes AppKit's own default, which for the minimum is zero and for
/// the maximum is [`f64::MAX`] — the values the class documents as "no limit",
/// rather than a number this backend invented.
#[must_use]
pub fn content_limits(constraints: SizeConstraints) -> (NSSize, NSSize) {
    let min = constraints.min.map_or(NSSize::new(0.0, 0.0), points);
    let max = constraints
        .max
        .map_or(NSSize::new(f64::MAX, f64::MAX), points);
    (min, max)
}

/// An aspect ratio as the `NSSize` `setContentAspectRatio:` takes.
///
/// This is the macOS half of
/// [`ASPECT_HINT_HONORED`](crate::ShellCaps::ASPECT_HINT_HONORED), and
/// the windowing rules in `docs/notes/backends.md` name it exactly — "macOS
/// `setContentAspectRatio`". Unlike the Win32 backend there is no rectangle
/// algebra to write: AppKit enforces the ratio during the drag itself, and the
/// terms go across as a size whose *proportion* is what is read.
///
/// `NSSize::new(0.0, 0.0)` is what clears it, which is why the return type is
/// not an `Option`: there is one call either way.
#[must_use]
pub fn aspect_size(aspect: Option<AspectRatio>) -> NSSize {
    aspect.map_or(NSSize::new(0.0, 0.0), |ratio| {
        NSSize::new(f64::from(ratio.numerator()), f64::from(ratio.denominator()))
    })
}

/// Where to put a window of `size` so it sits in the middle of `area`, in
/// AppKit points.
///
/// `area` is a screen's `visibleFrame` — the part not covered by the menu bar or
/// the Dock — and the result is a bottom-left origin in the same space, so no
/// flip is involved. Clamped to the area's origin so that a window larger than
/// the visible frame starts with its **title bar** on screen: in a Y-up space
/// that is the *top*, so the clamp is on the high side of Y rather than the low
/// one, which is the sign error this function exists to have made once.
#[must_use]
pub fn centred(area: NSRect, size: NSSize) -> NSRect {
    let free_x = area.size.width - size.width;
    let free_y = area.size.height - size.height;
    NSRect::new(
        area.origin.x + (free_x / 2.0).max(0.0),
        // A negative gap would put the window's top above the visible frame,
        // where the title bar cannot be grabbed. Pinning the *top* to the top of
        // the area means the origin lands at `max_y − height`.
        if free_y >= 0.0 {
            area.origin.y + free_y / 2.0
        } else {
            area.max_y() - size.height
        },
        size.width,
        size.height,
    )
}

/// Rounds to the nearest `i32`, saturating.
fn round_to_i32(value: f64) -> i32 {
    let rounded = value.round();
    if rounded.is_nan() {
        0
    } else {
        rounded.clamp(f64::from(i32::MIN), f64::from(i32::MAX)) as i32
    }
}

/// Rounds to the nearest `u32`, saturating, with negatives becoming zero.
fn round_to_u32(value: f64) -> u32 {
    let rounded = value.round();
    if rounded.is_nan() {
        0
    } else {
        rounded.clamp(0.0, f64::from(u32::MAX)) as u32
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::MonitorId;

    /// A 1920×1080 primary display, which puts the flip at 1080.
    const PRIMARY: NSRect = NSRect::new(0.0, 0.0, 1920.0, 1080.0);

    #[test]
    fn the_primary_display_is_the_same_rectangle_in_both_spaces() {
        // The one arrangement where the flip is invisible, and therefore the one
        // a CI runner with a single display would pass whatever this function
        // did. Every other case below is the reason the fixtures exist.
        let flip = Flip::about_primary(PRIMARY);
        assert_eq!(flip.rect(PRIMARY), PRIMARY);
        assert_eq!(
            physical_rect(flip.rect(PRIMARY), 1.0),
            PhysicalRect::new(0, 0, 1920, 1080)
        );
    }

    #[test]
    fn a_display_above_the_primary_one_has_a_negative_desktop_origin() {
        // AppKit stacks upwards from the primary display's bottom-left, so a
        // display *above* the primary has a positive Y there — and a negative
        // one in the space Win32 and X11 report, which is what the seam means.
        let flip = Flip::about_primary(PRIMARY);
        let above = NSRect::new(0.0, 1080.0, 1920.0, 1080.0);
        assert_eq!(flip.rect(above), NSRect::new(0.0, -1080.0, 1920.0, 1080.0));

        // And below, which is the sign the naive `top - y` gets wrong: the
        // rectangle's *height* is part of the reflection.
        let below = NSRect::new(0.0, -1080.0, 1920.0, 1080.0);
        assert_eq!(flip.rect(below), NSRect::new(0.0, 1080.0, 1920.0, 1080.0));

        // Left and right are untouched — only Y is flipped.
        let left = NSRect::new(-1920.0, 0.0, 1920.0, 1080.0);
        assert_eq!(flip.rect(left), NSRect::new(-1920.0, 0.0, 1920.0, 1080.0));
    }

    #[test]
    fn a_display_of_a_different_height_reflects_about_its_own_top_edge() {
        // The case that catches a flip written as `height - y` rather than
        // `top - (y + height)`: a shorter display sitting beside a taller one,
        // bottom-aligned the way macOS's own arrangement pane does it.
        let flip = Flip::about_primary(PRIMARY);
        let shorter = NSRect::new(1920.0, 0.0, 1280.0, 720.0);
        // Its bottom edge is the primary's bottom edge, so its *top* is 360
        // points below the primary's top.
        assert_eq!(
            flip.rect(shorter),
            NSRect::new(1920.0, 360.0, 1280.0, 720.0)
        );
    }

    #[test]
    fn the_flip_is_its_own_inverse() {
        // The round trip is the property, and it is what makes one function
        // correct where a `to_`/`from_` pair would be two chances to disagree.
        let flip = Flip::about_primary(PRIMARY);
        for rect in [
            PRIMARY,
            NSRect::new(0.0, 1080.0, 1920.0, 1080.0),
            NSRect::new(-1440.0, -320.0, 1440.0, 900.0),
            NSRect::new(37.5, -0.5, 1.0, 1.0),
        ] {
            assert_eq!(flip.rect(flip.rect(rect)), rect, "{rect:?}");
        }
    }

    #[test]
    fn a_point_reflects_about_the_same_edge_a_rectangle_does() {
        // The relation that makes the pair one function rather than two: a
        // rectangle's flipped origin is its *top* edge flipped as a point. A
        // `Flip::point` written as `top - y - height`, or a `Flip::rect` written
        // without the height, would disagree here and nowhere else.
        let flip = Flip::about_primary(PRIMARY);
        for rect in [
            PRIMARY,
            NSRect::new(1920.0, 0.0, 1280.0, 720.0),
            NSRect::new(-1440.0, -320.0, 1440.0, 900.0),
        ] {
            let top_edge = NSPoint {
                x: rect.origin.x,
                y: rect.max_y(),
            };
            assert_eq!(flip.point(top_edge), flip.rect(rect).origin, "{rect:?}");
        }

        // The CoreGraphics space a warp is expressed in: the top-left of the
        // primary display is the origin, and AppKit calls the same place
        // `(0, 1080)`.
        assert_eq!(
            flip.point(NSPoint { x: 0.0, y: 1080.0 }),
            NSPoint { x: 0.0, y: 0.0 }
        );
        assert_eq!(
            flip.point(NSPoint { x: 640.0, y: 540.0 }),
            NSPoint { x: 640.0, y: 540.0 },
            "the middle of a 1080-tall display is its own reflection"
        );

        // And it is its own inverse, which is what makes one function correct
        // where a `to_`/`from_` pair would be two chances to disagree.
        for point in [
            NSPoint { x: 0.0, y: 0.0 },
            NSPoint {
                x: -37.5,
                y: 2160.0,
            },
            NSPoint { x: 1919.0, y: -1.0 },
        ] {
            assert_eq!(flip.point(flip.point(point)), point, "{point:?}");
        }
    }

    #[test]
    fn a_window_at_the_top_of_the_primary_display_is_at_desktop_y_zero() {
        // The concrete case a menu-bar-aware placement produces: a window whose
        // top edge is the screen's top edge. In AppKit its origin is
        // `1080 - height`; in desktop space it must be zero, not `1080`.
        let flip = Flip::about_primary(PRIMARY);
        let window = NSRect::new(320.0, 1080.0 - 720.0, 1280.0, 720.0);
        assert_eq!(flip.rect(window), NSRect::new(320.0, 0.0, 1280.0, 720.0));
    }

    #[test]
    fn points_become_backing_pixels_and_a_retina_window_is_twice_the_size() {
        // The half-size-on-every-Mac bug, asserted at the scale that can see it.
        // At 1.0 this function is the identity and proves nothing, which is
        // exactly what a single-scale CI runner would exercise.
        let content = NSSize::new(1280.0, 720.0);
        assert_eq!(physical_size(content, 1.0), PhysicalSize::new(1280, 720));
        assert_eq!(physical_size(content, 2.0), PhysicalSize::new(2560, 1440));

        // A whole screen, through both steps in the order they must happen.
        let flip = Flip::about_primary(NSRect::new(0.0, 0.0, 1440.0, 900.0));
        let retina = NSRect::new(0.0, 0.0, 1440.0, 900.0);
        assert_eq!(
            physical_rect(flip.rect(retina), 2.0),
            PhysicalRect::new(0, 0, 2880, 1800),
            "a 1440x900-point Retina panel is 2880x1800 backing pixels"
        );

        // A zero or absent scale factor leaves the window the size it asked for.
        assert_eq!(physical_size(content, 0.0), PhysicalSize::new(1280, 720));
        assert_eq!(
            physical_size(content, f64::NAN),
            PhysicalSize::new(1280, 720)
        );
        // And a window dragged to nothing still yields a legal extent.
        assert_eq!(
            physical_size(NSSize::new(0.0, 0.0), 2.0),
            PhysicalSize::new(1, 1)
        );
    }

    #[test]
    fn a_scaled_hidpi_mode_is_two_times_a_larger_point_size() {
        // What macOS calls "looks like 1680x1050" on a 2880x1800 panel: the
        // point resolution changes and `backingScaleFactor` stays 2.0. So the
        // backing store is 3360x2100 and is *then* downscaled by the compositor,
        // and 3360x2100 is what a swapchain must be created at. Reporting the
        // panel's 2880x1800 instead would be a swapchain that does not match the
        // layer.
        assert_eq!(
            physical_size(NSSize::new(1680.0, 1050.0), 2.0),
            PhysicalSize::new(3360, 2100)
        );
    }

    #[test]
    fn a_panel_is_a_frameless_nonactivating_panel_at_its_layers_level() {
        assert_eq!(PANEL_STYLE_MASK, 1 << 7, "borderless is zero");
        assert_eq!(style::NONACTIVATING_PANEL, 1 << 7);
        assert_eq!(
            [
                PanelLayer::Background,
                PanelLayer::Bottom,
                PanelLayer::Top,
                PanelLayer::Overlay
            ]
            .map(panel_level),
            [(2, 0), (18, 1), (9, 0), (11, 0)],
            "CGWindowLevelKey's own numbers"
        );
        assert_eq!(panel_collection(PanelLayer::Top), 0b101_0001);
        assert_eq!(panel_collection(PanelLayer::Overlay), 0b1_0101_0001);
    }

    #[test]
    fn a_popup_frame_is_below_its_anchor_with_y_up_and_flips_at_the_bottom() {
        let visible = NSRect::new(0.0, 0.0, 1440.0, 875.0);
        let menu = NSSize::new(200.0, 300.0);
        // A button near the top of the screen: 30 high, its top 25 below it.
        let button = NSRect::new(100.0, 820.0, 80.0, 30.0);
        assert_eq!(
            popup_frame(button, menu, PopupPlacement::Below, visible),
            NSRect::new(100.0, 520.0, 200.0, 300.0),
            "below the button is lower Y, top edge at the button's bottom"
        );
        let low = NSRect::new(1400.0, 10.0, 30.0, 20.0);
        assert_eq!(
            popup_frame(low, menu, PopupPlacement::Below, visible),
            NSRect::new(1240.0, 30.0, 200.0, 300.0),
            "no room below, so flipped above, and slid back onto the screen"
        );
    }

    #[test]
    fn a_panel_frame_is_layer_shell_placement_with_y_up() {
        // A visible frame below a 25-point menu bar on a 1440x900 screen.
        let area = NSRect::new(0.0, 0.0, 1440.0, 875.0);
        let margins = Margins {
            top: 4.0,
            right: 8.0,
            bottom: 12.0,
            left: 16.0,
        };
        let across_the_top = PanelAnchor::TOP | PanelAnchor::LEFT | PanelAnchor::RIGHT;
        assert_eq!(
            panel_frame(
                area,
                across_the_top,
                NSSize::new(0.0, 30.0),
                Margins::default()
            ),
            NSRect::new(0.0, 845.0, 1440.0, 30.0),
            "the top of a Y-up area is its high end"
        );
        assert_eq!(
            panel_frame(area, across_the_top, NSSize::new(0.0, 30.0), margins),
            NSRect::new(16.0, 841.0, 1416.0, 30.0),
        );
        assert_eq!(
            panel_frame(
                area,
                PanelAnchor::BOTTOM | PanelAnchor::LEFT,
                NSSize::new(64.0, 500.0),
                margins
            ),
            NSRect::new(16.0, 12.0, 64.0, 500.0),
            "the bottom is the low end"
        );
        assert_eq!(
            panel_frame(
                area,
                PanelAnchor::empty(),
                NSSize::new(600.0, 400.0),
                margins
            ),
            NSRect::new(420.0, 238.0, 600.0, 400.0),
            "centred on a whole point, and an unanchored panel's margins do nothing"
        );
    }

    #[test]
    fn a_windowed_mask_has_a_title_bar_and_a_borderless_one_has_nothing() {
        let windowed = style_mask(DisplayMode::Windowed, true);
        assert_ne!(windowed & style::TITLED, 0, "the title bar is why");
        assert_ne!(windowed & style::RESIZABLE, 0);
        assert_ne!(windowed & style::CLOSABLE, 0);

        // The bit `SERVER_DECORATIONS` is claiming: a decorated window really
        // does carry a title bar, so the UI layer must not draw its own.
        let borderless = style_mask(DisplayMode::Borderless { monitor: None }, true);
        assert_eq!(borderless, style::BORDERLESS);
        assert_eq!(borderless & style::TITLED, 0, "no title bar");
        assert_eq!(
            style_mask(DisplayMode::Borderless { monitor: None }, false),
            borderless,
            "there is nothing left to take away"
        );
    }

    #[test]
    fn a_fixed_size_window_loses_its_resize_handles() {
        // `WindowDesc::resizable` is documented as doing exactly this, and as
        // being a different thing from `min == max`.
        let fixed = style_mask(DisplayMode::Windowed, false);
        assert_eq!(fixed & style::RESIZABLE, 0);
        assert_ne!(fixed & style::TITLED, 0, "still a normal window");
        assert_ne!(fixed & style::MINIATURIZABLE, 0, "minimize survives");
    }

    #[test]
    fn borderless_asks_for_both_presentation_bits_or_neither() {
        // Setting `AutoHideMenuBar` without a Dock bit raises an Objective-C
        // exception, which unwinds into Rust and is undefined behaviour. This
        // asserts the pairing rather than the number.
        let borderless = presentation_options(DisplayMode::Borderless { monitor: None });
        assert_ne!(borderless & value::PRESENTATION_AUTO_HIDE_MENU_BAR, 0);
        assert_ne!(
            borderless & value::PRESENTATION_AUTO_HIDE_DOCK,
            0,
            "the menu-bar bit is only legal beside a Dock bit"
        );
        assert_eq!(
            presentation_options(DisplayMode::Windowed),
            value::PRESENTATION_DEFAULT
        );
        assert_eq!(
            presentation_options(DisplayMode::Borderless {
                monitor: Some(MonitorId(2))
            }),
            borderless,
            "which display it covers does not change what the menu bar does"
        );
    }

    #[test]
    fn content_limits_are_logical_units_unchanged() {
        // The macOS difference from the other two desktop backends: no scale
        // factor and no frame. A minimum of 320x240 logical is a minimum of
        // 320x240 points, at 1x and at 2x alike.
        let constraints = SizeConstraints {
            min: Some(LogicalSize::new(320.0, 240.0)),
            max: Some(LogicalSize::new(1920.0, 1080.0)),
            aspect: None,
        };
        let (min, max) = content_limits(constraints);
        assert_eq!(min, NSSize::new(320.0, 240.0));
        assert_eq!(max, NSSize::new(1920.0, 1080.0));

        // Absent limits are AppKit's own "no limit", not a number invented here.
        let (min, max) = content_limits(SizeConstraints::NONE);
        assert_eq!(min, NSSize::new(0.0, 0.0));
        assert_eq!(max, NSSize::new(f64::MAX, f64::MAX));
    }

    #[test]
    fn an_aspect_ratio_crosses_as_its_two_terms_and_zero_clears_it() {
        assert_eq!(
            aspect_size(Some(AspectRatio::WIDESCREEN)),
            NSSize::new(16.0, 9.0)
        );
        assert_eq!(
            aspect_size(Some(AspectRatio::CLASSIC)),
            NSSize::new(4.0, 3.0)
        );
        assert_eq!(
            aspect_size(None),
            NSSize::new(0.0, 0.0),
            "zero is what AppKit reads as no ratio at all"
        );
    }

    #[test]
    fn a_window_is_centred_in_the_visible_frame_and_never_above_it() {
        // A visible frame with a 25-point menu bar taken off the top: in a Y-up
        // space that is height, not origin.
        let visible = NSRect::new(0.0, 0.0, 1920.0, 1055.0);
        let placed = centred(visible, NSSize::new(1280.0, 720.0));
        assert_eq!(placed.origin.x, 320.0);
        assert_eq!(placed.origin.y, (1055.0 - 720.0) / 2.0);
        assert_eq!(placed.size, NSSize::new(1280.0, 720.0));

        // A window taller than the visible frame is pinned by its **top**, so
        // its origin goes *below* the area's origin — the opposite sign to the
        // Win32 equivalent, and the whole reason this function is not a copy of
        // it. The runner this has to hold on is the one whose display is
        // smaller than `WindowDesc::default`.
        let tall = centred(visible, NSSize::new(3000.0, 2000.0));
        assert_eq!(tall.max_y(), visible.max_y(), "the top edges line up");
        assert!(tall.origin.y < visible.origin.y, "{tall:?}");

        // And the origin is carried, so a second display is a second display.
        let right = NSRect::new(1920.0, 0.0, 1280.0, 1024.0);
        assert_eq!(
            centred(right, NSSize::new(1280.0, 1024.0)).origin,
            super::super::ffi::NSPoint { x: 1920.0, y: 0.0 }
        );
    }

    #[test]
    fn rounding_saturates_rather_than_wrapping() {
        // A display arrangement can put a screen at a fractional point origin,
        // and `as i32` on an out-of-range float is a wrap in release.
        assert_eq!(round_to_i32(0.5), 1);
        assert_eq!(round_to_i32(-0.5), -1);
        assert_eq!(round_to_i32(f64::INFINITY), i32::MAX);
        assert_eq!(round_to_i32(f64::NEG_INFINITY), i32::MIN);
        assert_eq!(round_to_i32(f64::NAN), 0);
        assert_eq!(round_to_u32(-5.0), 0);
        assert_eq!(round_to_u32(f64::INFINITY), u32::MAX);
        assert_eq!(round_to_u32(f64::NAN), 0);
    }
}