device_envoy_core/cyd/display/
tiling.rs1use embedded_graphics::{
17 prelude::{Point, Size},
18 primitives::Rectangle,
19};
20
21use super::super::CydDisplay;
22
23#[must_use]
25pub const fn rectangle_pixel_count(rectangle: Rectangle) -> usize {
26 (rectangle.size.width * rectangle.size.height) as usize
27}
28
29#[must_use]
31pub const fn max_rectangle_pixel_count(first: Rectangle, second: Rectangle) -> usize {
32 if rectangle_pixel_count(first) > rectangle_pixel_count(second) {
33 rectangle_pixel_count(first)
34 } else {
35 rectangle_pixel_count(second)
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct TileGrid {
48 pub top_left: Point,
49 pub size: Size,
50 columns: usize,
51 rows: usize,
52}
53
54impl TileGrid {
55 #[must_use]
61 pub const fn new(top_left: Point, size: Size, columns: usize, rows: usize) -> Self {
62 assert!(columns > 0, "columns must be greater than zero");
63 assert!(rows > 0, "rows must be greater than zero");
64 assert!(
65 columns <= size.width as usize,
66 "columns must not exceed rectangle width in pixels"
67 );
68 assert!(
69 rows <= size.height as usize,
70 "rows must not exceed rectangle height in pixels"
71 );
72 Self {
73 top_left,
74 size,
75 columns,
76 rows,
77 }
78 }
79
80 #[must_use]
82 pub const fn columns(&self) -> usize {
83 self.columns
84 }
85
86 #[must_use]
88 pub const fn rows(&self) -> usize {
89 self.rows
90 }
91
92 #[must_use]
94 pub const fn tile_width(&self) -> usize {
95 (self.size.width as usize).div_ceil(self.columns)
96 }
97
98 #[must_use]
100 pub const fn tile_height(&self) -> usize {
101 (self.size.height as usize).div_ceil(self.rows)
102 }
103
104 #[must_use]
109 pub const fn max_tile_pixel_count(&self) -> usize {
110 let widest = min_usize(self.tile_width(), self.size.width as usize);
111 let tallest = min_usize(self.tile_height(), self.size.height as usize);
112 widest * tallest
113 }
114
115 #[must_use]
123 pub(crate) fn tile(&self, column: usize, row: usize) -> Option<Rectangle> {
124 let tile_width = self.tile_width();
125 let tile_height = self.tile_height();
126 let column_offset = column * tile_width;
127 let row_offset = row * tile_height;
128
129 let region_width = self.size.width as usize;
130 let region_height = self.size.height as usize;
131 if column_offset >= region_width || row_offset >= region_height {
132 return None;
133 }
134
135 let width = min_usize(tile_width, region_width - column_offset);
136 let height = min_usize(tile_height, region_height - row_offset);
137 let size = Size::new(width as u32, height as u32);
138 let top_left = Point::new(
139 self.top_left.x + column_offset as i32,
140 self.top_left.y + row_offset as i32,
141 );
142 Some(Rectangle::new(top_left, size))
143 }
144}
145
146const fn min_usize(first: usize, second: usize) -> usize {
147 if first < second { first } else { second }
148}
149
150pub struct Tiles<'a, C: CydDisplay> {
157 cyd: &'a mut C,
158 grid: TileGrid,
159 column: usize,
160 row: usize,
161}
162
163impl<'a, C: CydDisplay> Tiles<'a, C> {
164 pub(crate) fn new(cyd: &'a mut C, grid: TileGrid) -> Self {
165 Self {
166 cyd,
167 grid,
168 column: 0,
169 row: 0,
170 }
171 }
172}
173
174impl<C: CydDisplay> Tiles<'_, C> {
175 #[allow(clippy::should_implement_trait)]
185 pub fn next(&mut self) -> Option<C::Frame<'_>> {
186 let (columns, rows) = (self.grid.columns(), self.grid.rows());
187 loop {
188 if self.row >= rows {
189 return None;
190 }
191 let rectangle = self.grid.tile(self.column, self.row);
192 self.column += 1;
193 if self.column >= columns {
194 self.column = 0;
195 self.row += 1;
196 }
197 if let Some(rectangle) = rectangle {
198 let tile_top_left = rectangle.top_left;
199 return Some(
200 self.cyd
201 .frame_mut_with_tile_top_left(rectangle, tile_top_left),
202 );
203 }
204 }
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 const BODY_GRID: TileGrid = TileGrid::new(Point::new(0, 34), Size::new(240, 286), 3, 3);
215
216 #[test]
217 fn exact_fit_columns_and_rows() {
218 assert_eq!(BODY_GRID.columns(), 3);
219 assert_eq!(BODY_GRID.rows(), 3);
220 assert_eq!(BODY_GRID.tile_width(), 80);
222 assert_eq!(BODY_GRID.tile_height(), 96);
223 }
224
225 #[test]
226 fn final_row_is_clipped() {
227 let tile = BODY_GRID.tile(0, 2).expect("tile (0, 2) is in range");
230 assert_eq!(tile.top_left, Point::new(0, 34 + 192));
231 assert_eq!(tile.size.height, 94);
232 assert_eq!(tile.size.width, 80);
233 }
234
235 #[test]
236 fn exact_division_has_no_clipping() {
237 let grid = TileGrid::new(Point::new(0, 0), Size::new(240, 288), 3, 3);
239 assert_eq!(grid.tile_width(), 80);
240 assert_eq!(grid.tile_height(), 96);
241 let tile = grid.tile(2, 2).expect("tile (2, 2) is in range");
242 assert_eq!(tile.size, Size::new(80, 96));
243 }
244
245 #[test]
246 fn final_column_and_row_clipping_for_uneven_dimensions() {
247 let grid = TileGrid::new(Point::new(5, 7), Size::new(250, 290), 4, 4);
250 assert_eq!(grid.columns(), 4);
251 assert_eq!(grid.rows(), 4);
252 assert_eq!(grid.tile_width(), 63);
253 assert_eq!(grid.tile_height(), 73);
254
255 let last_column = grid.tile(3, 0).expect("tile (3, 0) is in range");
256 assert_eq!(last_column.top_left, Point::new(5 + 189, 7));
257 assert_eq!(last_column.size.width, 61);
258 assert_eq!(last_column.size.height, 73);
259
260 let last_row = grid.tile(0, 3).expect("tile (0, 3) is in range");
261 assert_eq!(last_row.size.height, 71);
262
263 let corner = grid.tile(3, 3).expect("tile (3, 3) is in range");
264 assert_eq!(corner.size, Size::new(61, 71));
265
266 assert_eq!(grid.tile(4, 0), None);
268 assert_eq!(grid.tile(0, 4), None);
269 }
270
271 #[test]
272 fn max_tile_pixel_count_is_full_tile() {
273 assert_eq!(BODY_GRID.max_tile_pixel_count(), 80 * 96);
274
275 let small = TileGrid::new(Point::new(0, 0), Size::new(40, 50), 1, 1);
278 assert_eq!(small.max_tile_pixel_count(), 40 * 50);
279 }
280
281 #[test]
282 #[should_panic(expected = "columns must be greater than zero")]
283 fn zero_columns_panics() {
284 let _ = TileGrid::new(Point::new(0, 0), Size::new(240, 286), 0, 3);
285 }
286
287 #[test]
288 #[should_panic(expected = "rows must be greater than zero")]
289 fn zero_rows_panics() {
290 let _ = TileGrid::new(Point::new(0, 0), Size::new(240, 286), 3, 0);
291 }
292
293 #[test]
294 #[should_panic(expected = "columns must not exceed rectangle width")]
295 fn too_many_columns_panics() {
296 let _ = TileGrid::new(Point::new(0, 0), Size::new(4, 286), 5, 3);
297 }
298
299 #[test]
300 #[should_panic(expected = "rows must not exceed rectangle height")]
301 fn too_many_rows_panics() {
302 let _ = TileGrid::new(Point::new(0, 0), Size::new(240, 4), 3, 5);
303 }
304
305 #[test]
306 fn text_band_pixel_count() {
307 let text_band = Rectangle::new(Point::new(0, 0), Size::new(240, 34));
308 assert_eq!(
309 (text_band.size.width * text_band.size.height) as usize,
310 8160
311 );
312 }
313
314 #[test]
315 fn tile_grid_is_row_major() {
316 let top_left = |column, row| BODY_GRID.tile(column, row).expect("tile in range").top_left;
318 assert_eq!(top_left(0, 0), Point::new(0, 34));
319 assert_eq!(top_left(1, 0), Point::new(80, 34));
320 assert_eq!(top_left(2, 0), Point::new(160, 34));
321 assert_eq!(top_left(0, 1), Point::new(0, 34 + 96));
322 }
323}