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,
};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Flip {
top: f64,
}
impl Flip {
#[must_use]
pub const fn about_primary(primary: NSRect) -> Self {
Self {
top: primary.max_y(),
}
}
#[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,
)
}
#[must_use]
pub const fn point(self, point: NSPoint) -> NSPoint {
NSPoint {
x: point.x,
y: self.top - point.y,
}
}
}
#[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),
)
}
#[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),
)
}
#[must_use]
pub const fn points(size: LogicalSize) -> NSSize {
NSSize::new(size.width, size.height)
}
#[must_use]
pub fn usable_scale(backing: f64) -> f64 {
if backing.is_finite() && backing > 0.0 {
backing
} else {
1.0
}
}
#[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
}
DisplayMode::Borderless { .. } => style::BORDERLESS,
}
}
pub const PANEL_STYLE_MASK: NSUInteger = style::BORDERLESS | style::NONACTIVATING_PANEL;
#[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),
}
}
#[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,
}
}
#[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)
}
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)
}
#[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,
)
}
#[must_use]
pub const fn presentation_options(mode: DisplayMode) -> usize {
match mode {
DisplayMode::Windowed => value::PRESENTATION_DEFAULT,
DisplayMode::Borderless { .. } => value::PRESENTATION_BORDERLESS,
}
}
#[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)
}
#[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()))
})
}
#[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),
if free_y >= 0.0 {
area.origin.y + free_y / 2.0
} else {
area.max_y() - size.height
},
size.width,
size.height,
)
}
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
}
}
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;
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() {
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() {
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));
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));
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() {
let flip = Flip::about_primary(PRIMARY);
let shorter = NSRect::new(1920.0, 0.0, 1280.0, 720.0);
assert_eq!(
flip.rect(shorter),
NSRect::new(1920.0, 360.0, 1280.0, 720.0)
);
}
#[test]
fn the_flip_is_its_own_inverse() {
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() {
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:?}");
}
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"
);
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() {
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() {
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));
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"
);
assert_eq!(physical_size(content, 0.0), PhysicalSize::new(1280, 720));
assert_eq!(
physical_size(content, f64::NAN),
PhysicalSize::new(1280, 720)
);
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() {
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);
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() {
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);
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() {
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() {
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() {
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));
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() {
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));
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:?}");
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() {
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);
}
}