grixy 0.6.0

Zero-cost 2D grids for embedded systems and graphics
Documentation
use crate::{
    core::{GridError, HasSize, Pos, Rect},
    ops::{
        GridBase, GridWrite,
        layout::{self, Layout as _},
        unchecked::TrustedSizeGrid,
    },
};

/// Write elements to a 2-dimensional grid position without bounds checking.
pub trait GridWriteUnchecked {
    /// The type of elements in the grid.
    type Element;

    /// The type of layout used for the grid.
    type Layout: layout::Layout;

    /// Sets the element at a specified position without bounds checking.
    ///
    /// ## Safety
    ///
    /// The caller must ensure `pos` is a valid position within this grid. A position is valid
    /// if `pos.x < width()` and `pos.y < height()`.
    ///
    /// Calling this method with an out-of-bounds position is _[undefined behavior][]_,
    /// regardless of whether the written value is subsequently read.
    ///
    /// [undefined behavior]: https://doc.rust-lang.org/reference/behavior-considered-undefined.html
    unsafe fn set_unchecked(&mut self, pos: Pos, value: Self::Element);

    /// Sets elements within a rectangular region of the grid without bounds checking.
    ///
    /// Each position in `dst` is filled with the value returned by `f(pos)`. Elements are set
    /// in an order agreeable to the grid's internal layout.
    ///
    /// The bounding rectangle is treated as _exclusive_ of the right and bottom edges.
    ///
    /// ## Safety
    ///
    /// The caller must ensure that **every position** in `dst` is a valid position in the
    /// grid. A position `(x, y)` is valid if `x < width()` and `y < height()`. This means the
    /// rectangle must satisfy `dst.right() <= width()` and `dst.bottom() <= height()`.
    ///
    /// Writing to memory outside the grid's allocated storage is _[undefined behavior][]_.
    ///
    /// ## Performance
    ///
    /// The default implementation uses [`Layout::iter_pos`] to iterate over the rectangle,
    /// calling [`set_unchecked`](GridWriteUnchecked::set_unchecked) for each position.
    ///
    /// [undefined behavior]: https://doc.rust-lang.org/reference/behavior-considered-undefined.html
    /// [`Layout::iter_pos`]: layout::Layout::iter_pos
    unsafe fn fill_rect_unchecked(&mut self, dst: Rect, mut f: impl FnMut(Pos) -> Self::Element) {
        Self::Layout::iter_pos(dst).for_each(|pos| unsafe {
            self.set_unchecked(pos, f(pos));
        });
    }

    /// Sets elements within a rectangular region of the grid without bounds checking.
    ///
    /// Each position in `dst` is filled from the iterator in traversal order. If the iterator
    /// yields fewer elements than the rectangle contains, remaining positions are left unchanged.
    ///
    /// The bounding rectangle is treated as _exclusive_ of the right and bottom edges.
    ///
    /// ## Safety
    ///
    /// The caller must ensure that **every position** in `dst` is a valid position in the
    /// grid (see [`fill_rect_unchecked`](GridWriteUnchecked::fill_rect_unchecked)).
    ///
    /// Additionally, if the iterator **yields more elements** than the number of positions in
    /// `dst`, the excess elements are silently dropped. If the iterator is an exact-size iterator
    /// whose reported length exceeds the rectangle area, this is _not_ undefined behavior on its
    /// own — but implementations may rely on the length for buffer sizing, so callers should
    /// ensure the iterator does not report a size larger than `dst.area()`.
    ///
    /// ## Performance
    ///
    /// The default implementation zips the iterator with [`Layout::iter_pos`], calling
    /// [`set_unchecked`](GridWriteUnchecked::set_unchecked) for each yielded pair.
    ///
    /// [`Layout::iter_pos`]: layout::Layout::iter_pos
    unsafe fn fill_rect_iter_unchecked(
        &mut self,
        dst: Rect,
        iter: impl IntoIterator<Item = Self::Element>,
    ) {
        Self::Layout::iter_pos(dst)
            .zip(iter)
            .for_each(|(pos, value)| unsafe {
                self.set_unchecked(pos, value);
            });
    }

