mirui 0.46.0

A lightweight, no_std ECS-driven UI framework for embedded, mobile, desktop, and WebAssembly
Documentation
use super::{Fixed, PhysicalRect, Point, Rect, Transform};

/// Mapping from a widget's logical coordinate space to the physical
/// pixels of the backing surface — DPI scale today, with rotation and
/// sub-region projection reserved for future extension.
///
/// `scale = 1` means 1 logical pixel == 1 physical pixel; `scale = 2` is
/// a typical HiDPI desktop ratio. Widget-level 2D affine transforms are
/// a separate concern — see [`super::Transform`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Viewport {
    physical_w: u16,
    physical_h: u16,
    scale: Fixed,
}

impl Viewport {
    /// Construct. `scale <= 0` is normalized to 1 so downstream consumers
    /// never have to guard against a zero scale.
    #[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,
        }
    }

    /// Convert a logical-pixel Rect to an integer physical-pixel bound
    /// `(x0, y0, x1, y1)`. Top-left floors, bottom-right ceils so the
    /// returned region fully contains the source.
    #[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);
        }
    }
}