pixelcoords_core/space.rs
1//! Where a coordinate is measured from, and what units it is in.
2//!
3//! These are two independent questions and the CLI spells them as two
4//! flags. An origin says which corner `(0, 0)` is; units say whether one
5//! step is a device pixel or a logical point. Folding them into one
6//! vocabulary — the shape `resolve` was first drafted with — cannot
7//! express `--space monitor --units logical`, which is a perfectly
8//! ordinary thing to ask for on a Retina secondary display.
9//!
10//! The arithmetic lives here; the monitor lookup does not. Each caller
11//! finds its own monitor record and raises its own error for a missing
12//! one, because a shared error type for that would belong to nobody.
13
14use crate::geometry::Point;
15
16/// Which origin a coordinate is measured from.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Origin {
19 /// The desktop's own grid: `global_px`.
20 Global,
21 /// One monitor's top-left, by its index in the session: `px`.
22 Monitor(usize),
23 /// The `--target` window's top-left: `window_px`.
24 Window,
25}
26
27impl Origin {
28 /// The name this origin carries in JSON output.
29 #[must_use]
30 pub const fn label(self) -> &'static str {
31 match self {
32 Self::Global => "global",
33 Self::Monitor(_) => "monitor",
34 Self::Window => "window",
35 }
36 }
37}
38
39/// The OS a coordinate will be handed to. Passed as a value rather than
40/// read from `cfg!` so core stays platform-free and one headless test run
41/// can cover all three.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum Platform {
44 MacOs,
45 Windows,
46 Linux,
47}
48
49/// The units a coordinate is expressed in, as spelled on the CLI.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum Units {
52 Physical,
53 Logical,
54 /// Whatever this platform's input APIs expect. The one value most
55 /// callers want, and the reason the flag exists: the mismatch it
56 /// hides is where consumers of these coordinates go wrong.
57 Auto,
58}
59
60/// `Units` with `Auto` already answered — the only thing the arithmetic
61/// accepts, so "did I resolve auto?" cannot be forgotten at a call site.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum Resolved {
64 Physical,
65 Logical,
66}
67
68impl Units {
69 /// macOS input APIs speak logical points; Windows and X11 speak
70 /// physical pixels. This is the same split `emit`'s per-format table
71 /// documents, stated once.
72 #[must_use]
73 pub const fn resolve(self, platform: Platform) -> Resolved {
74 match self {
75 Self::Physical => Resolved::Physical,
76 Self::Logical => Resolved::Logical,
77 Self::Auto => match platform {
78 Platform::MacOs => Resolved::Logical,
79 Platform::Windows | Platform::Linux => Resolved::Physical,
80 },
81 }
82 }
83}
84
85/// Physical pixels to logical points at `scale`.
86///
87/// Monitor origins were divided by this same per-monitor factor when the
88/// session was written, so the conversion inverts cleanly even when two
89/// displays disagree about scale.
90#[must_use]
91pub fn logical_of(physical: Point, scale: f64) -> Point {
92 Point::new(
93 (f64::from(physical.x) / scale).round() as i32,
94 (f64::from(physical.y) / scale).round() as i32,
95 )
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn auto_follows_the_platforms_input_api() {
104 assert_eq!(Units::Auto.resolve(Platform::MacOs), Resolved::Logical);
105 assert_eq!(Units::Auto.resolve(Platform::Windows), Resolved::Physical);
106 assert_eq!(Units::Auto.resolve(Platform::Linux), Resolved::Physical);
107 }
108
109 #[test]
110 fn an_explicit_unit_ignores_the_platform() {
111 for platform in [Platform::MacOs, Platform::Windows, Platform::Linux] {
112 assert_eq!(Units::Physical.resolve(platform), Resolved::Physical);
113 assert_eq!(Units::Logical.resolve(platform), Resolved::Logical);
114 }
115 }
116
117 #[test]
118 fn logical_halves_a_retina_point_and_leaves_scale_one_alone() {
119 assert_eq!(logical_of(Point::new(100, 50), 2.0), Point::new(50, 25));
120 assert_eq!(logical_of(Point::new(100, 50), 1.0), Point::new(100, 50));
121 }
122
123 #[test]
124 fn logical_rounds_rather_than_truncating() {
125 // 1.5 physical at scale 2 is 0.75 logical: 1, not 0. Truncation
126 // would bias every odd coordinate toward the origin.
127 assert_eq!(logical_of(Point::new(3, 3), 2.0), Point::new(2, 2));
128 assert_eq!(logical_of(Point::new(-3, -3), 2.0), Point::new(-2, -2));
129 }
130
131 #[test]
132 fn origins_label_themselves_for_json() {
133 assert_eq!(Origin::Global.label(), "global");
134 assert_eq!(Origin::Monitor(3).label(), "monitor");
135 assert_eq!(Origin::Window.label(), "window");
136 }
137}