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
}
#[must_use]
pub fn rounded(self, radius: u32) -> Vec<Self> {
if self.width == 0 || self.height == 0 {
return Vec::new();
}
let radius = radius.min(self.width / 2).min(self.height / 2);
let insets: Vec<u32> = (0..radius).map(|row| corner_inset(radius, row)).collect();
let mut rects: Vec<Self> = Vec::new();
let mut push = |top: u32, height: u32, inset: u32| {
if height == 0 {
return;
}
let row = Self::new(
self.x.saturating_add_unsigned(inset),
self.y.saturating_add_unsigned(top),
self.width - 2 * inset,
height,
);
match rects.last_mut() {
Some(last)
if last.x == row.x
&& last.width == row.width
&& i64::from(last.y) + i64::from(last.height) == i64::from(row.y) =>
{
last.height += height;
}
_ => rects.push(row),
}
};
for (row, &inset) in (0..radius).zip(&insets) {
push(row, 1, inset);
}
push(radius, self.height - 2 * radius, 0);
for (row, &inset) in (0..radius).zip(&insets).rev() {
push(self.height - 1 - row, 1, inset);
}
rects
}
}
fn corner_inset(radius: u32, row: u32) -> u32 {
let (radius, d) = (u64::from(radius), u64::from(radius - row - 1));
let squared = radius * radius - d * d;
let root = squared.isqrt();
let ceil = if root * root == squared {
root
} else {
root + 1
};
u32::try_from(radius - ceil).expect("the inset is at most the radius, which is a u32")
}
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));
}
fn corner_covers(radius: i64, x: i64, y: i64) -> bool {
if x >= radius || y >= radius {
return true;
}
let (dx, dy) = (radius - x - 1, radius - y - 1);
dx * dx + dy * dy < radius * radius
}
#[test]
fn a_rounded_rectangle_is_exactly_the_pixels_its_corners_cover() {
for (rect, radius) in [
(PhysicalRect::new(0, 0, 40, 30), 10),
(PhysicalRect::new(-7, 13, 31, 64), 6),
(PhysicalRect::new(5, 5, 20, 20), 10),
(PhysicalRect::new(0, 0, 9, 50), 30),
(PhysicalRect::new(0, 0, 12, 8), 1),
(PhysicalRect::new(3, 4, 12, 8), 0),
] {
let rects = rect.rounded(radius);
let r = i64::from(radius.min(rect.width / 2).min(rect.height / 2));
let (width, height) = (i64::from(rect.width), i64::from(rect.height));
for y in 0..height {
for x in 0..width {
let covered = corner_covers(r, x.min(width - 1 - x), y.min(height - 1 - y));
let px = i32::try_from(i64::from(rect.x) + x).expect("small");
let py = i32::try_from(i64::from(rect.y) + y).expect("small");
let hits = rects.iter().filter(|part| part.contains(px, py)).count();
assert_eq!(
hits,
usize::from(covered),
"{rect} r{radius}: pixel ({x},{y}) in {hits} of {rects:?}"
);
}
}
for part in &rects {
assert!(part.x >= rect.x && part.y >= rect.y, "{part} in {rect}");
assert!(
i64::from(part.x) + i64::from(part.width) <= i64::from(rect.x) + width
&& i64::from(part.y) + i64::from(part.height) <= i64::from(rect.y) + height,
"{part} in {rect}"
);
}
}
}
#[test]
fn a_rounded_corner_insets_rows_by_the_known_amounts_and_merges_equal_ones() {
let rects = PhysicalRect::new(0, 0, 100, 60).rounded(10);
let insets: Vec<(i32, u32)> = rects.iter().map(|part| (part.x, part.height)).collect();
assert_eq!(
insets,
[
(5, 1),
(4, 1),
(2, 2),
(1, 1),
(0, 50),
(1, 1),
(2, 2),
(4, 1),
(5, 1)
]
);
assert_eq!(rects[4], PhysicalRect::new(0, 5, 100, 50));
let square = PhysicalRect::new(3, 4, 12, 8);
assert_eq!(square.rounded(0), [square]);
assert!(PhysicalRect::new(0, 0, 0, 8).rounded(4).is_empty());
}
}