Skip to main content

embedded_graphics/mock_display/
fancy_panic.rs

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 in self.bounding_box.rows().step_by(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)) => {
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)) => {
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}