Skip to main content

devela/ui/view/scale/
cell.rs

1// devela/ui/view/scale/cell.rs
2//
3//! Defines [`UiCellMetric`].
4//
5
6use crate::UiRound::{self, Ceil, Floor, Inward, Nearest, Outward};
7use crate::{Cmp, Lunit, RegionS2, UiNum, UiRect};
8
9#[doc = crate::_tags!(ui layout quant)]
10/// Cell size in logical UI layout space.
11#[doc = crate::_doc_meta! {
12    location("ui/view/scale", struct UiCellMetric),
13    test_size_of(UiCellMetric = 8|64; niche !Option),
14}]
15/// Defines the logical width and height represented by one discrete output cell.
16///
17/// A cell need not be square. Terminal cells, tile grids, text grids, and other
18/// cell-oriented projections may use different horizontal and vertical measures.
19#[must_use]
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
21pub struct UiCellMetric {
22    width: Lunit,
23    height: Lunit,
24}
25crate::_impl_init![Self::ONE => UiCellMetric];
26impl Default for UiCellMetric {
27    fn default() -> Self {
28        Self::ONE
29    }
30}
31#[rustfmt::skip]
32impl UiCellMetric {
33    /// One logical pixel per cell in each dimension.
34    pub const ONE: Self = Self::new_unchecked(Lunit::px(1), Lunit::px(1));
35
36    const fn new_unchecked(width: Lunit, height: Lunit) -> Self { Self { width, height } }
37
38    /* constructors */
39
40    /// Constructs a metric from positive logical cell dimensions.
41    ///
42    /// Returns `None` if either dimension is zero or negative.
43    pub const fn new(width: Lunit, height: Lunit) -> Option<Self> {
44        if width.quanta() <= 0 || height.quanta() <= 0 { None }
45        else { Some(Self { width, height }) }
46    }
47    /// Constructs a metric from whole logical-pixel dimensions.
48    ///
49    /// Returns `None` if either dimension is zero or negative.
50    pub const fn from_px(width: i32, height: i32) -> Option<Self> {
51        Self::new(Lunit::px(width), Lunit::px(height))
52    }
53
54    /* queries */
55
56    /// Returns the logical width represented by one cell.
57    pub const fn width(self) -> Lunit { self.width }
58
59    /// Returns the logical height represented by one cell.
60    pub const fn height(self) -> Lunit { self.height }
61
62    /* scalar projection */
63
64    /// Projects a logical horizontal coordinate to a cell column.
65    ///
66    /// `Inward` and `Outward` are rectangle policies; for scalar projection
67    /// they behave like `Nearest`.
68    pub const fn x_to_col(self, x: Lunit, round: UiRound) -> i32 {
69        UiNum::round_div_scalar_i64_to_i32(x.quanta() as i64, self.width.quanta() as i64, round)
70    }
71
72    /// Projects a logical vertical coordinate to a cell row.
73    ///
74    /// `Inward` and `Outward` are rectangle policies; for scalar projection
75    /// they behave like `Nearest`.
76    pub const fn y_to_row(self, y: Lunit, round: UiRound) -> i32 {
77        UiNum::round_div_scalar_i64_to_i32(y.quanta() as i64, self.height.quanta() as i64, round)
78    }
79    /// Projects a cell column back into logical horizontal space.
80    pub const fn col_to_x(self, col: i32) -> Lunit {
81        Lunit::new_saturated_up((col as i64).saturating_mul(self.width.quanta() as i64))
82    }
83    /// Projects a cell row back into logical vertical space.
84    pub const fn row_to_y(self, row: i32) -> Lunit {
85        Lunit::new_saturated_up((row as i64).saturating_mul(self.height.quanta() as i64))
86    }
87
88    /* rectangle projection */
89
90    /// Projects a logical UI rectangle to a discrete cell rectangle.
91    pub const fn rect_to_cells(self, rect: UiRect, round: UiRound) -> RegionS2<i32> {
92        match round {
93            Outward => {
94                let x0 = self.x_to_col(rect.left(), Floor);
95                let y0 = self.y_to_row(rect.top(), Floor);
96                let x1 = self.x_to_col(rect.right(), Ceil);
97                let y1 = self.y_to_row(rect.bottom(), Ceil);
98                RegionS2::from_xy_wh(x0, y0,
99                    Cmp(x1.saturating_sub(x0)).max(0), Cmp(y1.saturating_sub(y0)).max(0))
100            }
101            Inward => {
102                let x0 = self.x_to_col(rect.left(), Ceil);
103                let y0 = self.y_to_row(rect.top(), Ceil);
104                let x1 = self.x_to_col(rect.right(), Floor);
105                let y1 = self.y_to_row(rect.bottom(), Floor);
106                RegionS2::from_xy_wh(x0, y0,
107                    Cmp(x1.saturating_sub(x0)).max(0), Cmp(y1.saturating_sub(y0)).max(0))
108            }
109            Floor | Ceil | Nearest => RegionS2::from_xy_wh(
110                self.x_to_col(rect.x(), round),
111                self.y_to_row(rect.y(), round),
112                Cmp(self.x_to_col(rect.w(), round)).max(0),
113                Cmp(self.y_to_row(rect.h(), round)).max(0),
114            ),
115        }
116    }
117    /// Projects a discrete cell rectangle back into logical UI space.
118    pub const fn cells_to_rect(self, cells: RegionS2<i32>) -> UiRect {
119        UiRect::from_xy_wh(
120            self.col_to_x(cells.x()),
121            self.row_to_y(cells.y()),
122            self.col_to_x(cells.w()).max_zero(),
123            self.row_to_y(cells.h()).max_zero(),
124        )
125    }
126}
127#[cfg(test)]
128mod _test {
129    use super::*;
130
131    #[test]
132    fn rejects_nonpositive_dimensions() {
133        assert!(UiCellMetric::new(Lunit::ZERO, Lunit::px(16)).is_none());
134        assert!(UiCellMetric::new(Lunit::px(8), Lunit::ZERO).is_none());
135        assert!(UiCellMetric::new(Lunit::px(-8), Lunit::px(16)).is_none());
136    }
137    #[test]
138    fn projects_independent_cell_dimensions() {
139        let metric = UiCellMetric::from_px(8, 16).unwrap();
140        assert_eq!(metric.x_to_col(Lunit::px(24), UiRound::Nearest), 3,);
141        assert_eq!(metric.y_to_row(Lunit::px(48), UiRound::Nearest), 3,);
142    }
143    #[test]
144    fn outward_projection_covers_partial_cells() {
145        let metric = UiCellMetric::from_px(8, 16).unwrap();
146        let rect = UiRect::from_xy_wh(Lunit::px(4), Lunit::px(8), Lunit::px(8), Lunit::px(16));
147        let cells = metric.rect_to_cells(rect, UiRound::Outward);
148        assert_eq!(cells, RegionS2::from_xy_wh(0, 0, 2, 2));
149    }
150    #[test]
151    fn cell_rectangle_roundtrips_exactly() {
152        let metric = UiCellMetric::from_px(8, 16).unwrap();
153        let cells = RegionS2::from_xy_wh(2, 3, 10, 4);
154        assert_eq!(metric.rect_to_cells(metric.cells_to_rect(cells), UiRound::Nearest,), cells,);
155    }
156}