| 1 | use crate::{ |
| 2 | geometry::Point, |
| 3 | mock_display::{ColorMapping, MockDisplay}, |
| 4 | pixelcolor::{PixelColor, Rgb888, RgbColor}, |
| 5 | primitives::Rectangle, |
| 6 | }; |
| 7 | use core::fmt::{self, Display, Write}; |
| 8 | |
| 9 | pub struct FancyPanic<'a, C> |
| 10 | where |
| 11 | C: PixelColor + ColorMapping, |
| 12 | { |
| 13 | display: FancyDisplay<'a, C>, |
| 14 | expected: FancyDisplay<'a, C>, |
| 15 | } |
| 16 | |
| 17 | impl<'a, C> FancyPanic<'a, C> |
| 18 | where |
| 19 | C: PixelColor + ColorMapping, |
| 20 | { |
| 21 | pub fn new( |
| 22 | display: &'a MockDisplay<C>, |
| 23 | expected: &'a MockDisplay<C>, |
| 24 | max_column_width: usize, |
| 25 | ) -> Self { |
| 26 | let bounding_box_display = display.affected_area_origin(); |
| 27 | let bounding_box_expected = expected.affected_area_origin(); |
| 28 | |
| 29 | let bounding_box = Rectangle::new( |
| 30 | Point::zero(), |
| 31 | bounding_box_display |
| 32 | .size |
| 33 | .component_max(bounding_box_expected.size), |
| 34 | ); |
| 35 | |
| 36 | // Output the 3 displays in columns if they are less than max_column_width pixels wide. |
| 37 | let column_width = if bounding_box.size.width as usize <= max_column_width { |
| 38 | // Set the width of the output columns to the width of the bounding box, |
| 39 | // but at least 10 characters to ensure the column labels fit. |
| 40 | (bounding_box.size.width as usize).max(10) |
| 41 | } else { |
| 42 | 0 |
| 43 | }; |
| 44 | |
| 45 | Self { |
| 46 | display: FancyDisplay::new(display, bounding_box, column_width), |
| 47 | expected: FancyDisplay::new(expected, bounding_box, column_width), |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | fn write_vertical_border(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 52 | writeln!( |
| 53 | f, |
| 54 | "+- {:-<width$}-+- {:-<width$}-+- {:-<width$}-+" , |
| 55 | "" , |
| 56 | "" , |
| 57 | "" , |
| 58 | width = self.display.column_width |
| 59 | ) |
| 60 | } |
| 61 | |
| 62 | fn write_header(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 63 | writeln!( |
| 64 | f, |
| 65 | "| {:^width$} | {:^width$} | {:^width$} |" , |
| 66 | "display" , |
| 67 | "expected" , |
| 68 | "diff" , |
| 69 | width = self.display.column_width |
| 70 | ) |
| 71 | } |
| 72 | |
| 73 | fn write_footer(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 74 | write!( |
| 75 | f, |
| 76 | "diff colors: {}\u{25FC}{} additional pixel" , |
| 77 | Ansi::Foreground(Some(Rgb888::GREEN)), |
| 78 | Ansi::Foreground(None) |
| 79 | )?; |
| 80 | |
| 81 | write!( |
| 82 | f, |
| 83 | ", {}\u{25FC}{} missing pixel" , |
| 84 | Ansi::Foreground(Some(Rgb888::RED)), |
| 85 | Ansi::Foreground(None) |
| 86 | )?; |
| 87 | |
| 88 | writeln!( |
| 89 | f, |
| 90 | ", {}\u{25FC}{} wrong color" , |
| 91 | Ansi::Foreground(Some(Rgb888::BLUE)), |
| 92 | Ansi::Foreground(None) |
| 93 | ) |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | impl<C> Display for FancyPanic<'_, C> |
| 98 | where |
| 99 | C: PixelColor + ColorMapping, |
| 100 | { |
| 101 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 102 | let diff = self.display.display.diff(self.expected.display); |
| 103 | let diff = FancyDisplay::new(&diff, self.display.bounding_box, self.display.column_width); |
| 104 | |
| 105 | // Output the 3 displays in columns if they are less than 30 pixels wide. |
| 106 | if self.display.column_width > 0 { |
| 107 | self.write_vertical_border(f)?; |
| 108 | self.write_header(f)?; |
| 109 | self.write_vertical_border(f)?; |
| 110 | |
| 111 | // Skip all odd y coordinates, because `write_row` outputs two rows of pixels. |
| 112 | for y in self.display.bounding_box.rows().step_by(2) { |
| 113 | f.write_str("| " )?; |
| 114 | self.display.write_row(f, y)?; |
| 115 | f.write_str(" | " )?; |
| 116 | self.expected.write_row(f, y)?; |
| 117 | f.write_str(" | " )?; |
| 118 | diff.write_row(f, y)?; |
| 119 | f.write_str(" | \n" )?; |
| 120 | } |
| 121 | |
| 122 | self.write_vertical_border(f)?; |
| 123 | } else { |
| 124 | let width = self.display.bounding_box.size.width as usize; |
| 125 | |
| 126 | write!(f, "display \n{:-<w$}\n{}" , "" , self.display, w = width)?; |
| 127 | write!(f, " \nexpected \n{:-<w$}\n{}" , "" , self.expected, w = width)?; |
| 128 | write!(f, " \ndiff \n{:-<width$}\n{}" , "" , diff, width = width)?; |
| 129 | } |
| 130 | self.write_footer(f)?; |
| 131 | |
| 132 | Ok(()) |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | struct FancyDisplay<'a, C> |
| 137 | where |
| 138 | C: PixelColor + ColorMapping, |
| 139 | { |
| 140 | display: &'a MockDisplay<C>, |
| 141 | bounding_box: Rectangle, |
| 142 | column_width: usize, |
| 143 | } |
| 144 | |
| 145 | impl<'a, C> FancyDisplay<'a, C> |
| 146 | where |
| 147 | C: PixelColor + ColorMapping, |
| 148 | { |
| 149 | const fn new( |
| 150 | display: &'a MockDisplay<C>, |
| 151 | bounding_box: Rectangle, |
| 152 | column_width: usize, |
| 153 | ) -> Self { |
| 154 | Self { |
| 155 | display, |
| 156 | bounding_box, |
| 157 | column_width, |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | fn write_row(&self, f: &mut fmt::Formatter<'_>, y: i32) -> fmt::Result { |
| 162 | for x in self.bounding_box.columns() { |
| 163 | let point_top = Point::new(x, y); |
| 164 | let point_bottom = Point::new(x, y + 1); |
| 165 | |
| 166 | let foreground = if self.bounding_box.contains(point_top) { |
| 167 | self.display |
| 168 | .get_pixel(point_top) |
| 169 | .map(|c| Some(c.into())) |
| 170 | .unwrap_or(Some(C::NONE_COLOR)) |
| 171 | } else { |
| 172 | None |
| 173 | }; |
| 174 | |
| 175 | let background = if self.bounding_box.contains(point_bottom) { |
| 176 | self.display |
| 177 | .get_pixel(point_bottom) |
| 178 | .map(|c| Some(c.into())) |
| 179 | .unwrap_or(Some(C::NONE_COLOR)) |
| 180 | } else { |
| 181 | None |
| 182 | }; |
| 183 | |
| 184 | // Write "upper half block" character. |
| 185 | write!( |
| 186 | f, |
| 187 | " {}{}\u{2580}" , |
| 188 | Ansi::Foreground(foreground), |
| 189 | Ansi::Background(background) |
| 190 | )?; |
| 191 | } |
| 192 | |
| 193 | // Reset colors. |
| 194 | Ansi::Reset.fmt(f)?; |
| 195 | |
| 196 | // Pad output with spaces if column width is larger than the width of the bounding box. |
| 197 | for _ in self.bounding_box.size.width as usize..self.column_width { |
| 198 | f.write_char(' ' )? |
| 199 | } |
| 200 | |
| 201 | Ok(()) |
| 202 | } |
| 203 | } |
| 204 | |
| 205 | impl<C> Display for FancyDisplay<'_, C> |
| 206 | where |
| 207 | C: PixelColor + ColorMapping, |
| 208 | { |
| 209 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 210 | // Skip all odd y coordinates, because `write_row` outputs two rows of pixels. |
| 211 | for y: i32 in self.bounding_box.rows().step_by(step:2) { |
| 212 | self.write_row(f, y)?; |
| 213 | f.write_char(' \n' )? |
| 214 | } |
| 215 | |
| 216 | Ok(()) |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | enum Ansi { |
| 221 | Foreground(Option<Rgb888>), |
| 222 | Background(Option<Rgb888>), |
| 223 | Reset, |
| 224 | } |
| 225 | |
| 226 | impl Display for Ansi { |
| 227 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 228 | match self { |
| 229 | Self::Foreground(Some(color: &Rgb888)) => { |
| 230 | write!(f, " \x1b[38;2; {}; {}; {}m" , color.r(), color.g(), color.b()) |
| 231 | } |
| 232 | Self::Foreground(None) => write!(f, " \x1b[39m" ), |
| 233 | Self::Background(Some(color: &Rgb888)) => { |
| 234 | write!(f, " \x1b[48;2; {}; {}; {}m" , color.r(), color.g(), color.b()) |
| 235 | } |
| 236 | Self::Background(None) => write!(f, " \x1b[49m" ), |
| 237 | Self::Reset => write!(f, " \x1b[0m" ), |
| 238 | } |
| 239 | } |
| 240 | } |
| 241 | |
| 242 | #[cfg (test)] |
| 243 | mod tests { |
| 244 | use super::*; |
| 245 | use crate::pixelcolor::BinaryColor; |
| 246 | |
| 247 | #[test ] |
| 248 | fn fancy_panic_columns() { |
| 249 | let display = MockDisplay::<BinaryColor>::from_pattern(&[ |
| 250 | " " , // |
| 251 | ".##" , // |
| 252 | ]); |
| 253 | |
| 254 | let expected = MockDisplay::<BinaryColor>::from_pattern(&[ |
| 255 | ".# " , // |
| 256 | " #" , // |
| 257 | ]); |
| 258 | |
| 259 | let mut out = arrayvec::ArrayString::<1024>::new(); |
| 260 | write!(&mut out, "{}" , FancyPanic::new(&display, &expected, 30)).unwrap(); |
| 261 | |
| 262 | assert_eq!(&out, concat!( |
| 263 | "+------------+------------+------------+ \n" , |
| 264 | "| display | expected | diff | \n" , |
| 265 | "+------------+------------+------------+ \n" , |
| 266 | "| \x1b[38;2;128;128;128m \x1b[48;2;0;0;0m▀ \x1b[38;2;128;128;128m \x1b[48;2;255;255;255m▀ \x1b[38;2;128;128;128m \x1b[48;2;255;255;255m▀ \x1b[0m " , |
| 267 | "| \x1b[38;2;0;0;0m \x1b[48;2;128;128;128m▀ \x1b[38;2;255;255;255m \x1b[48;2;128;128;128m▀ \x1b[38;2;128;128;128m \x1b[48;2;255;255;255m▀ \x1b[0m " , |
| 268 | "| \x1b[38;2;255;0;0m \x1b[48;2;0;255;0m▀ \x1b[38;2;255;0;0m \x1b[48;2;0;255;0m▀ \x1b[38;2;128;128;128m \x1b[48;2;128;128;128m▀ \x1b[0m | \n" , |
| 269 | "+------------+------------+------------+ \n" , |
| 270 | "diff colors: \x1b[38;2;0;255;0m◼ \x1b[39m additional pixel, \x1b[38;2;255;0;0m◼ \x1b[39m missing pixel, \x1b[38;2;0;0;255m◼ \x1b[39m wrong color \n" , |
| 271 | )); |
| 272 | } |
| 273 | |
| 274 | #[test ] |
| 275 | fn fancy_panic_no_columns() { |
| 276 | let display = MockDisplay::<BinaryColor>::from_pattern(&[ |
| 277 | " " , // |
| 278 | ".##" , // |
| 279 | ]); |
| 280 | |
| 281 | let expected = MockDisplay::<BinaryColor>::from_pattern(&[ |
| 282 | ".# " , // |
| 283 | " #" , // |
| 284 | ]); |
| 285 | |
| 286 | let mut out = arrayvec::ArrayString::<1024>::new(); |
| 287 | write!(&mut out, "{}" , FancyPanic::new(&display, &expected, 0)).unwrap(); |
| 288 | |
| 289 | assert_eq!(&out, concat!( |
| 290 | "display \n" , |
| 291 | "--- \n" , |
| 292 | " \x1b[38;2;128;128;128m \x1b[48;2;0;0;0m▀ \x1b[38;2;128;128;128m \x1b[48;2;255;255;255m▀ \x1b[38;2;128;128;128m \x1b[48;2;255;255;255m▀ \x1b[0m \n" , |
| 293 | " \n" , |
| 294 | "expected \n" , |
| 295 | "--- \n" , |
| 296 | " \x1b[38;2;0;0;0m \x1b[48;2;128;128;128m▀ \x1b[38;2;255;255;255m \x1b[48;2;128;128;128m▀ \x1b[38;2;128;128;128m \x1b[48;2;255;255;255m▀ \x1b[0m \n" , |
| 297 | " \n" , |
| 298 | "diff \n" , |
| 299 | "--- \n" , |
| 300 | " \x1b[38;2;255;0;0m \x1b[48;2;0;255;0m▀ \x1b[38;2;255;0;0m \x1b[48;2;0;255;0m▀ \x1b[38;2;128;128;128m \x1b[48;2;128;128;128m▀ \x1b[0m \n" , |
| 301 | "diff colors: \x1b[38;2;0;255;0m◼ \x1b[39m additional pixel, \x1b[38;2;255;0;0m◼ \x1b[39m missing pixel, \x1b[38;2;0;0;255m◼ \x1b[39m wrong color \n" , |
| 302 | )); |
| 303 | } |
| 304 | } |
| 305 | |