use embedded_graphics::{
prelude::{Point, Size},
primitives::Rectangle,
};
use super::super::CydDisplay;
#[must_use]
pub const fn rectangle_pixel_count(rectangle: Rectangle) -> usize {
(rectangle.size.width * rectangle.size.height) as usize
}
#[must_use]
pub const fn max_rectangle_pixel_count(first: Rectangle, second: Rectangle) -> usize {
if rectangle_pixel_count(first) > rectangle_pixel_count(second) {
rectangle_pixel_count(first)
} else {
rectangle_pixel_count(second)
}
}
#[cfg_attr(
feature = "doc-images",
doc = ::embed_doc_image::embed_image!("tile_grid", "docs/assets/tile_grid.png")
)]
#[cfg_attr(
feature = "doc-images",
doc = "\n![A circle and diagonal line drawn continuously across a four-by-three tile grid][tile_grid]\n"
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TileGrid {
rectangle: Rectangle,
columns: usize,
rows: usize,
}
impl TileGrid {
#[must_use]
pub const fn new(rectangle: Rectangle, columns: usize, rows: usize) -> Self {
assert!(columns > 0, "columns must be greater than zero");
assert!(rows > 0, "rows must be greater than zero");
assert!(
columns <= rectangle.size.width as usize,
"columns must not exceed rectangle width in pixels"
);
assert!(
rows <= rectangle.size.height as usize,
"rows must not exceed rectangle height in pixels"
);
Self {
rectangle,
columns,
rows,
}
}
#[must_use]
pub const fn rectangle(&self) -> Rectangle {
self.rectangle
}
#[must_use]
pub const fn columns(&self) -> usize {
self.columns
}
#[must_use]
pub const fn rows(&self) -> usize {
self.rows
}
#[must_use]
pub const fn tile_width(&self) -> usize {
(self.rectangle.size.width as usize).div_ceil(self.columns)
}
#[must_use]
pub const fn tile_height(&self) -> usize {
(self.rectangle.size.height as usize).div_ceil(self.rows)
}
#[must_use]
pub const fn max_tile_pixel_count(&self) -> usize {
let widest = min_usize(self.tile_width(), self.rectangle.size.width as usize);
let tallest = min_usize(self.tile_height(), self.rectangle.size.height as usize);
widest * tallest
}
#[must_use]
pub(crate) fn tile(&self, column: usize, row: usize) -> Option<Rectangle> {
let tile_width = self.tile_width();
let tile_height = self.tile_height();
let column_offset = column * tile_width;
let row_offset = row * tile_height;
let region_width = self.rectangle.size.width as usize;
let region_height = self.rectangle.size.height as usize;
if column_offset >= region_width || row_offset >= region_height {
return None;
}
let width = min_usize(tile_width, region_width - column_offset);
let height = min_usize(tile_height, region_height - row_offset);
let size = Size::new(width as u32, height as u32);
let top_left = Point::new(
self.rectangle.top_left.x + column_offset as i32,
self.rectangle.top_left.y + row_offset as i32,
);
Some(Rectangle::new(top_left, size))
}
}
const fn min_usize(first: usize, second: usize) -> usize {
if first < second { first } else { second }
}
pub(crate) struct Tiles<'a, C: CydDisplay> {
cyd: &'a mut C,
grid: TileGrid,
column: usize,
row: usize,
}
impl<'a, C: CydDisplay> Tiles<'a, C> {
pub(crate) fn new(cyd: &'a mut C, grid: TileGrid) -> Self {
Self {
cyd,
grid,
column: 0,
row: 0,
}
}
}
impl<C: CydDisplay> Tiles<'_, C> {
#[allow(clippy::should_implement_trait)]
pub(crate) fn next(&mut self) -> Option<C::Frame<'_>> {
let (columns, rows) = (self.grid.columns(), self.grid.rows());
loop {
if self.row >= rows {
return None;
}
let rectangle = self.grid.tile(self.column, self.row);
self.column += 1;
if self.column >= columns {
self.column = 0;
self.row += 1;
}
if let Some(rectangle) = rectangle {
return Some(super::super::backend::DisplayBackend::create_frame_mut(
self.cyd, rectangle,
));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const BODY_GRID: TileGrid =
TileGrid::new(Rectangle::new(Point::new(0, 34), Size::new(240, 286)), 3, 3);
#[test]
fn exact_fit_columns_and_rows() {
assert_eq!(BODY_GRID.columns(), 3);
assert_eq!(BODY_GRID.rows(), 3);
assert_eq!(BODY_GRID.tile_width(), 80);
assert_eq!(BODY_GRID.tile_height(), 96);
}
#[test]
fn final_row_is_clipped() {
let tile = BODY_GRID.tile(0, 2).expect("tile (0, 2) is in range");
assert_eq!(tile.top_left, Point::new(0, 34 + 192));
assert_eq!(tile.size.height, 94);
assert_eq!(tile.size.width, 80);
}
#[test]
fn exact_division_has_no_clipping() {
let grid = TileGrid::new(Rectangle::new(Point::new(0, 0), Size::new(240, 288)), 3, 3);
assert_eq!(grid.tile_width(), 80);
assert_eq!(grid.tile_height(), 96);
let tile = grid.tile(2, 2).expect("tile (2, 2) is in range");
assert_eq!(tile.size, Size::new(80, 96));
}
#[test]
fn final_column_and_row_clipping_for_uneven_dimensions() {
let grid = TileGrid::new(Rectangle::new(Point::new(5, 7), Size::new(250, 290)), 4, 4);
assert_eq!(grid.columns(), 4);
assert_eq!(grid.rows(), 4);
assert_eq!(grid.tile_width(), 63);
assert_eq!(grid.tile_height(), 73);
let last_column = grid.tile(3, 0).expect("tile (3, 0) is in range");
assert_eq!(last_column.top_left, Point::new(5 + 189, 7));
assert_eq!(last_column.size.width, 61);
assert_eq!(last_column.size.height, 73);
let last_row = grid.tile(0, 3).expect("tile (0, 3) is in range");
assert_eq!(last_row.size.height, 71);
let corner = grid.tile(3, 3).expect("tile (3, 3) is in range");
assert_eq!(corner.size, Size::new(61, 71));
assert_eq!(grid.tile(4, 0), None);
assert_eq!(grid.tile(0, 4), None);
}
#[test]
fn max_tile_pixel_count_is_full_tile() {
assert_eq!(BODY_GRID.max_tile_pixel_count(), 80 * 96);
let small = TileGrid::new(Rectangle::new(Point::new(0, 0), Size::new(40, 50)), 1, 1);
assert_eq!(small.max_tile_pixel_count(), 40 * 50);
}
#[test]
#[should_panic(expected = "columns must be greater than zero")]
fn zero_columns_panics() {
let _tile_grid = TileGrid::new(Rectangle::new(Point::new(0, 0), Size::new(240, 286)), 0, 3);
}
#[test]
#[should_panic(expected = "rows must be greater than zero")]
fn zero_rows_panics() {
let _tile_grid = TileGrid::new(Rectangle::new(Point::new(0, 0), Size::new(240, 286)), 3, 0);
}
#[test]
#[should_panic(expected = "columns must not exceed rectangle width")]
fn too_many_columns_panics() {
let _tile_grid = TileGrid::new(Rectangle::new(Point::new(0, 0), Size::new(4, 286)), 5, 3);
}
#[test]
#[should_panic(expected = "rows must not exceed rectangle height")]
fn too_many_rows_panics() {
let _tile_grid = TileGrid::new(Rectangle::new(Point::new(0, 0), Size::new(240, 4)), 3, 5);
}
#[test]
fn text_band_pixel_count() {
let text_band = Rectangle::new(Point::new(0, 0), Size::new(240, 34));
assert_eq!(
(text_band.size.width * text_band.size.height) as usize,
8160
);
}
#[test]
fn tile_grid_is_row_major() {
let top_left = |column, row| BODY_GRID.tile(column, row).expect("tile in range").top_left;
assert_eq!(top_left(0, 0), Point::new(0, 34));
assert_eq!(top_left(1, 0), Point::new(80, 34));
assert_eq!(top_left(2, 0), Point::new(160, 34));
assert_eq!(top_left(0, 1), Point::new(0, 34 + 96));
}
}