    /// Fills a rectangular region of the grid with a single value without bounds checking.
    ///
    /// All positions in `bounds` are set to `value`. The bounding rectangle is treated as
    /// _exclusive_ of the right and bottom edges.
    ///
    /// ## Safety
    ///
    /// The caller must ensure that **every position** in `bounds` is a valid position in the
    /// grid (see [`fill_rect_unchecked`](GridWriteUnchecked::fill_rect_unchecked)).
    ///
    /// Writing to memory outside the grid's allocated storage is _[undefined behavior][]_.
    ///
    /// ## Performance
    ///
    /// The default implementation delegates to [`Self::fill_rect_unchecked`], wrapping the value in a
    /// closure. Specialized implementations may use `memset`-style operations for `Copy` types.
    ///
    /// [undefined behavior]: https://doc.rust-lang.org/reference/behavior-considered-undefined.html
    unsafe fn fill_rect_solid_unchecked(&mut self, bounds: Rect, value: Self::Element)
    where
        Self::Element: Copy,
    {
        unsafe { self.fill_rect_unchecked(bounds, |_| value) };
    }
}

/// Automatically implement `GridWrite` when `GridWriteUnchecked` + `TrustedSizeGrid` are implemented.
impl<T: GridBase + GridWriteUnchecked + TrustedSizeGrid> GridWrite for T {
    type Element = T::Element;
    type Layout = T::Layout;

    fn set(&mut self, pos: Pos, value: Self::Element) -> Result<(), GridError> {
        if self.contains(pos) {
            unsafe {
                self.set_unchecked(pos, value);
            }
            Ok(())
        } else {
            Err(GridError::OutOfBounds { pos })
        }
    }

    fn fill_rect(&mut self, bounds: Rect, f: impl FnMut(Pos) -> Self::Element) {
        let size = self.size().to_rect();
        let rect = bounds.intersect(size);
        unsafe { self.fill_rect_unchecked(rect, f) }
    }

    fn fill_rect_iter(&mut self, dst: Rect, iter: impl IntoIterator<Item = Self::Element>) {
        let size = self.size().to_rect();
        let rect = dst.intersect(size);
        unsafe { self.fill_rect_iter_unchecked(rect, iter) }
    }

    fn fill_rect_solid(&mut self, dst: Rect, value: Self::Element)
    where
        Self::Element: Copy,
    {
        let size = self.size().to_rect();
        let rect = dst.intersect(size);
        unsafe { self.fill_rect_solid_unchecked(rect, value) }
    }
}

#[cfg(test)]
mod tests {
    extern crate alloc;

    use crate::{
        core::Size,
        ops::{ExactSizeGrid, layout::RowMajor},
    };

    use super::*;
    use alloc::vec;

    struct UncheckedTestGrid {
        grid: [[u8; 3]; 3],
    }

    impl GridBase for UncheckedTestGrid {
        fn size_hint(&self) -> (Size, Option<Size>) {
            let size = Size::new(3, 3);
            (size, Some(size))
        }
    }

    impl ExactSizeGrid for UncheckedTestGrid {
        fn width(&self) -> usize {
            3
        }

        fn height(&self) -> usize {
            3
        }
    }

    unsafe impl TrustedSizeGrid for UncheckedTestGrid {}

    impl GridWriteUnchecked for UncheckedTestGrid {
        type Element = u8;
        type Layout = RowMajor;

        unsafe fn set_unchecked(&mut self, pos: Pos, value: Self::Element) {
            self.grid[pos.y][pos.x] = value;
        }
    }

    #[test]
    fn impl_unsafe_set_ok() {
        let mut grid = UncheckedTestGrid { grid: [[0; 3]; 3] };
        let pos = Pos { x: 1, y: 1 };
        grid.set(pos, 42).unwrap();
        assert_eq!(grid.grid[1][1], 42);
    }

    #[test]
    fn impl_unsafe_set_out_of_bounds_x() {
        let mut grid = UncheckedTestGrid { grid: [[0; 3]; 3] };
        let pos = Pos { x: 3, y: 1 };
        assert!(grid.set(pos, 42).is_err());
        assert_eq!(grid.grid, [[0, 0, 0], [0, 0, 0], [0, 0, 0]]);
    }

    #[test]
    fn impl_unsafe_set_out_of_bounds_y() {
        let mut grid = UncheckedTestGrid { grid: [[0; 3]; 3] };
        let pos = Pos { x: 1, y: 3 };
        assert!(grid.set(pos, 42).is_err());
        assert_eq!(grid.grid, [[0, 0, 0], [0, 0, 0], [0, 0, 0]]);
    }

