buffer_graphics_lib/text/
mod.rs

1pub mod font;
2pub mod format;
3pub mod pos;
4pub mod wrapping;
5
6use crate::drawing::Renderable;
7use crate::prelude::font::*;
8use crate::text::format::TextFormat;
9use crate::text::pos::TextPos;
10use crate::Graphics;
11use graphics_shapes::prelude::Rect;
12use ici_files::prelude::*;
13#[cfg(feature = "serde")]
14use serde::{Deserialize, Serialize};
15
16//from Latin-1/ISO 8859-1
17pub const ASCII_DEGREE: u8 = 176;
18pub const ASCII_CURRENCY: u8 = 164;
19pub const ASCII_EURO: u8 = 127;
20pub const ASCII_POUND: u8 = 163;
21pub const ASCII_YEN: u8 = 165;
22pub const ASCII_CENT: u8 = 162;
23
24//override control codes
25pub const ASCII_ELLIPSIS: u8 = 31;
26pub const ASCII_CHECK: u8 = 25;
27
28pub const SUPPORTED_SYMBOLS: [char; 38] = [
29    '!', '@', '£', '$', '%', '^', '&', '*', '(', ')', '_', '+', '-', '=', '#', '{', '}', ':', '"',
30    '|', '<', '?', '>', ',', '/', '.', ';', '\'', '\\', '[', ']', '`', '~', '°', '…', '¢', '¥',
31    '✓',
32];
33
34const fn custom_ascii_code(chr: char) -> u8 {
35    match chr {
36        '°' => ASCII_DEGREE,
37        '…' => ASCII_ELLIPSIS,
38        '¤' => ASCII_CURRENCY,
39        '£' => ASCII_POUND,
40        '¥' => ASCII_YEN,
41        '¢' => ASCII_CENT,
42        '✓' => ASCII_CHECK,
43        '€' => ASCII_EURO,
44        _ => 0,
45    }
46}
47
48pub const fn chr_to_code(chr: char) -> u8 {
49    if chr.is_ascii() {
50        chr as u8
51    } else {
52        custom_ascii_code(chr)
53    }
54}
55
56#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
57#[derive(Debug, PartialEq)]
58pub struct Text {
59    content: Vec<Vec<u8>>,
60    pos: TextPos,
61    formatting: TextFormat,
62    bounds: Rect,
63}
64
65impl Text {
66    pub fn new<P: Into<TextPos>, F: Into<TextFormat>>(
67        content: &str,
68        pos: P,
69        formatting: F,
70    ) -> Self {
71        let formatting = formatting.into();
72        let content = formatting.wrapping().wrap(content);
73        let content: Vec<Vec<u8>> = content
74            .iter()
75            .map(|line| line.chars().map(chr_to_code).collect::<Vec<u8>>())
76            .collect();
77        let pos = pos.into();
78        let bounds = Self::calc_bounds(pos, &formatting, &content);
79        Self {
80            content,
81            pos,
82            formatting,
83            bounds,
84        }
85    }
86
87    fn calc_bounds(pos: TextPos, formatting: &TextFormat, text: &[Vec<u8>]) -> Rect {
88        let content = text
89            .iter()
90            .map(|chrs| String::from_utf8_lossy(chrs).to_string())
91            .collect::<Vec<String>>()
92            .join("\n");
93        let pos_coord = pos.to_coord(formatting.font());
94        let (w, h) = formatting.font().measure(&content);
95        Rect::new_with_size(formatting.positioning().calc(pos_coord, w, h), w, h)
96    }
97}
98
99impl Text {
100    #[inline]
101    pub fn pos(&self) -> TextPos {
102        self.pos
103    }
104
105    #[inline]
106    pub fn formatting(&self) -> &TextFormat {
107        &self.formatting
108    }
109
110    #[inline]
111    pub fn bounds(&self) -> &Rect {
112        &self.bounds
113    }
114
115    #[inline]
116    pub fn contents(&self) -> &[Vec<u8>] {
117        &self.content
118    }
119
120    fn with_formatting<F: Into<TextFormat>>(&self, format: F) -> Self {
121        let format = format.into();
122        Text {
123            content: self.content.clone(),
124            pos: self.pos,
125            formatting: format,
126            bounds: self.bounds.clone(),
127        }
128    }
129
130    #[inline]
131    pub fn with_color(&self, color: Color) -> Self {
132        self.with_formatting(self.formatting().with_color(color))
133    }
134
135    pub fn with_pos<P: Into<TextPos>>(&self, pos: P) -> Self {
136        let pos = pos.into();
137        Text {
138            content: self.content.clone(),
139            pos,
140            formatting: self.formatting.clone(),
141            bounds: Self::calc_bounds(pos, &self.formatting, &self.content),
142        }
143    }
144}
145
146impl Renderable<Text> for Text {
147    fn render(&self, graphics: &mut Graphics) {
148        graphics.draw_ascii(&self.content, self.pos, self.formatting.clone());
149    }
150}
151
152/// Return size in pixels for text
153/// Run `text` through wrapping strategy first
154pub fn measure_text(text: &str, char_width: usize, line_height: usize) -> (usize, usize) {
155    let lines = text.split('\n').collect::<Vec<_>>();
156    let longest_len = lines.iter().map(|line| line.len()).max().unwrap();
157    (longest_len * char_width, lines.len() * line_height)
158}
159
160/// PixelFont is used to set the size and positioning in pixels of text
161#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
162#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Default)]
163pub enum PixelFont {
164    Outline7x9,
165    Standard4x4,
166    Standard4x5,
167    #[default]
168    Standard6x7,
169    Standard8x10,
170    Script8x8,
171    /// No lower case, some symbols look bad
172    Limited3x5,
173}
174
175impl PixelFont {
176    /// Returns width, height of text size in pixels
177    #[inline]
178    pub const fn size(&self) -> (usize, usize) {
179        match self {
180            PixelFont::Outline7x9 => (outline_7x9::CHAR_WIDTH, outline_7x9::CHAR_HEIGHT),
181            PixelFont::Standard4x4 => (standard_4x4::CHAR_WIDTH, standard_4x4::CHAR_HEIGHT),
182            PixelFont::Standard4x5 => (standard_4x5::CHAR_WIDTH, standard_4x5::CHAR_HEIGHT),
183            PixelFont::Standard6x7 => (standard_6x7::CHAR_WIDTH, standard_6x7::CHAR_HEIGHT),
184            PixelFont::Standard8x10 => (standard_8x10::CHAR_WIDTH, standard_8x10::CHAR_HEIGHT),
185            PixelFont::Script8x8 => (script_8x8::CHAR_WIDTH, script_8x8::CHAR_HEIGHT),
186            PixelFont::Limited3x5 => (limited_3x5::CHAR_WIDTH, limited_3x5::CHAR_HEIGHT),
187        }
188    }
189
190    /// Return size in pixels for text
191    /// Run `text` through wrapping strategy first
192    #[inline]
193    pub fn measure(&self, text: &str) -> (usize, usize) {
194        measure_text(text, self.char_width(), self.line_height())
195    }
196
197    #[inline]
198    pub fn char_width(&self) -> usize {
199        self.size().0 + self.spacing()
200    }
201
202    #[inline]
203    pub fn line_height(&self) -> usize {
204        self.size().1 + self.spacing()
205    }
206
207    /// Returns the spacing between letters in pixels
208    #[inline]
209    pub const fn spacing(&self) -> usize {
210        match self {
211            PixelFont::Outline7x9 => 0,
212            PixelFont::Standard4x4 => 1,
213            PixelFont::Standard4x5 => 1,
214            PixelFont::Standard6x7 => 1,
215            PixelFont::Standard8x10 => 2,
216            PixelFont::Script8x8 => 1,
217            PixelFont::Limited3x5 => 1,
218        }
219    }
220
221    #[inline]
222    pub const fn pixels(&self, code: u8) -> &[bool] {
223        match self {
224            PixelFont::Outline7x9 => outline_7x9::get_px_ascii(code),
225            PixelFont::Standard4x4 => standard_4x4::get_px_ascii(code),
226            PixelFont::Standard4x5 => standard_4x5::get_px_ascii(code),
227            PixelFont::Standard6x7 => standard_6x7::get_px_ascii(code),
228            PixelFont::Standard8x10 => standard_8x10::get_px_ascii(code),
229            PixelFont::Script8x8 => script_8x8::get_px_ascii(code),
230            PixelFont::Limited3x5 => limited_3x5::get_px_ascii(code),
231        }
232    }
233
234    /// Converts pixels to columns
235    #[inline]
236    pub const fn px_to_cols(&self, px: usize) -> usize {
237        px / (self.size().0 + self.spacing())
238    }
239
240    /// Returns the max of (columns, rows) for this text size for the specified screen size
241    pub fn get_max_characters(&self, screen_width: usize, screen_height: usize) -> (usize, usize) {
242        let size = self.size();
243        if screen_width < size.0 || screen_height < size.1 {
244            return (0, 0);
245        }
246        let sw = screen_width as f32;
247        let cw = (size.0 + self.spacing()) as f32;
248        let sh = screen_height as f32;
249        let ch = (size.1 + self.spacing()) as f32;
250        let columns = (sw / cw).floor() as usize;
251        let rows = (sh / ch).floor() as usize;
252        (columns - 1, rows - 1)
253    }
254}