Skip to main content

embedded_gui/
text.rs

1use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
2
3use crate::render::{
4    CHAR_HEIGHT, CHAR_WIDTH, TextAlign, TextMetrics, TextStyle, TextWrap, VerticalAlign,
5};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub struct Span<'a> {
9    pub content: &'a str,
10    pub style: TextStyle,
11}
12
13impl<'a> Span<'a> {
14    pub const fn raw(content: &'a str) -> Self {
15        Self {
16            content,
17            style: TextStyle::new(Rgb565::WHITE),
18        }
19    }
20
21    pub const fn styled(content: &'a str, style: TextStyle) -> Self {
22        Self { content, style }
23    }
24
25    pub fn width_chars(&self) -> usize {
26        self.content.chars().filter(|&ch| ch != '\n').count()
27    }
28
29    pub fn metrics(&self) -> TextMetrics {
30        TextMetrics {
31            width: self.width_chars() as u32 * self.style.font.advance(),
32            height: self.style.font.line_height(),
33        }
34    }
35}
36
37impl<'a> From<&'a str> for Span<'a> {
38    fn from(value: &'a str) -> Self {
39        Self::raw(value)
40    }
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub struct Line<'a> {
45    pub spans: &'a [Span<'a>],
46    pub align: TextAlign,
47}
48
49impl<'a> Line<'a> {
50    pub const fn from_spans(spans: &'a [Span<'a>]) -> Self {
51        Self {
52            spans,
53            align: TextAlign::Left,
54        }
55    }
56
57    pub const fn aligned(mut self, align: TextAlign) -> Self {
58        self.align = align;
59        self
60    }
61
62    pub fn width_chars(&self) -> usize {
63        self.widest_line_chars(u32::MAX, TextWrap::None)
64    }
65
66    pub fn char_count(&self) -> usize {
67        self.spans
68            .iter()
69            .map(|span| span.content.chars().count())
70            .sum()
71    }
72
73    pub fn metrics(&self) -> TextMetrics {
74        let mut width = 0u32;
75        let mut height = CHAR_HEIGHT;
76        for span in self.spans {
77            width = width.saturating_add(
78                span.content.chars().filter(|&ch| ch != '\n').count() as u32
79                    * span.style.font.advance(),
80            );
81            height = height.max(span.style.font.line_height());
82        }
83        TextMetrics { width, height }
84    }
85
86    pub fn visual_line_count(&self, max_width: u32, wrap: TextWrap) -> usize {
87        let char_count = self.char_count();
88        if char_count == 0 {
89            return 1;
90        }
91
92        let mut lines = 0;
93        let mut start = 0;
94        while start < char_count {
95            let (len, consumed_newline) = self.segment_len_at(start, max_width, wrap);
96            lines += 1;
97            start += len + usize::from(consumed_newline);
98            if len == 0 && !consumed_newline {
99                break;
100            }
101        }
102        lines.max(1)
103    }
104
105    pub fn widest_line_chars(&self, max_width: u32, wrap: TextWrap) -> usize {
106        let char_count = self.char_count();
107        let mut widest = 0;
108        let mut start = 0;
109        while start < char_count {
110            let (len, consumed_newline) = self.segment_len_at(start, max_width, wrap);
111            widest = widest.max(len);
112            start += len + usize::from(consumed_newline);
113            if len == 0 && !consumed_newline {
114                break;
115            }
116        }
117        widest
118    }
119
120    pub fn widest_line_width(&self, max_width: u32, wrap: TextWrap) -> u32 {
121        let char_count = self.char_count();
122        let mut widest = 0u32;
123        let mut start = 0;
124        while start < char_count {
125            let (len, consumed_newline) = self.segment_len_at(start, max_width, wrap);
126            widest = widest.max(self.segment_width(start, len));
127            start += len + usize::from(consumed_newline);
128            if len == 0 && !consumed_newline {
129                break;
130            }
131        }
132        widest
133    }
134
135    pub fn max_line_height(&self) -> u32 {
136        self.spans
137            .iter()
138            .map(|span| span.style.font.line_height())
139            .max()
140            .unwrap_or(CHAR_HEIGHT)
141    }
142
143    pub(crate) fn segment_len_at(
144        &self,
145        start: usize,
146        max_width: u32,
147        wrap: TextWrap,
148    ) -> (usize, bool) {
149        let mut len = 0;
150        let mut width = 0u32;
151        let min_advance = self
152            .spans
153            .iter()
154            .map(|span| span.style.font.advance())
155            .min()
156            .unwrap_or(CHAR_WIDTH)
157            .max(1);
158        let limit_width = max_width.max(min_advance);
159        let mut last_ws_break = None;
160
161        for (ch, style) in self
162            .spans
163            .iter()
164            .flat_map(|span| span.content.chars().map(move |ch| (ch, span.style)))
165            .skip(start)
166        {
167            if ch == '\n' {
168                return (len, true);
169            }
170            if matches!(wrap, TextWrap::Character | TextWrap::Word) {
171                let advance = style.font.advance();
172                if len > 0 && width.saturating_add(advance) > limit_width {
173                    if matches!(wrap, TextWrap::Word) {
174                        if let Some(idx) = last_ws_break {
175                            return (idx, false);
176                        }
177                    }
178                    return (len, false);
179                }
180                width = width.saturating_add(advance);
181            }
182            if matches!(wrap, TextWrap::Word) && ch.is_whitespace() {
183                last_ws_break = Some(len + 1);
184            }
185            len += 1;
186        }
187
188        (len, false)
189    }
190
191    pub(crate) fn segment_width(&self, start: usize, len: usize) -> u32 {
192        self.spans
193            .iter()
194            .flat_map(|span| span.content.chars().map(move |ch| (ch, span.style)))
195            .enumerate()
196            .filter_map(|(idx, (ch, style))| {
197                if idx < start || idx >= start + len || ch == '\n' {
198                    None
199                } else {
200                    Some(style.font.advance())
201                }
202            })
203            .sum()
204    }
205}
206
207#[derive(Clone, Copy, Debug, PartialEq, Eq)]
208pub struct Text<'a> {
209    pub lines: &'a [Line<'a>],
210    pub align: TextAlign,
211    pub vertical_align: VerticalAlign,
212    pub wrap: TextWrap,
213    pub line_spacing: u8,
214}
215
216impl<'a> Text<'a> {
217    pub const fn from_lines(lines: &'a [Line<'a>]) -> Self {
218        Self {
219            lines,
220            align: TextAlign::Left,
221            vertical_align: VerticalAlign::Top,
222            wrap: TextWrap::None,
223            line_spacing: 1,
224        }
225    }
226
227    pub const fn aligned(mut self, align: TextAlign) -> Self {
228        self.align = align;
229        self
230    }
231
232    pub const fn vertical_aligned(mut self, align: VerticalAlign) -> Self {
233        self.vertical_align = align;
234        self
235    }
236
237    pub const fn wrapped(mut self, wrap: TextWrap) -> Self {
238        self.wrap = wrap;
239        self
240    }
241
242    pub const fn line_spacing(mut self, spacing: u8) -> Self {
243        self.line_spacing = spacing;
244        self
245    }
246
247    pub fn metrics(&self, max_width: u32) -> TextMetrics {
248        let mut lines = 0usize;
249        let mut widest = 0u32;
250        let mut max_line_height = CHAR_HEIGHT;
251
252        for line in self.lines {
253            lines += line.visual_line_count(max_width, self.wrap);
254            widest = widest.max(line.widest_line_width(max_width, self.wrap));
255            max_line_height = max_line_height.max(line.max_line_height());
256        }
257
258        let lines = lines.max(1);
259        TextMetrics {
260            width: widest.min(max_width),
261            height: lines as u32 * max_line_height
262                + lines.saturating_sub(1) as u32 * self.line_spacing as u32,
263        }
264    }
265}
266
267#[derive(Clone, Copy, Debug, PartialEq, Eq)]
268pub enum TextDirection {
269    Ltr,
270    Rtl,
271}
272
273#[derive(Clone, Copy, Debug, PartialEq, Eq)]
274pub struct ShapingConfig {
275    pub direction: TextDirection,
276    pub language_tag: Option<&'static str>,
277    pub enable_ligatures: bool,
278}
279
280impl Default for ShapingConfig {
281    fn default() -> Self {
282        Self {
283            direction: TextDirection::Ltr,
284            language_tag: None,
285            enable_ligatures: true,
286        }
287    }
288}
289
290#[derive(Clone, Copy, Debug, PartialEq, Eq)]
291pub struct ShapedGlyph {
292    pub ch: char,
293    pub x_advance: i16,
294}
295
296pub trait TextShaper {
297    fn shape<const N: usize>(
298        &self,
299        text: &str,
300        config: ShapingConfig,
301        out: &mut heapless::Vec<ShapedGlyph, N>,
302    );
303}
304
305#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
306pub struct BasicTextShaper;
307
308impl TextShaper for BasicTextShaper {
309    fn shape<const N: usize>(
310        &self,
311        text: &str,
312        config: ShapingConfig,
313        out: &mut heapless::Vec<ShapedGlyph, N>,
314    ) {
315        out.clear();
316        let iter = text.chars();
317        if matches!(config.direction, TextDirection::Rtl) {
318            for ch in iter.rev() {
319                let _ = out.push(ShapedGlyph { ch, x_advance: 1 });
320            }
321        } else {
322            for ch in iter {
323                let _ = out.push(ShapedGlyph { ch, x_advance: 1 });
324            }
325        }
326    }
327}