    #[test]
    fn impl_unsafe_set_unchecked_in_bounds() {
        let mut grid = UncheckedTestGrid { grid: [[0; 3]; 3] };
        let pos = Pos { x: 2, y: 2 };
        unsafe {
            grid.set_unchecked(pos, 99);
        }
        assert_eq!(grid.grid[2][2], 99);
    }

    #[test]
    fn impl_unsafe_fill_rect_complete() {
        let mut grid = UncheckedTestGrid { grid: [[0; 3]; 3] };
        let bounds = Rect::from_ltrb(0, 0, 3, 3).unwrap();
        grid.fill_rect(bounds, |_| 42);
        assert_eq!(grid.grid, [[42; 3]; 3]);
    }

    #[test]
    fn impl_unsafe_fill_rect_partial_in_bounds() {
        let mut grid = UncheckedTestGrid { grid: [[0; 3]; 3] };
        let bounds = Rect::from_ltrb(0, 0, 2, 2).unwrap();
        grid.fill_rect(bounds, |pos| if pos.x == 1 && pos.y == 1 { 99 } else { 42 });
        assert_eq!(grid.grid, [[42, 42, 0], [42, 99, 0], [0, 0, 0]]);
    }

    #[test]
    fn impl_unsafe_fill_rect_partial_out_of_bounds() {
        let mut grid = UncheckedTestGrid { grid: [[0; 3]; 3] };
        let bounds = Rect::from_ltrb(1, 1, 4, 4).unwrap(); // Out of bounds on the right and bottom
        grid.fill_rect(bounds, |_| 42);
        assert_eq!(grid.grid, [[0, 0, 0], [0, 42, 42], [0, 42, 42]]);
    }

    #[test]
    fn impl_unsafe_fill_rect_iter_complete() {
        let mut grid = UncheckedTestGrid { grid: [[0; 3]; 3] };
        let bounds = Rect::from_ltrb(0, 0, 3, 3).unwrap();
        grid.fill_rect_iter(bounds, vec![42; 9]);
        assert_eq!(grid.grid, [[42; 3]; 3]);
    }

    #[test]
    fn impl_unsafe_fill_rect_iter_partial_in_bounds() {
        let mut grid = UncheckedTestGrid { grid: [[0; 3]; 3] };
        let bounds = Rect::from_ltrb(0, 0, 2, 2).unwrap();
        grid.fill_rect_iter(bounds, vec![42, 99]);

        #[rustfmt::skip]
        assert_eq!(grid.grid, [
            [42, 99, 0],
            [0,  0,  0],
            [0,  0,  0]
        ]);
    }

    #[test]
    fn impl_unsafe_fill_rect_iter_partial_in_bounds_with_extra() {
        let mut grid = UncheckedTestGrid { grid: [[0; 3]; 3] };
        let bounds = Rect::from_ltrb(0, 0, 2, 1).unwrap();
        grid.fill_rect_iter(bounds, vec![42, 99, 100]);

        #[rustfmt::skip]
        assert_eq!(grid.grid, [
            [42, 99, 0],
            [0,  0,  0],
            [0,  0,  0]
        ]);
    }

    #[test]
    fn impl_unsafe_fill_rect_iter_partial_out_of_bounds() {
        let mut grid = UncheckedTestGrid { grid: [[0; 3]; 3] };
        let bounds = Rect::from_ltrb(1, 1, 4, 4).unwrap(); // Out of bounds on the right and bottom
        grid.fill_rect_iter(bounds, vec![42, 99, 100]);

        #[rustfmt::skip]
        assert_eq!(grid.grid, [
            [0, 0, 0],
            [0, 42, 99],
            [0, 100, 0]
        ]);
    }

    #[test]
    fn impl_unsafe_fill_rect_iter_out_of_bounds() {
        let mut grid = UncheckedTestGrid { grid: [[0; 3]; 3] };
        let bounds = Rect::from_ltrb(3, 3, 4, 4).unwrap(); // Out of bounds on the right and bottom
        grid.fill_rect_iter(bounds, vec![42, 99, 100]);

        #[rustfmt::skip]
        assert_eq!(grid.grid, [
            [0, 0, 0],
            [0, 0, 0],
            [0, 0, 0],
        ]);
    }

    #[test]
    fn impl_unsafe_fill_rect_solid() {
        let mut grid = UncheckedTestGrid { grid: [[0; 3]; 3] };
        let bounds = Rect::from_ltrb(0, 0, 3, 3).unwrap();
        grid.fill_rect_solid(bounds, 42);

        assert_eq!(grid.grid, [[42; 3]; 3]);
    }
}