mycelium_trace/
embedded_graphics.rs

1use crate::color::{Color, SetColor};
2use core::{
3    fmt,
4    sync::atomic::{AtomicU64, Ordering},
5};
6use embedded_graphics::{
7    geometry::Point,
8    mono_font::{self, MonoFont, MonoTextStyle},
9    pixelcolor::{self, RgbColor},
10    text::{self, Text},
11    Drawable,
12};
13use hal_core::framebuffer::{Draw, DrawTarget};
14#[derive(Debug)]
15pub struct MakeTextWriter<D> {
16    mk: fn() -> D,
17    settings: TextWriterBuilder,
18    next_point: AtomicU64,
19    line_len: u32,
20    char_height: u32,
21    last_line: i32,
22}
23
24#[derive(Debug, Clone, Copy)]
25pub struct TextWriterBuilder {
26    default_color: Color,
27    start_point: Point,
28}
29
30#[derive(Clone, Debug)]
31pub struct TextWriter<'mk, D> {
32    target: DrawTarget<D>,
33    color: Color,
34    mk: &'mk MakeTextWriter<D>,
35}
36
37const fn pack_point(Point { x, y }: Point) -> u64 {
38    ((x as u64) << 32) | y as u64
39}
40
41const fn unpack_point(u: u64) -> Point {
42    const Y_MASK: u64 = u32::MAX as u64;
43    let x = (u >> 32) as i32;
44    let y = (u & Y_MASK) as i32;
45    Point { x, y }
46}
47
48impl<D> fmt::Write for TextWriter<'_, D>
49where
50    D: Draw,
51{
52    fn write_str(&mut self, s: &str) -> fmt::Result {
53        let curr_packed = self.mk.next_point.load(Ordering::Relaxed);
54        let mut curr_point = unpack_point(curr_packed);
55
56        // for a couple of reasons, we don't trust the `embedded-graphics` crate
57        // to handle newlines for us:
58        //
59        // 1. it currently only actually produces a carriage return when a
60        //    newline character appears in the *middle* of a string. this means
61        //    that strings beginning or ending with newlines (and strings that
62        //    are *just* newlines) won't advance the write position the way we'd
63        //    expect them to. so, we have to do that part ourself --- it turns
64        //    out that most `fmt::Debug`/`fmt::Display` implementations will
65        //    write a bunch of strings that begin or end with `\n`.
66        // 2. when we reach the bottom of the screen, we want to scroll the
67        //    previous text up to make room for a new line of text.
68        //    `embedded-graphics` doesn't implement this behavior. because we
69        //    want to scroll every time we encounter a newline if we have
70        //    reached the bottom of the screen, this means we have to implement
71        //    *all* newline handling ourselves.
72        //
73        // TODO(eliza): currently, our newline handling doesn't honor
74        // configurable line height. all lines are simply a single character
75        // high. if we want to do something nicer about line height, we'd have
76        // to implement that here...
77        for mut line in s.split_inclusive('\n') {
78            // does this chunk end with a newline? it might not, if:
79            // (a) it's the last chunk in a string where newlines only occur in
80            //     the beginning/middle.
81            // (b) the string being written has no newlines (so
82            //     `split_inclusive` will only yield a single chunk)
83            let has_newline = line.ends_with('\n');
84            if has_newline {
85                // if there's a trailing newline, trim it off --- no sense
86                // making the `embedded-graphics` crate draw an extra character
87                // it will essentially nop for.
88                line = &line[..line.len() - 1];
89            }
90
91            // if we have reached the bottom of the screen, we'll need to scroll
92            // previous framebuffer contents up to make room for new line(s) of
93            // text.
94            if curr_point.y > self.mk.last_line {
95                let ydiff = curr_point.y - self.mk.last_line;
96                curr_point = Point {
97                    y: self.mk.last_line,
98                    x: 10,
99                };
100                self.target.inner_mut().scroll_vert(ydiff as isize);
101            }
102
103            let next_point = if line.is_empty() {
104                // if this line is now empty, it was *just* a newline character,
105                // so all we have to do is advance the write position.
106                curr_point
107            } else {
108                // otherwise, actually draw the text.
109                Text::with_alignment(s, curr_point, self.text_style(), text::Alignment::Left)
110                    .draw(&mut self.target)
111                    .map_err(|_| fmt::Error)?
112            };
113
114            if has_newline {
115                // carriage return
116                curr_point = Point {
117                    y: curr_point.y + self.mk.char_height as i32,
118                    x: 10,
119                };
120            } else {
121                curr_point = next_point;
122            }
123        }
124
125        match self.mk.next_point.compare_exchange(
126            curr_packed,
127            pack_point(curr_point),
128            Ordering::Relaxed,
129            Ordering::Relaxed,
130        ) {
131            Ok(_) => Ok(()),
132            Err(actual_point) => unsafe {
133                mycelium_util::unreachable_unchecked!(
134                    "lock should guard this, could actually be totally unsync; curr_point={}; actual_point={}",
135                    unpack_point(curr_packed),
136                    unpack_point(actual_point)
137                );
138            },
139        }
140    }
141}
142
143impl<D> SetColor for TextWriter<'_, D>
144where
145    D: Draw,
146{
147    fn set_fg_color(&mut self, color: Color) {
148        let color = if color == Color::Default {
149            self.mk.settings.default_color
150        } else {
151            color
152        };
153        self.color = color;
154    }
155
156    fn fg_color(&self) -> Color {
157        self.color
158    }
159
160    fn set_bold(&mut self, bold: bool) {
161        use Color::*;
162        let next_color = if bold {
163            match self.color {
164                Black => BrightBlack,
165                Red => BrightRed,
166                Green => BrightGreen,
167                Yellow => BrightYellow,
168                Blue => BrightBlue,
169                Magenta => BrightMagenta,
170                Cyan => BrightCyan,
171                White => BrightWhite,
172                x => x,
173            }
174        } else {
175            match self.color {
176                BrightBlack => Black,
177                BrightRed => Red,
178                BrightGreen => Green,
179                BrightYellow => Yellow,
180                BrightBlue => Blue,
181                BrightMagenta => Magenta,
182                BrightCyan => Cyan,
183                BrightWhite => White,
184                x => x,
185            }
186        };
187        self.set_fg_color(next_color);
188    }
189}
190
191impl<D> TextWriter<'_, D>
192where
193    D: Draw,
194{
195    fn text_style(&self) -> MonoTextStyle<'static, pixelcolor::Rgb888> {
196        use pixelcolor::Rgb888;
197        const COLOR_TABLE: [Rgb888; 17] = [
198            Rgb888::BLACK,              // black
199            Rgb888::new(128, 0, 0),     // red
200            Rgb888::new(0, 128, 0),     // green
201            Rgb888::new(128, 128, 0),   // yellow
202            Rgb888::new(0, 0, 128),     // blue
203            Rgb888::new(128, 0, 128),   // magenta
204            Rgb888::new(0, 128, 128),   // cyan
205            Rgb888::new(192, 192, 192), // white
206            Rgb888::new(192, 192, 192), // default
207            Rgb888::new(128, 128, 128), // bright black
208            Rgb888::new(255, 0, 0),     // bright red
209            Rgb888::new(0, 255, 0),     // bright green
210            Rgb888::new(255, 255, 0),   // bright yellow
211            Rgb888::new(0, 0, 255),     // bright blue
212            Rgb888::new(255, 0, 255),   // bright magenta
213            Rgb888::new(0, 255, 255),   // bright cyan
214            Rgb888::new(255, 255, 255), // bright white
215        ];
216        MonoTextStyle::new(
217            self.mk.settings.get_font(),
218            COLOR_TABLE[self.color as usize],
219        )
220    }
221}
222
223// === impl MakeTextWriter ===
224impl<D> MakeTextWriter<D> {
225    #[must_use]
226    pub const fn builder() -> TextWriterBuilder {
227        TextWriterBuilder::new()
228    }
229}
230
231impl<D: Draw> MakeTextWriter<D> {
232    #[must_use]
233    pub fn new(mk: fn() -> D) -> Self {
234        Self::build(mk, TextWriterBuilder::new())
235    }
236
237    fn build(mk: fn() -> D, settings: TextWriterBuilder) -> Self {
238        let (pixel_width, pixel_height) = {
239            let buf = (mk)();
240            (buf.width() as u32, buf.height() as u32)
241        };
242        let text_style = MonoTextStyle::new(settings.get_font(), pixelcolor::Rgb888::WHITE);
243        let line_len = Self::line_len(pixel_width, &text_style);
244        let char_height = text_style.font.character_size.height;
245        let last_line = (pixel_height - char_height - 10) as i32;
246        Self {
247            settings,
248            next_point: AtomicU64::new(pack_point(settings.start_point)),
249            char_height,
250            mk,
251            line_len,
252            last_line,
253        }
254    }
255
256    fn line_len(pixel_width: u32, text_style: &MonoTextStyle<'static, pixelcolor::Rgb888>) -> u32 {
257        pixel_width / text_style.font.character_size.width
258    }
259}
260
261impl<'a, D> crate::writer::MakeWriter<'a> for MakeTextWriter<D>
262where
263    D: Draw + 'a,
264{
265    type Writer = TextWriter<'a, D>;
266
267    fn make_writer(&'a self) -> Self::Writer {
268        TextWriter {
269            color: self.settings.default_color,
270            target: (self.mk)().into_draw_target(),
271            mk: self,
272        }
273    }
274
275    fn line_len(&self) -> usize {
276        self.line_len as usize
277    }
278}
279
280impl TextWriterBuilder {
281    #[must_use]
282    pub const fn new() -> Self {
283        Self {
284            // TODO(eliza): it would be nice if this was configurable via the builder,
285            // but it's not, because `MonoFont` is `!Sync` due to containing a trait
286            // object without a `Sync` bound...this should be fixed upstream in
287            // `embedded-graphics`.
288            // font: &mono_font::ascii::FONT_6X13,
289            default_color: Color::White,
290            start_point: Point { x: 10, y: 10 },
291        }
292    }
293
294    // #[must_use]
295    // pub fn font(self, font: &'static MonoFont<'static>) -> Self {
296    //     Self { font, ..self }
297    // }
298
299    #[must_use]
300    pub fn default_color(self, default_color: Color) -> Self {
301        Self {
302            default_color,
303            ..self
304        }
305    }
306
307    #[must_use]
308    pub fn starting_point(self, start_point: Point) -> Self {
309        Self {
310            start_point,
311            ..self
312        }
313    }
314
315    #[must_use]
316    pub fn build<D: Draw>(self, mk: fn() -> D) -> MakeTextWriter<D> {
317        MakeTextWriter::build(mk, self)
318    }
319
320    // TODO(eliza): it would be nice if this was configurable via the builder,
321    // but it's not, because `MonoFont` is `!Sync` due to containing a trait
322    // object without a `Sync` bound...this should be fixed upstream in
323    // `embedded-graphics`.
324    fn get_font(&self) -> &'static MonoFont<'static> {
325        &mono_font::ascii::FONT_6X13
326    }
327}
328
329impl Default for TextWriterBuilder {
330    fn default() -> Self {
331        Self::new()
332    }
333}