1use std::cell::RefCell;
8use std::collections::HashMap;
9use std::sync::LazyLock;
10
11use truce_core::cast::len_u32;
12
13pub use truce_font::JETBRAINS_MONO;
14
15struct CachedGlyph {
17 bitmap: Vec<u8>, width: u32,
19 height: u32,
20 advance: f32, y_offset: f32, }
23
24struct GlyphCache {
25 font: fontdue::Font,
26 glyphs: HashMap<(char, u32), CachedGlyph>,
27}
28
29thread_local! {
37 static CACHE: RefCell<Option<GlyphCache>> = const { RefCell::new(None) };
38}
39
40fn with_cache<R>(f: impl FnOnce(&mut GlyphCache) -> R) -> R {
41 CACHE.with(|cell| {
42 let mut guard = cell.borrow_mut();
43 let cache = guard.get_or_insert_with(|| {
44 let font = fontdue::Font::from_bytes(JETBRAINS_MONO, fontdue::FontSettings::default())
45 .expect("failed to parse embedded font");
46 GlyphCache {
47 font,
48 glyphs: HashMap::new(),
49 }
50 });
51 f(cache)
52 })
53}
54
55#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
58fn size_key(size: f32) -> u32 {
59 (size * 10.0) as u32
60}
61
62#[allow(clippy::cast_precision_loss)]
67fn get_glyph(cache: &mut GlyphCache, ch: char, size: f32) -> &CachedGlyph {
68 let key = (ch, size_key(size));
69 let GlyphCache { font, glyphs } = cache;
70 glyphs.entry(key).or_insert_with(|| {
71 let (metrics, bitmap) = font.rasterize(ch, size);
72 CachedGlyph {
73 bitmap,
74 width: len_u32(metrics.width),
75 height: len_u32(metrics.height),
76 advance: metrics.advance_width,
77 y_offset: metrics.ymin as f32,
78 }
79 })
80}
81
82#[allow(clippy::cast_precision_loss)]
86static SRGB_TO_LINEAR: LazyLock<[f32; 256]> = LazyLock::new(|| {
87 let mut table = [0.0f32; 256];
88 for (i, slot) in table.iter_mut().enumerate() {
89 let s = i as f32 / 255.0;
90 *slot = if s <= 0.04045 {
91 s / 12.92
92 } else {
93 ((s + 0.055) / 1.055).powf(2.4)
94 };
95 }
96 table
97});
98
99#[inline]
100fn srgb_f32_to_linear(s: f32) -> f32 {
101 if s <= 0.04045 {
102 s / 12.92
103 } else {
104 ((s + 0.055) / 1.055).powf(2.4)
105 }
106}
107
108#[inline]
109#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
112fn linear_to_srgb_u8(lin: f32) -> u8 {
113 let lin = lin.clamp(0.0, 1.0);
114 let s = if lin <= 0.003_130_8 {
115 12.92 * lin
116 } else {
117 1.055 * lin.powf(1.0 / 2.4) - 0.055
118 };
119 (s * 255.0 + 0.5) as u8
120}
121
122#[allow(
143 clippy::cast_precision_loss,
144 clippy::cast_sign_loss,
145 clippy::cast_possible_wrap,
146 clippy::many_single_char_names
147)]
148pub fn draw_text_fontdue(
149 pixmap_data: &mut [u8],
150 pixmap_width: u32,
151 pixmap_height: u32,
152 text: &str,
153 x: f32,
154 y: f32,
155 size: f32,
156 r: f32,
157 g: f32,
158 b: f32,
159 a: f32,
160) {
161 with_cache(|cache| {
162 let mut cursor_x = x;
163
164 let line_metrics = cache.font.horizontal_line_metrics(size);
165 let ascent = line_metrics.map_or(size * 0.8, |m| m.ascent);
166
167 let src_lin_r = srgb_f32_to_linear(r.clamp(0.0, 1.0));
170 let src_lin_g = srgb_f32_to_linear(g.clamp(0.0, 1.0));
171 let src_lin_b = srgb_f32_to_linear(b.clamp(0.0, 1.0));
172
173 for ch in text.chars() {
174 let glyph = get_glyph(cache, ch, size);
175 let gw = glyph.width;
176 let gh = glyph.height;
177
178 #[allow(clippy::cast_possible_truncation)]
180 let gx = cursor_x as i32;
181 #[allow(clippy::cast_possible_truncation)]
182 let gy = (y + ascent - glyph.y_offset - gh as f32) as i32;
183
184 for row in 0..gh {
185 for col in 0..gw {
186 let px = gx + col as i32;
187 let py = gy + row as i32;
188
189 if px < 0 || py < 0 || px >= pixmap_width as i32 || py >= pixmap_height as i32 {
190 continue;
191 }
192
193 let coverage = glyph.bitmap[(row * gw + col) as usize];
194 if coverage == 0 {
195 continue;
196 }
197
198 let ga = (f32::from(coverage) / 255.0) * a;
199 let idx = ((py as u32 * pixmap_width + px as u32) * 4) as usize;
200 if idx + 3 >= pixmap_data.len() {
201 continue;
202 }
203
204 let dst_lin_r = SRGB_TO_LINEAR[pixmap_data[idx] as usize];
209 let dst_lin_g = SRGB_TO_LINEAR[pixmap_data[idx + 1] as usize];
210 let dst_lin_b = SRGB_TO_LINEAR[pixmap_data[idx + 2] as usize];
211 let dst_a = f32::from(pixmap_data[idx + 3]) / 255.0;
212
213 let inv_sa = 1.0 - ga;
214 let out_lin_r = src_lin_r * ga + dst_lin_r * inv_sa;
215 let out_lin_g = src_lin_g * ga + dst_lin_g * inv_sa;
216 let out_lin_b = src_lin_b * ga + dst_lin_b * inv_sa;
217 let out_a = ga + dst_a * inv_sa;
218
219 pixmap_data[idx] = linear_to_srgb_u8(out_lin_r);
220 pixmap_data[idx + 1] = linear_to_srgb_u8(out_lin_g);
221 pixmap_data[idx + 2] = linear_to_srgb_u8(out_lin_b);
222 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
224 let out_a_u8 = (out_a * 255.0 + 0.5) as u8;
225 pixmap_data[idx + 3] = out_a_u8;
226 }
227 }
228
229 cursor_x += glyph.advance;
230 }
231 });
232}
233
234#[must_use]
236pub fn text_width_fontdue(text: &str, size: f32) -> f32 {
237 with_cache(|cache| {
238 let mut width = 0.0f32;
239 for ch in text.chars() {
240 let glyph = get_glyph(cache, ch, size);
241 width += glyph.advance;
242 }
243 width
244 })
245}