Skip to main content

multi_mono_font/
multi_mono_text_style.rs

1use embedded_graphics::{
2    draw_target::DrawTarget,
3    geometry::{Point, Size},
4    image::Image,
5    pixelcolor::{BinaryColor, PixelColor},
6    prelude::OriginDimensions,
7    primitives::Rectangle,
8    text::{
9        renderer::{CharacterStyle, TextMetrics, TextRenderer},
10        Baseline,
11    },
12    Drawable,
13};
14
15use crate::{
16    char_size::CharSize, draw_target::MultiMonoFontDrawTarget, ChSzTy, MultiMonoFont,
17    MultiMonoFontList,
18};
19
20#[derive(Copy, Clone, Debug, PartialEq)]
21pub enum MultiMonoLineHeight {
22    Max,
23    Min,
24    Specify(ChSzTy),
25}
26
27const fn get_line_height<'a>(
28    fonts_height: MultiMonoLineHeight,
29    fonts: MultiMonoFontList<'a>,
30) -> ChSzTy {
31    let mut idx = 0;
32    match fonts_height {
33        MultiMonoLineHeight::Max => {
34            let mut max = ChSzTy::MIN;
35            while idx < fonts.len() {
36                let h = fonts[idx].character_size.height as ChSzTy;
37                idx += 1;
38                if h > max {
39                    max = h;
40                }
41            }
42            max
43        }
44        MultiMonoLineHeight::Min => {
45            let mut min = ChSzTy::MAX;
46            while idx < fonts.len() {
47                let h = fonts[idx].character_size.height as ChSzTy;
48                idx += 1;
49                if h < min {
50                    min = h;
51                }
52            }
53            min
54        }
55        MultiMonoLineHeight::Specify(h) => h,
56    }
57}
58
59/// Style properties for text using a monospaced font.
60///
61/// A `MultiMonoTextStyle` can be applied to a [`Text`] object to define how the text is drawn.
62///
63/// Because `MultiMonoTextStyle` has the [`non_exhaustive`] attribute, it cannot be created using a
64/// struct literal. To create a `MultiMonoTextStyle` with a given text color and transparent
65/// background, use the [`new`] method. For more complex text styles, use the
66/// [`MultiMonoTextStyleBuilder`].
67///
68/// [`Text`]: crate::text::Text
69/// [`non_exhaustive`]: https://blog.rust-lang.org/2019/12/19/Rust-1.40.0.html#[non_exhaustive]-structs,-enums,-and-variants
70/// [`new`]: MultiMonoTextStyle::new()
71#[derive(Copy, Clone, Debug, PartialEq)]
72#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
73#[non_exhaustive]
74pub struct MultiMonoTextStyle<'a, C> {
75    /// Text color.
76    pub text_color: C,
77
78    /// Background color.
79    pub background_color: Option<C>,
80
81    /// Font.
82    pub fonts: MultiMonoFontList<'a>,
83
84    ///Line height
85    pub line_height: ChSzTy,
86}
87
88impl<'a, C> MultiMonoTextStyle<'a, C>
89where
90    C: PixelColor,
91{
92    /// Creates a text style with transparent background.
93    pub const fn new(font_list: MultiMonoFontList<'a>, text_color: C) -> Self {
94        MultiMonoTextStyleBuilder::new(text_color)
95            .font(font_list, MultiMonoLineHeight::Max)
96            .build()
97    }
98
99    pub const fn new_with_line_height(
100        font_list: MultiMonoFontList<'a>,
101        line_height: MultiMonoLineHeight,
102        text_color: C,
103    ) -> Self {
104        MultiMonoTextStyleBuilder::new(text_color)
105            .font(font_list, line_height)
106            .build()
107    }
108
109    fn get_font_info(&self, c: char) -> &MultiMonoFont<'a> {
110        for font in self.fonts {
111            if font.glyph_mapping.contains(c) {
112                return font;
113            }
114        }
115
116        self.fonts[0]
117    }
118
119    fn draw_string_binary<D>(
120        &self,
121        text: &str,
122        position: Point,
123        baseline: Baseline,
124        mut target: D,
125    ) -> Result<Point, D::Error>
126    where
127        D: DrawTarget<Color = BinaryColor>,
128    {
129        let mut next_pos = position;
130        let mut draw_pos;
131
132        for c in text.chars() {
133            let font = self.get_font_info(c);
134            let glyph = font.glyph(c);
135            draw_pos = next_pos - Point::new(0, self.baseline_offset(baseline, font));
136            Image::new(&glyph, draw_pos).draw(&mut target)?;
137            next_pos.x += font.character_size.width as i32;
138            if font.character_spacing > 0 {
139                draw_pos.x += font.character_size.width as i32;
140                if self.background_color.is_some() {
141                    target.fill_solid(
142                        &Rectangle::new(
143                            draw_pos,
144                            CharSize::new(font.character_spacing, font.character_size.height)
145                                .size(),
146                        ),
147                        BinaryColor::Off,
148                    )?;
149                }
150                next_pos.x += font.character_spacing as i32;
151            }
152        }
153
154        Ok(next_pos)
155    }
156
157    /// Returns the vertical offset between the line position and the top edge of the bounding box.
158    fn baseline_offset(&self, baseline: Baseline, font: &MultiMonoFont<'a>) -> i32 {
159        match baseline {
160            Baseline::Top => 0,
161            Baseline::Bottom => font.character_size.height.saturating_sub(1) as i32,
162            Baseline::Middle => (font.character_size.height.saturating_sub(1) / 2) as i32,
163            Baseline::Alphabetic => font.baseline as i32,
164        }
165    }
166}
167
168impl<C> TextRenderer for MultiMonoTextStyle<'_, C>
169where
170    C: PixelColor,
171{
172    type Color = C;
173
174    fn draw_string<D>(
175        &self,
176        text: &str,
177        position: Point,
178        baseline: Baseline,
179        target: &mut D,
180    ) -> Result<Point, D::Error>
181    where
182        D: DrawTarget<Color = Self::Color>,
183    {
184        self.draw_string_binary(
185            text,
186            position,
187            baseline,
188            MultiMonoFontDrawTarget::new(target, self.text_color, self.background_color),
189        )
190    }
191
192    fn draw_whitespace<D>(
193        &self,
194        width: u32,
195        position: Point,
196        baseline: Baseline,
197        target: &mut D,
198    ) -> Result<Point, D::Error>
199    where
200        D: DrawTarget<Color = Self::Color>,
201    {
202        let (offet_y, height) = match baseline {
203            Baseline::Top => (0, self.line_height),
204            Baseline::Bottom => (self.line_height.saturating_sub(1) as i32, self.line_height),
205            Baseline::Middle => (
206                (self.line_height.saturating_sub(1) / 2) as i32,
207                self.line_height,
208            ),
209            Baseline::Alphabetic => (
210                self.fonts[0].baseline as i32,
211                self.fonts[0].character_size.height,
212            ),
213        };
214        let position = position - Point::new(0, offet_y);
215
216        if width != 0 {
217            if let Some(background_color) = self.background_color {
218                target.fill_solid(
219                    &Rectangle::new(position, Size::new(width, height as u32)),
220                    background_color,
221                )?;
222            }
223        }
224
225        Ok(position + Point::new(width as i32, offet_y))
226    }
227
228    fn measure_string(&self, text: &str, position: Point, baseline: Baseline) -> TextMetrics {
229        let mut bb_width = 0;
230        let mut bb_height = 0;
231        let mut baseline_max = 0;
232        let mut font = self.fonts[0];
233        for c in text.chars() {
234            font = self.get_font_info(c);
235            bb_width += (font.character_size.width + font.character_spacing) as u32;
236            bb_height = bb_height.max(font.character_size.height as u32);
237
238            baseline_max = baseline_max.max(self.baseline_offset(baseline, font));
239        }
240        bb_width = bb_width.saturating_sub(font.character_spacing as u32);
241
242        let bb_size = Size::new(bb_width, bb_height);
243
244        let bb_position = position - Point::new(0, baseline_max);
245        TextMetrics {
246            bounding_box: Rectangle::new(bb_position, bb_size),
247            next_position: position + bb_size.x_axis(),
248        }
249    }
250
251    fn line_height(&self) -> u32 {
252        self.line_height as u32
253    }
254}
255
256impl<C> CharacterStyle for MultiMonoTextStyle<'_, C>
257where
258    C: PixelColor,
259{
260    type Color = C;
261
262    fn set_text_color(&mut self, text_color: Option<Self::Color>) {
263        if let Some(color) = text_color {
264            self.text_color = color;
265        }
266    }
267
268    fn set_background_color(&mut self, background_color: Option<Self::Color>) {
269        self.background_color = background_color;
270    }
271}
272
273/// Text style builder for monospaced fonts.
274///
275/// Use this builder to create [`MultiMonoTextStyle`]s for [`Text`].
276///
277/// # Examples
278///
279/// ## Render yellow text on a blue background
280///
281/// This uses the [`FONT_6X9`] font, but [other fonts] can also be used.
282///
283/// ```rust
284/// use multi_mono_font::{ascii::FONT_6X9, MultiMonoTextStyle, MultiMonoTextStyleBuilder};
285/// use embedded_graphics::{
286///     pixelcolor::Rgb565,
287///     prelude::*,
288///     text::Text,
289/// };
290///
291/// let style = MultiMonoTextStyleBuilder::new()
292///     .font(&FONT_6X9)
293///     .text_color(Rgb565::YELLOW)
294///     .background_color(Rgb565::BLUE)
295///     .build();
296///
297/// let text = Text::new("Hello Rust!", Point::new(0, 0), style);
298/// ```
299///
300/// ## Transparent background
301///
302/// If a property is omitted, it will remain at its default value in the resulting
303/// `MultiMonoTextStyle` returned by `.build()`. This example draws white text with no background at
304/// all.
305///
306/// ```rust
307/// use multi_mono_font::{ascii::FONT_6X9, MultiMonoTextStyle, MultiMonoTextStyleBuilder};
308/// use embedded_graphics::{
309///     pixelcolor::Rgb565,
310///     prelude::*,
311///     text::Text,
312/// };
313///
314/// let style = MultiMonoTextStyleBuilder::new()
315///     .font(&FONT_6X9)
316///     .text_color(Rgb565::WHITE)
317///     .build();
318///
319/// let text = Text::new("Hello Rust!", Point::new(0, 0), style);
320/// ```
321///
322/// ## Modifying an existing style
323///
324/// The builder can also be used to modify an existing style.
325///
326/// ```
327/// use multi_mono_font::{ascii::FONT_6X9, MultiMonoTextStyle, MultiMonoTextStyleBuilder};
328/// use embedded_graphics::{
329///     pixelcolor::Rgb565,
330///     prelude::*,
331///     text::Text,
332/// };
333///
334/// let style = MultiMonoTextStyle::new(&FONT_6X9, Rgb565::YELLOW);
335///
336/// let style_larger = MultiMonoTextStyleBuilder::from(&style)
337///     .font(&FONT_10X20)
338///     .build();
339/// ```
340///
341/// [`FONT_6X9`]: crate::::ascii::FONT_6X9
342/// [other fonts]: super
343/// [`Text`]: crate::text::Text
344#[derive(Copy, Clone, Debug)]
345#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
346pub struct MultiMonoTextStyleBuilder<'a, C> {
347    style: MultiMonoTextStyle<'a, C>,
348}
349
350impl<'a, C> MultiMonoTextStyleBuilder<'a, C>
351where
352    C: PixelColor,
353{
354    /// Creates a new text style builder.
355    pub const fn new(text_color: C) -> Self {
356        Self {
357            style: MultiMonoTextStyle {
358                fonts: &[&super::NULL_FONT],
359                background_color: None,
360                text_color,
361                line_height: 0,
362            },
363        }
364    }
365
366    /// Sets the font.
367    pub const fn font<'b>(
368        self,
369        font_list: &'b [&'b MultiMonoFont<'b>],
370        line_height: MultiMonoLineHeight,
371    ) -> MultiMonoTextStyleBuilder<'b, C> {
372        let fonts = if font_list.is_empty() {
373            &[&crate::NULL_FONT]
374        } else {
375            font_list
376        };
377        let line_height = get_line_height(line_height, fonts);
378        let style = MultiMonoTextStyle {
379            fonts,
380            background_color: self.style.background_color,
381            text_color: self.style.text_color,
382            line_height,
383        };
384
385        MultiMonoTextStyleBuilder { style }
386    }
387
388    /// Resets the background color to transparent.
389    pub const fn reset_background_color(mut self) -> Self {
390        self.style.background_color = None;
391
392        self
393    }
394
395    /// Sets the text color.
396    pub const fn text_color(mut self, text_color: C) -> Self {
397        self.style.text_color = text_color;
398
399        self
400    }
401
402    /// Sets the line height.
403    pub const fn line_height(mut self, line_height: MultiMonoLineHeight) -> Self {
404        self.style.line_height = get_line_height(line_height, self.style.fonts);
405
406        self
407    }
408
409    /// Sets the background color.
410    pub const fn background_color(mut self, background_color: C) -> Self {
411        self.style.background_color = Some(background_color);
412
413        self
414    }
415
416    /// Builds the text style.
417    ///
418    /// This method can only be called after a font was set by using the [`font`] method. All other
419    /// settings are optional and they will be set to their default value if they are missing.
420    ///
421    /// [`font`]: MultiMonoTextStyleBuilder::font()
422    pub const fn build(self) -> MultiMonoTextStyle<'a, C> {
423        self.style
424    }
425}
426
427impl<'a, C> From<&MultiMonoTextStyle<'a, C>> for MultiMonoTextStyleBuilder<'a, C>
428where
429    C: PixelColor,
430{
431    fn from(style: &MultiMonoTextStyle<'a, C>) -> Self {
432        Self { style: *style }
433    }
434}