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.engine
60 .shape_layout(&scaled_spans, scaled_max_width, align, overflow)
61 .ok()
62 }
63
64 pub(crate) fn draw_shaped_text_impl(&mut self, shaped: &cvkg_runic_text::ShapedText, x: f32, y: f32) {
66 for glyph in &shaped.glyphs {
67 let byte_idx = shaped
68 .grapheme_boundaries
69 .get(glyph.cluster as usize)
70 .copied()
71 .unwrap_or(0);
72 let mut span_color = [1.0, 1.0, 1.0, 1.0];
73 for span in &shaped.spans {
74 if byte_idx >= span.byte_offset && byte_idx < span.byte_offset + span.text.len() {
75 span_color = [
76 span.style.color[0] as f32 / 255.0,
77 span.style.color[1] as f32 / 255.0,
78 span.style.color[2] as f32 / 255.0,
79 span.style.color[3] as f32 / 255.0,
80 ];
81 break;
82 }
83 }
84 let c = self.apply_opacity(span_color);
85
86 let cache_key = glyph.cache_key;
87 let (uv_rect, w, h, x_off, y_off) = if let Some(info) = self.text.glyph_cache.get(&cache_key)
88 {
89 *info
90 } else {
91 if let Some(image) = self.text.engine.rasterize(cache_key) {
92 let glyph_id = image.glyph_id;
93 let data_len = image.data.len();
94 let gw = image.width;
95 let gh = image.height;
96 let x_offset = image.x_offset;
97 let y_offset = image.y_offset;
98 let (rgba_data, gw, gh) = glyph_image_to_rgba(image);
99 if gw == 0 || gh == 0 {
100 let info = (Rect::zero(), 0.0, 0.0, 0.0, 0.0);
101 self.text.glyph_cache.put(cache_key, info);
102 continue;
103 }
104 if rgba_data.is_empty() {
105 log::warn!(
106 "Glyph rasterizer returned unsupported pixel format for glyph {} ({} bytes, {}x{}), skipping",
107 glyph_id,
108 data_len,
109 gw,
110 gh
111 );
112 continue;
113 }
114
115 let pack_res = self.heim_packer.pack(gw, gh);
116 let (nx, ny) = if let Some(pos) = pack_res {
117 pos
118 } else {
119 self.reclaim_vram();
120 match self.heim_packer.pack(gw, gh) {
121 Some(pos) => pos,
122 None => {
123 log::error!(
124 "Glyph heim critically full after reclaim: cannot pack {}x{} glyph, skipping",
125 gw,
126 gh
127 );
128 continue;
129 }
130 }
131 };
132
133 self.queue.write_texture(
134 wgpu::TexelCopyTextureInfo {
135 texture: &self.mega_heim_tex,
136 mip_level: 0,
137 origin: wgpu::Origin3d { x: nx, y: ny, z: 0 },
138 aspect: wgpu::TextureAspect::All,
139 },
140 &rgba_data,
141 wgpu::TexelCopyBufferLayout {
142 offset: 0,
143 bytes_per_row: Some(gw * 4),
144 rows_per_image: Some(gh),
145 },
146 wgpu::Extent3d {
147 width: gw,
148 height: gh,
149 depth_or_array_layers: 1,
150 },
151 );
152
153 let tex_w = self.mega_heim_tex.width() as f32;
154 let tex_h = self.mega_heim_tex.height() as f32;
155 let info = (
156 Rect {
157 x: nx as f32 / tex_w,
158 y: ny as f32 / tex_h,
159 width: gw as f32 / tex_w,
160 height: gh as f32 / tex_h,
161 },
162 gw as f32,
163 gh as f32,
164 x_offset,
165 y_offset,
166 );
167 self.text.glyph_cache.put(cache_key, info);
168 info
169 } else {
170 (Rect::zero(), 0.0, 0.0, 0.0, 0.0)
171 }
172 };
173
174 if w > 0.0 {
175 let sf = self.current_scale_factor();
176 let baseline_y = y + shaped.ascent / sf;
177 let glyph_rect = Rect {
178 x: x + (glyph.x + x_off) / sf,
179 y: baseline_y + (glyph.y - y_off) / sf,
180 width: w / sf,
181 height: h / sf,
182 };
183 let tid = self.get_texture_id("__mega_heim");
184 let slice = self
185 .slice_stack
186 .last()
187 .copied()
188 .map(|(a, o)| [a, o, 1.0, 1.0])
189 .unwrap_or([0.0, 0.0, 0.0, 1.0]);
190 self.fill_rect_with_full_params_and_slice(
191 glyph_rect,
192 c,
193 6,
194 tid,
195 0.0,
196 uv_rect,
197 slice,
198 [glyph.glyph_index as f32, glyph.time_offset],
199 );
200 }
201 }
202 }
203
204 pub(crate) fn draw_text_impl(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
206 let cache_key = (text.to_string(), (size * 100.0) as u32);
207 let r = (color[0] * 255.0).clamp(0.0, 255.0) as u8;
208 let g = (color[1] * 255.0).clamp(0.0, 255.0) as u8;
209 let b = (color[2] * 255.0).clamp(0.0, 255.0) as u8;
210 let a = (color[3] * 255.0).clamp(0.0, 255.0) as u8;
211 let cached = self.text.shaped_cache.get(&cache_key).cloned();
212 if let Some(shaped) = cached {
213 let color_matches = shaped.spans.first()
214 .map(|s| s.style.color == [r, g, b, a])
215 .unwrap_or(false);
216 if color_matches {
217 self.draw_shaped_text_impl(&shaped, x, y);
218 return;
219 }
220 let mut shaped = (*shaped).clone();
221 for span in &mut shaped.spans {
222 span.style.color = [r, g, b, a];
223 }
224 self.draw_shaped_text_impl(&shaped, x, y);
225 return;
226 }
227 let mut style = cvkg_runic_text::TextStyle::new("Inter", size);
228 style.color = [r, g, b, a];
229 let spans = [cvkg_runic_text::TextSpan::new(text, style)];
230 if let Some(shaped) = self.shape_rich_text_impl(
231 &spans,
232 None,
233 cvkg_runic_text::TextAlign::Start,
234 cvkg_runic_text::TextOverflow::Visible,
235 ) {
236 let shaped = std::sync::Arc::new(shaped);
237 self.draw_shaped_text_impl(&shaped, x, y);
238 self.text.shaped_cache.put(cache_key, shaped);
239 }
240 }
241}
242
243fn glyph_image_to_rgba(image: cvkg_runic_text::GlyphImage) -> (Vec<u8>, u32, u32) {
244 let width = image.width;
245 let height = image.height;
246 let pixels = width.saturating_mul(height) as usize;
247
248 if pixels == 0 || image.data.is_empty() {
249 return (Vec::new(), width, height);
250 }
251
252 let (bytes_per_pixel, remainder) = (image.data.len() / pixels, image.data.len() % pixels);
253 if remainder != 0 {
254 log::warn!(
255 "Glyph rasterizer returned {} bytes for {}x{} glyph; expected whole pixels ({} bytes per pixel)",
256 image.data.len(),
257 width,
258 height,
259 bytes_per_pixel
260 );
261 return (Vec::new(), width, height);
262 }
263
264 let rgba_data = match bytes_per_pixel {
265 1 => {
266 let mut data = Vec::with_capacity(pixels * 4);
267 for alpha in &image.data {
268 data.push(255);
269 data.push(255);
270 data.push(255);
271 data.push(*alpha);
272 }
273 data
274 }
275 3 => {
276 let mut data = Vec::with_capacity(pixels * 4);
277 for rgb in image.data.chunks_exact(3) {
278 let alpha = rgb.iter().copied().max().unwrap_or(0);
279 data.push(255);
280 data.push(255);
281 data.push(255);
282 data.push(alpha);
283 }
284 data
285 }
286 4 => {
287 let mut data = image.data;
288 for chunk in data.chunks_exact_mut(4) {
289 if chunk[3] == 0 && (chunk[0] > 0 || chunk[1] > 0 || chunk[2] > 0) {
290 chunk[3] = chunk[0].max(chunk[1]).max(chunk[2]);
291 }
292 }
293 data
294 }
295 _ => {
296 log::warn!(
297 "Glyph rasterizer returned unsupported {} bytes per pixel for {}x{} glyph ({} bytes total)",
298 bytes_per_pixel,
299 width,
300 height,
301 image.data.len()
302 );
303 Vec::new()
304 }
305 };
306
307 (rgba_data, width, height)
308}
309
310#[cfg(test)]
311mod tests {
312 use super::glyph_image_to_rgba;
313
314 #[test]
315 fn glyph_image_to_rgba_keeps_rgba_color_data() {
316 let image = cvkg_runic_text::GlyphImage {
317 glyph_id: 1,
318 width: 2,
319 height: 1,
320 data: vec![1, 2, 3, 4, 5, 6, 7, 8],
321 x_offset: 0.0,
322 y_offset: 0.0,
323 cache_key: 42,
324 };
325
326 assert_eq!(
327 glyph_image_to_rgba(image),
328 (vec![1, 2, 3, 4, 5, 6, 7, 8], 2, 1)
329 );
330 }
331
332 #[test]
333 fn glyph_image_to_rgba_expands_grayscale_alpha() {
334 let image = cvkg_runic_text::GlyphImage {
335 glyph_id: 1,
336 width: 3,
337 height: 1,
338 data: vec![0, 128, 255],
339 x_offset: 0.0,
340 y_offset: 0.0,
341 cache_key: 42,
342 };
343
344 assert_eq!(
345 glyph_image_to_rgba(image),
346 (
347 vec![255, 255, 255, 0, 255, 255, 255, 128, 255, 255, 255, 255],
348 3,
349 1
350 )
351 );
352 }
353
354 #[test]
355 fn glyph_image_to_rgba_collapses_subpixel_rgb_to_alpha() {
356 let image = cvkg_runic_text::GlyphImage {
357 glyph_id: 1,
358 width: 2,
359 height: 1,
360 data: vec![0, 128, 255, 255, 0, 64],
361 x_offset: 0.0,
362 y_offset: 0.0,
363 cache_key: 42,
364 };
365
366 assert_eq!(
367 glyph_image_to_rgba(image),
368 (vec![255, 255, 255, 255, 255, 255, 255, 255], 2, 1)
369 );
370 }
371}