Windows, light removals, move more functions to traits #15

Merged
vinzenz merged 18 commits from next into main 2025-07-10 22:08:51 +02:00
21 changed files with 1067 additions and 464 deletions

14
Cargo.lock generated
View file

@ -201,17 +201,6 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "inherent"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c38228f24186d9cc68c729accb4d413be9eaed6ad07ff79e0270d9e56f3de13"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
@ -347,13 +336,12 @@ dependencies = [
[[package]]
name = "servicepoint"
version = "0.15.2"
version = "0.16.0"
dependencies = [
"bitvec",
"bzip2",
"clap",
"flate2",
"inherent",
"log",
"once_cell",
"rand",

View file

@ -1,6 +1,6 @@
[package]
name = "servicepoint"
version = "0.15.2"
version = "0.16.0"
publish = true
edition = "2021"
license = "GPL-3.0-or-later"
@ -24,7 +24,6 @@ rust-lzma = { version = "0.6", optional = true }
rand = { version = "0.9", optional = true }
once_cell = { version = "1.20", optional = true }
thiserror = "2.0"
inherent = "1.0"
[features]
default = ["compression_lzma", "cp437"]

View file

@ -3,7 +3,7 @@
use clap::Parser;
use servicepoint::{
Bitmap, BitmapCommand, Brightness, BrightnessGrid, BrightnessGridCommand,
DataRef, Grid, UdpSocketExt, TILE_HEIGHT, TILE_WIDTH,
DataRef, GridMut, UdpSocketExt, TILE_HEIGHT, TILE_WIDTH,
};
use std::net::UdpSocket;

View file

@ -2,8 +2,8 @@
use clap::Parser;
use servicepoint::{
Bitmap, BitmapCommand, CompressionCode, Grid, Origin, Packet, UdpSocketExt,
FRAME_PACING, PIXEL_HEIGHT, PIXEL_WIDTH,
Bitmap, BitmapCommand, CompressionCode, GridMut, Origin, Packet,
UdpSocketExt, FRAME_PACING, PIXEL_HEIGHT, PIXEL_WIDTH,
};
use std::{net::UdpSocket, thread, time::Duration};

View file

@ -2,7 +2,9 @@
use clap::Parser;
use rand::Rng;
use servicepoint::{Bitmap, BitmapCommand, Grid, UdpSocketExt, FRAME_PACING};
use servicepoint::{
Bitmap, BitmapCommand, Grid, GridMut, UdpSocketExt, FRAME_PACING,
};
use std::{net::UdpSocket, thread};
#[derive(Parser, Debug)]

View file

@ -2,8 +2,8 @@
use clap::Parser;
use servicepoint::{
Bitmap, BitmapCommand, CompressionCode, Grid, UdpSocketExt, FRAME_PACING,
PIXEL_HEIGHT, PIXEL_WIDTH,
Bitmap, BitmapCommand, CompressionCode, GridMut, UdpSocketExt,
FRAME_PACING, PIXEL_HEIGHT, PIXEL_WIDTH,
};
use std::{net::UdpSocket, thread};

View file

@ -5,7 +5,7 @@ use clap::Parser;
use rand::Rng;
use servicepoint::{
Bitmap, BitmapCommand, Brightness, BrightnessGrid, BrightnessGridCommand,
GlobalBrightnessCommand, Grid, Origin, UdpSocketExt, TILE_HEIGHT,
GlobalBrightnessCommand, GridMut, Origin, UdpSocketExt, TILE_HEIGHT,
TILE_WIDTH,
};
use std::{net::UdpSocket, time::Duration};

View file

@ -2,7 +2,7 @@
use clap::Parser;
use servicepoint::{
Bitmap, BitmapCommand, Grid, UdpSocketExt, FRAME_PACING, PIXEL_HEIGHT,
Bitmap, BitmapCommand, GridMut, UdpSocketExt, FRAME_PACING, PIXEL_HEIGHT,
PIXEL_WIDTH,
};
use std::{net::UdpSocket, thread, time::Duration};

View file

@ -126,8 +126,7 @@
default = pkgs.mkShell rec {
inputsFrom = [ self.packages.${system}.servicepoint ];
packages = with pkgs; [
(pkgs.symlinkJoin
{
(pkgs.symlinkJoin {
name = "rust-toolchain";
paths = with pkgs; [
rustc
@ -145,6 +144,8 @@
];
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath (builtins.concatMap (d: d.buildInputs) inputsFrom)}";
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
RUST_LOG = "all";
RUST_BACKTRACE = "1";
};
}
);

View file

@ -106,10 +106,15 @@ mod tests {
#[cfg(feature = "rand")]
fn test() {
let mut rng = rand::rng();
// two so test failure is less likely
// more so random failure is less likely
assert_ne!(
[rng.random::<Brightness>(), rng.random()],
[rng.random(), rng.random()]
[
rng.random::<Brightness>(),
rng.random(),
rng.random(),
rng.random()
],
[rng.random(), rng.random(), rng.random(), rng.random()]
);
}
}

View file

@ -183,6 +183,7 @@ mod tests {
use super::*;
use crate::{
command_code::CommandCode, commands::tests::TestImplementsCommand,
GridMut,
};
impl TestImplementsCommand for BitmapCommand {}
@ -304,7 +305,7 @@ mod tests {
crate::commands::tests::round_trip(
BitmapCommand {
origin: Origin::ZERO,
bitmap: Bitmap::max_sized(),
bitmap: Bitmap::new(8, 2).unwrap(),
compression: *compression,
}
.into(),
@ -325,4 +326,17 @@ mod tests {
);
}
}
#[test]
fn invalid_y() {
let command = BitmapCommand {
bitmap: Bitmap::new(8, 3).unwrap(),
origin: Origin::new(4, u16::MAX as usize + 1),
compression: CompressionCode::Uncompressed,
};
assert!(matches!(
Packet::try_from(command),
Err(TryIntoPacketError::ConversionError(_)),
))
}
}

View file

@ -414,4 +414,18 @@ mod tests {
}
);
}
#[test]
fn invalid_offset() {
let command = BitVecCommand {
bitvec: DisplayBitVec::repeat(false, 16),
offset: u16::MAX as usize + 1,
operation: BinaryOperation::Or,
compression: CompressionCode::Uncompressed,
};
assert!(matches!(
Packet::try_from(command),
Err(TryIntoPacketError::ConversionError(_)),
))
}
}

View file

@ -1,8 +1,9 @@
use crate::{
DataRef, DisplayBitVec, Grid, Payload, ValueGrid, PIXEL_HEIGHT, PIXEL_WIDTH,
containers::absolute_bounds_to_abs_range, DataRef, DisplayBitVec, Grid,
GridMut, Payload, ValueGrid, Window, WindowMut, PIXEL_HEIGHT, PIXEL_WIDTH,
};
use ::bitvec::{order::Msb0, prelude::BitSlice, slice::IterMut};
use inherent::inherent;
use std::ops::RangeBounds;
/// A fixed-size 2D grid of booleans.
///
@ -143,7 +144,7 @@ impl Bitmap {
///
/// Order is equivalent to the following loop:
/// ```
/// # use servicepoint::{Bitmap, Grid};
/// # use servicepoint::{Bitmap, Grid, GridMut};
/// # let mut grid = Bitmap::new(8, 2).unwrap();
/// # let value = false;
/// for y in 0..grid.height() {
@ -164,7 +165,7 @@ impl Bitmap {
/// ```
#[must_use]
#[allow(clippy::iter_without_into_iter)]
pub fn iter_mut(&mut self) -> IterMut<u8, Msb0> {
pub fn iter_mut(&mut self) -> IterMut<'_, u8, Msb0> {
self.bit_vec.iter_mut()
}
@ -175,10 +176,55 @@ impl Bitmap {
row: 0,
}
}
/// Creates a window into the bitmap.
///
/// Returns None in case the window does not fit.
#[must_use]
pub fn window(
&self,
xs: impl RangeBounds<usize>,
ys: impl RangeBounds<usize>,
) -> Option<Window<'_, bool, Self>> {
let xs = absolute_bounds_to_abs_range(xs, self.width)?;
let ys = absolute_bounds_to_abs_range(ys, self.height)?;
Window::new(self, xs, ys)
}
/// Creates a mutable window into the bitmap.
///
/// Returns None in case the window does not fit.
pub fn window_mut(
&mut self,
xs: impl RangeBounds<usize>,
ys: impl RangeBounds<usize>,
) -> Option<WindowMut<'_, bool, Self>> {
let xs = absolute_bounds_to_abs_range(xs, self.width)?;
let ys = absolute_bounds_to_abs_range(ys, self.height)?;
WindowMut::new(self, xs, ys)
}
}
#[inherent]
impl Grid<bool> for Bitmap {
fn get_optional(&self, x: usize, y: usize) -> Option<bool> {
let index = x + y * self.width;
if self.is_in_bounds(x, y) {
Some(self.bit_vec[index])
} else {
None
}
}
fn width(&self) -> usize {
self.width
}
fn height(&self) -> usize {
self.height
}
}
impl GridMut<bool> for Bitmap {
/// Sets the value of the specified position in the [Bitmap].
///
/// # Arguments
@ -191,16 +237,13 @@ impl Grid<bool> for Bitmap {
/// # Panics
///
/// When accessing `x` or `y` out of bounds.
#[allow(unused, reason = "False positive because of #[inherent]")]
fn set(&mut self, x: usize, y: usize, value: bool) {
self.assert_in_bounds(x, y);
self.bit_vec.set(x + y * self.width, value);
}
#[allow(unused, reason = "False positive because of #[inherent]")]
fn get(&self, x: usize, y: usize) -> bool {
self.assert_in_bounds(x, y);
self.bit_vec[x + y * self.width]
fn set_optional(&mut self, x: usize, y: usize, value: bool) -> bool {
if self.is_in_bounds(x, y) {
self.bit_vec.set(x + y * self.width, value);
true
} else {
false
}
}
/// Sets the state of all pixels in the [Bitmap].
@ -209,18 +252,9 @@ impl Grid<bool> for Bitmap {
///
/// - `this`: instance to write to
/// - `value`: the value to set all pixels to
#[allow(unused, reason = "False positive because of #[inherent]")]
fn fill(&mut self, value: bool) {
self.bit_vec.fill(value);
}
fn width(&self) -> usize {
self.width
}
fn height(&self) -> usize {
self.height
}
}
impl DataRef<u8> for Bitmap {
@ -250,26 +284,20 @@ impl From<Bitmap> for DisplayBitVec {
impl TryFrom<&ValueGrid<bool>> for Bitmap {
type Error = ();
/// Converts a grid of [bool]s into a [Bitmap].
///
/// Returns Err if the width of `value` is not dividable by 8
fn try_from(value: &ValueGrid<bool>) -> Result<Self, Self::Error> {
let mut result = Self::new(value.width(), value.height()).ok_or(())?;
for (mut to, from) in result.iter_mut().zip(value.iter()) {
*to = *from;
}
result.deref_assign(value);
Ok(result)
}
}
impl From<&Bitmap> for ValueGrid<bool> {
/// Converts a [Bitmap] into a grid of [bool]s.
fn from(value: &Bitmap) -> Self {
let mut result = Self::new(value.width(), value.height());
for (to, from) in result.iter_mut().zip(value.iter()) {
*to = *from;
}
result
impl<T: Grid<bool>> TryFrom<&Window<'_, bool, T>> for Bitmap {
type Error = ();
fn try_from(value: &Window<bool, T>) -> Result<Self, Self::Error> {
let mut result = Self::new(value.width(), value.height()).ok_or(())?;
result.deref_assign(value);
Ok(result)
}
}
@ -316,7 +344,8 @@ pub enum LoadBitmapError {
#[cfg(test)]
mod tests {
use crate::{
Bitmap, DataRef, DisplayBitVec, LoadBitmapError, ValueGrid,
Bitmap, DataRef, DisplayBitVec, Grid, GridMut, LoadBitmapError,
ValueGrid,
};
#[test]
@ -367,7 +396,7 @@ mod tests {
#[should_panic]
fn out_of_bounds_x() {
let vec = Bitmap::new(8, 2).unwrap();
vec.get(8, 1);
_ = vec.get(8, 1);
}
#[test]

View file

@ -93,14 +93,14 @@ impl TryFrom<ByteGrid> for BrightnessGrid {
#[cfg(test)]
mod tests {
use crate::{Brightness, BrightnessGrid, DataRef, Grid, ValueGrid};
use crate::{Brightness, BrightnessGrid, DataRef, GridMut, ValueGrid};
#[test]
fn to_u8_grid() {
let mut grid = BrightnessGrid::new(2, 2);
grid.set(1, 0, Brightness::MIN);
grid.set(0, 1, Brightness::MAX);
let actual = ValueGrid::from(&grid);
let actual: ValueGrid<u8> = ValueGrid::from(&grid);
assert_eq!(actual.data_ref(), &[11, 0, 11, 11]);
}

View file

@ -1,5 +1,4 @@
use crate::{Grid, SetValueSeriesError, TryLoadValueGridError, ValueGrid};
use std::string::FromUtf8Error;
use crate::{CharGridMutExt, GridMut, ValueGrid};
/// A grid containing UTF-8 characters.
///
@ -58,151 +57,6 @@ impl CharGrid {
}
result
}
/// Copies a column from the grid as a String.
///
/// Returns [None] if x is out of bounds.
///
/// # Examples
///
/// ```
/// # use servicepoint::CharGrid;
/// let grid = CharGrid::from("ab\ncd");
/// let col = grid.get_col_str(0).unwrap(); // "ac"
/// ```
#[must_use]
pub fn get_col_str(&self, x: usize) -> Option<String> {
Some(String::from_iter(self.get_col(x)?))
}
/// Copies a row from the grid as a String.
///
/// Returns [None] if y is out of bounds.
///
/// # Examples
///
/// ```
/// # use servicepoint::CharGrid;
/// let grid = CharGrid::from("ab\ncd");
/// let row = grid.get_row_str(0).unwrap(); // "ab"
/// ```
#[must_use]
pub fn get_row_str(&self, y: usize) -> Option<String> {
Some(String::from_iter(self.get_row(y)?))
}
/// Overwrites a row in the grid with a str.
///
/// Returns [`SetValueSeriesError`] if y is out of bounds or `row` is not of the correct size.
///
/// # Examples
///
/// ```
/// # use servicepoint::CharGrid;
/// let mut grid = CharGrid::from("ab\ncd");
/// grid.set_row_str(0, "ef").unwrap();
/// ```
pub fn set_row_str(
&mut self,
y: usize,
value: &str,
) -> Result<(), SetValueSeriesError> {
let width = self.width();
let len = value.len();
if len > width {
return Err(SetValueSeriesError::InvalidLength {
actual: len,
expected: width,
});
}
let height = self.height();
if y >= height {
return Err(SetValueSeriesError::OutOfBounds {
index: y,
size: height,
});
}
let chars = value.chars().take(width);
for (x, c) in chars.enumerate() {
self.set(x, y, c);
}
Ok(())
}
/// Overwrites a column in the grid with a str.
///
/// Returns [`SetValueSeriesError`] if y is out of bounds or `row` is not of the correct size.
///
/// # Examples
///
/// ```
/// # use servicepoint::CharGrid;
/// let mut grid = CharGrid::from("ab\ncd");
/// grid.set_col_str(0, "ef").unwrap();
/// ```
pub fn set_col_str(
&mut self,
x: usize,
value: &str,
) -> Result<(), SetValueSeriesError> {
let height = self.height();
let len = value.len();
if len > height {
return Err(SetValueSeriesError::InvalidLength {
actual: len,
expected: height,
});
}
let width = self.width();
if x >= width {
return Err(SetValueSeriesError::OutOfBounds {
index: x,
size: width,
});
}
let chars = value.chars().take(height);
for (y, c) in chars.enumerate() {
self.set(x, y, c);
}
Ok(())
}
/// Loads a [`CharGrid`] with the specified dimensions from the provided UTF-8 bytes.
///
/// returns: [`CharGrid`] that contains the provided data, or [`FromUtf8Error`] if the data is invalid.
///
/// # Examples
///
/// ```
/// # use servicepoint::CharGrid;
/// let grid = CharGrid::load_utf8(2, 2, [97u8, 98, 99, 100].to_vec());
/// ```
pub fn load_utf8(
width: usize,
height: usize,
bytes: Vec<u8>,
) -> Result<CharGrid, LoadUtf8Error> {
let s: Vec<char> = String::from_utf8(bytes)?.chars().collect();
CharGrid::load(width, height, &s).ok_or(LoadUtf8Error::TryLoadError(
TryLoadValueGridError::InvalidDimensions,
))
}
}
#[derive(Debug, thiserror::Error)]
pub enum LoadUtf8Error {
#[error(transparent)]
FromUtf8Error(#[from] FromUtf8Error),
#[error(transparent)]
TryLoadError(#[from] TryLoadValueGridError),
}
impl From<&str> for CharGrid {
@ -274,7 +128,7 @@ impl From<&CharGrid> for Vec<u8> {
/// let grid = CharGrid::from("ab\ncd");
/// let height = grid.height();
/// let width = grid.width();
/// let grid = CharGrid::load_utf8(width, height, grid.into());
/// let bytes = Vec::<u8>::from(grid);
/// ```
fn from(value: &CharGrid) -> Self {
value.iter().collect::<String>().into_bytes()
@ -291,31 +145,7 @@ impl From<CharGrid> for Vec<u8> {
#[cfg(test)]
mod test {
use super::*;
#[test]
fn col_str() {
let mut grid = CharGrid::new(2, 3);
assert_eq!(grid.get_col_str(2), None);
assert_eq!(grid.get_col_str(1), Some(String::from("\0\0\0")));
assert_eq!(grid.set_col_str(1, "abc"), Ok(()));
assert_eq!(grid.get_col_str(1), Some(String::from("abc")));
}
#[test]
fn row_str() {
let mut grid = CharGrid::new(2, 3);
assert_eq!(grid.get_row_str(3), None);
assert_eq!(grid.get_row_str(1), Some(String::from("\0\0")));
assert_eq!(
grid.set_row_str(1, "abc"),
Err(SetValueSeriesError::InvalidLength {
expected: 2,
actual: 3
})
);
assert_eq!(grid.set_row_str(1, "ab"), Ok(()));
assert_eq!(grid.get_row_str(1), Some(String::from("ab")));
}
use crate::Grid;
#[test]
fn str_to_char_grid() {
// conversion with .to_string() covers one more line
@ -330,8 +160,15 @@ mod test {
fn round_trip_bytes() {
let grid = CharGrid::from("Hello\0\nWorld!\n...\0\0\0");
let bytes: Vec<u8> = grid.clone().into();
let copy =
CharGrid::load_utf8(grid.width(), grid.height(), bytes).unwrap();
let copy = CharGrid::load(
grid.width(),
grid.height(),
&String::from_utf8(bytes)
.unwrap()
.chars()
.collect::<Vec<_>>(),
)
.unwrap();
assert_eq!(grid, copy);
}

View file

@ -0,0 +1,173 @@
use crate::{Grid, GridMut, SetValueSeriesError};
/// Extension methods for any [`Grid<char>`]
pub trait CharGridExt {
/// Copies a column from the grid as a String.
///
/// Returns [None] if x is out of bounds.
///
/// # Examples
///
/// ```
/// # use servicepoint::{CharGrid, CharGridExt};
/// let grid = CharGrid::from("ab\ncd");
/// let col = grid.get_col_str(0).unwrap(); // "ac"
/// ```
#[must_use]
fn get_col_str(&self, x: usize) -> Option<String>;
/// Copies a row from the grid as a String.
///
/// Returns [None] if y is out of bounds.
///
/// # Examples
///
/// ```
/// # use servicepoint::{CharGrid, CharGridExt};
/// let grid = CharGrid::from("ab\ncd");
/// let row = grid.get_row_str(0).unwrap(); // "ab"
/// ```
#[must_use]
fn get_row_str(&self, y: usize) -> Option<String>;
}
/// Extension methods for any [`GridMut<char>`].
pub trait CharGridMutExt {
/// Overwrites a row in the grid with a str.
///
/// Returns [`SetValueSeriesError`] if y is out of bounds or `row` is not of the correct size.
///
/// # Examples
///
/// ```
/// # use servicepoint::{CharGrid, CharGridMutExt};
/// let mut grid = CharGrid::from("ab\ncd");
/// grid.set_row_str(0, "ef").unwrap();
/// ```
fn set_row_str(
&mut self,
y: usize,
value: &str,
) -> Result<(), SetValueSeriesError>;
/// Overwrites a column in the grid with a str.
///
/// Returns [`SetValueSeriesError`] if y is out of bounds or `row` is not of the correct size.
///
/// # Examples
///
/// ```
/// # use servicepoint::{CharGrid, CharGridMutExt};
/// let mut grid = CharGrid::from("ab\ncd");
/// grid.set_col_str(0, "ef").unwrap();
/// ```
fn set_col_str(
&mut self,
x: usize,
value: &str,
) -> Result<(), SetValueSeriesError>;
}
impl<G: Grid<char>> CharGridExt for G {
fn get_col_str(&self, x: usize) -> Option<String> {
Some(String::from_iter(self.get_col(x)?))
}
fn get_row_str(&self, y: usize) -> Option<String> {
Some(String::from_iter(self.get_row(y)?))
}
}
impl<G: GridMut<char>> CharGridMutExt for G {
fn set_row_str(
&mut self,
y: usize,
value: &str,
) -> Result<(), SetValueSeriesError> {
let width = self.width();
let len = value.len();
if len > width {
return Err(SetValueSeriesError::InvalidLength {
actual: len,
expected: width,
});
}
let height = self.height();
if y >= height {
return Err(SetValueSeriesError::OutOfBounds {
index: y,
size: height,
});
}
let chars = value.chars().take(width);
for (x, c) in chars.enumerate() {
self.set(x, y, c);
}
Ok(())
}
fn set_col_str(
&mut self,
x: usize,
value: &str,
) -> Result<(), SetValueSeriesError> {
let height = self.height();
let len = value.len();
if len > height {
return Err(SetValueSeriesError::InvalidLength {
actual: len,
expected: height,
});
}
let width = self.width();
if x >= width {
return Err(SetValueSeriesError::OutOfBounds {
index: x,
size: width,
});
}
let chars = value.chars().take(height);
for (y, c) in chars.enumerate() {
self.set(x, y, c);
}
Ok(())
}
}
#[cfg(test)]
mod test {
use crate::{CharGrid, CharGridExt, CharGridMutExt, SetValueSeriesError};
#[test]
fn col_str() {
let mut grid = CharGrid::new(2, 3);
assert_eq!(grid.get_col_str(2), None);
assert_eq!(grid.get_col_str(1), Some(String::from("\0\0\0")));
assert_eq!(grid.set_col_str(1, "abc"), Ok(()));
assert_eq!(grid.get_col_str(1), Some(String::from("abc")));
}
#[test]
fn row_str() {
let mut grid = CharGrid::new(2, 3);
assert_eq!(grid.get_row_str(3), None);
assert_eq!(grid.get_row_str(1), Some(String::from("\0\0")));
assert_eq!(
grid.set_row_str(1, "abc"),
Err(SetValueSeriesError::InvalidLength {
expected: 2,
actual: 3
})
);
assert_eq!(grid.set_row_str(1, "ab"), Ok(()));
assert_eq!(grid.get_row_str(1), Some(String::from("ab")));
}
}

View file

@ -1,4 +1,4 @@
use crate::{Grid, ValueGrid};
use crate::{GridMut, ValueGrid};
/// A grid containing codepage 437 characters.
///

View file

@ -1,16 +1,7 @@
/// A two-dimensional grid of `T`
pub trait Grid<T> {
/// Sets the value at the specified position
///
/// # Arguments
///
/// - `x` and `y`: position of the cell to read
///
/// # Panics
///
/// When accessing `x` or `y` out of bounds.
fn set(&mut self, x: usize, y: usize, value: T);
use crate::SetValueSeriesError;
/// A two-dimensional readonly grid of `T`
pub trait Grid<T> {
/// Get the current value at the specified position
///
/// # Arguments
@ -20,7 +11,12 @@ pub trait Grid<T> {
/// # Panics
///
/// When accessing `x` or `y` out of bounds.
fn get(&self, x: usize, y: usize) -> T;
#[must_use]
fn get(&self, x: usize, y: usize) -> T {
#[allow(clippy::panic, reason = "This is the version that panics - _optional does not")]
self.get_optional(x, y)
.unwrap_or_else(|| panic!("Cannot access index ({x}, {y}) because it is out of bounds for a grid of dimension {}x{}", self.width(), self.height()))
}
/// Get the current value at the specified position if the position is inside of bounds
///
@ -29,40 +25,19 @@ pub trait Grid<T> {
/// - `x` and `y`: position of the cell to read
///
/// returns: Value at position or None
fn get_optional(&self, x: usize, y: usize) -> Option<T> {
if self.is_in_bounds(x, y) {
Some(self.get(x, y))
} else {
None
}
}
/// Sets the value at the specified position if the position is inside of bounds
///
/// # Arguments
///
/// - `x` and `y`: position of the cell to read
///
/// returns: the old value or None
fn set_optional(&mut self, x: usize, y: usize, value: T) -> bool {
if self.is_in_bounds(x, y) {
self.set(x, y, value);
true
} else {
false
}
}
/// Sets all cells in the grid to the specified value
fn fill(&mut self, value: T);
#[must_use]
fn get_optional(&self, x: usize, y: usize) -> Option<T>;
/// the size in x-direction
#[must_use]
fn width(&self) -> usize;
/// the height in y-direction
#[must_use]
fn height(&self) -> usize;
/// Checks whether the specified signed position is in grid bounds
#[must_use]
fn is_in_bounds(&self, x: usize, y: usize) -> bool {
x < self.width() && y < self.height()
}
@ -78,4 +53,219 @@ pub trait Grid<T> {
let height = self.height();
assert!(y < height, "cannot access index [{x}, {y}] because y is outside of bounds [0..{height})");
}
/// Copies a row from the grid.
///
/// Returns [None] if y is out of bounds.
#[must_use]
fn get_row(&self, y: usize) -> Option<Vec<T>> {
if y >= self.height() {
return None;
}
let width = self.width();
let mut row = Vec::with_capacity(width);
for x in 0..width {
row.push(self.get(x, y));
}
Some(row)
}
/// Copies a column from the grid.
///
/// Returns [None] if x is out of bounds.
#[must_use]
fn get_col(&self, x: usize) -> Option<Vec<T>> {
if x >= self.width() {
return None;
}
let height = self.height();
let mut col = Vec::with_capacity(height);
for y in 0..height {
col.push(self.get(x, y));
}
Some(col)
}
}
/// A two-dimensional mutable grid of `T`
pub trait GridMut<T: Clone>: Grid<T> {
/// Sets the value at the specified position
///
/// # Arguments
///
/// - `x` and `y`: position of the cell to read
///
/// # Panics
///
/// When accessing `x` or `y` out of bounds.
fn set(&mut self, x: usize, y: usize, value: T) {
#[allow(
clippy::expect_used,
reason = "This is the version that panics - _optional does not"
)]
let worked = self.set_optional(x, y, value);
assert!(worked, "Cannot access index ({x}, {y}) because it is out of bounds for a grid of dimension {}x{}", self.width(), self.height());
}
/// Sets the value at the specified position if the position is inside of bounds
///
/// # Arguments
///
/// - `x` and `y`: position of the cell to read
///
/// returns: true if the value has been set
#[must_use]
fn set_optional(&mut self, x: usize, y: usize, value: T) -> bool;
/// Sets all cells in the grid to the specified value
fn fill(&mut self, value: T);
/// Fills the grid with the values from the provided grid.
///
/// The grids have to match in size exactly.
///
/// For 1D slices the equivalent would be `*slice = other_slice`.
fn deref_assign<O: Grid<T>>(&mut self, other: &O) {
let width = self.width();
let height = self.height();
assert_eq!(
width,
other.width(),
"Cannot assign grid of width {} to a window of width {}",
other.width(),
self.width()
);
assert_eq!(
height,
other.height(),
"Cannot assign grid of height {} to a height of width {}",
other.height(),
self.height()
);
for y in 0..height {
for x in 0..width {
self.set(x, y, other.get(x, y));
}
}
}
/// Overwrites a column in the grid.
///
/// Returns [Err] if x is out of bounds or `col` is not of the correct size.
fn set_col(
&mut self,
x: usize,
col: &[T],
) -> Result<(), SetValueSeriesError> {
let height = self.height();
if col.len() != height {
return Err(SetValueSeriesError::InvalidLength {
expected: height,
actual: col.len(),
});
}
if x >= self.width() {
return Err(SetValueSeriesError::OutOfBounds {
size: self.width(),
index: x,
});
}
for (y, item) in col.iter().enumerate().take(height) {
self.set(x, y, item.clone());
}
Ok(())
}
/// Overwrites a row in the grid.
///
/// Returns [Err] if y is out of bounds or `row` is not of the correct size.
fn set_row(
&mut self,
y: usize,
row: &[T],
) -> Result<(), SetValueSeriesError> {
let width = self.width();
if row.len() != width {
return Err(SetValueSeriesError::InvalidLength {
expected: width,
actual: row.len(),
});
}
if y >= self.height() {
return Err(SetValueSeriesError::OutOfBounds {
size: self.height(),
index: y,
});
}
for (x, item) in row.iter().enumerate().take(width) {
self.set(x, y, item.clone());
}
Ok(())
}
}
#[cfg(test)]
mod test {
use crate::{DataRef, Grid, GridMut, SetValueSeriesError, ValueGrid};
#[test]
fn optional() {
let mut grid = ValueGrid::load(2, 2, &[0, 1, 2, 3]).unwrap();
assert!(grid.set_optional(0, 0, 5));
assert!(!grid.set_optional(0, 8, 42));
assert_eq!(grid.data_ref(), [5, 1, 2, 3]);
assert_eq!(grid.get_optional(0, 0), Some(5));
assert_eq!(grid.get_optional(0, 8), None);
}
#[test]
fn col() {
let mut grid = ValueGrid::load(2, 3, &[0, 1, 2, 3, 4, 5]).unwrap();
assert_eq!(grid.get_col(0), Some(vec![0, 2, 4]));
assert_eq!(grid.get_col(1), Some(vec![1, 3, 5]));
assert_eq!(grid.get_col(2), None);
assert_eq!(grid.set_col(0, &[5, 7, 9]), Ok(()));
assert_eq!(
grid.set_col(2, &[5, 7, 9]),
Err(SetValueSeriesError::OutOfBounds { size: 2, index: 2 })
);
assert_eq!(
grid.set_col(0, &[5, 7]),
Err(SetValueSeriesError::InvalidLength {
expected: 3,
actual: 2
})
);
assert_eq!(grid.get_col(0), Some(vec![5, 7, 9]));
}
#[test]
fn row() {
let mut grid = ValueGrid::load(2, 3, &[0, 1, 2, 3, 4, 5]).unwrap();
assert_eq!(grid.get_row(0), Some(vec![0, 1]));
assert_eq!(grid.get_row(2), Some(vec![4, 5]));
assert_eq!(grid.get_row(3), None);
assert_eq!(grid.set_row(0, &[5, 7]), Ok(()));
assert_eq!(grid.get_row(0), Some(vec![5, 7]));
assert_eq!(
grid.set_row(3, &[5, 7]),
Err(SetValueSeriesError::OutOfBounds { size: 3, index: 3 })
);
assert_eq!(
grid.set_row(2, &[5, 7, 3]),
Err(SetValueSeriesError::InvalidLength {
expected: 2,
actual: 3
})
);
}
}

View file

@ -3,19 +3,63 @@ mod bitmap;
mod brightness_grid;
mod byte_grid;
mod char_grid;
mod char_grid_ext;
mod cp437_grid;
mod data_ref;
mod grid;
mod value_grid;
mod window;
pub use bit_vec::{bitvec, DisplayBitVec};
pub use bitmap::*;
pub use bitmap::{Bitmap, LoadBitmapError};
pub use brightness_grid::BrightnessGrid;
pub use byte_grid::ByteGrid;
pub use char_grid::CharGrid;
pub use cp437_grid::Cp437Grid;
pub use char_grid_ext::{CharGridExt, CharGridMutExt};
pub use cp437_grid::{Cp437Grid, InvalidCharError};
pub use data_ref::DataRef;
pub use grid::Grid;
pub use grid::{Grid, GridMut};
pub use value_grid::{
IterGridRows, SetValueSeriesError, TryLoadValueGridError, Value, ValueGrid,
SetValueSeriesError, TryLoadValueGridError, Value, ValueGrid,
};
pub use window::{Window, WindowMut};
use std::{
collections::Bound,
ops::{Range, RangeBounds},
};
pub(crate) fn absolute_bounds_to_abs_range(
bounds: impl RangeBounds<usize>,
len: usize,
) -> Option<Range<usize>> {
let start = match bounds.start_bound() {
Bound::Included(start) => *start,
Bound::Excluded(start) => start + 1,
Bound::Unbounded => 0,
};
let end = match bounds.end_bound() {
Bound::Included(end) => end + 1,
Bound::Excluded(end) => *end,
Bound::Unbounded => len,
};
if end > len {
return None;
}
Some(start..end)
}
pub(crate) fn relative_bounds_to_abs_range(
bounds: impl RangeBounds<usize>,
range: Range<usize>,
) -> Option<Range<usize>> {
let relative = absolute_bounds_to_abs_range(bounds, range.len())?;
let start = range.start + relative.start;
let end = range.start + relative.end;
if end > range.end {
return None;
}
Some(start..end)
}

View file

@ -1,7 +1,12 @@
use crate::{DataRef, Grid};
use inherent::inherent;
use std::fmt::Debug;
use std::slice::{Iter, IterMut};
use crate::{
containers::absolute_bounds_to_abs_range, DataRef, Grid, GridMut, Window,
WindowMut,
};
use std::{
fmt::Debug,
ops::RangeBounds,
slice::{Iter, IterMut},
};
/// A type that can be stored in a [`ValueGrid`], e.g. [char], [u8].
pub trait Value: Sized + Default + Copy + Clone + Debug {}
@ -139,14 +144,14 @@ impl<T: Value> ValueGrid<T> {
}
/// Iterate over all rows in [`ValueGrid`] top to bottom.
pub fn iter_rows(&self) -> IterGridRows<T> {
pub fn iter_rows(&self) -> impl Iterator<Item = Iter<'_, T>> {
IterGridRows { grid: self, row: 0 }
}
/// Returns an iterator that allows modifying each value.
///
/// The iterator yields all cells from top left to bottom right.
pub fn iter_mut(&mut self) -> IterMut<T> {
pub fn iter_mut(&mut self) -> IterMut<'_, T> {
self.data.iter_mut()
}
@ -218,88 +223,6 @@ impl<T: Value> ValueGrid<T> {
}
}
/// Copies a row from the grid.
///
/// Returns [None] if y is out of bounds.
#[must_use]
pub fn get_row(&self, y: usize) -> Option<Vec<T>> {
self.data
.chunks_exact(self.width())
.nth(y)
.map(<[T]>::to_vec)
}
/// Copies a column from the grid.
///
/// Returns [None] if x is out of bounds.
#[must_use]
pub fn get_col(&self, x: usize) -> Option<Vec<T>> {
self.data
.chunks_exact(self.width())
.map(|row| row.get(x).copied())
.collect()
}
/// Overwrites a column in the grid.
///
/// Returns [Err] if x is out of bounds or `col` is not of the correct size.
pub fn set_col(
&mut self,
x: usize,
col: &[T],
) -> Result<(), SetValueSeriesError> {
if col.len() != self.height() {
return Err(SetValueSeriesError::InvalidLength {
expected: self.height(),
actual: col.len(),
});
}
let width = self.width();
if self
.data
.chunks_exact_mut(width)
.zip(col.iter())
.map(|(row, column_value)| {
row.get_mut(x).map(move |cell| *cell = *column_value)
})
.all(|cell| cell.is_some())
{
Ok(())
} else {
Err(SetValueSeriesError::OutOfBounds {
index: x,
size: width,
})
}
}
/// Overwrites a row in the grid.
///
/// Returns [Err] if y is out of bounds or `row` is not of the correct size.
pub fn set_row(
&mut self,
y: usize,
row: &[T],
) -> Result<(), SetValueSeriesError> {
let width = self.width();
if row.len() != width {
return Err(SetValueSeriesError::InvalidLength {
expected: width,
actual: row.len(),
});
}
let Some(chunk) = self.data.chunks_exact_mut(width).nth(y) else {
return Err(SetValueSeriesError::OutOfBounds {
size: self.height(),
index: y,
});
};
chunk.copy_from_slice(row);
Ok(())
}
/// Enumerates all values in the grid.
pub fn enumerate(
&self,
@ -310,6 +233,33 @@ impl<T: Value> ValueGrid<T> {
row: 0,
}
}
#[must_use]
/// Creates a window into the grid.
///
/// Returns None in case the window does not fit.
pub fn window(
&self,
xs: impl RangeBounds<usize>,
ys: impl RangeBounds<usize>,
) -> Option<Window<'_, T, Self>> {
let xs = absolute_bounds_to_abs_range(xs, self.width)?;
let ys = absolute_bounds_to_abs_range(ys, self.height)?;
Window::new(self, xs, ys)
}
/// Creates a mutable window into the grid.
///
/// Returns None in case the window does not fit.
pub fn window_mut(
&mut self,
xs: impl RangeBounds<usize>,
ys: impl RangeBounds<usize>,
) -> Option<WindowMut<'_, T, Self>> {
let xs = absolute_bounds_to_abs_range(xs, self.width)?;
let ys = absolute_bounds_to_abs_range(ys, self.height)?;
WindowMut::new(self, xs, ys)
}
}
/// Errors that can occur when loading a grid
@ -320,8 +270,25 @@ pub enum TryLoadValueGridError {
InvalidDimensions,
}
#[inherent]
impl<T: Value> Grid<T> for ValueGrid<T> {
fn get_optional(&self, x: usize, y: usize) -> Option<T> {
if self.is_in_bounds(x, y) {
Some(self.data[x + y * self.width])
} else {
None
}
}
fn width(&self) -> usize {
self.width
}
fn height(&self) -> usize {
self.height
}
}
impl<T: Value> GridMut<T> for ValueGrid<T> {
/// Sets the value of the cell at the specified position in the grid.
///
/// # Arguments
@ -332,39 +299,18 @@ impl<T: Value> Grid<T> for ValueGrid<T> {
/// # Panics
///
/// When accessing `x` or `y` out of bounds.
#[allow(unused, reason = "False positive because of #[inherent]")]
fn set(&mut self, x: usize, y: usize, value: T) {
self.assert_in_bounds(x, y);
self.data[x + y * self.width] = value;
fn set_optional(&mut self, x: usize, y: usize, value: T) -> bool {
if self.is_in_bounds(x, y) {
self.data[x + y * self.width] = value;
true
} else {
false
}
}
/// Gets the current value at the specified position.
///
/// # Arguments
///
/// - `x` and `y`: position of the cell to read
///
/// # Panics
///
/// When accessing `x` or `y` out of bounds.
#[allow(unused, reason = "False positive because of #[inherent]")]
fn get(&self, x: usize, y: usize) -> T {
self.assert_in_bounds(x, y);
self.data[x + y * self.width]
}
#[allow(unused, reason = "False positive because of #[inherent]")]
fn fill(&mut self, value: T) {
self.data.fill(value);
}
fn width(&self) -> usize {
self.width
}
fn height(&self) -> usize {
self.height
}
}
impl<T: Value> DataRef<T> for ValueGrid<T> {
@ -393,9 +339,19 @@ impl<T: Value> From<&ValueGrid<T>> for Vec<T> {
}
}
impl<T: Value, G: Grid<T>> From<&G> for ValueGrid<T> {
fn from(grid: &G) -> Self {
let width = grid.width();
let height = grid.height();
let mut result = Self::new(width, height);
result.deref_assign(grid);
result
}
}
/// An iterator iver the rows in a [`ValueGrid`]
#[must_use]
pub struct IterGridRows<'t, T: Value> {
struct IterGridRows<'t, T: Value> {
grid: &'t ValueGrid<T>,
row: usize,
}
@ -416,7 +372,7 @@ impl<'t, T: Value> Iterator for IterGridRows<'t, T> {
}
}
pub struct EnumerateGrid<'t, T: Value> {
struct EnumerateGrid<'t, T: Value> {
grid: &'t ValueGrid<T>,
row: usize,
column: usize,
@ -443,7 +399,7 @@ impl<T: Value> Iterator for EnumerateGrid<'_, T> {
#[cfg(test)]
mod tests {
use crate::{SetValueSeriesError, ValueGrid, *};
use crate::{DataRef, Grid, GridMut, ValueGrid};
#[test]
fn fill() {
@ -540,7 +496,7 @@ mod tests {
#[should_panic]
fn out_of_bounds_y() {
let vec = ValueGrid::load(2, 2, &[0, 1, 2, 3]).unwrap();
vec.get(1, 2);
_ = vec.get(1, 2);
}
#[test]
@ -560,59 +516,6 @@ mod tests {
assert_eq!(Some(&mut 8), vec.get_ref_mut_optional(2, 2));
}
#[test]
fn optional() {
let mut grid = ValueGrid::load(2, 2, &[0, 1, 2, 3]).unwrap();
grid.set_optional(0, 0, 5);
grid.set_optional(0, 8, 42);
assert_eq!(grid.data, [5, 1, 2, 3]);
assert_eq!(grid.get_optional(0, 0), Some(5));
assert_eq!(grid.get_optional(0, 8), None);
}
#[test]
fn col() {
let mut grid = ValueGrid::load(2, 3, &[0, 1, 2, 3, 4, 5]).unwrap();
assert_eq!(grid.get_col(0), Some(vec![0, 2, 4]));
assert_eq!(grid.get_col(1), Some(vec![1, 3, 5]));
assert_eq!(grid.get_col(2), None);
assert_eq!(grid.set_col(0, &[5, 7, 9]), Ok(()));
assert_eq!(
grid.set_col(2, &[5, 7, 9]),
Err(SetValueSeriesError::OutOfBounds { size: 2, index: 2 })
);
assert_eq!(
grid.set_col(0, &[5, 7]),
Err(SetValueSeriesError::InvalidLength {
expected: 3,
actual: 2
})
);
assert_eq!(grid.get_col(0), Some(vec![5, 7, 9]));
}
#[test]
fn row() {
let mut grid = ValueGrid::load(2, 3, &[0, 1, 2, 3, 4, 5]).unwrap();
assert_eq!(grid.get_row(0), Some(vec![0, 1]));
assert_eq!(grid.get_row(2), Some(vec![4, 5]));
assert_eq!(grid.get_row(3), None);
assert_eq!(grid.set_row(0, &[5, 7]), Ok(()));
assert_eq!(grid.get_row(0), Some(vec![5, 7]));
assert_eq!(
grid.set_row(3, &[5, 7]),
Err(SetValueSeriesError::OutOfBounds { size: 3, index: 3 })
);
assert_eq!(
grid.set_row(2, &[5, 7, 3]),
Err(SetValueSeriesError::InvalidLength {
expected: 2,
actual: 3
})
);
}
#[test]
fn wrap() {
let grid = ValueGrid::from_vec(2, vec![0, 1, 2, 3, 4, 5]).unwrap();

404
src/containers/window.rs Normal file
View file

@ -0,0 +1,404 @@
use crate::{
containers::{absolute_bounds_to_abs_range, relative_bounds_to_abs_range},
Grid, GridMut,
};
use std::{
marker::PhantomData,
ops::{Range, RangeBounds},
};
macro_rules! define_window {
($name:ident, $grid:ty) => {
/// A window into a 2D grid.
///
/// All operations are done directly on the contained reference,
/// but translated to where the window is.
#[derive(Debug)]
pub struct $name<'t, TElement: Copy, TGrid: Grid<TElement>> {
grid: $grid,
xs: Range<usize>,
ys: Range<usize>,
phantom: PhantomData<TElement>,
}
impl<'t, TElement: Copy, TGrid: Grid<TElement>>
$name<'t, TElement, TGrid>
{
/// Create a new window into `grid`.
#[must_use]
pub fn new(
grid: $grid,
xs: impl RangeBounds<usize>,
ys: impl RangeBounds<usize>,
) -> Option<Self> {
let xs = absolute_bounds_to_abs_range(xs, grid.width())?;
let ys = absolute_bounds_to_abs_range(ys, grid.height())?;
Some(Self {
grid,
xs,
ys,
phantom: PhantomData::default(),
})
}
#[must_use]
/// Creates a window into the window.
///
/// Returns None in case the window does not fit.
pub fn window(
&self,
xs: impl RangeBounds<usize>,
ys: impl RangeBounds<usize>,
) -> Option<Window<'_, TElement, TGrid>> {
let xs = relative_bounds_to_abs_range(xs, self.xs.clone())?;
let ys = relative_bounds_to_abs_range(ys, self.ys.clone())?;
Window::new(self.grid, xs, ys)
}
/// Splits the window horizontally, returning windows to the left and right parts.
///
/// The right window fills the remaining width, which may be zero.
///
/// Returns None for out-of-bounds.
#[must_use]
pub fn split_horizontal(
&'t self,
left_width: usize,
) -> Option<(
Window<'t, TElement, TGrid>,
Window<'t, TElement, TGrid>,
)> {
assert!(left_width <= self.width());
let middle_abs = self.xs.start + left_width;
let left = Window::new(
self.grid,
self.xs.start..middle_abs,
self.ys.clone(),
)?;
let right = Window::new(
self.grid,
middle_abs..self.xs.end,
self.ys.clone(),
)?;
Some((left, right))
}
/// Splits the window vertically, returning windows to the top and bottom parts.
///
/// The bottom window fills the remaining height, which may be zero.
///
/// Returns None for out-of-bounds.
#[must_use]
pub fn split_vertical(
&'t self,
top_height: usize,
) -> Option<(
Window<'t, TElement, TGrid>,
Window<'t, TElement, TGrid>,
)> {
assert!(top_height <= self.height());
let middle_abs = self.ys.start + top_height;
let top = Window::new(
self.grid,
self.xs.clone(),
self.ys.start..middle_abs,
)?;
let bottom = Window::new(
self.grid,
self.xs.clone(),
middle_abs..self.ys.end,
)?;
Some((top, bottom))
}
}
impl<TElement: Copy, TGrid: Grid<TElement>> Grid<TElement>
for $name<'_, TElement, TGrid>
{
fn get_optional(&self, x: usize, y: usize) -> Option<TElement> {
if self.is_in_bounds(x, y) {
Some(self.grid.get(self.xs.start + x, self.ys.start + y))
} else {
None
}
}
fn width(&self) -> usize {
self.xs.len()
}
fn height(&self) -> usize {
self.ys.len()
}
}
};
}
define_window!(Window, &'t TGrid);
define_window!(WindowMut, &'t mut TGrid);
impl<TElement: Copy, TGrid: GridMut<TElement>> GridMut<TElement>
for WindowMut<'_, TElement, TGrid>
{
fn set_optional(&mut self, x: usize, y: usize, value: TElement) -> bool {
if self.is_in_bounds(x, y) {
self.grid.set(self.xs.start + x, self.ys.start + y, value);
true
} else {
false
}
}
fn fill(&mut self, value: TElement) {
for y in self.ys.clone() {
for x in self.xs.clone() {
self.grid.set(x, y, value);
}
}
}
}
impl<TElement: Copy, TGrid: GridMut<TElement>> WindowMut<'_, TElement, TGrid> {
/// Creates a mutable window into the grid.
///
/// Returns None in case the window does not fit.
pub fn window_mut(
&mut self,
xs: impl RangeBounds<usize>,
ys: impl RangeBounds<usize>,
) -> Option<WindowMut<'_, TElement, TGrid>> {
let xs = relative_bounds_to_abs_range(xs, self.xs.clone())?;
let ys = relative_bounds_to_abs_range(ys, self.ys.clone())?;
WindowMut::new(self.grid, xs, ys)
}
/// Splits the window horizontally, returning windows to the left and right parts.
///
/// The right window fills the remaining width, which may be zero.
///
/// Returns None for out-of-bounds.
#[must_use]
pub fn split_horizontal_mut<'t>(
&'t mut self,
left_width: usize,
) -> Option<(
WindowMut<'t, TElement, TGrid>,
WindowMut<'t, TElement, TGrid>,
)> {
assert!(left_width <= self.width());
let (grid1, grid2): (&'t mut TGrid, &'t mut TGrid) =
unsafe { Self::duplicate_mutable_ref(self.grid) };
let middle_abs = self.xs.start + left_width;
let left =
WindowMut::new(grid1, self.xs.start..middle_abs, self.ys.clone())?;
let right =
WindowMut::new(grid2, middle_abs..self.xs.end, self.ys.clone())?;
Some((left, right))
}
/// Splits the window vertically, returning windows to the top and bottom parts.
///
/// The bottom window fills the remaining height, which may be zero.
///
/// Returns None for out-of-bounds.
#[must_use]
pub fn split_vertical_mut<'t>(
&'t mut self,
top_height: usize,
) -> Option<(
WindowMut<'t, TElement, TGrid>,
WindowMut<'t, TElement, TGrid>,
)> {
assert!(top_height <= self.height());
let (grid1, grid2): (&'t mut TGrid, &'t mut TGrid) =
unsafe { Self::duplicate_mutable_ref(self.grid) };
let middle_abs = self.ys.start + top_height;
let top = WindowMut::<'t>::new(
grid1,
self.xs.clone(),
self.ys.start..middle_abs,
)?;
let bottom = WindowMut::<'t>::new(
grid2,
self.xs.clone(),
middle_abs..self.ys.end,
)?;
Some((top, bottom))
}
/// SAFETY: the returned windows do not alias
/// Does not work if the grid uses the same memory location for multiple cells internally.
/// That means for e.g. a Bitmap, middle must be byte aligned or bit refs only used with the alias flag on.
unsafe fn duplicate_mutable_ref<T>(it: &mut T) -> (&mut T, &mut T) {
let mut ptr = std::ptr::NonNull::from(it);
unsafe { (ptr.as_mut(), ptr.as_mut()) }
}
}
#[cfg(test)]
mod tests {
use super::{Window, WindowMut};
use crate::{Bitmap, ByteGrid, CharGrid, DataRef, Grid, GridMut};
#[test]
fn grid_view_bitmap() {
let mut bitmap = Bitmap::new(8, 4).unwrap();
// non-byte-aligned views work
let mut view = bitmap.window_mut(3..7, 1..3).unwrap();
view.fill(true);
assert_eq!(bitmap.data_ref(), &[0, 30, 30, 0]);
assert_eq!(bitmap.set_optional(99, 99, false), false);
// full size view works
bitmap.window(0..8, 0..4).unwrap();
// zero size view works
assert!(Window::new(&mut bitmap, 1..4, 2..2).is_some());
assert!(WindowMut::new(&mut bitmap, 1..1, 2..3)
.is_some_and(|w| w.get_optional(0, 0).is_none()));
// oob does not work
assert!(Window::new(&mut bitmap, 30..33, 43..44).is_none());
assert!(WindowMut::new(&mut bitmap, 0..9, 0..1).is_none());
assert!(Window::new(&mut bitmap, 0..1, 0..5).is_none());
}
#[test]
fn grid_view_char_grid() {
let mut grid = CharGrid::new(3, 4);
grid.fill(' ');
let mut view = grid.window_mut(1..2, 1..4).unwrap();
view.fill('#');
view.set(0, 0, '!');
assert_eq!(
grid.data_ref(),
&[' ', ' ', ' ', ' ', '!', ' ', ' ', '#', ' ', ' ', '#', ' ']
);
// full size view works
_ = grid.window(0..3, 0..4).unwrap();
// zero size view works
assert!(grid
.window(1..3, 2..2)
.is_some_and(|w| w.get_optional(0, 0).is_none()));
assert!(grid.window(1..1, 2..3).is_some());
}
#[test]
fn round_trip_bitmap() {
let bitmap = Bitmap::new(8, 4).unwrap();
let non_aligned = bitmap.window(3..7, 1..3).unwrap();
assert_eq!(Bitmap::try_from(&non_aligned), Err(()));
let aligned = bitmap.window(0..8, 1..3).unwrap();
assert!(matches!(Bitmap::try_from(&aligned), Ok(_)));
}
#[test]
fn split_vertical() {
let grid = ByteGrid::new(5, 4);
let window = grid.window(0..grid.width(), 0..grid.height()).unwrap();
let (left, right) = window.split_vertical(3).unwrap();
assert_eq!(3, left.height());
assert_eq!(1, right.height());
assert_eq!(5, left.width());
assert_eq!(5, right.width())
}
#[test]
fn split_horizontal() {
let grid = ByteGrid::new(4, 5);
let window = grid.window(.., ..).unwrap();
let (top, bottom) = window.split_horizontal(3).unwrap();
assert_eq!(3, top.width());
assert_eq!(1, bottom.width());
assert_eq!(5, top.height());
assert_eq!(5, bottom.height())
}
#[test]
fn window_in_window() {
let mut grid = ByteGrid::new(6, 7);
grid.fill(1);
let mut w1 = grid
.window_mut(1..grid.width() - 1, 1..grid.height() - 1)
.unwrap();
w1.fill(2);
let w1_1 = w1.window(.., ..).unwrap();
assert_eq!(w1_1.get(0, 0), 2);
assert!(matches!(w1.window(.., 0..=w1.height()), None));
let mut w2 = w1
.window_mut(1..w1.width() - 1, 1..w1.height() - 1)
.unwrap();
w2.fill(3);
// zero-sized
let mut w3 = w2
.window_mut(1..w2.width() - 1, 1..w2.height() - 1)
.unwrap();
w3.fill(4);
#[rustfmt::skip]
assert_eq!(
grid.data_ref(),
&[
1, 1, 1, 1, 1, 1,
1, 2, 2, 2, 2, 1,
1, 2, 3, 3, 2, 1,
1, 2, 3, 3, 2, 1,
1, 2, 3, 3, 2, 1,
1, 2, 2, 2, 2, 1,
1, 1, 1, 1, 1, 1
]
);
}
#[test]
fn width_height() {
let grid = ByteGrid::new(4, 4);
let w1 = grid.window(0.., ..grid.height()).unwrap();
assert_eq!(grid.width(), w1.width());
assert_eq!(grid.height(), w1.height());
let w2 = w1.window(.., 0..w1.height()).unwrap();
assert_eq!(grid.width(), w2.width());
assert_eq!(grid.height(), w2.height());
}
#[test]
fn split_mut() {
let mut grid = ByteGrid::new(5, 5);
grid.fill(1);
let mut win = grid.window_mut(.., ..).unwrap();
let (mut top, mut bottom) = win.split_vertical_mut(2).unwrap();
let (mut left, mut right) = bottom.split_horizontal_mut(2).unwrap();
top.fill(2);
left.fill(3);
right.fill(4);
let grid2 = ByteGrid::from(&win.window(1..4, 1..4).unwrap());
assert_eq!(grid2.data_ref(), &[2, 2, 2, 3, 4, 4, 3, 4, 4,]);
assert_eq!(
grid.data_ref(),
&[
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 4, 3, 3, 4, 4, 4, 3,
3, 4, 4, 4,
]
);
}
}