mod color_mapping;
mod fancy_panic;
use crate::{
draw_target::DrawTarget,
geometry::{Dimensions, OriginDimensions, Point, Size},
pixelcolor::{PixelColor, Rgb888, RgbColor},
primitives::{PointsIter, Rectangle},
Pixel,
};
pub use color_mapping::ColorMapping;
use core::{
fmt::{self, Write},
iter,
};
use fancy_panic::FancyPanic;
const SIZE: usize = 64;
const DISPLAY_AREA: Rectangle = Rectangle::new(Point::zero(), Size::new_equal(SIZE as u32));
#[derive(Clone)]
pub struct MockDisplay<C>
where
C: PixelColor,
{
pixels: [Option<C>; SIZE * SIZE],
allow_overdraw: bool,
allow_out_of_bounds_drawing: bool,
}
impl<C> MockDisplay<C>
where
C: PixelColor,
{
pub fn new() -> Self {
Self::default()
}
pub fn from_points<I>(points: I, color: C) -> Self
where
I: IntoIterator<Item = Point>,
{
let mut display = Self::new();
display.set_pixels(points, Some(color));
display
}
pub fn set_allow_out_of_bounds_drawing(&mut self, value: bool) {
self.allow_out_of_bounds_drawing = value;
}
pub fn set_allow_overdraw(&mut self, value: bool) {
self.allow_overdraw = value;
}
pub const fn get_pixel(&self, p: Point) -> Option<C> {
let Point { x, y } = p;
self.pixels[x as usize + y as usize * SIZE]
}
pub fn set_pixel(&mut self, point: Point, color: Option<C>) {
assert!(
point.x >= 0 && point.y >= 0 && point.x < SIZE as i32 && point.y < SIZE as i32,
"point must be inside display bounding box: {:?}",
point
);
let i = point.x + point.y * SIZE as i32;
self.pixels[i as usize] = color;
}
fn set_pixel_unchecked(&mut self, point: Point, color: Option<C>) {
let i = point.x + point.y * SIZE as i32;
self.pixels[i as usize] = color;
}
pub fn set_pixels(&mut self, points: impl IntoIterator<Item = Point>, color: Option<C>) {
for point in points {
self.set_pixel(point, color);
}
}
pub fn affected_area(&self) -> Rectangle {
let (tl, br) = self
.bounding_box()
.points()
.zip(self.pixels.iter())
.filter_map(|(point, color)| color.map(|_| point))
.fold(
(None, None),
|(tl, br): (Option<Point>, Option<Point>), point| {
(
tl.map(|tl| tl.component_min(point)).or(Some(point)),
br.map(|br| br.component_max(point)).or(Some(point)),
)
},
);
if let (Some(tl), Some(br)) = (tl, br) {
Rectangle::with_corners(tl, br)
} else {
Rectangle::zero()
}
}
fn affected_area_origin(&self) -> Rectangle {
self.affected_area()
.bottom_right()
.map(|bottom_right| Rectangle::with_corners(Point::zero(), bottom_right))
.unwrap_or_default()
}
pub fn draw_pixel(&mut self, point: Point, color: C) {
if !DISPLAY_AREA.contains(point) {
if !self.allow_out_of_bounds_drawing {
panic!(
"tried to draw pixel outside the display area (x: {}, y: {})",
point.x, point.y
);
} else {
return;
}
}
if !self.allow_overdraw && self.get_pixel(point).is_some() {
panic!("tried to draw pixel twice (x: {}, y: {})", point.x, point.y);
}
self.set_pixel_unchecked(point, Some(color));
}
pub fn swap_xy(&self) -> MockDisplay<C> {
let mut mirrored = MockDisplay::new();
for point in self.bounding_box().points() {
mirrored.set_pixel_unchecked(point, self.get_pixel(Point::new(point.y, point.x)));
}
mirrored
}
pub fn map<CT, F>(&self, f: F) -> MockDisplay<CT>
where
CT: PixelColor,
F: Fn(C) -> CT + Copy,
{
let mut target = MockDisplay::new();
for point in self.bounding_box().points() {
target.set_pixel_unchecked(point, self.get_pixel(point).map(f))
}
target
}
pub fn diff(&self, other: &MockDisplay<C>) -> MockDisplay<Rgb888> {
let mut display = MockDisplay::new();
for point in display.bounding_box().points() {
let self_color = self.get_pixel(point);
let other_color = other.get_pixel(point);
let diff_color = match (self_color, other_color) {
(Some(_), None) => Some(Rgb888::GREEN),
(None, Some(_)) => Some(Rgb888::RED),
(Some(s), Some(o)) if s != o => Some(Rgb888::BLUE),
_ => None,
};
display.set_pixel_unchecked(point, diff_color);
}
display
}
}
impl<C: PixelColor> PartialEq for MockDisplay<C> {
fn eq(&self, other: &Self) -> bool {
self.pixels.iter().eq(other.pixels.iter())
}
}
impl<C> MockDisplay<C>
where
C: PixelColor + ColorMapping,
{
pub fn from_pattern(pattern: &[&str]) -> MockDisplay<C> {
let pattern_width = pattern.first().map_or(0, |row| row.len());
let pattern_height = pattern.len();
assert!(
pattern_width <= SIZE,
"Test pattern must not be wider than {} columns",
SIZE
);
assert!(
pattern_height <= SIZE,
"Test pattern must not be taller than {} rows",
SIZE
);
for (row_idx, row) in pattern.iter().enumerate() {
assert_eq!(
row.len(),
pattern_width,
"Row #{} is {} characters wide (must be {} characters to match previous rows)",
row_idx + 1,
row.len(),
pattern_width
);
}
let pattern_colors = pattern
.iter()
.flat_map(|row| {
row.chars()
.map(|c| match c {
' ' => None,
_ => Some(C::char_to_color(c)),
})
.chain(iter::repeat(None))
.take(SIZE)
})
.chain(iter::repeat(None))
.take(SIZE * SIZE);
let mut display = MockDisplay::new();
for (i, color) in pattern_colors.enumerate() {
display.pixels[i] = color;
}
display
}
#[track_caller]
pub fn assert_eq(&self, other: &MockDisplay<C>) {
if !self.eq(other) {
if option_env!("EG_FANCY_PANIC") == Some("1") {
let fancy_panic = FancyPanic::new(self, other, 30);
panic!("\n{}", fancy_panic);
} else {
panic!("\ndisplay\n{:?}\nexpected\n{:?}", self, other);
}
}
}
#[track_caller]
pub fn assert_eq_with_message<F>(&self, other: &MockDisplay<C>, msg: F)
where
F: Fn(&mut fmt::Formatter<'_>) -> fmt::Result,
{
if !self.eq(other) {
if option_env!("EG_FANCY_PANIC") == Some("1") {
let fancy_panic = FancyPanic::new(self, other, 30);
panic!("\n{}\n\n{}", MessageWrapper(msg), fancy_panic);
} else {
panic!(
"\n{}\n\ndisplay:\n{:?}\nexpected:\n{:?}",
MessageWrapper(msg),
self,
other
);
}
}
}
#[track_caller]
pub fn assert_pattern(&self, pattern: &[&str]) {
let other = MockDisplay::<C>::from_pattern(pattern);
self.assert_eq(&other);
}
#[track_caller]
pub fn assert_pattern_with_message<F>(&self, pattern: &[&str], msg: F)
where
F: Fn(&mut fmt::Formatter<'_>) -> fmt::Result,
{
let other = MockDisplay::<C>::from_pattern(pattern);
self.assert_eq_with_message(&other, msg);
}
}
impl<C> Default for MockDisplay<C>
where
C: PixelColor,
{
fn default() -> Self {
Self {
pixels: [None; SIZE * SIZE],
allow_overdraw: false,
allow_out_of_bounds_drawing: false,
}
}
}
impl<C> fmt::Debug for MockDisplay<C>
where
C: PixelColor + ColorMapping,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let empty_rows = self
.pixels
.rchunks(SIZE)
.take_while(|row| row.iter().all(Option::is_none))
.count();
writeln!(f, "MockDisplay[")?;
for row in self.pixels.chunks(SIZE).take(SIZE - empty_rows) {
for color in row {
f.write_char(color.map_or(' ', C::color_to_char))?;
}
writeln!(f)?;
}
if empty_rows > 0 {
writeln!(f, "({} empty rows skipped)", empty_rows)?;
}
writeln!(f, "]")?;
Ok(())
}
}
impl<C> DrawTarget for MockDisplay<C>
where
C: PixelColor,
{
type Color = C;
type Error = core::convert::Infallible;
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Pixel<Self::Color>>,
{
for pixel in pixels.into_iter() {
let Pixel(point, color) = pixel;
self.draw_pixel(point, color);
}
Ok(())
}
}
impl<C> OriginDimensions for MockDisplay<C>
where
C: PixelColor,
{
fn size(&self) -> Size {
DISPLAY_AREA.size
}
}
struct MessageWrapper<F>(F);
impl<F> fmt::Display for MessageWrapper<F>
where
F: Fn(&mut fmt::Formatter<'_>) -> fmt::Result,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0(f)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
pixelcolor::{BinaryColor, Rgb565},
Drawable,
};
#[test]
#[should_panic(expected = "tried to draw pixel outside the display area (x: 65, y: 0)")]
fn panic_on_out_of_bounds_drawing() {
let mut display = MockDisplay::new();
Pixel(Point::new(65, 0), BinaryColor::On)
.draw(&mut display)
.unwrap();
}
#[test]
fn allow_out_of_bounds_drawing() {
let mut display = MockDisplay::new();
display.set_allow_out_of_bounds_drawing(true);
Pixel(Point::new(65, 0), BinaryColor::On)
.draw(&mut display)
.unwrap();
}
#[test]
#[should_panic(expected = "tried to draw pixel twice (x: 1, y: 2)")]
fn panic_on_overdraw() {
let mut display = MockDisplay::new();
let p = Pixel(Point::new(1, 2), BinaryColor::On);
p.draw(&mut display).unwrap();
p.draw(&mut display).unwrap();
}
#[test]
fn allow_overdraw() {
let mut display = MockDisplay::new();
display.set_allow_overdraw(true);
let p = Pixel(Point::new(1, 2), BinaryColor::On);
p.draw(&mut display).unwrap();
p.draw(&mut display).unwrap();
}
#[test]
fn zero_sized_affected_area() {
let disp: MockDisplay<BinaryColor> = MockDisplay::new();
assert!(disp.affected_area().is_zero_sized(),);
}
#[test]
fn diff() {
let display1 = MockDisplay::<Rgb565>::from_pattern(&[" R RR"]);
let display2 = MockDisplay::<Rgb565>::from_pattern(&[" RR B"]);
let expected = MockDisplay::<Rgb888>::from_pattern(&[" RGB"]);
display1.diff(&display2).assert_eq(&expected);
}
}