1use alloc::vec::Vec;
8use core::fmt;
9
10use crate::widget::Rect;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[repr(transparent)]
15pub struct FontId(pub u16);
16
17impl FontId {
18 pub const DEFAULT: Self = Self(0);
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct GlyphInfo {
25 pub advance_fp16: u16,
27 pub bearing_x: i16,
29 pub bearing_y: i16,
31 pub width: u16,
33 pub height: u16,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct FontLineMetrics {
40 pub line_height: u16,
42 pub ascent: i16,
44 pub descent: i16,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct GlyphPlacement {
51 pub ch: char,
53 pub info: GlyphInfo,
55 pub x: i32,
57 pub y: i32,
59}
60
61impl GlyphPlacement {
62 pub fn extent(&self) -> Rect {
64 Rect {
65 x: self.x + self.info.bearing_x as i32,
66 y: self.y - self.info.bearing_y as i32,
67 width: self.info.width as i32,
68 height: self.info.height as i32,
69 }
70 }
71}
72
73#[derive(Clone)]
75pub struct ShapedText<'a> {
76 pub glyphs: Vec<GlyphPlacement>,
78 pub total_advance_fp16: i32,
80 pub bounds: Rect,
82 pub bidi_level: u8,
84 pub font: Option<&'a dyn FontMetrics>,
91}
92
93impl fmt::Debug for ShapedText<'_> {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 f.debug_struct("ShapedText")
96 .field("glyphs", &self.glyphs)
97 .field("total_advance_fp16", &self.total_advance_fp16)
98 .field("bounds", &self.bounds)
99 .field("bidi_level", &self.bidi_level)
100 .field("has_font", &self.font.is_some())
101 .finish()
102 }
103}
104
105impl PartialEq for ShapedText<'_> {
106 fn eq(&self, other: &Self) -> bool {
107 self.glyphs == other.glyphs
108 && self.total_advance_fp16 == other.total_advance_fp16
109 && self.bounds == other.bounds
110 && self.bidi_level == other.bidi_level
111 }
112}
113
114impl Eq for ShapedText<'_> {}
115
116impl<'a> ShapedText<'a> {
117 pub fn empty(origin: (i32, i32)) -> Self {
119 Self {
120 glyphs: Vec::new(),
121 total_advance_fp16: 0,
122 bounds: Rect {
123 x: origin.0,
124 y: origin.1,
125 width: 0,
126 height: 0,
127 },
128 bidi_level: 0,
129 font: None,
130 }
131 }
132}
133
134pub trait FontMetrics {
138 fn glyph_metrics(&self, ch: char) -> Option<GlyphInfo>;
140
141 fn line_metrics(&self) -> FontLineMetrics;
143
144 fn glyph_coverage_row(
152 &self,
153 _ch: char,
154 _row: u16,
155 _x_offset: u16,
156 _coverage: &mut [u8],
157 ) -> bool {
158 false
159 }
160
161 fn measure_fp16(&self, text: &str) -> i32 {
163 measure_text_fp16(self, text, 0)
164 }
165
166 fn shape(&self, text: &str, origin: (i32, i32)) -> ShapedText<'_>
172 where
173 Self: Sized,
174 {
175 shape_text_ltr(self, text, origin, 0)
176 }
177}
178
179#[derive(Clone, Copy, Default)]
191pub struct WidgetFont(Option<&'static dyn FontMetrics>);
192
193impl WidgetFont {
194 pub const fn new() -> Self {
197 Self(None)
198 }
199
200 pub const fn with_font(font: &'static dyn FontMetrics) -> Self {
202 Self(Some(font))
203 }
204
205 pub fn set(&mut self, font: &'static dyn FontMetrics) {
207 self.0 = Some(font);
208 }
209
210 pub fn clear(&mut self) {
213 self.0 = None;
214 }
215
216 pub fn is_set(&self) -> bool {
218 self.0.is_some()
219 }
220
221 pub fn resolve(&self) -> &'static dyn FontMetrics {
223 match self.0 {
224 Some(font) => font,
225 None => &crate::bitmap_font::FONT_6X10,
226 }
227 }
228}
229
230#[derive(Clone, Copy)]
240pub struct FontRegistry<'a> {
241 entries: &'a [(FontId, &'static dyn FontMetrics)],
242}
243
244impl<'a> FontRegistry<'a> {
245 pub const fn new(entries: &'a [(FontId, &'static dyn FontMetrics)]) -> Self {
255 Self { entries }
256 }
257
258 pub fn resolve(&self, id: FontId) -> Option<&'static dyn FontMetrics> {
265 if id == FontId::DEFAULT {
266 return None;
267 }
268 self.entries
269 .iter()
270 .find(|(fid, _)| *fid == id)
271 .map(|(_, handle)| *handle)
272 }
273}
274
275impl FontRegistry<'static> {
276 pub const EMPTY: Self = Self { entries: &[] };
279}
280
281pub fn apply_font_registry(root: &crate::object::ObjectNode, registry: &FontRegistry<'_>) {
299 crate::style_cascade::resolve_tree_with_text(root, &mut |node, _style, text| {
300 let Some(handle) = registry.resolve(text.font_id) else {
301 return;
302 };
303 let widget = node.widget();
304 let mut w = widget.borrow_mut();
305 if let Some(slot) = w.widget_font_mut() {
306 slot.set(handle);
307 }
308 });
309}
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
313pub struct WrappedLine {
314 pub start: usize,
316 pub end: usize,
318 pub advance_fp16: i32,
320}
321
322#[derive(Debug, Clone, PartialEq, Eq)]
324pub struct WrappedText {
325 pub lines: Vec<WrappedLine>,
327 pub used_height: i32,
329}
330
331pub fn measure_text_fp16<F: FontMetrics + ?Sized>(
336 font: &F,
337 text: &str,
338 letter_spacing_px: i8,
339) -> i32 {
340 let mut total = 0i32;
341 let mut glyph_count = 0u32;
342 let spacing = letter_spacing_px as i32 * 16;
343 for ch in text.chars() {
344 if is_zero_width_break(ch) {
345 continue;
346 }
347 if glyph_count > 0 {
348 total += spacing;
349 }
350 total += glyph_advance_fp16(font, ch);
351 glyph_count += 1;
352 }
353 total.max(0)
354}
355
356pub fn shape_text_ltr<'a>(
358 font: &'a dyn FontMetrics,
359 text: &str,
360 origin: (i32, i32),
361 letter_spacing_px: i8,
362) -> ShapedText<'a> {
363 let mut shaped = ShapedText::empty(origin);
364 shaped.font = Some(font);
365 let mut cursor_fp16 = 0i32;
366 let mut has_bounds = false;
367 let mut glyph_count = 0u32;
368 let spacing = letter_spacing_px as i32 * 16;
369
370 for ch in text.chars() {
371 if is_zero_width_break(ch) {
372 continue;
373 }
374 if glyph_count > 0 {
375 cursor_fp16 += spacing;
376 }
377 let Some(info) = font.glyph_metrics(ch) else {
378 cursor_fp16 += fallback_advance_fp16(font);
379 glyph_count += 1;
380 continue;
381 };
382 let placement = GlyphPlacement {
383 ch,
384 info,
385 x: origin.0 + ((cursor_fp16 + 8) >> 4),
386 y: origin.1,
387 };
388 let extent = placement.extent();
389 shaped.bounds = if has_bounds {
390 shaped.bounds.union(extent)
391 } else {
392 has_bounds = true;
393 extent
394 };
395 shaped.glyphs.push(placement);
396 cursor_fp16 += info.advance_fp16 as i32;
397 glyph_count += 1;
398 }
399
400 shaped.total_advance_fp16 = cursor_fp16.max(0);
401 if !has_bounds {
402 shaped.bounds = Rect {
403 x: origin.0,
404 y: origin.1,
405 width: 0,
406 height: 0,
407 };
408 }
409 shaped
410}
411
412pub fn wrap_greedy_ltr<F: FontMetrics + ?Sized>(
419 font: &F,
420 text: &str,
421 max_width_px: i32,
422 letter_spacing_px: i8,
423 line_spacing_px: i8,
424) -> WrappedText {
425 let mut lines = Vec::new();
426 let mut paragraph_start = 0usize;
427
428 for (idx, ch) in text.char_indices() {
429 if ch == '\n' {
430 wrap_span(
431 font,
432 text,
433 paragraph_start,
434 idx,
435 max_width_px,
436 letter_spacing_px,
437 &mut lines,
438 );
439 paragraph_start = idx + ch.len_utf8();
440 }
441 }
442 wrap_span(
443 font,
444 text,
445 paragraph_start,
446 text.len(),
447 max_width_px,
448 letter_spacing_px,
449 &mut lines,
450 );
451
452 let metrics = font.line_metrics();
453 let line_count = lines.len() as i32;
454 let used_height = if line_count == 0 {
455 0
456 } else {
457 line_count * metrics.line_height as i32 + (line_count - 1) * line_spacing_px as i32
458 };
459 WrappedText { lines, used_height }
460}
461
462fn wrap_span<F: FontMetrics + ?Sized>(
463 font: &F,
464 text: &str,
465 start: usize,
466 end: usize,
467 max_width_px: i32,
468 letter_spacing_px: i8,
469 out: &mut Vec<WrappedLine>,
470) {
471 if start == end {
472 push_line(font, text, start, end, letter_spacing_px, out);
473 return;
474 }
475
476 let max_width_fp16 = max_width_px.max(0) * 16;
477 let mut line_start = skip_leading_spaces(text, start, end);
478
479 while line_start < end {
480 let mut line_end = line_start;
481 let mut last_break: Option<usize> = None;
482 let mut overflow_at: Option<usize> = None;
483
484 for (rel, ch) in text[line_start..end].char_indices() {
485 let abs = line_start + rel;
486 let candidate_end = abs + ch.len_utf8();
487 let width =
488 measure_text_fp16(font, &text[line_start..candidate_end], letter_spacing_px);
489 if width > max_width_fp16 {
490 overflow_at = Some(abs);
491 break;
492 }
493 line_end = candidate_end;
494 if is_soft_break(ch) {
495 last_break = Some(candidate_end);
496 }
497 }
498
499 match overflow_at {
500 None => {
501 push_line(
502 font,
503 text,
504 line_start,
505 trim_trailing_spaces(text, line_start, line_end),
506 letter_spacing_px,
507 out,
508 );
509 break;
510 }
511 Some(overflow) => {
512 if let Some(break_after) = last_break
513 && break_after > line_start
514 {
515 let soft_end = trim_trailing_spaces(text, line_start, break_after);
516 push_line(font, text, line_start, soft_end, letter_spacing_px, out);
517 line_start = skip_leading_spaces(text, break_after, end);
518 } else {
519 let hard_end = if overflow == line_start {
520 next_char_end(text, line_start, end).unwrap_or(end)
521 } else {
522 overflow
523 };
524 push_line(font, text, line_start, hard_end, letter_spacing_px, out);
525 line_start = skip_leading_spaces(text, hard_end, end);
526 }
527 }
528 }
529 }
530}
531
532fn push_line<F: FontMetrics + ?Sized>(
533 font: &F,
534 text: &str,
535 start: usize,
536 end: usize,
537 letter_spacing_px: i8,
538 out: &mut Vec<WrappedLine>,
539) {
540 let advance_fp16 = measure_text_fp16(font, &text[start..end], letter_spacing_px);
541 out.push(WrappedLine {
542 start,
543 end,
544 advance_fp16,
545 });
546}
547
548fn glyph_advance_fp16<F: FontMetrics + ?Sized>(font: &F, ch: char) -> i32 {
549 font.glyph_metrics(ch)
550 .map(|info| info.advance_fp16 as i32)
551 .unwrap_or_else(|| fallback_advance_fp16(font))
552}
553
554fn fallback_advance_fp16<F: FontMetrics + ?Sized>(font: &F) -> i32 {
555 ((font.line_metrics().line_height as i32 + 1) / 2) * 16
556}
557
558fn is_soft_break(ch: char) -> bool {
559 ch == ' ' || ch == '-' || is_zero_width_break(ch)
560}
561
562fn is_zero_width_break(ch: char) -> bool {
563 ch == '\u{200B}'
564}
565
566fn skip_leading_spaces(text: &str, mut start: usize, end: usize) -> usize {
567 while start < end {
568 let Some(ch) = text[start..end].chars().next() else {
569 break;
570 };
571 if ch != ' ' {
572 break;
573 }
574 start += ch.len_utf8();
575 }
576 start
577}
578
579fn trim_trailing_spaces(text: &str, start: usize, mut end: usize) -> usize {
580 while start < end {
581 let Some((idx, ch)) = text[start..end].char_indices().next_back() else {
582 break;
583 };
584 if ch != ' ' {
585 break;
586 }
587 end = start + idx;
588 }
589 end
590}
591
592fn next_char_end(text: &str, start: usize, end: usize) -> Option<usize> {
593 text[start..end]
594 .chars()
595 .next()
596 .map(|ch| start + ch.len_utf8())
597}
598
599#[cfg(test)]
600mod widget_font_tests {
601 use super::*;
602
603 #[test]
604 fn unset_resolves_to_default_font() {
605 let wf = WidgetFont::new();
606 assert!(!wf.is_set());
607 let lm = wf.resolve().line_metrics();
609 assert_eq!(
610 lm.line_height,
611 crate::bitmap_font::FONT_6X10.line_metrics().line_height
612 );
613 }
614
615 #[test]
616 fn set_then_resolve_returns_assigned_font() {
617 let mut wf = WidgetFont::with_font(&crate::bitmap_font::FONT_6X10);
622 assert!(wf.is_set());
623 assert!(wf.resolve().measure_fp16("A") > 0);
625 wf.clear();
626 assert!(!wf.is_set());
627 }
628}