sensehat_screen/
fonts.rs

1//! 8x8 font collection
2use super::{
3    color::{BackgroundColor, StrokeColor},
4    FrameLine, PixelColor, PixelFrame,
5};
6
7use super::error::ScreenError;
8pub use font8x8::{
9    FontUnicode, UnicodeFonts, BASIC_FONTS, BLOCK_FONTS, BOX_FONTS, GREEK_FONTS, HIRAGANA_FONTS,
10    LATIN_FONTS,
11};
12use std::collections::HashMap;
13use std::fmt;
14
15lazy_static! {
16    /// A `static HashMap<char, FontUnicode>` that holds the entire set of fonts supported
17    /// for the `Screen`.
18    pub static ref FONT_HASHMAP: HashMap<char, FontUnicode> = default_hashmap();
19    /// A `static FontCollection` that offers a higher-level API for working with
20    /// pixel frames, clips, scrolls, etc.
21    ///
22    /// `FONT_COLLECTION.sanitize_str(&str)` returns a sanitized `FontString`,
23    /// and use that to render pixel frames..
24    ///
25    /// `FONT_COLLECTION.get(font: char)` returns the low-level `FontUnicode` if the font
26    /// is found in the collection.
27    pub static ref FONT_COLLECTION: FontCollection = FontCollection(default_hashmap());
28}
29
30fn default_hashmap() -> HashMap<char, FontUnicode> {
31    BASIC_FONTS
32        .iter()
33        .chain(LATIN_FONTS.iter())
34        .chain(BLOCK_FONTS.iter())
35        .chain(BOX_FONTS.iter())
36        .chain(GREEK_FONTS.iter())
37        .chain(HIRAGANA_FONTS.iter())
38        .map(|x| (x.0, *x))
39        .collect()
40}
41
42// A set of font symbols that can be printed on a `Screen`.
43#[derive(Clone, Debug, PartialEq)]
44//#[cfg_attr(feature = "serde-support", derive(Serialize, Deserialize))]
45pub struct FontCollection(HashMap<char, FontUnicode>);
46
47impl FontCollection {
48    /// Create a default `FontCollection`, containing the Unicode constants
49    /// from the [font8x8](https://github.com/saibatizoku/font8x8-rs) crate, except for
50    /// `MISC_FONTS`, and `SGA_FONTS` (which are non-standard).
51    pub fn new() -> Self {
52        FontCollection(default_hashmap())
53    }
54
55    /// Create a `FontCollection` with a custom HashMap of font symbols.
56    pub fn from_hashmap(hashmap: HashMap<char, FontUnicode>) -> Self {
57        FontCollection(hashmap)
58    }
59
60    /// Get an `Option` with the symbol's byte rendering.
61    pub fn get(&self, symbol: char) -> Option<&FontUnicode> {
62        self.0.get(&symbol)
63    }
64
65    /// Search if collection has a symbol by its unicode key.
66    pub fn contains_key(&self, symbol: char) -> bool {
67        self.0.contains_key(&symbol)
68    }
69
70    /// Sanitize a `&str` and create a new `FontString`.
71    pub fn sanitize_str(&self, s: &str) -> Result<FontString, ScreenError> {
72        let valid = s
73            .chars()
74            .filter(|c| self.0.contains_key(c))
75            .map(|sym| *self.get(sym).unwrap())
76            .collect::<Vec<FontUnicode>>();
77        Ok(FontString(valid))
78    }
79}
80
81impl Default for FontCollection {
82    fn default() -> Self {
83        FontCollection::new()
84    }
85}
86
87/// A `FontString` is a collection of `FontUnicode` which can be rendered to frames for the LED
88/// Matrix.
89#[derive(Clone, Debug, Default, PartialEq)]
90pub struct FontString(Vec<FontUnicode>);
91
92impl FontString {
93    /// Create an empty `FontString`.
94    pub fn new() -> Self {
95        FontString(Default::default())
96    }
97
98    /// Render the font string as a collection of unicode value points, `Vec<char>`.
99    pub fn chars(&self) -> Vec<char> {
100        self.0.iter().map(|font| font.char()).collect::<Vec<char>>()
101    }
102
103    /// Returns a `Vec<FontFrame>` for each inner font.
104    pub fn font_frames(&self, stroke: PixelColor, bg: PixelColor) -> Vec<FontFrame> {
105        self.0
106            .iter()
107            .map(|font| FontFrame::new(*font, stroke, bg))
108            .collect::<Vec<FontFrame>>()
109    }
110
111    /// Returns a `Vec<PixelFrame>` for each inner font.
112    pub fn pixel_frames(&self, stroke: PixelColor, bg: PixelColor) -> Vec<PixelFrame> {
113        self.font_frames(stroke, bg)
114            .into_iter()
115            .map(|f| f.into())
116            .collect::<Vec<PixelFrame>>()
117    }
118}
119
120impl fmt::Display for FontString {
121    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
122        write!(
123            f,
124            "{}",
125            self.0.iter().map(|font| font.char()).collect::<String>()
126        )
127    }
128}
129
130/// A font that can be rendered as a `PixelFrame` with a `stroke` color, and a `background` color.
131#[derive(Debug, PartialEq)]
132pub struct FontFrame {
133    /// `UTF16` font
134    font: FontUnicode,
135    /// Color for the font stroke
136    stroke: PixelColor,
137    /// Color for the font background
138    background: PixelColor,
139}
140
141impl FontFrame {
142    /// Create a new font frame with a `stroke` color, and a `background` color.
143    pub fn new(font: FontUnicode, stroke: PixelColor, background: PixelColor) -> Self {
144        FontFrame {
145            font,
146            stroke,
147            background,
148        }
149    }
150
151    /// The `PixelFrame` for this font.
152    pub fn pixel_frame(&self) -> PixelFrame {
153        let pixels =
154            font_to_pixel_color_array_with_bg(self.font.byte_array(), self.stroke, self.background);
155        pixels.into()
156    }
157}
158
159impl From<FontFrame> for PixelFrame {
160    fn from(font: FontFrame) -> Self {
161        font.pixel_frame()
162    }
163}
164
165impl BackgroundColor for FontFrame {
166    fn set_background_color(&mut self, color: PixelColor) {
167        self.background = color;
168    }
169    fn get_background_color(&self) -> PixelColor {
170        self.background
171    }
172}
173
174impl StrokeColor for FontFrame {
175    fn set_stroke_color(&mut self, color: PixelColor) {
176        self.stroke = color;
177    }
178    fn get_stroke_color(&self) -> PixelColor {
179        self.stroke
180    }
181}
182
183// Render a font symbol with a stroke color and a background color.
184fn font_to_pixel_color_array_with_bg(
185    symbol: [u8; 8],
186    color: PixelColor,
187    background: PixelColor,
188) -> [PixelColor; 64] {
189    let mut pixels = [background; 64];
190    for (row_idx, encoded_row) in symbol.iter().enumerate() {
191        for col_idx in 0..8 {
192            if (*encoded_row & 1 << col_idx) > 0 {
193                pixels[row_idx * 8 + col_idx] = color;
194            }
195        }
196    }
197    pixels
198}
199
200// Render a font symbol with a `PixelColor` into a `[PixelColor; 64]`.
201fn font_to_pixel_color_array(symbol: [u8; 8], color: PixelColor) -> [PixelColor; 64] {
202    font_to_pixel_color_array_with_bg(symbol, color, Default::default())
203}
204
205/// Render a font symbol with a `PixelColor` into a `FrameLine`.
206pub fn font_to_pixel_frame(symbol: [u8; 8], color: PixelColor) -> PixelFrame {
207    let pixels = font_to_pixel_color_array(symbol, color);
208    PixelFrame::new(&pixels)
209}
210
211/// Render a font symbol with a `PixelColor` into a `FrameLine`.
212pub fn font_to_frame(symbol: [u8; 8], color: PixelColor) -> FrameLine {
213    let pixels = font_to_pixel_color_array(symbol, color);
214    FrameLine::from_pixels(&pixels)
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    const BLK: PixelColor = PixelColor::BLACK;
222    const RED: PixelColor = PixelColor::RED;
223    const GRN: PixelColor = PixelColor::GREEN;
224    const BLU: PixelColor = PixelColor::BLUE;
225    const YLW: PixelColor = PixelColor::YELLOW;
226    const BASIC_FONT: [PixelColor; 64] = [
227        BLU, BLU, BLK, BLK, BLK, BLU, BLU, BLK, //
228        BLU, BLU, BLU, BLK, BLU, BLU, BLU, BLK, //
229        BLU, BLU, BLU, BLU, BLU, BLU, BLU, BLK, //
230        BLU, BLU, BLU, BLU, BLU, BLU, BLU, BLK, //
231        BLU, BLU, BLK, BLU, BLK, BLU, BLU, BLK, //
232        BLU, BLU, BLK, BLK, BLK, BLU, BLU, BLK, //
233        BLU, BLU, BLK, BLK, BLK, BLU, BLU, BLK, //
234        BLK, BLK, BLK, BLK, BLK, BLK, BLK, BLK, //
235    ];
236    const BOX_FONT: [PixelColor; 64] = [
237        BLK, BLK, BLK, GRN, BLK, BLK, BLK, BLK, //
238        BLK, BLK, BLK, GRN, BLK, BLK, BLK, BLK, //
239        BLK, BLK, BLK, GRN, BLK, BLK, BLK, BLK, //
240        BLK, BLK, BLK, GRN, GRN, GRN, GRN, GRN, //
241        GRN, GRN, GRN, GRN, GRN, GRN, GRN, GRN, //
242        BLK, BLK, BLK, BLK, BLK, BLK, BLK, BLK, //
243        BLK, BLK, BLK, BLK, BLK, BLK, BLK, BLK, //
244        BLK, BLK, BLK, BLK, BLK, BLK, BLK, BLK, //
245    ];
246    const BOX_FONT_BG: [PixelColor; 64] = [
247        YLW, YLW, YLW, BLU, YLW, YLW, YLW, YLW, //
248        YLW, YLW, YLW, BLU, YLW, YLW, YLW, YLW, //
249        YLW, YLW, YLW, BLU, YLW, YLW, YLW, YLW, //
250        YLW, YLW, YLW, BLU, BLU, BLU, BLU, BLU, //
251        BLU, BLU, BLU, BLU, BLU, BLU, BLU, BLU, //
252        YLW, YLW, YLW, YLW, YLW, YLW, YLW, YLW, //
253        YLW, YLW, YLW, YLW, YLW, YLW, YLW, YLW, //
254        YLW, YLW, YLW, YLW, YLW, YLW, YLW, YLW, //
255    ];
256    const HIRAGANA_FONT: [PixelColor; 64] = [
257        BLK, BLK, BLK, RED, BLK, BLK, BLK, BLK, //
258        BLK, RED, RED, RED, RED, RED, RED, BLK, //
259        BLK, BLK, BLK, RED, BLK, BLK, BLK, BLK, //
260        BLK, BLK, RED, RED, RED, RED, BLK, BLK, //
261        BLK, BLK, BLK, BLK, BLK, BLK, RED, BLK, //
262        BLK, BLK, BLK, BLK, BLK, BLK, RED, BLK, //
263        BLK, BLK, BLK, RED, RED, RED, BLK, BLK, //
264        BLK, BLK, BLK, BLK, BLK, BLK, BLK, BLK, //
265    ];
266
267    #[test]
268    fn font_collection_sanitizes_text_by_filtering_known_unicode_points() {
269        let font_set = FontCollection::new();
270        let valid_text = font_set.sanitize_str("hola niño @¶øþ¥").unwrap();
271        assert_eq!(format!("{}", valid_text), "hola niño @¶øþ¥");
272    }
273
274    #[test]
275    fn font_collection_sanitizes_text_by_removing_symbols_not_in_set() {
276        let font_set = FontCollection::new();
277        let invalid_text = font_set.sanitize_str("ŧ←→ł").unwrap();
278        assert_eq!(format!("{}", invalid_text), "");
279
280        let font_set = FontCollection::from_hashmap(HashMap::new());
281        let invalid_text = font_set.sanitize_str("hola niño @¶øþ¥").unwrap();
282        assert_eq!(format!("{}", invalid_text), "");
283    }
284
285    #[test]
286    fn font_collection_gets_optional_symbol_by_unicode_key() {
287        let font_set = FontCollection::new();
288        let symbol = font_set.get('ñ');
289        assert!(symbol.is_some());
290    }
291
292    #[test]
293    fn font_collection_searches_for_symbols_by_unicode_key() {
294        let font_set = FontCollection::new();
295        let has_symbol = font_set.contains_key('ñ');
296        assert!(has_symbol);
297    }
298
299    #[test]
300    fn font_string_new_method_starts_emtpy_instance() {
301        let font_string = FontString::new();
302        assert_eq!(font_string.0, Vec::new());
303    }
304
305    #[test]
306    fn font_string_chars_method_returns_vec_of_chars() {
307        let font_set = FontCollection::new();
308        let font_string = font_set.sanitize_str("┷│││┯").unwrap();
309        assert_eq!(font_string.chars(), vec!['┷', '│', '│', '│', '┯']);
310    }
311
312    #[test]
313    fn font_string_implements_display_trait() {
314        let font_set = FontCollection::new();
315        let font_string = font_set.sanitize_str("┷│││┯").unwrap();
316        assert_eq!(format!("{}", font_string), "┷│││┯".to_string());
317    }
318
319    #[test]
320    fn font_string_font_frames_returns_a_vec_of_font_frame() {
321        let font_set = FontCollection::new();
322        let font_string = font_set.sanitize_str("Mち┶").unwrap();
323        let bas_font = font_set.get('M').unwrap();
324        let hir_font = font_set.get('ち').unwrap();
325        let box_font = font_set.get('┶').unwrap();
326        let ft_frames = font_string.font_frames(PixelColor::YELLOW, PixelColor::BLACK);
327        assert_eq!(
328            ft_frames,
329            vec![
330                FontFrame {
331                    font: *bas_font,
332                    stroke: PixelColor::YELLOW,
333                    background: PixelColor::BLACK,
334                },
335                FontFrame {
336                    font: *hir_font,
337                    stroke: PixelColor::YELLOW,
338                    background: PixelColor::BLACK,
339                },
340                FontFrame {
341                    font: *box_font,
342                    stroke: PixelColor::YELLOW,
343                    background: PixelColor::BLACK,
344                },
345            ]
346        );
347    }
348
349    #[test]
350    fn font_string_font_frames_returns_a_vec_of_pixel_frame() {
351        let font_set = FontCollection::new();
352        let font_string = font_set.sanitize_str("MM").unwrap();
353        let px_frames = font_string.pixel_frames(PixelColor::BLUE, PixelColor::BLACK);
354        assert_eq!(
355            px_frames,
356            vec![PixelFrame::from(BASIC_FONT), PixelFrame::from(BASIC_FONT),]
357        );
358    }
359
360    #[test]
361    fn fn_font_to_pixel_color_array_with_bg_creates_new_array() {
362        let font_set = FontCollection::new();
363        let font = font_set.get('┶').unwrap();
364        let px_array = font_to_pixel_color_array_with_bg(
365            font.byte_array(),
366            PixelColor::BLUE,
367            PixelColor::YELLOW,
368        );
369        for (idx, px) in px_array.iter().enumerate() {
370            assert_eq!(*px, BOX_FONT_BG[idx]);
371        }
372    }
373
374    #[test]
375    fn fn_font_to_pixel_color_array_creates_new_array() {
376        let font_set = FontCollection::new();
377        let font = font_set.get('M').unwrap();
378        let px_array = font_to_pixel_color_array(font.byte_array(), PixelColor::BLUE);
379        for (idx, px) in px_array.iter().enumerate() {
380            assert_eq!(*px, BASIC_FONT[idx]);
381        }
382    }
383
384    #[test]
385    fn fn_font_to_pixel_frame_takes_a_byte_array_and_pixel_color() {
386        let font_set = FontCollection::new();
387        let chi_font = font_set.get('ち').unwrap();
388        let px_frame = font_to_pixel_frame(chi_font.byte_array(), PixelColor::RED);
389        assert_eq!(px_frame, PixelFrame::from(HIRAGANA_FONT));
390    }
391
392    #[test]
393    fn fn_font_to_frame_takes_a_byte_array_and_pixel_color() {
394        let font_set = FontCollection::new();
395        let box_font = font_set.get('┶').unwrap();
396        let px_frame_line = font_to_frame(box_font.byte_array(), PixelColor::GREEN);
397        assert_eq!(px_frame_line, PixelFrame::from(BOX_FONT).frame_line());
398    }
399
400    #[test]
401    fn font_frames_are_created_from_ut16_font_a_stroke_and_a_background_color() {
402        let font_set = FontCollection::new();
403        let letter_a = font_set.get('a').unwrap();
404        let font_frame = FontFrame::new(letter_a.clone(), PixelColor::WHITE, PixelColor::BLACK);
405        assert_eq!(
406            font_frame,
407            FontFrame {
408                font: *letter_a,
409                stroke: PixelColor::WHITE,
410                background: PixelColor::BLACK
411            }
412        );
413    }
414
415    #[test]
416    fn font_frames_is_represented_as_a_pixel_frame() {
417        let font_set = FontCollection::new();
418        let hiragana_font = font_set.get('ち').unwrap();
419        let font_frame = FontFrame::new(hiragana_font.clone(), PixelColor::RED, PixelColor::BLACK);
420        let px_frame = font_frame.pixel_frame();
421        assert_eq!(px_frame, PixelFrame::from(HIRAGANA_FONT));
422    }
423
424    #[test]
425    fn pixel_frame_implements_from_font_frame_trait() {
426        let font_set = FontCollection::new();
427        let hiragana_font = font_set.get('ち').unwrap();
428        let font_frame = FontFrame::new(hiragana_font.clone(), PixelColor::RED, PixelColor::BLACK);
429        let px_frame = PixelFrame::from(font_frame);
430        assert_eq!(px_frame, PixelFrame::from(HIRAGANA_FONT));
431    }
432
433    #[test]
434    fn font_frame_sets_background_color() {
435        let font_set = FontCollection::new();
436        let letter_a = font_set.get('a').unwrap();
437        let mut font_frame = FontFrame::new(letter_a.clone(), PixelColor::WHITE, PixelColor::BLACK);
438        font_frame.set_background_color(PixelColor::RED);
439        assert_eq!(
440            font_frame,
441            FontFrame {
442                font: *letter_a,
443                stroke: PixelColor::WHITE,
444                background: PixelColor::RED
445            }
446        );
447    }
448
449    #[test]
450    fn font_frame_gets_background_color() {
451        let font_set = FontCollection::new();
452        let letter_a = font_set.get('a').unwrap();
453        let font_frame = FontFrame::new(letter_a.clone(), PixelColor::WHITE, PixelColor::GREEN);
454        assert_eq!(font_frame.get_background_color(), PixelColor::GREEN);
455    }
456
457    #[test]
458    fn font_frame_sets_stroke_color() {
459        let font_set = FontCollection::new();
460        let letter_a = font_set.get('a').unwrap();
461        let mut font_frame = FontFrame::new(letter_a.clone(), PixelColor::WHITE, PixelColor::BLACK);
462        font_frame.set_stroke_color(PixelColor::YELLOW);
463        assert_eq!(
464            font_frame,
465            FontFrame {
466                font: *letter_a,
467                stroke: PixelColor::YELLOW,
468                background: PixelColor::BLACK
469            }
470        );
471    }
472
473    #[test]
474    fn font_frame_gets_stroke_color() {
475        let font_set = FontCollection::new();
476        let letter_a = font_set.get('a').unwrap();
477        let font_frame = FontFrame::new(letter_a.clone(), PixelColor::BLUE, PixelColor::WHITE);
478        assert_eq!(font_frame.get_stroke_color(), PixelColor::BLUE);
479    }
480}