rlvgl_core/plugins/
fontdue.rs1use crate::widget::Color;
3use alloc::vec::Vec;
4use blake3;
5use fontdue::{Font, FontResult, FontSettings};
6pub use fontdue::{LineMetrics, Metrics};
7use once_cell::sync::OnceCell;
8use std::collections::HashMap;
9use std::sync::Mutex;
10
11static FONT_CACHE: OnceCell<Mutex<HashMap<u64, Font>>> = OnceCell::new();
13
14fn hash_font_data(font_data: &[u8]) -> u64 {
16 let key = blake3::hash(font_data);
17 u64::from_le_bytes(key.as_bytes()[..8].try_into().unwrap())
18}
19
20fn get_cached_font(font_data: &[u8]) -> Font {
22 let key = hash_font_data(font_data);
23 let cache = FONT_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
24 let mut map = cache.lock().unwrap();
25
26 map.entry(key)
27 .or_insert_with(|| {
28 Font::from_bytes(font_data, FontSettings::default()).expect("valid font")
29 })
30 .clone()
31}
32
33pub fn rasterize_glyph(font_data: &[u8], ch: char, px: f32) -> FontResult<(Metrics, Vec<u8>)> {
41 let font = get_cached_font(font_data);
42 Ok(font.rasterize(ch, px))
43}
44
45pub fn line_metrics(font_data: &[u8], px: f32) -> FontResult<LineMetrics> {
50 let font = Font::from_bytes(font_data, FontSettings::default())?;
51 font.horizontal_line_metrics(px)
52 .ok_or("missing horizontal metrics")
53}
54
55pub trait FontdueRenderTarget {
57 fn dimensions(&self) -> (usize, usize);
59
60 fn blend_pixel(&mut self, x: i32, y: i32, color: Color, alpha: u8);
62}
63
64pub fn render_text<R: FontdueRenderTarget>(
66 target: &mut R,
67 font_data: &[u8],
68 position: (i32, i32),
69 text: &str,
70 color: Color,
71 px: f32,
72) -> FontResult<()> {
73 let vm = line_metrics(font_data, px)?;
74 let ascent = vm.ascent.round() as i32;
75 let baseline = position.1 + ascent;
76 let (width, height) = target.dimensions();
77 let mut x_cursor = position.0;
78 for ch in text.chars() {
79 if let Ok((metrics, bitmap)) = rasterize_glyph(font_data, ch, px) {
80 let w = metrics.width as i32;
81 let h = metrics.height as i32;
82 let draw_y = baseline - ascent - metrics.ymin;
83 for y in 0..h {
84 let py = draw_y - y;
85 if py < 0 || (py as usize) >= height {
86 continue;
87 }
88 for x in 0..w {
89 let px_coord = x_cursor + metrics.xmin + x;
90 if px_coord < 0 || (px_coord as usize) >= width {
91 continue;
92 }
93 let alpha = bitmap[(h - 1 - y) as usize * metrics.width + x as usize];
94 if alpha > 0 {
95 target.blend_pixel(px_coord, py, color, alpha);
96 }
97 }
98 }
99 x_cursor += metrics.advance_width.round() as i32;
100 }
101 }
102 Ok(())
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 const FONT_DATA: &[u8] = include_bytes!("../../../assets/fonts/DejaVuSans.ttf");
110
111 #[test]
112 fn rasterize_a() {
113 let (metrics, bitmap) = rasterize_glyph(FONT_DATA, 'A', 16.0).unwrap();
114 assert_eq!(bitmap.len(), metrics.width * metrics.height);
115 assert!(metrics.width > 0 && metrics.height > 0);
116 }
117
118 #[test]
119 fn line_metrics_present() {
120 let vm = line_metrics(FONT_DATA, 16.0).unwrap();
121 assert!(vm.ascent > 0.0 && vm.descent < 0.0);
122 }
123
124 struct Surface {
125 buf: [u8; 32 * 32 * 4],
126 }
127
128 impl Surface {
129 fn new() -> Self {
130 Self {
131 buf: [0; 32 * 32 * 4],
132 }
133 }
134 }
135
136 impl FontdueRenderTarget for Surface {
137 fn dimensions(&self) -> (usize, usize) {
138 (32, 32)
139 }
140
141 fn blend_pixel(&mut self, x: i32, y: i32, color: Color, alpha: u8) {
142 if x >= 0 && y >= 0 && x < 32 && y < 32 {
143 let idx = ((y as usize) * 32 + x as usize) * 4;
144 let r = (color.0 as u16 * alpha as u16 / 255) as u8;
145 let g = (color.1 as u16 * alpha as u16 / 255) as u8;
146 let b = (color.2 as u16 * alpha as u16 / 255) as u8;
147 self.buf[idx] = r;
148 self.buf[idx + 1] = g;
149 self.buf[idx + 2] = b;
150 self.buf[idx + 3] = 0xff;
151 }
152 }
153 }
154
155 #[test]
156 fn render_text_draws_pixels() {
157 let mut surf = Surface::new();
158 render_text(
159 &mut surf,
160 FONT_DATA,
161 (0, 0),
162 "A",
163 Color(255, 255, 255, 255),
164 16.0,
165 )
166 .unwrap();
167 assert!(surf.buf.iter().any(|&p| p != 0));
168 }
169}