1use crate::renderer::GpuRenderer;
2use crate::vertex::{InstanceData, Vertex};
3use cvkg_core::Rect;
4use std::sync::Arc;
5
6impl GpuRenderer {
7 pub fn clear_text_cache_impl(&mut self) {
9 self.text.shaped_cache.clear();
10 }
11
12 pub(crate) fn measure_text_impl(&mut self, text: &str, size: f32) -> (f32, f32) {
14 let cache_key = (text.to_string(), (size * 100.0) as u32);
15 if let Some(shaped) = self.text.shaped_cache.get(&cache_key) {
16 return (shaped.width, shaped.height);
17 }
18 let style = cvkg_runic_text::TextStyle::new("Inter", size);
19 let spans = [cvkg_runic_text::TextSpan::new(text, style)];
20 if let Some(shaped) = self.shape_rich_text_impl(
21 &spans,
22 None,
23 cvkg_runic_text::TextAlign::Start,
24 cvkg_runic_text::TextOverflow::Visible,
25 ) {
26 let shaped = std::sync::Arc::new(shaped);
27 let result = (shaped.width, shaped.height);
28 self.text.shaped_cache.put(cache_key, shaped);
29 result
30 } else {
31 (0.0, 0.0)
32 }
33 }
34
35 pub(crate) fn shape_rich_text_impl(
37 &mut self,
38 spans: &[cvkg_runic_text::TextSpan],
39 max_width: Option<f32>,
40 align: cvkg_runic_text::TextAlign,
41 overflow: cvkg_runic_text::TextOverflow,
42 ) -> Option<cvkg_runic_text::ShapedText> {
43 let sf = self.current_scale_factor();
44 let mut scaled_spans = spans.to_vec();
45 for span in &mut scaled_spans {
46 span.style.font_size *= sf;
47 if span.style.fallback_families.is_empty() {
48 span.style.fallback_families = vec![
49 "SF Pro".to_string(),
50 "Inter".to_string(),
51 "Helvetica Neue".to_string(),
52 "Helvetica".to_string(),
53 "Arial".to_string(),
54 "sans-serif".to_string(),
55 ];
56 }
57 }
58 let scaled_max_width = max_width.map(|w| w * sf);
59 self.text
60 .engine
61 .shape_layout(&scaled_spans, scaled_max_width, align, overflow)
62 .ok()
63 }
64
65 pub(crate) fn draw_shaped_text_impl(
67 &mut self,
68 shaped: &cvkg_runic_text::ShapedText,
69 x: f32,
70 y: f32,
71 ) {
72 for glyph in &shaped.glyphs {
73 let byte_idx = shaped
74 .grapheme_boundaries
75 .get(glyph.cluster as usize)
76 .copied()
77 .unwrap_or(0);
78 let mut span_color = [1.0, 1.0, 1.0, 1.0];
79 for span in &shaped.spans {
80 if byte_idx >= span.byte_offset && byte_idx < span.byte_offset + span.text.len() {
81 span_color = [
82 span.style.color[0] as f32 / 255.0,
83 span.style.color[1] as f32 / 255.0,
84 span.style.color[2] as f32 / 255.0,
85 span.style.color[3] as f32 / 255.0,
86 ];
87 break;
88 }
89 }
90 let c = self.apply_opacity(span_color);
91
92 let cache_key = glyph.cache_key;
93 let (uv_rect, w, h, x_off, y_off) = if let Some(info) =
94 self.text.glyph_cache.get(&cache_key)
95 {
96 *info
97 } else {
98 if let Some(image) = self.text.engine.rasterize(cache_key) {
99 let glyph_id = image.glyph_id;
100 let data_len = image.data.len();
101 let gw = image.width;
102 let gh = image.height;
103 let x_offset = image.x_offset;
104 let y_offset = image.y_offset;
105 let (rgba_data, gw, gh) = glyph_image_to_rgba(image);
106 if gw == 0 || gh == 0 {
107 let info = (Rect::zero(), 0.0, 0.0, 0.0, 0.0);
108 self.text.glyph_cache.put(cache_key, info);
109 continue;
110 }
111 if rgba_data.is_empty() {
112 tracing::warn!(
113 "Glyph rasterizer returned unsupported pixel format for glyph {} ({} bytes, {}x{}), skipping",
114 glyph_id,
115 data_len,
116 gw,
117 gh
118 );
119 continue;
120 }
121
122 let pack_res = self.heim_packer.pack(gw, gh);
123 let (nx, ny) = if let Some(pos) = pack_res {
124 pos
125 } else {
126 self.reclaim_vram();
127 match self.heim_packer.pack(gw, gh) {
128 Some(pos) => pos,
129 None => {
130 tracing::error!(
131 "Glyph heim critically full after reclaim: cannot pack {}x{} glyph, skipping",
132 gw,
133 gh
134 );
135 continue;
136 }
137 }
138 };
139
140 self.queue.write_texture(
141 wgpu::TexelCopyTextureInfo {
142 texture: &self.mega_heim_tex,
143 mip_level: 0,
144 origin: wgpu::Origin3d { x: nx, y: ny, z: 0 },
145 aspect: wgpu::TextureAspect::All,
146 },
147 &rgba_data,
148 wgpu::TexelCopyBufferLayout {
149 offset: 0,
150 bytes_per_row: Some(gw * 4),
151 rows_per_image: Some(gh),
152 },
153 wgpu::Extent3d {
154 width: gw,
155 height: gh,
156 depth_or_array_layers: 1,
157 },
158 );
159
160 let tex_w = self.mega_heim_tex.width() as f32;
161 let tex_h = self.mega_heim_tex.height() as f32;
162 let info = (
163 Rect {
164 x: nx as f32 / tex_w,
165 y: ny as f32 / tex_h,
166 width: gw as f32 / tex_w,
167 height: gh as f32 / tex_h,
168 },
169 gw as f32,
170 gh as f32,
171 x_offset,
172 y_offset,
173 );
174 self.text.glyph_cache.put(cache_key, info);
175 info
176 } else {
177 (Rect::zero(), 0.0, 0.0, 0.0, 0.0)
178 }
179 };
180
181 if w > 0.0 {
182 let sf = self.current_scale_factor();
183 let glyph_rect = Rect {
184 x: x + (glyph.x + x_off) / sf,
185 y: y + (glyph.y - y_off) / sf,
186 width: w / sf,
187 height: h / sf,
188 };
189 let tid = self.get_texture_id("__mega_heim");
190 let slice = self
191 .slice_stack
192 .last()
193 .copied()
194 .map(|(a, o)| [a, o, 1.0, 1.0])
195 .unwrap_or([0.0, 0.0, 0.0, 1.0]);
196 self.fill_rect_with_full_params_and_slice(
197 glyph_rect,
198 c,
199 6,
200 tid,
201 0.0,
202 uv_rect,
203 slice,
204 [glyph.glyph_index as f32, glyph.time_offset],
205 );
206 }
207 }
208 }
209
210 pub(crate) fn draw_text_impl(
212 &mut self,
213 text: &str,
214 rect: &cvkg_core::Rect,
215 size: f32,
216 color: [f32; 4],
217 h_align: cvkg_core::TextHAlign,
218 v_align: cvkg_core::TextVAlign,
219 ) {
220 let cache_key = (text.to_string(), (size * 100.0) as u32);
221 let r = (color[0] * 255.0).clamp(0.0, 255.0) as u8;
222 let g = (color[1] * 255.0).clamp(0.0, 255.0) as u8;
223 let b = (color[2] * 255.0).clamp(0.0, 255.0) as u8;
224 let a = (color[3] * 255.0).clamp(0.0, 255.0) as u8;
225 let cached = self.text.shaped_cache.get(&cache_key).cloned();
226 if let Some(shaped) = cached {
227 let color_matches = shaped
228 .spans
229 .first()
230 .map(|s| s.style.color == [r, g, b, a])
231 .unwrap_or(false);
232 if color_matches {
233 self.draw_shaped_text_aligned(&shaped, rect, size, h_align, v_align);
234 return;
235 }
236 let mut shaped = (*shaped).clone();
237 for span in &mut shaped.spans {
238 span.style.color = [r, g, b, a];
239 }
240 self.draw_shaped_text_aligned(&shaped, rect, size, h_align, v_align);
241 return;
242 }
243 let mut style = cvkg_runic_text::TextStyle::new("Inter", size);
244 style.color = [r, g, b, a];
245 let spans = [cvkg_runic_text::TextSpan::new(text, style)];
246 let h_text_align = match h_align {
247 cvkg_core::TextHAlign::Left => cvkg_runic_text::TextAlign::Start,
248 cvkg_core::TextHAlign::Center => cvkg_runic_text::TextAlign::Center,
249 cvkg_core::TextHAlign::Right => cvkg_runic_text::TextAlign::End,
250 };
251 if let Some(shaped) = self.shape_rich_text_impl(
252 &spans,
253 None,
254 h_text_align,
255 cvkg_runic_text::TextOverflow::Visible,
256 ) {
257 let shaped = std::sync::Arc::new(shaped);
258 self.draw_shaped_text_aligned(&shaped, rect, size, h_align, v_align);
259 self.text.shaped_cache.put(cache_key, shaped);
260 }
261 }
262
263 fn draw_shaped_text_aligned(
265 &mut self,
266 shaped: &cvkg_runic_text::ShapedText,
267 rect: &cvkg_core::Rect,
268 size: f32,
269 h_align: cvkg_core::TextHAlign,
270 v_align: cvkg_core::TextVAlign,
271 ) {
272 let text_w: f32 = shaped.glyphs.iter().map(|g| g.advance_width).sum();
274
275 let x = match h_align {
277 cvkg_core::TextHAlign::Left => rect.x,
278 cvkg_core::TextHAlign::Center => rect.x + (rect.width - text_w) / 2.0,
279 cvkg_core::TextHAlign::Right => rect.x + rect.width - text_w,
280 };
281
282 let ascent = size * 0.8;
285 let y = match v_align {
286 cvkg_core::TextVAlign::Top => rect.y + ascent,
287 cvkg_core::TextVAlign::Middle => rect.y + (rect.height + ascent * 0.75) / 2.0,
288 cvkg_core::TextVAlign::Bottom => rect.y + rect.height - (size - ascent),
289 };
290
291 self.draw_shaped_text_impl(shaped, x, y);
292 }
293}
294
295fn glyph_image_to_rgba(image: cvkg_runic_text::GlyphImage) -> (Vec<u8>, u32, u32) {
296 let width = image.width;
297 let height = image.height;
298 let pixels = width.saturating_mul(height) as usize;
299
300 if pixels == 0 || image.data.is_empty() {
301 return (Vec::new(), width, height);
302 }
303
304 let (bytes_per_pixel, remainder) = (image.data.len() / pixels, image.data.len() % pixels);
305 if remainder != 0 {
306 tracing::warn!(
307 "Glyph rasterizer returned {} bytes for {}x{} glyph; expected whole pixels ({} bytes per pixel)",
308 image.data.len(),
309 width,
310 height,
311 bytes_per_pixel
312 );
313 return (Vec::new(), width, height);
314 }
315
316 let rgba_data = match bytes_per_pixel {
317 1 => {
318 let mut data = Vec::with_capacity(pixels * 4);
319 for alpha in &image.data {
320 data.push(255);
321 data.push(255);
322 data.push(255);
323 data.push(*alpha);
324 }
325 data
326 }
327 3 => {
328 let mut data = Vec::with_capacity(pixels * 4);
329 for rgb in image.data.chunks_exact(3) {
330 let alpha = rgb.iter().copied().max().unwrap_or(0);
331 data.push(255);
332 data.push(255);
333 data.push(255);
334 data.push(alpha);
335 }
336 data
337 }
338 4 => {
339 let mut data = image.data;
340 for chunk in data.chunks_exact_mut(4) {
341 if chunk[3] == 0 && (chunk[0] > 0 || chunk[1] > 0 || chunk[2] > 0) {
342 chunk[3] = chunk[0].max(chunk[1]).max(chunk[2]);
343 }
344 }
345 data
346 }
347 _ => {
348 tracing::warn!(
349 "Glyph rasterizer returned unsupported {} bytes per pixel for {}x{} glyph ({} bytes total)",
350 bytes_per_pixel,
351 width,
352 height,
353 image.data.len()
354 );
355 Vec::new()
356 }
357 };
358
359 (rgba_data, width, height)
360}
361
362#[cfg(test)]
363mod tests {
364 use super::glyph_image_to_rgba;
365
366 #[test]
367 fn glyph_image_to_rgba_keeps_rgba_color_data() {
368 let image = cvkg_runic_text::GlyphImage {
369 glyph_id: 1,
370 width: 2,
371 height: 1,
372 data: vec![1, 2, 3, 4, 5, 6, 7, 8],
373 x_offset: 0.0,
374 y_offset: 0.0,
375 cache_key: 42,
376 };
377
378 assert_eq!(
379 glyph_image_to_rgba(image),
380 (vec![1, 2, 3, 4, 5, 6, 7, 8], 2, 1)
381 );
382 }
383
384 #[test]
385 fn glyph_image_to_rgba_expands_grayscale_alpha() {
386 let image = cvkg_runic_text::GlyphImage {
387 glyph_id: 1,
388 width: 3,
389 height: 1,
390 data: vec![0, 128, 255],
391 x_offset: 0.0,
392 y_offset: 0.0,
393 cache_key: 42,
394 };
395
396 assert_eq!(
397 glyph_image_to_rgba(image),
398 (
399 vec![255, 255, 255, 0, 255, 255, 255, 128, 255, 255, 255, 255],
400 3,
401 1
402 )
403 );
404 }
405
406 #[test]
407 fn glyph_image_to_rgba_collapses_subpixel_rgb_to_alpha() {
408 let image = cvkg_runic_text::GlyphImage {
409 glyph_id: 1,
410 width: 2,
411 height: 1,
412 data: vec![0, 128, 255, 255, 0, 64],
413 x_offset: 0.0,
414 y_offset: 0.0,
415 cache_key: 42,
416 };
417
418 assert_eq!(
419 glyph_image_to_rgba(image),
420 (vec![255, 255, 255, 255, 255, 255, 255, 255], 2, 1)
421 );
422 }
423}