1mod pipeline;
7mod render;
8mod scene;
9mod shaders;
10
11pub use scene::{ClickAction, DrawShape, HitRegion, Scene, TextDraw};
12
13use cranpose_render_common::{RenderScene, Renderer};
14use cranpose_ui::{set_text_measurer, LayoutTree, TextMeasurer};
15use cranpose_ui_graphics::Size;
16use glyphon::{Attrs, Buffer, FontSystem, Metrics, Shaping};
17use lru::LruCache;
18use render::GpuRenderer;
19use std::collections::HashMap;
20use std::hash::{Hash, Hasher};
21use std::num::NonZeroUsize;
22use std::sync::{Arc, Mutex};
23
24#[derive(Debug)]
25pub enum WgpuRendererError {
26 Layout(String),
27 Wgpu(String),
28}
29
30#[derive(Clone)]
33pub(crate) struct TextCacheKey {
34 text: String,
35 scale_bits: u32, }
37
38impl TextCacheKey {
39 fn new(text: &str, font_size: f32) -> Self {
40 Self {
41 text: text.to_string(),
42 scale_bits: font_size.to_bits(),
43 }
44 }
45}
46
47impl Hash for TextCacheKey {
48 fn hash<H: Hasher>(&self, state: &mut H) {
49 self.text.hash(state);
50 self.scale_bits.hash(state);
51 }
52}
53
54impl PartialEq for TextCacheKey {
55 fn eq(&self, other: &Self) -> bool {
56 self.text == other.text && self.scale_bits == other.scale_bits
57 }
58}
59
60impl Eq for TextCacheKey {}
61
62pub(crate) struct SharedTextBuffer {
64 pub(crate) buffer: Buffer,
65 text: String,
66 font_size: f32,
67 cached_size: Option<Size>,
69}
70
71impl SharedTextBuffer {
72 pub(crate) fn ensure(
74 &mut self,
75 font_system: &mut FontSystem,
76 text: &str,
77 font_size: f32,
78 attrs: Attrs,
79 ) {
80 let text_changed = self.text != text;
81 let font_changed = (self.font_size - font_size).abs() > 0.1;
82
83 if !text_changed && !font_changed {
85 return; }
87
88 let metrics = Metrics::new(font_size, font_size * 1.4);
90 self.buffer.set_metrics(font_system, metrics);
91 self.buffer
92 .set_size(font_system, Some(f32::MAX), Some(f32::MAX));
93
94 self.buffer
96 .set_text(font_system, text, &attrs, Shaping::Advanced);
97 self.buffer.shape_until_scroll(font_system, false);
98
99 self.text.clear();
101 self.text.push_str(text);
102 self.font_size = font_size;
103 self.cached_size = None; }
105
106 pub(crate) fn size(&mut self, font_size: f32) -> Size {
108 if let Some(size) = self.cached_size {
109 return size;
110 }
111
112 let mut max_width = 0.0f32;
114 let layout_runs = self.buffer.layout_runs();
115 for run in layout_runs {
116 max_width = max_width.max(run.line_w);
117 }
118 let total_height = self.buffer.lines.len() as f32 * font_size * 1.4;
119
120 let size = Size {
121 width: max_width,
122 height: total_height,
123 };
124
125 self.cached_size = Some(size);
126 size
127 }
128}
129
130pub(crate) type SharedTextCache = Arc<Mutex<HashMap<TextCacheKey, SharedTextBuffer>>>;
132
133fn trim_text_cache(cache: &mut HashMap<TextCacheKey, SharedTextBuffer>) {
136 if cache.len() > MAX_CACHE_ITEMS {
137 let target_size = MAX_CACHE_ITEMS / 2;
138 let to_remove = cache.len() - target_size;
139
140 let keys_to_remove: Vec<TextCacheKey> = cache.keys().take(to_remove).cloned().collect();
142
143 for key in keys_to_remove {
144 cache.remove(&key);
145 }
146
147 log::debug!(
148 "Trimmed text cache from {} to {} entries",
149 cache.len() + to_remove,
150 cache.len()
151 );
152 }
153}
154
155const MAX_CACHE_ITEMS: usize = 256;
157
158pub struct WgpuRenderer {
166 scene: Scene,
167 gpu_renderer: Option<GpuRenderer>,
168 font_system: Arc<Mutex<FontSystem>>,
169 text_cache: SharedTextCache,
171 root_scale: f32,
173}
174
175impl WgpuRenderer {
176 pub fn new_with_fonts(fonts: &[&[u8]]) -> Self {
189 let mut font_system = FontSystem::new();
190
191 #[cfg(target_os = "android")]
196 {
197 log::info!("Skipping Android system fonts - using application-provided fonts");
198 }
200
201 for (i, font_data) in fonts.iter().enumerate() {
203 log::info!("Loading font #{}, size: {} bytes", i, font_data.len());
204 font_system.db_mut().load_font_data(font_data.to_vec());
205 }
206
207 let face_count = font_system.db().faces().count();
208 log::info!("Total font faces loaded: {}", face_count);
209
210 if face_count == 0 {
211 log::error!("No fonts loaded! Text rendering will fail!");
212 }
213
214 let font_system = Arc::new(Mutex::new(font_system));
215
216 let text_cache = Arc::new(Mutex::new(HashMap::new()));
218
219 let text_measurer = WgpuTextMeasurer::new(font_system.clone(), text_cache.clone());
220 set_text_measurer(text_measurer.clone());
221
222 Self {
223 scene: Scene::new(),
224 gpu_renderer: None,
225 font_system,
226 text_cache,
227 root_scale: 1.0,
228 }
229 }
230
231 pub fn new() -> Self {
236 let font_system = FontSystem::new();
237 let font_system = Arc::new(Mutex::new(font_system));
238 let text_cache = Arc::new(Mutex::new(HashMap::new()));
239
240 let text_measurer = WgpuTextMeasurer::new(font_system.clone(), text_cache.clone());
241 set_text_measurer(text_measurer.clone());
242
243 Self {
244 scene: Scene::new(),
245 gpu_renderer: None,
246 font_system,
247 text_cache,
248 root_scale: 1.0,
249 }
250 }
251
252 pub fn init_gpu(
254 &mut self,
255 device: Arc<wgpu::Device>,
256 queue: Arc<wgpu::Queue>,
257 surface_format: wgpu::TextureFormat,
258 ) {
259 self.gpu_renderer = Some(GpuRenderer::new(
260 device,
261 queue,
262 surface_format,
263 self.font_system.clone(),
264 self.text_cache.clone(),
265 ));
266 }
267
268 pub fn set_root_scale(&mut self, scale: f32) {
270 self.root_scale = scale;
271 }
272
273 pub fn render(
275 &mut self,
276 view: &wgpu::TextureView,
277 width: u32,
278 height: u32,
279 ) -> Result<(), WgpuRendererError> {
280 if let Some(gpu_renderer) = &mut self.gpu_renderer {
281 gpu_renderer
282 .render(
283 view,
284 &self.scene.shapes,
285 &self.scene.texts,
286 width,
287 height,
288 self.root_scale,
289 )
290 .map_err(WgpuRendererError::Wgpu)
291 } else {
292 Err(WgpuRendererError::Wgpu(
293 "GPU renderer not initialized. Call init_gpu() first.".to_string(),
294 ))
295 }
296 }
297
298 pub fn device(&self) -> &wgpu::Device {
300 self.gpu_renderer
301 .as_ref()
302 .map(|r| &*r.device)
303 .expect("GPU renderer not initialized")
304 }
305}
306
307impl Default for WgpuRenderer {
308 fn default() -> Self {
309 Self::new()
310 }
311}
312
313impl Renderer for WgpuRenderer {
314 type Scene = Scene;
315 type Error = WgpuRendererError;
316
317 fn scene(&self) -> &Self::Scene {
318 &self.scene
319 }
320
321 fn scene_mut(&mut self) -> &mut Self::Scene {
322 &mut self.scene
323 }
324
325 fn rebuild_scene(
326 &mut self,
327 layout_tree: &LayoutTree,
328 _viewport: Size,
329 ) -> Result<(), Self::Error> {
330 self.scene.clear();
331 pipeline::render_layout_tree(layout_tree.root(), &mut self.scene);
333 Ok(())
334 }
335
336 fn draw_dev_overlay(&mut self, text: &str, viewport: Size) {
337 use cranpose_ui_graphics::{Brush, Color, Rect, RoundedCornerShape};
338
339 let padding = 8.0;
342 let font_size = 14.0;
343
344 let char_width = 7.0;
346 let text_width = text.len() as f32 * char_width;
347 let text_height = font_size * 1.4;
348
349 let x = viewport.width - text_width - padding * 2.0;
350 let y = padding;
351
352 let bg_rect = Rect {
354 x,
355 y,
356 width: text_width + padding,
357 height: text_height + padding / 2.0,
358 };
359 self.scene.push_shape(
360 bg_rect,
361 Brush::Solid(Color(0.0, 0.0, 0.0, 0.7)),
362 Some(RoundedCornerShape::uniform(4.0)),
363 None,
364 );
365
366 let text_rect = Rect {
368 x: x + padding / 2.0,
369 y: y + padding / 4.0,
370 width: text_width,
371 height: text_height,
372 };
373 self.scene.push_text(
374 text_rect,
375 text.to_string(),
376 Color(0.0, 1.0, 0.0, 1.0), font_size / BASE_FONT_SIZE, None,
379 );
380 }
381}
382
383#[derive(Clone)]
386struct WgpuTextMeasurer {
387 font_system: Arc<Mutex<FontSystem>>,
388 size_cache: Arc<Mutex<LruCache<(String, i32), Size>>>,
390 text_cache: SharedTextCache,
392}
393
394impl WgpuTextMeasurer {
395 fn new(font_system: Arc<Mutex<FontSystem>>, text_cache: SharedTextCache) -> Self {
396 Self {
397 font_system,
398 size_cache: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(64).unwrap()))),
399 text_cache,
400 }
401 }
402}
403
404pub(crate) const BASE_FONT_SIZE: f32 = 14.0;
406
407impl TextMeasurer for WgpuTextMeasurer {
408 fn measure(&self, text: &str) -> cranpose_ui::TextMetrics {
409 let size_key = (text.to_string(), (BASE_FONT_SIZE * 100.0) as i32);
410
411 {
413 let mut cache = self.size_cache.lock().unwrap();
414 if let Some(size) = cache.get(&size_key) {
415 return cranpose_ui::TextMetrics {
417 width: size.width,
418 height: size.height,
419 line_height: BASE_FONT_SIZE * 1.4,
420 line_count: 1, };
422 }
423 }
424
425 let cache_key = TextCacheKey::new(text, BASE_FONT_SIZE);
427 let mut font_system = self.font_system.lock().unwrap();
428 let mut text_cache = self.text_cache.lock().unwrap();
429
430 let size = {
432 let buffer = text_cache.entry(cache_key).or_insert_with(|| {
433 let buffer = Buffer::new(
434 &mut font_system,
435 Metrics::new(BASE_FONT_SIZE, BASE_FONT_SIZE * 1.4),
436 );
437 SharedTextBuffer {
438 buffer,
439 text: String::new(),
440 font_size: 0.0,
441 cached_size: None,
442 }
443 });
444
445 buffer.ensure(&mut font_system, text, BASE_FONT_SIZE, Attrs::new());
447
448 buffer.size(BASE_FONT_SIZE)
450 };
451
452 trim_text_cache(&mut text_cache);
454
455 drop(font_system);
456 drop(text_cache);
457
458 let mut size_cache = self.size_cache.lock().unwrap();
460 size_cache.put(size_key, size);
461
462 let line_height = BASE_FONT_SIZE * 1.4;
464 let line_count = text.split('\n').count().max(1);
465
466 cranpose_ui::TextMetrics {
467 width: size.width,
468 height: size.height,
469 line_height,
470 line_count,
471 }
472 }
473
474 fn get_offset_for_position(&self, text: &str, x: f32, y: f32) -> usize {
475 if text.is_empty() {
476 return 0;
477 }
478
479 let line_height = BASE_FONT_SIZE * 1.4;
480
481 let line_index = (y / line_height).floor().max(0.0) as usize;
483 let lines: Vec<&str> = text.split('\n').collect();
484 let target_line = line_index.min(lines.len().saturating_sub(1));
485
486 let mut line_start_byte = 0;
488 for line in lines.iter().take(target_line) {
489 line_start_byte += line.len() + 1; }
491
492 let line_text = lines.get(target_line).unwrap_or(&"");
494
495 if line_text.is_empty() {
496 return line_start_byte;
497 }
498
499 let cache_key = TextCacheKey::new(line_text, BASE_FONT_SIZE);
501 let mut font_system = self.font_system.lock().unwrap();
502 let mut text_cache = self.text_cache.lock().unwrap();
503
504 let buffer = text_cache.entry(cache_key).or_insert_with(|| {
505 let buffer = Buffer::new(
506 &mut font_system,
507 Metrics::new(BASE_FONT_SIZE, BASE_FONT_SIZE * 1.4),
508 );
509 SharedTextBuffer {
510 buffer,
511 text: String::new(),
512 font_size: 0.0,
513 cached_size: None,
514 }
515 });
516
517 buffer.ensure(&mut font_system, line_text, BASE_FONT_SIZE, Attrs::new());
518
519 let mut best_offset = 0;
521 let mut best_distance = f32::INFINITY;
522
523 for run in buffer.buffer.layout_runs() {
524 let mut glyph_x = 0.0f32;
525 for glyph in run.glyphs.iter() {
526 let left_dist = (x - glyph_x).abs();
528 if left_dist < best_distance {
529 best_distance = left_dist;
530 best_offset = glyph.start;
532 }
533
534 glyph_x += glyph.w;
536
537 let right_dist = (x - glyph_x).abs();
539 if right_dist < best_distance {
540 best_distance = right_dist;
541 best_offset = glyph.end;
542 }
543 }
544 }
545
546 line_start_byte + best_offset.min(line_text.len())
548 }
549
550 fn get_cursor_x_for_offset(&self, text: &str, offset: usize) -> f32 {
551 let clamped_offset = offset.min(text.len());
552 if clamped_offset == 0 {
553 return 0.0;
554 }
555
556 let prefix = &text[..clamped_offset];
558 self.measure(prefix).width
559 }
560
561 fn layout(&self, text: &str) -> cranpose_ui::text_layout_result::TextLayoutResult {
562 use cranpose_ui::text_layout_result::{LineLayout, TextLayoutResult};
563
564 let line_height = BASE_FONT_SIZE * 1.4;
565
566 let cache_key = TextCacheKey::new(text, BASE_FONT_SIZE);
568 let mut font_system = self.font_system.lock().unwrap();
569 let mut text_cache = self.text_cache.lock().unwrap();
570
571 let buffer = text_cache.entry(cache_key.clone()).or_insert_with(|| {
572 let buffer = Buffer::new(&mut font_system, Metrics::new(BASE_FONT_SIZE, line_height));
573 SharedTextBuffer {
574 buffer,
575 text: String::new(),
576 font_size: 0.0,
577 cached_size: None,
578 }
579 });
580 buffer.ensure(&mut font_system, text, BASE_FONT_SIZE, Attrs::new());
581
582 let mut glyph_x_positions = Vec::new();
584 let mut char_to_byte = Vec::new();
585 let mut lines = Vec::new();
586
587 let mut current_line_y = 0.0f32;
588 let mut line_start_offset = 0usize;
589
590 for run in buffer.buffer.layout_runs() {
591 let line_idx = run.line_i;
592 let line_y = line_idx as f32 * line_height;
593
594 if lines.is_empty() || line_y != current_line_y {
596 if !lines.is_empty() {
597 if let Some(_last) = lines.last_mut() {
599 }
601 }
602 current_line_y = line_y;
603 }
604
605 for glyph in run.glyphs.iter() {
606 glyph_x_positions.push(glyph.x);
607 char_to_byte.push(glyph.start);
608
609 if glyph.end > line_start_offset {
611 line_start_offset = glyph.end;
612 }
613 }
614 }
615
616 let total_width = self.measure(text).width;
618 glyph_x_positions.push(total_width);
619 char_to_byte.push(text.len());
620
621 let mut y = 0.0f32;
623 let mut line_start = 0usize;
624 for (i, line_text) in text.split('\n').enumerate() {
625 let line_end = if i == text.split('\n').count() - 1 {
626 text.len()
627 } else {
628 line_start + line_text.len()
629 };
630
631 lines.push(LineLayout {
632 start_offset: line_start,
633 end_offset: line_end,
634 y,
635 height: line_height,
636 });
637
638 line_start = line_end + 1;
639 y += line_height;
640 }
641
642 if lines.is_empty() {
643 lines.push(LineLayout {
644 start_offset: 0,
645 end_offset: 0,
646 y: 0.0,
647 height: line_height,
648 });
649 }
650
651 let metrics = self.measure(text);
652 TextLayoutResult::new(
653 metrics.width,
654 metrics.height,
655 line_height,
656 glyph_x_positions,
657 char_to_byte,
658 lines,
659 text,
660 )
661 }
662}