1use std::cell::RefCell;
9use std::collections::HashMap;
10use std::sync::LazyLock;
11
12pub use truce_font::JETBRAINS_MONO;
13
14struct CachedGlyph {
16 bitmap: Vec<u8>, width: u32,
18 height: u32,
19 advance: f32, y_offset: f32, }
22
23struct GlyphCache {
24 font: truce_font::raster::FontRaster,
25 glyphs: HashMap<(char, u32), CachedGlyph>,
26}
27
28thread_local! {
36 static CACHE: RefCell<Option<GlyphCache>> = const { RefCell::new(None) };
37}
38
39fn with_cache<R>(f: impl FnOnce(&mut GlyphCache) -> R) -> R {
40 CACHE.with(|cell| {
41 let mut guard = cell.borrow_mut();
42 let cache = guard.get_or_insert_with(|| GlyphCache {
43 font: truce_font::raster::FontRaster::new(),
44 glyphs: HashMap::new(),
45 });
46 f(cache)
47 })
48}
49
50#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
53fn size_key(size: f32) -> u32 {
54 (size * 10.0) as u32
55}
56
57fn get_glyph(cache: &mut GlyphCache, ch: char, size: f32) -> &CachedGlyph {
59 let key = (ch, size_key(size));
60 let GlyphCache { font, glyphs } = cache;
61 glyphs.entry(key).or_insert_with(|| {
62 let glyph = font.rasterize(ch, size);
63 CachedGlyph {
64 bitmap: glyph.coverage,
65 width: glyph.width,
66 height: glyph.height,
67 advance: glyph.advance,
68 y_offset: glyph.y_min,
69 }
70 })
71}
72
73#[allow(clippy::cast_precision_loss)]
77static SRGB_TO_LINEAR: LazyLock<[f32; 256]> = LazyLock::new(|| {
78 let mut table = [0.0f32; 256];
79 for (i, slot) in table.iter_mut().enumerate() {
80 let s = i as f32 / 255.0;
81 *slot = if s <= 0.04045 {
82 s / 12.92
83 } else {
84 ((s + 0.055) / 1.055).powf(2.4)
85 };
86 }
87 table
88});
89
90#[inline]
91fn srgb_f32_to_linear(s: f32) -> f32 {
92 if s <= 0.04045 {
93 s / 12.92
94 } else {
95 ((s + 0.055) / 1.055).powf(2.4)
96 }
97}
98
99#[inline]
100#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
103fn linear_to_srgb_u8(lin: f32) -> u8 {
104 let lin = lin.clamp(0.0, 1.0);
105 let s = if lin <= 0.003_130_8 {
106 12.92 * lin
107 } else {
108 1.055 * lin.powf(1.0 / 2.4) - 0.055
109 };
110 (s * 255.0 + 0.5) as u8
111}
112
113#[allow(
134 clippy::cast_precision_loss,
135 clippy::cast_sign_loss,
136 clippy::cast_possible_wrap,
137 clippy::many_single_char_names
138)]
139pub fn draw_text(
140 pixmap_data: &mut [u8],
141 pixmap_width: u32,
142 pixmap_height: u32,
143 text: &str,
144 x: f32,
145 y: f32,
146 size: f32,
147 r: f32,
148 g: f32,
149 b: f32,
150 a: f32,
151) {
152 with_cache(|cache| {
153 let mut cursor_x = x;
154
155 let ascent = cache.font.ascent(size);
156
157 let src_lin_r = srgb_f32_to_linear(r.clamp(0.0, 1.0));
160 let src_lin_g = srgb_f32_to_linear(g.clamp(0.0, 1.0));
161 let src_lin_b = srgb_f32_to_linear(b.clamp(0.0, 1.0));
162
163 for ch in text.chars() {
164 let glyph = get_glyph(cache, ch, size);
165 let gw = glyph.width;
166 let gh = glyph.height;
167
168 #[allow(clippy::cast_possible_truncation)]
170 let gx = cursor_x as i32;
171 #[allow(clippy::cast_possible_truncation)]
172 let gy = (y + ascent - glyph.y_offset - gh as f32) as i32;
173
174 for row in 0..gh {
175 for col in 0..gw {
176 let px = gx + col as i32;
177 let py = gy + row as i32;
178
179 if px < 0 || py < 0 || px >= pixmap_width as i32 || py >= pixmap_height as i32 {
180 continue;
181 }
182
183 let coverage = glyph.bitmap[(row * gw + col) as usize];
184 if coverage == 0 {
185 continue;
186 }
187
188 let ga = (f32::from(coverage) / 255.0) * a;
189 let idx = ((py as u32 * pixmap_width + px as u32) * 4) as usize;
190 if idx + 3 >= pixmap_data.len() {
191 continue;
192 }
193
194 let dst_lin_r = SRGB_TO_LINEAR[pixmap_data[idx] as usize];
199 let dst_lin_g = SRGB_TO_LINEAR[pixmap_data[idx + 1] as usize];
200 let dst_lin_b = SRGB_TO_LINEAR[pixmap_data[idx + 2] as usize];
201 let dst_a = f32::from(pixmap_data[idx + 3]) / 255.0;
202
203 let inv_sa = 1.0 - ga;
204 let out_lin_r = src_lin_r * ga + dst_lin_r * inv_sa;
205 let out_lin_g = src_lin_g * ga + dst_lin_g * inv_sa;
206 let out_lin_b = src_lin_b * ga + dst_lin_b * inv_sa;
207 let out_a = ga + dst_a * inv_sa;
208
209 pixmap_data[idx] = linear_to_srgb_u8(out_lin_r);
210 pixmap_data[idx + 1] = linear_to_srgb_u8(out_lin_g);
211 pixmap_data[idx + 2] = linear_to_srgb_u8(out_lin_b);
212 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
214 let out_a_u8 = (out_a * 255.0 + 0.5) as u8;
215 pixmap_data[idx + 3] = out_a_u8;
216 }
217 }
218
219 cursor_x += glyph.advance;
220 }
221 });
222}
223
224#[must_use]
226pub fn text_width(text: &str, size: f32) -> f32 {
227 with_cache(|cache| {
228 let mut width = 0.0f32;
229 for ch in text.chars() {
230 let glyph = get_glyph(cache, ch, size);
231 width += glyph.advance;
232 }
233 width
234 })
235}