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}