1use crate::{
2 geometry::Point,
3 mock_display::{ColorMapping, MockDisplay},
4 pixelcolor::{PixelColor, Rgb888, RgbColor},
5 primitives::Rectangle,
6};
7use core::fmt::{self, Display, Write};
8
9pub struct FancyPanic<'a, C>
10where
11 C: PixelColor + ColorMapping,
12{
13 display: FancyDisplay<'a, C>,
14 expected: FancyDisplay<'a, C>,
15}
16
17impl<'a, C> FancyPanic<'a, C>
18where
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
97impl<C> Display for FancyPanic<'_, C>
98where
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
136struct FancyDisplay<'a, C>
137where
138 C: PixelColor + ColorMapping,
139{
140 display: &'a MockDisplay<C>,
141 bounding_box: Rectangle,
142 column_width: usize,
143}
144
145impl<'a, C> FancyDisplay<'a, C>
146where
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
205impl<C> Display for FancyDisplay<'_, C>
206where
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
220enum Ansi {
221 Foreground(Option<Rgb888>),
222 Background(Option<Rgb888>),
223 Reset,
224}
225
226impl 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)]
243mod 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