use core::fmt;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct PhysicalSize {
pub width: u32,
pub height: u32,
}
impl PhysicalSize {
#[inline]
#[must_use]
pub const fn new(width: u32, height: u32) -> Self {
Self { width, height }
}
#[inline]
#[must_use]
pub const fn is_empty(self) -> bool {
self.width == 0 || self.height == 0
}
#[inline]
#[must_use]
pub const fn area(self) -> u64 {
self.width as u64 * self.height as u64
}
#[inline]
#[must_use]
pub fn to_logical(self, scale_factor: f64) -> LogicalSize {
debug_assert!(scale_factor > 0.0, "scale factor must be positive");
LogicalSize {
width: f64::from(self.width) / scale_factor,
height: f64::from(self.height) / scale_factor,
}
}
#[must_use]
pub fn aspect(self) -> Option<f64> {
if self.is_empty() {
None
} else {
Some(f64::from(self.width) / f64::from(self.height))
}
}
}
impl fmt::Display for PhysicalSize {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}x{}px", self.width, self.height)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct LogicalSize {
pub width: f64,
pub height: f64,
}
impl LogicalSize {
#[inline]
#[must_use]
pub const fn new(width: f64, height: f64) -> Self {
Self { width, height }
}
#[must_use]
pub fn to_physical(self, scale_factor: f64) -> PhysicalSize {
debug_assert!(scale_factor > 0.0, "scale factor must be positive");
let scale = |value: f64| {
let scaled = (value * scale_factor).round();
if scaled.is_nan() {
1
} else {
scaled.clamp(1.0, f64::from(u32::MAX)) as u32
}
};
PhysicalSize {
width: scale(self.width),
height: scale(self.height),
}
}
}
impl fmt::Display for LogicalSize {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}x{}dp", self.width, self.height)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct PhysicalPoint {
pub x: f64,
pub y: f64,
}
impl PhysicalPoint {
pub const ORIGIN: Self = Self { x: 0.0, y: 0.0 };
#[inline]
#[must_use]
pub const fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
#[inline]
#[must_use]
pub fn to_logical(self, scale_factor: f64) -> LogicalPoint {
debug_assert!(scale_factor > 0.0, "scale factor must be positive");
LogicalPoint {
x: self.x / scale_factor,
y: self.y / scale_factor,
}
}
#[must_use]
pub fn is_inside(self, size: PhysicalSize) -> bool {
self.x >= 0.0
&& self.y >= 0.0
&& self.x < f64::from(size.width)
&& self.y < f64::from(size.height)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct LogicalPoint {
pub x: f64,
pub y: f64,
}
impl LogicalPoint {
#[inline]
#[must_use]
pub const fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct LogicalRect {
pub origin: LogicalPoint,
pub size: LogicalSize,
}
impl LogicalRect {
#[inline]
#[must_use]
pub const fn new(origin: LogicalPoint, size: LogicalSize) -> Self {
Self { origin, size }
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct PhysicalRect {
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
}
impl PhysicalRect {
#[inline]
#[must_use]
pub const fn new(x: i32, y: i32, width: u32, height: u32) -> Self {
Self {
x,
y,
width,
height,
}
}
#[inline]
#[must_use]
pub const fn size(self) -> PhysicalSize {
PhysicalSize::new(self.width, self.height)
}
#[must_use]
pub const fn contains(self, x: i32, y: i32) -> bool {
x >= self.x
&& y >= self.y
&& (x as i64 - self.x as i64) < self.width as i64
&& (y as i64 - self.y as i64) < self.height as i64
}
}
impl fmt::Display for PhysicalRect {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct AspectRatio {
numerator: u32,
denominator: u32,
}
impl AspectRatio {
pub const WIDESCREEN: Self = Self {
numerator: 16,
denominator: 9,
};
pub const CLASSIC: Self = Self {
numerator: 4,
denominator: 3,
};
pub const SQUARE: Self = Self {
numerator: 1,
denominator: 1,
};
#[must_use]
pub const fn new(width: u32, height: u32) -> Self {
assert!(
width > 0 && height > 0,
"aspect ratio terms must be positive"
);
Self {
numerator: width,
denominator: height,
}
}
#[inline]
#[must_use]
pub const fn numerator(self) -> u32 {
self.numerator
}
#[inline]
#[must_use]
pub const fn denominator(self) -> u32 {
self.denominator
}
#[inline]
#[must_use]
pub fn as_f64(self) -> f64 {
f64::from(self.numerator) / f64::from(self.denominator)
}
#[must_use]
pub fn fit(self, bounds: PhysicalSize) -> PhysicalSize {
if bounds.is_empty() {
return PhysicalSize::new(1, 1);
}
let bounds_ratio = u64::from(bounds.width) * u64::from(self.denominator);
let target_ratio = u64::from(bounds.height) * u64::from(self.numerator);
if bounds_ratio > target_ratio {
let width = target_ratio / u64::from(self.denominator);
PhysicalSize::new(width.max(1) as u32, bounds.height)
} else {
let height = bounds_ratio / u64::from(self.numerator);
PhysicalSize::new(bounds.width, height.max(1) as u32)
}
}
}
impl fmt::Display for AspectRatio {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.numerator, self.denominator)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scale_conversions_round_trip_at_integer_scales() {
let logical = LogicalSize::new(1280.0, 720.0);
assert_eq!(logical.to_physical(1.0), PhysicalSize::new(1280, 720));
assert_eq!(logical.to_physical(2.0), PhysicalSize::new(2560, 1440));
assert_eq!(
PhysicalSize::new(2560, 1440).to_logical(2.0),
LogicalSize::new(1280.0, 720.0)
);
}
#[test]
fn fractional_scale_rounds_and_never_produces_zero() {
assert_eq!(
LogicalSize::new(1280.0, 720.0).to_physical(1.25),
PhysicalSize::new(1600, 900)
);
assert_eq!(
LogicalSize::new(0.0, 0.0).to_physical(1.0),
PhysicalSize::new(1, 1)
);
assert_eq!(
LogicalSize::new(f64::NAN, 4.0).to_physical(1.0),
PhysicalSize::new(1, 4)
);
assert_eq!(
LogicalSize::new(1e300, 4.0).to_physical(1e300),
PhysicalSize::new(u32::MAX, u32::MAX)
);
}
#[test]
fn empty_sizes_are_detectable_rather_than_clamped_away() {
let minimized = PhysicalSize::new(0, 0);
assert!(minimized.is_empty());
assert_eq!(minimized.aspect(), None);
assert_eq!(minimized.area(), 0);
assert!(!PhysicalSize::new(1, 1).is_empty());
assert_eq!(PhysicalSize::new(3840, 2160).area(), 8_294_400);
assert_eq!(PhysicalSize::new(1920, 1080).to_string(), "1920x1080px");
assert_eq!(LogicalSize::new(16.0, 9.0).to_string(), "16x9dp");
}
#[test]
fn letterbox_fit_is_exact_for_the_common_ratios() {
let fitted = AspectRatio::WIDESCREEN.fit(PhysicalSize::new(1920, 1200));
assert_eq!(fitted, PhysicalSize::new(1920, 1080));
let fitted = AspectRatio::WIDESCREEN.fit(PhysicalSize::new(2560, 1080));
assert_eq!(fitted, PhysicalSize::new(1920, 1080));
let exact = PhysicalSize::new(1280, 720);
let mut size = exact;
for _ in 0..1_000 {
size = AspectRatio::WIDESCREEN.fit(size);
}
assert_eq!(size, exact);
assert_eq!(
AspectRatio::CLASSIC.fit(PhysicalSize::new(1000, 1000)),
PhysicalSize::new(1000, 750)
);
assert_eq!(
AspectRatio::SQUARE.fit(PhysicalSize::new(1000, 400)),
PhysicalSize::new(400, 400)
);
assert_eq!(
AspectRatio::WIDESCREEN.fit(PhysicalSize::new(0, 0)),
PhysicalSize::new(1, 1)
);
assert_eq!(
AspectRatio::new(1000, 1).fit(PhysicalSize::new(10, 10)),
PhysicalSize::new(10, 1)
);
}
#[test]
fn aspect_ratio_accessors_and_display() {
let ratio = AspectRatio::new(21, 9);
assert_eq!(ratio.numerator(), 21);
assert_eq!(ratio.denominator(), 9);
assert!((ratio.as_f64() - 21.0 / 9.0).abs() < 1e-12);
assert_eq!(ratio.to_string(), "21:9");
assert_eq!(AspectRatio::WIDESCREEN, AspectRatio::new(16, 9));
}
#[test]
#[should_panic(expected = "aspect ratio terms must be positive")]
fn a_degenerate_aspect_ratio_is_rejected() {
let _ = AspectRatio::new(16, 0);
}
#[test]
fn points_convert_and_hit_test() {
let point = PhysicalPoint::new(200.0, 100.0);
assert_eq!(point.to_logical(2.0), LogicalPoint::new(100.0, 50.0));
assert!(point.is_inside(PhysicalSize::new(640, 360)));
assert!(!point.is_inside(PhysicalSize::new(100, 100)));
assert!(!PhysicalPoint::new(-1.0, 0.0).is_inside(PhysicalSize::new(640, 360)));
assert_eq!(PhysicalPoint::ORIGIN, PhysicalPoint::default());
assert_eq!(LogicalPoint::new(0.0, 0.0), LogicalPoint::default());
}
#[test]
fn monitor_rectangles_handle_negative_origins() {
let left = PhysicalRect::new(-1920, 0, 1920, 1080);
assert!(left.contains(-1000, 500));
assert!(!left.contains(10, 500));
assert_eq!(left.size(), PhysicalSize::new(1920, 1080));
assert_eq!(left.to_string(), "1920x1080+-1920+0");
}
#[test]
fn tiled_monitors_partition_the_desktop() {
let first = PhysicalRect::new(0, 0, 1920, 1080);
let second = PhysicalRect::new(1920, 0, 1920, 1080);
for (x, y) in [(0, 0), (1919, 0), (0, 1079), (1919, 1079)] {
assert!(first.contains(x, y), "({x},{y}) is on the first monitor");
assert!(!second.contains(x, y));
}
for (x, y) in [(1920, 0), (3839, 1079)] {
assert!(second.contains(x, y), "({x},{y}) is on the second monitor");
assert!(!first.contains(x, y), "the shared edge belongs to one");
}
assert!(!first.contains(0, 1080));
assert!(!second.contains(3840, 0));
}
#[test]
fn hit_testing_a_far_away_point_does_not_overflow() {
let negative_origin = PhysicalRect::new(-1, 0, 1920, 1080);
assert!(!negative_origin.contains(i32::MAX, 0));
assert!(!PhysicalRect::new(i32::MIN, 0, 1920, 1080).contains(i32::MAX, 0));
let widest = PhysicalRect::new(i32::MIN, 0, u32::MAX, 1080);
assert!(widest.contains(i32::MAX - 1, 0));
assert!(!widest.contains(i32::MAX, 0));
}
}