1use core::fmt;
32
33#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
37pub struct PhysicalSize {
38 pub width: u32,
40 pub height: u32,
42}
43
44impl PhysicalSize {
45 #[inline]
47 #[must_use]
48 pub const fn new(width: u32, height: u32) -> Self {
49 Self { width, height }
50 }
51
52 #[inline]
58 #[must_use]
59 pub const fn is_empty(self) -> bool {
60 self.width == 0 || self.height == 0
61 }
62
63 #[inline]
65 #[must_use]
66 pub const fn area(self) -> u64 {
67 self.width as u64 * self.height as u64
68 }
69
70 #[inline]
72 #[must_use]
73 pub fn to_logical(self, scale_factor: f64) -> LogicalSize {
74 debug_assert!(scale_factor > 0.0, "scale factor must be positive");
75 LogicalSize {
76 width: f64::from(self.width) / scale_factor,
77 height: f64::from(self.height) / scale_factor,
78 }
79 }
80
81 #[must_use]
83 pub fn aspect(self) -> Option<f64> {
84 if self.is_empty() {
85 None
86 } else {
87 Some(f64::from(self.width) / f64::from(self.height))
88 }
89 }
90}
91
92impl fmt::Display for PhysicalSize {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 write!(f, "{}x{}px", self.width, self.height)
95 }
96}
97
98#[derive(Clone, Copy, Debug, Default, PartialEq)]
103pub struct LogicalSize {
104 pub width: f64,
106 pub height: f64,
108}
109
110impl LogicalSize {
111 #[inline]
113 #[must_use]
114 pub const fn new(width: f64, height: f64) -> Self {
115 Self { width, height }
116 }
117
118 #[must_use]
123 pub fn to_physical(self, scale_factor: f64) -> PhysicalSize {
124 debug_assert!(scale_factor > 0.0, "scale factor must be positive");
125 let scale = |value: f64| {
126 let scaled = (value * scale_factor).round();
127 if scaled.is_nan() {
131 1
132 } else {
133 scaled.clamp(1.0, f64::from(u32::MAX)) as u32
134 }
135 };
136 PhysicalSize {
137 width: scale(self.width),
138 height: scale(self.height),
139 }
140 }
141}
142
143impl fmt::Display for LogicalSize {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 write!(f, "{}x{}dp", self.width, self.height)
146 }
147}
148
149#[derive(Clone, Copy, Debug, Default, PartialEq)]
155pub struct PhysicalPoint {
156 pub x: f64,
158 pub y: f64,
160}
161
162impl PhysicalPoint {
163 pub const ORIGIN: Self = Self { x: 0.0, y: 0.0 };
165
166 #[inline]
168 #[must_use]
169 pub const fn new(x: f64, y: f64) -> Self {
170 Self { x, y }
171 }
172
173 #[inline]
175 #[must_use]
176 pub fn to_logical(self, scale_factor: f64) -> LogicalPoint {
177 debug_assert!(scale_factor > 0.0, "scale factor must be positive");
178 LogicalPoint {
179 x: self.x / scale_factor,
180 y: self.y / scale_factor,
181 }
182 }
183
184 #[must_use]
186 pub fn is_inside(self, size: PhysicalSize) -> bool {
187 self.x >= 0.0
188 && self.y >= 0.0
189 && self.x < f64::from(size.width)
190 && self.y < f64::from(size.height)
191 }
192}
193
194#[derive(Clone, Copy, Debug, Default, PartialEq)]
196pub struct LogicalPoint {
197 pub x: f64,
199 pub y: f64,
201}
202
203impl LogicalPoint {
204 #[inline]
206 #[must_use]
207 pub const fn new(x: f64, y: f64) -> Self {
208 Self { x, y }
209 }
210}
211
212#[derive(Clone, Copy, Debug, Default, PartialEq)]
215pub struct LogicalRect {
216 pub origin: LogicalPoint,
218 pub size: LogicalSize,
220}
221
222impl LogicalRect {
223 #[inline]
225 #[must_use]
226 pub const fn new(origin: LogicalPoint, size: LogicalSize) -> Self {
227 Self { origin, size }
228 }
229}
230
231#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
239pub struct PhysicalRect {
240 pub x: i32,
242 pub y: i32,
244 pub width: u32,
246 pub height: u32,
248}
249
250impl PhysicalRect {
251 #[inline]
253 #[must_use]
254 pub const fn new(x: i32, y: i32, width: u32, height: u32) -> Self {
255 Self {
256 x,
257 y,
258 width,
259 height,
260 }
261 }
262
263 #[inline]
265 #[must_use]
266 pub const fn size(self) -> PhysicalSize {
267 PhysicalSize::new(self.width, self.height)
268 }
269
270 #[must_use]
284 pub const fn contains(self, x: i32, y: i32) -> bool {
285 x >= self.x
286 && y >= self.y
287 && (x as i64 - self.x as i64) < self.width as i64
288 && (y as i64 - self.y as i64) < self.height as i64
289 }
290}
291
292impl fmt::Display for PhysicalRect {
293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294 write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
295 }
296}
297
298#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
306pub struct AspectRatio {
307 numerator: u32,
308 denominator: u32,
309}
310
311impl AspectRatio {
312 pub const WIDESCREEN: Self = Self {
314 numerator: 16,
315 denominator: 9,
316 };
317 pub const CLASSIC: Self = Self {
319 numerator: 4,
320 denominator: 3,
321 };
322 pub const SQUARE: Self = Self {
324 numerator: 1,
325 denominator: 1,
326 };
327
328 #[must_use]
335 pub const fn new(width: u32, height: u32) -> Self {
336 assert!(
337 width > 0 && height > 0,
338 "aspect ratio terms must be positive"
339 );
340 Self {
341 numerator: width,
342 denominator: height,
343 }
344 }
345
346 #[inline]
348 #[must_use]
349 pub const fn numerator(self) -> u32 {
350 self.numerator
351 }
352
353 #[inline]
355 #[must_use]
356 pub const fn denominator(self) -> u32 {
357 self.denominator
358 }
359
360 #[inline]
362 #[must_use]
363 pub fn as_f64(self) -> f64 {
364 f64::from(self.numerator) / f64::from(self.denominator)
365 }
366
367 #[must_use]
377 pub fn fit(self, bounds: PhysicalSize) -> PhysicalSize {
378 if bounds.is_empty() {
379 return PhysicalSize::new(1, 1);
380 }
381 let bounds_ratio = u64::from(bounds.width) * u64::from(self.denominator);
384 let target_ratio = u64::from(bounds.height) * u64::from(self.numerator);
385 if bounds_ratio > target_ratio {
386 let width = target_ratio / u64::from(self.denominator);
388 PhysicalSize::new(width.max(1) as u32, bounds.height)
389 } else {
390 let height = bounds_ratio / u64::from(self.numerator);
392 PhysicalSize::new(bounds.width, height.max(1) as u32)
393 }
394 }
395}
396
397impl fmt::Display for AspectRatio {
398 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
399 write!(f, "{}:{}", self.numerator, self.denominator)
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 #[test]
408 fn scale_conversions_round_trip_at_integer_scales() {
409 let logical = LogicalSize::new(1280.0, 720.0);
410 assert_eq!(logical.to_physical(1.0), PhysicalSize::new(1280, 720));
411 assert_eq!(logical.to_physical(2.0), PhysicalSize::new(2560, 1440));
412 assert_eq!(
413 PhysicalSize::new(2560, 1440).to_logical(2.0),
414 LogicalSize::new(1280.0, 720.0)
415 );
416 }
417
418 #[test]
419 fn fractional_scale_rounds_and_never_produces_zero() {
420 assert_eq!(
422 LogicalSize::new(1280.0, 720.0).to_physical(1.25),
423 PhysicalSize::new(1600, 900)
424 );
425 assert_eq!(
427 LogicalSize::new(0.0, 0.0).to_physical(1.0),
428 PhysicalSize::new(1, 1)
429 );
430 assert_eq!(
431 LogicalSize::new(f64::NAN, 4.0).to_physical(1.0),
432 PhysicalSize::new(1, 4)
433 );
434 assert_eq!(
438 LogicalSize::new(1e300, 4.0).to_physical(1e300),
439 PhysicalSize::new(u32::MAX, u32::MAX)
440 );
441 }
442
443 #[test]
444 fn empty_sizes_are_detectable_rather_than_clamped_away() {
445 let minimized = PhysicalSize::new(0, 0);
449 assert!(minimized.is_empty());
450 assert_eq!(minimized.aspect(), None);
451 assert_eq!(minimized.area(), 0);
452 assert!(!PhysicalSize::new(1, 1).is_empty());
453 assert_eq!(PhysicalSize::new(3840, 2160).area(), 8_294_400);
454 assert_eq!(PhysicalSize::new(1920, 1080).to_string(), "1920x1080px");
455 assert_eq!(LogicalSize::new(16.0, 9.0).to_string(), "16x9dp");
456 }
457
458 #[test]
459 fn letterbox_fit_is_exact_for_the_common_ratios() {
460 let fitted = AspectRatio::WIDESCREEN.fit(PhysicalSize::new(1920, 1200));
462 assert_eq!(fitted, PhysicalSize::new(1920, 1080));
463
464 let fitted = AspectRatio::WIDESCREEN.fit(PhysicalSize::new(2560, 1080));
466 assert_eq!(fitted, PhysicalSize::new(1920, 1080));
467
468 let exact = PhysicalSize::new(1280, 720);
471 let mut size = exact;
472 for _ in 0..1_000 {
473 size = AspectRatio::WIDESCREEN.fit(size);
474 }
475 assert_eq!(size, exact);
476
477 assert_eq!(
478 AspectRatio::CLASSIC.fit(PhysicalSize::new(1000, 1000)),
479 PhysicalSize::new(1000, 750)
480 );
481 assert_eq!(
482 AspectRatio::SQUARE.fit(PhysicalSize::new(1000, 400)),
483 PhysicalSize::new(400, 400)
484 );
485 assert_eq!(
486 AspectRatio::WIDESCREEN.fit(PhysicalSize::new(0, 0)),
487 PhysicalSize::new(1, 1)
488 );
489 assert_eq!(
491 AspectRatio::new(1000, 1).fit(PhysicalSize::new(10, 10)),
492 PhysicalSize::new(10, 1)
493 );
494 }
495
496 #[test]
497 fn aspect_ratio_accessors_and_display() {
498 let ratio = AspectRatio::new(21, 9);
499 assert_eq!(ratio.numerator(), 21);
500 assert_eq!(ratio.denominator(), 9);
501 assert!((ratio.as_f64() - 21.0 / 9.0).abs() < 1e-12);
502 assert_eq!(ratio.to_string(), "21:9");
503 assert_eq!(AspectRatio::WIDESCREEN, AspectRatio::new(16, 9));
504 }
505
506 #[test]
507 #[should_panic(expected = "aspect ratio terms must be positive")]
508 fn a_degenerate_aspect_ratio_is_rejected() {
509 let _ = AspectRatio::new(16, 0);
510 }
511
512 #[test]
513 fn points_convert_and_hit_test() {
514 let point = PhysicalPoint::new(200.0, 100.0);
515 assert_eq!(point.to_logical(2.0), LogicalPoint::new(100.0, 50.0));
516 assert!(point.is_inside(PhysicalSize::new(640, 360)));
517 assert!(!point.is_inside(PhysicalSize::new(100, 100)));
518 assert!(!PhysicalPoint::new(-1.0, 0.0).is_inside(PhysicalSize::new(640, 360)));
519 assert_eq!(PhysicalPoint::ORIGIN, PhysicalPoint::default());
520 assert_eq!(LogicalPoint::new(0.0, 0.0), LogicalPoint::default());
521 }
522
523 #[test]
524 fn monitor_rectangles_handle_negative_origins() {
525 let left = PhysicalRect::new(-1920, 0, 1920, 1080);
527 assert!(left.contains(-1000, 500));
528 assert!(!left.contains(10, 500));
529 assert_eq!(left.size(), PhysicalSize::new(1920, 1080));
530 assert_eq!(left.to_string(), "1920x1080+-1920+0");
531 }
532
533 #[test]
534 fn tiled_monitors_partition_the_desktop() {
535 let first = PhysicalRect::new(0, 0, 1920, 1080);
539 let second = PhysicalRect::new(1920, 0, 1920, 1080);
540 for (x, y) in [(0, 0), (1919, 0), (0, 1079), (1919, 1079)] {
541 assert!(first.contains(x, y), "({x},{y}) is on the first monitor");
542 assert!(!second.contains(x, y));
543 }
544 for (x, y) in [(1920, 0), (3839, 1079)] {
545 assert!(second.contains(x, y), "({x},{y}) is on the second monitor");
546 assert!(!first.contains(x, y), "the shared edge belongs to one");
547 }
548 assert!(!first.contains(0, 1080));
550 assert!(!second.contains(3840, 0));
551 }
552
553 #[test]
554 fn hit_testing_a_far_away_point_does_not_overflow() {
555 let negative_origin = PhysicalRect::new(-1, 0, 1920, 1080);
557 assert!(!negative_origin.contains(i32::MAX, 0));
558 assert!(!PhysicalRect::new(i32::MIN, 0, 1920, 1080).contains(i32::MAX, 0));
559 let widest = PhysicalRect::new(i32::MIN, 0, u32::MAX, 1080);
562 assert!(widest.contains(i32::MAX - 1, 0));
563 assert!(!widest.contains(i32::MAX, 0));
564 }
565}