use super::{Fixed, PhysicalRect, Point, Rect, Transform};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Viewport {
physical_w: u16,
physical_h: u16,
scale: Fixed,
}
impl Viewport {
#[inline]
pub fn new(physical_w: u16, physical_h: u16, scale: Fixed) -> Self {
let scale = if scale <= Fixed::ZERO {
Fixed::ONE
} else {
scale
};
Self {
physical_w,
physical_h,
scale,
}
}
#[inline]
pub fn scale(&self) -> Fixed {
self.scale
}
#[inline]
pub fn physical_size(&self) -> (u16, u16) {
(self.physical_w, self.physical_h)
}
#[inline]
pub fn logical_size(&self) -> (u16, u16) {
let w = (Fixed::from(self.physical_w) / self.scale)
.to_int()
.clamp(0, i32::from(u16::MAX)) as u16;
let h = (Fixed::from(self.physical_h) / self.scale)
.to_int()
.clamp(0, i32::from(u16::MAX)) as u16;
(w, h)
}
#[inline]
pub fn point_to_physical(&self, p: Point) -> Point {
Point {
x: p.x * self.scale,
y: p.y * self.scale,
}
}
#[inline]
pub fn rect_to_physical(&self, r: Rect) -> Rect {
Rect {
x: r.x * self.scale,
y: r.y * self.scale,
w: r.w * self.scale,
h: r.h * self.scale,
}
}
#[inline]
pub fn rect_to_physical_pixel_bounds(&self, r: Rect) -> (i32, i32, i32, i32) {
let x0 = (r.x * self.scale).to_int();
let y0 = (r.y * self.scale).to_int();
let x1 = ((r.x + r.w) * self.scale).ceil().to_int();
let y1 = ((r.y + r.h) * self.scale).ceil().to_int();
(x0, y0, x1, y1)
}
pub fn physical_rect(&self, logical: Rect) -> Option<PhysicalRect> {
let (width, height) = self.physical_size();
self.physical_rect_in(logical, u32::from(width), u32::from(height))
}
pub(crate) fn physical_rect_in(
&self,
logical: Rect,
target_width: u32,
target_height: u32,
) -> Option<PhysicalRect> {
let (x0, y0, x1, y1) = self.rect_to_physical_pixel_bounds(logical);
let width = target_width.min(u32::from(self.physical_w)) as i32;
let height = target_height.min(u32::from(self.physical_h)) as i32;
let left = x0.clamp(0, width);
let top = y0.clamp(0, height);
let right = x1.clamp(0, width);
let bottom = y1.clamp(0, height);
if right <= left || bottom <= top {
return None;
}
PhysicalRect::new(
u16::try_from(left).ok()?,
u16::try_from(top).ok()?,
u16::try_from(right - left).ok()?,
u16::try_from(bottom - top).ok()?,
)
}
#[inline]
pub fn point_to_logical(&self, p: Point) -> Point {
Point {
x: p.x / self.scale,
y: p.y / self.scale,
}
}
#[inline]
pub fn as_transform(&self) -> Transform {
Transform::scale(self.scale, self.scale)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_scale_is_normalized_to_one() {
let t = Viewport::new(100, 50, Fixed::ZERO);
assert_eq!(t.scale(), Fixed::ONE);
assert_eq!(t.logical_size(), (100, 50));
}
#[test]
fn logical_size_divides_physical() {
let t = Viewport::new(200, 100, Fixed::from_int(2));
assert_eq!(t.logical_size(), (100, 50));
}
#[test]
fn logical_size_saturates_instead_of_wrapping() {
let t = Viewport::new(u16::MAX, u16::MAX, Fixed::from_ratio(1, 2));
assert_eq!(t.logical_size(), (u16::MAX, u16::MAX));
}
#[test]
fn point_roundtrip_within_fixed_precision() {
let t = Viewport::new(200, 100, Fixed::from_int(2));
let p = Point {
x: Fixed::from_int(10),
y: Fixed::from_int(20),
};
let phys = t.point_to_physical(p);
assert_eq!(phys.x, Fixed::from_int(20));
assert_eq!(phys.y, Fixed::from_int(40));
let back = t.point_to_logical(phys);
assert_eq!(back, p);
}
#[test]
fn rect_bounds_ceil_bottom_right() {
let t = Viewport::new(200, 100, Fixed::from_f32(1.5));
let r = Rect {
x: Fixed::ZERO,
y: Fixed::ZERO,
w: Fixed::from_int(10),
h: Fixed::from_int(10),
};
let (x0, y0, x1, y1) = t.rect_to_physical_pixel_bounds(r);
assert_eq!((x0, y0), (0, 0));
assert_eq!((x1, y1), (15, 15));
}
#[test]
fn clipped_readback_rect_uses_whole_physical_pixels() {
let viewport = Viewport::new(12, 12, Fixed::from_f32(1.5));
let logical = Rect::new(
Fixed::from_f32(1.25),
Fixed::ZERO,
Fixed::from_f32(2.5),
Fixed::ONE,
);
assert_eq!(
viewport.physical_rect_in(logical, 12, 12),
PhysicalRect::new(1, 0, 5, 2)
);
assert_eq!(
viewport.physical_rect_in(Rect::new(-2, 6, 5, 4), 12, 8),
None
);
}
#[test]
fn physical_rect_clips_negative_fractional_edges_once() {
let viewport = Viewport::new(20, 12, Fixed::from_f32(1.5));
assert_eq!(
viewport.physical_rect(Rect::new(-1.25, 1.25, 4.0, 2.5)),
PhysicalRect::new(0, 1, 5, 5)
);
}
#[test]
fn physical_rect_quantization_is_stable_across_scales() {
let logical = Rect::new(
Fixed::from_ratio(-1, 4),
Fixed::from_ratio(5, 4),
Fixed::from_ratio(7, 2),
Fixed::from_ratio(9, 4),
);
let cases = [
(
Viewport::new(20, 20, Fixed::ONE),
PhysicalRect::new(0, 1, 4, 3),
),
(
Viewport::new(30, 30, Fixed::from_ratio(3, 2)),
PhysicalRect::new(0, 1, 5, 5),
),
(
Viewport::new(40, 40, Fixed::from_int(2)),
PhysicalRect::new(0, 2, 7, 5),
),
];
for (viewport, expected) in cases {
assert_eq!(viewport.physical_rect(logical), expected);
}
}
}