add Window

This commit is contained in:
Vinzenz Schroeter 2025-07-05 14:30:21 +02:00
parent c9ad4e3357
commit 260ae6a1a0
14 changed files with 277 additions and 224 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ use crate::{
command_code::{CommandCode, InvalidCommandCodeError},
commands::errors::{TryFromPacketError, TryIntoPacketError},
compression::{compress, decompress, CompressionError},
Bitmap, CompressionCode, DataRef, Grid, Header, Origin, Packet, Pixels,
Bitmap, CompressionCode, DataRef, Header, Origin, Packet, Pixels,
TypedCommand, TILE_SIZE,
};
@ -304,7 +304,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(),

View file

@ -1,6 +1,6 @@
use crate::{
DataRef, DisplayBitVec, Grid, Payload, ValueGrid, WindowMut, PIXEL_HEIGHT,
PIXEL_WIDTH,
DataRef, DisplayBitVec, Grid, GridMut, Payload, ValueGrid, Window,
PIXEL_HEIGHT, PIXEL_WIDTH,
};
use ::bitvec::{order::Msb0, prelude::BitSlice, slice::IterMut};
use inherent::inherent;
@ -180,6 +180,26 @@ impl Bitmap {
#[inherent]
impl Grid<bool> for Bitmap {
#[must_use]
#[allow(unused, reason = "False positive because of #[inherent]")]
pub fn get(&self, x: usize, y: usize) -> bool {
self.assert_in_bounds(x, y);
self.bit_vec[x + y * self.width]
}
#[must_use]
pub fn width(&self) -> usize {
self.width
}
#[must_use]
pub fn height(&self) -> usize {
self.height
}
}
#[inherent]
impl GridMut<bool> for Bitmap {
/// Sets the value of the specified position in the [Bitmap].
///
/// # Arguments
@ -193,17 +213,11 @@ impl Grid<bool> for Bitmap {
///
/// 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) {
pub 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]
}
/// Sets the state of all pixels in the [Bitmap].
///
/// # Arguments
@ -211,17 +225,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) {
pub 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 {
@ -263,10 +269,10 @@ impl TryFrom<&ValueGrid<bool>> for Bitmap {
}
}
impl<G: Grid<bool>> TryFrom<&WindowMut<'_, bool, G>> for Bitmap {
impl<G: Grid<bool>> TryFrom<&Window<'_, bool, G>> for Bitmap {
type Error = ();
fn try_from(value: &WindowMut<'_, bool, G>) -> Result<Self, Self::Error> {
fn try_from(value: &Window<'_, bool, G>) -> Result<Self, Self::Error> {
let mut result = Self::new(value.width(), value.height()).ok_or(())?;
for y in 0..value.height() {
for x in 0..value.width() {
@ -380,7 +386,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

@ -1,4 +1,4 @@
use crate::{Brightness, ByteGrid, Grid, ValueGrid};
use crate::{Brightness, ByteGrid, ValueGrid};
/// A grid containing brightness values.
///
@ -93,7 +93,7 @@ impl TryFrom<ByteGrid> for BrightnessGrid {
#[cfg(test)]
mod tests {
use crate::{Brightness, BrightnessGrid, DataRef, Grid, ValueGrid};
use crate::{Brightness, BrightnessGrid, DataRef, ValueGrid};
#[test]
fn to_u8_grid() {

View file

@ -1,4 +1,4 @@
use crate::{Grid, SetValueSeriesError, TryLoadValueGridError, ValueGrid};
use crate::{SetValueSeriesError, TryLoadValueGridError, ValueGrid};
use std::string::FromUtf8Error;
/// A grid containing UTF-8 characters.

View file

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

View file

@ -1,16 +1,5 @@
/// A two-dimensional grid of `T`
/// A two-dimensional readonly 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);
/// Get the current value at the specified position
///
/// # Arguments
@ -20,6 +9,7 @@ pub trait Grid<T> {
/// # Panics
///
/// When accessing `x` or `y` out of bounds.
#[must_use]
fn get(&self, x: usize, y: usize) -> T;
/// Get the current value at the specified position if the position is inside of bounds
@ -29,6 +19,7 @@ pub trait Grid<T> {
/// - `x` and `y`: position of the cell to read
///
/// returns: Value at position or None
#[must_use]
fn get_optional(&self, x: usize, y: usize) -> Option<T> {
if self.is_in_bounds(x, y) {
Some(self.get(x, y))
@ -37,32 +28,16 @@ pub trait Grid<T> {
}
}
/// 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);
/// 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()
}
@ -79,3 +54,37 @@ pub trait Grid<T> {
assert!(y < height, "cannot access index [{x}, {y}] because y is outside of bounds [0..{height})");
}
}
/// A two-dimensional mutable grid of `T`
pub trait GridMut<T>: 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);
/// 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 {
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);
}

View file

@ -1,128 +0,0 @@
use crate::Grid;
use std::marker::PhantomData;
#[derive(Debug)]
pub struct WindowMut<'t, TElement: Copy, TGrid: Grid<TElement>> {
grid: &'t mut TGrid,
x: usize,
y: usize,
width: usize,
height: usize,
phantom: PhantomData<TElement>,
}
impl<'t, TElement: Copy, TGrid: Grid<TElement>> WindowMut<'t, TElement, TGrid> {
#[must_use]
pub fn new(
grid: &'t mut TGrid,
x: usize,
y: usize,
width: usize,
height: usize,
) -> Option<Self> {
if width == 0 || height == 0 {
return None;
}
if !grid.is_in_bounds(x + width - 1, y + height - 1) {
return None;
}
Some(Self {
grid,
x,
y,
width,
height,
phantom: PhantomData,
})
}
}
impl<TElement: Copy, TGrid: Grid<TElement>> Grid<TElement>
for WindowMut<'_, TElement, TGrid>
{
fn set(&mut self, x: usize, y: usize, value: TElement) {
self.grid.set(x + self.x, y + self.y, value)
}
fn get(&self, x: usize, y: usize) -> TElement {
self.grid.get(x + self.x, y + self.y)
}
fn fill(&mut self, value: TElement) {
for y in self.y..(self.y + self.height) {
for x in self.x..(self.x + self.width) {
self.grid.set(x, y, value);
}
}
}
fn width(&self) -> usize {
self.width
}
fn height(&self) -> usize {
self.height
}
}
#[cfg(test)]
mod tests {
use crate::containers::grid_view::WindowMut;
use crate::{Bitmap, CharGrid, DataRef, Grid};
#[test]
fn test_grid_view_bitmap() {
let mut bitmap = Bitmap::new(8, 4).unwrap();
// non-byte-aligned views work
let mut view = WindowMut::new(&mut bitmap, 3, 1, 4, 2).unwrap();
view.fill(true);
assert_eq!(bitmap.data_ref(), &[0, 30, 30, 0]);
// full size view works
_ = WindowMut::new(&mut bitmap, 0, 0, 8, 4).unwrap();
// zero size view does not work
assert!(WindowMut::new(&mut bitmap, 1, 2, 3, 0).is_none());
assert!(WindowMut::new(&mut bitmap, 1, 2, 0, 1).is_none());
// oob does not work
assert!(WindowMut::new(&mut bitmap, 30, 43, 3, 1).is_none());
assert!(WindowMut::new(&mut bitmap, 0, 0, 9, 1).is_none());
assert!(WindowMut::new(&mut bitmap, 0, 0, 1, 5).is_none());
}
#[test]
fn test_grid_view_char_grid() {
let mut grid = CharGrid::new(3, 4);
grid.fill(' ');
let mut view = WindowMut::new(&mut grid, 1, 1, 1, 2).unwrap();
view.fill('#');
assert_eq!(
grid.data_ref(),
&[' ', ' ', ' ', ' ', '#', ' ', ' ', '#', ' ', ' ', ' ', ' ']
);
// full size view works
_ = WindowMut::new(&mut grid, 0, 0, 3, 4).unwrap();
// zero size view does not work
assert!(WindowMut::new(&mut grid, 1, 2, 2, 0).is_none());
assert!(WindowMut::new(&mut grid, 1, 2, 0, 1).is_none());
}
#[test]
fn round_trip_bitmap() {
let mut bitmap = Bitmap::new(8, 4).unwrap();
let non_aligned = WindowMut::new(&mut bitmap, 3, 1, 4, 2).unwrap();
assert_eq!(Bitmap::try_from(&non_aligned), Err(()));
let aligned = WindowMut::new(&mut bitmap, 0, 1, 8, 2).unwrap();
assert!(matches!(Bitmap::try_from(&aligned), Ok(_)));
}
}

View file

@ -6,8 +6,8 @@ mod char_grid;
mod cp437_grid;
mod data_ref;
mod grid;
mod grid_view;
mod value_grid;
mod window;
pub use bit_vec::{bitvec, DisplayBitVec};
pub use bitmap::*;
@ -16,8 +16,8 @@ pub use byte_grid::ByteGrid;
pub use char_grid::CharGrid;
pub use cp437_grid::Cp437Grid;
pub use data_ref::DataRef;
pub use grid::Grid;
pub use grid_view::WindowMut;
pub use grid::{Grid, GridMut};
pub use value_grid::{
IterGridRows, SetValueSeriesError, TryLoadValueGridError, Value, ValueGrid,
};
pub use window::{Window, WindowMut};

View file

@ -1,4 +1,4 @@
use crate::{DataRef, Grid, WindowMut};
use crate::{DataRef, Grid, GridMut, Window};
use inherent::inherent;
use std::fmt::Debug;
use std::slice::{Iter, IterMut};
@ -322,6 +322,35 @@ pub enum TryLoadValueGridError {
#[inherent]
impl<T: Value> Grid<T> for ValueGrid<T> {
/// 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.
#[must_use]
#[allow(unused, reason = "False positive because of #[inherent]")]
pub fn get(&self, x: usize, y: usize) -> T {
self.assert_in_bounds(x, y);
self.data[x + y * self.width]
}
#[must_use]
pub fn width(&self) -> usize {
self.width
}
#[must_use]
pub fn height(&self) -> usize {
self.height
}
}
#[inherent]
impl<T: Value> GridMut<T> for ValueGrid<T> {
/// Sets the value of the cell at the specified position in the grid.
///
/// # Arguments
@ -333,38 +362,15 @@ impl<T: Value> Grid<T> for ValueGrid<T> {
///
/// 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) {
pub fn set(&mut self, x: usize, y: usize, value: T) {
self.assert_in_bounds(x, y);
self.data[x + y * self.width] = value;
}
/// 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) {
pub 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> {
@ -441,8 +447,8 @@ impl<T: Value> Iterator for EnumerateGrid<'_, T> {
}
}
impl<E: Value> From<&WindowMut<'_, E, Self>> for ValueGrid<E> {
fn from(value: &WindowMut<'_, E, Self>) -> Self {
impl<E: Value> From<&Window<'_, E, Self>> for ValueGrid<E> {
fn from(value: &Window<'_, E, Self>) -> Self {
let mut result = Self::new(value.width(), value.height());
for y in 0..value.height() {
for x in 0..value.width() {
@ -552,7 +558,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]
@ -575,8 +581,8 @@ mod tests {
#[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!(grid.set_optional(0, 0, 5));
assert!(!grid.set_optional(0, 8, 42));
assert_eq!(grid.data, [5, 1, 2, 3]);
assert_eq!(grid.get_optional(0, 0), Some(5));

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

@ -0,0 +1,160 @@
use crate::{Grid, GridMut};
use std::marker::PhantomData;
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,
x: usize,
y: usize,
width: usize,
height: usize,
phantom: PhantomData<TElement>,
}
impl<'t, TElement: Copy, TGrid: Grid<TElement>>
$name<'t, TElement, TGrid>
{
/// Create a new window into `grid`.
#[must_use]
#[allow(unused, reason = "False positive because of #[inherent]")]
pub fn new(
grid: $grid,
x: usize,
y: usize,
width: usize,
height: usize,
) -> Option<Self> {
if width == 0 || height == 0 {
return None;
}
if !grid.is_in_bounds(x + width - 1, y + height - 1) {
return None;
}
Some(Self {
grid,
x,
y,
width,
height,
phantom: PhantomData,
})
}
}
#[inherent::inherent]
impl<TElement: Copy, TGrid: Grid<TElement>> Grid<TElement>
for $name<'_, TElement, TGrid>
{
#[must_use]
#[allow(unused, reason = "False positive because of #[inherent]")]
pub fn get(&self, x: usize, y: usize) -> TElement {
self.grid.get(x + self.x, y + self.y)
}
#[must_use]
#[allow(unused, reason = "False positive because of #[inherent]")]
pub fn width(&self) -> usize {
self.width
}
#[must_use]
#[allow(unused, reason = "False positive because of #[inherent]")]
pub fn height(&self) -> usize {
self.height
}
}
};
}
define_window!(Window, &'t TGrid);
define_window!(WindowMut, &'t mut TGrid);
#[inherent::inherent]
impl<TElement: Copy, TGrid: GridMut<TElement>> GridMut<TElement>
for WindowMut<'_, TElement, TGrid>
{
#[allow(unused, reason = "False positive because of #[inherent]")]
pub fn set(&mut self, x: usize, y: usize, value: TElement) {
self.grid.set(x + self.x, y + self.y, value);
}
#[allow(unused, reason = "False positive because of #[inherent]")]
pub fn fill(&mut self, value: TElement) {
for y in self.y..(self.y + self.height) {
for x in self.x..(self.x + self.width) {
self.grid.set(x, y, value);
}
}
}
}
#[cfg(test)]
mod tests {
use crate::containers::window::{Window, WindowMut};
use crate::{Bitmap, CharGrid, DataRef, GridMut};
#[test]
fn test_grid_view_bitmap() {
let mut bitmap = Bitmap::new(8, 4).unwrap();
// non-byte-aligned views work
let mut view = WindowMut::new(&mut bitmap, 3, 1, 4, 2).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
_ = Window::new(&mut bitmap, 0, 0, 8, 4).unwrap();
// zero size view does not work
assert!(Window::new(&mut bitmap, 1, 2, 3, 0).is_none());
assert!(WindowMut::new(&mut bitmap, 1, 2, 0, 1).is_none());
// oob does not work
assert!(Window::new(&mut bitmap, 30, 43, 3, 1).is_none());
assert!(WindowMut::new(&mut bitmap, 0, 0, 9, 1).is_none());
assert!(Window::new(&mut bitmap, 0, 0, 1, 5).is_none());
}
#[test]
fn test_grid_view_char_grid() {
let mut grid = CharGrid::new(3, 4);
grid.fill(' ');
let mut view = WindowMut::new(&mut grid, 1, 1, 1, 2).unwrap();
view.fill('#');
assert_eq!(
grid.data_ref(),
&[' ', ' ', ' ', ' ', '#', ' ', ' ', '#', ' ', ' ', ' ', ' ']
);
// full size view works
_ = Window::new(&mut grid, 0, 0, 3, 4).unwrap();
// zero size view does not work
assert!(Window::new(&mut grid, 1, 2, 2, 0).is_none());
assert!(Window::new(&mut grid, 1, 2, 0, 1).is_none());
}
#[test]
fn round_trip_bitmap() {
let mut bitmap = Bitmap::new(8, 4).unwrap();
let non_aligned = Window::new(&mut bitmap, 3, 1, 4, 2).unwrap();
assert_eq!(Bitmap::try_from(&non_aligned), Err(()));
let aligned = Window::new(&mut bitmap, 0, 1, 8, 2).unwrap();
assert!(matches!(Bitmap::try_from(&aligned), Ok(_)));
}
}