1mod pipeline;
7mod render;
8mod scene;
9mod shaders;
10
11pub use scene::{ClickAction, DrawShape, HitRegion, ImageDraw, Scene, TextDraw};
12
13use cranpose_core::{MemoryApplier, NodeId};
14use cranpose_render_common::{RenderScene, Renderer};
15use cranpose_ui::{set_text_measurer, LayoutTree, TextMeasurer};
16use cranpose_ui_graphics::Size;
17use glyphon::{Attrs, Buffer, FontSystem, Metrics, Shaping};
18use lru::LruCache;
19use render::GpuRenderer;
20use rustc_hash::FxHasher;
21use std::collections::HashMap;
22use std::hash::{Hash, Hasher};
23use std::num::NonZeroUsize;
24use std::rc::Rc;
25use std::sync::{Arc, Mutex};
26
27type TextSizeCache = Arc<Mutex<LruCache<(u64, i32), (String, Size)>>>;
31
32#[derive(Debug)]
33pub enum WgpuRendererError {
34 Layout(String),
35 Wgpu(String),
36}
37
38#[derive(Clone, PartialEq, Eq, Hash)]
40pub(crate) enum TextKey {
41 Content(String),
42 Node(NodeId),
43}
44
45#[derive(Clone, PartialEq, Eq)]
46pub(crate) struct TextCacheKey {
47 key: TextKey,
48 scale_bits: u32, }
50
51impl TextCacheKey {
52 fn new(text: &str, font_size: f32) -> Self {
53 Self {
54 key: TextKey::Content(text.to_string()),
55 scale_bits: font_size.to_bits(),
56 }
57 }
58
59 fn for_node(node_id: NodeId, font_size: f32) -> Self {
60 Self {
61 key: TextKey::Node(node_id),
62 scale_bits: font_size.to_bits(),
63 }
64 }
65}
66
67impl Hash for TextCacheKey {
68 fn hash<H: Hasher>(&self, state: &mut H) {
69 self.key.hash(state);
70 self.scale_bits.hash(state);
71 }
72}
73
74pub(crate) struct SharedTextBuffer {
76 pub(crate) buffer: Buffer,
77 text: String,
78 font_size: f32,
79 cached_size: Option<Size>,
81}
82
83impl SharedTextBuffer {
84 pub(crate) fn ensure(
86 &mut self,
87 font_system: &mut FontSystem,
88 text: &str,
89 font_size: f32,
90 attrs: Attrs,
91 ) {
92 let text_changed = self.text != text;
93 let font_changed = (self.font_size - font_size).abs() > 0.1;
94
95 if !text_changed && !font_changed {
97 return; }
99
100 let metrics = Metrics::new(font_size, font_size * 1.4);
102 self.buffer.set_metrics(font_system, metrics);
103 self.buffer
104 .set_size(font_system, Some(f32::MAX), Some(f32::MAX));
105
106 self.buffer
108 .set_text(font_system, text, &attrs, Shaping::Advanced);
109 self.buffer.shape_until_scroll(font_system, false);
110
111 self.text.clear();
113 self.text.push_str(text);
114 self.font_size = font_size;
115 self.cached_size = None; }
117
118 pub(crate) fn size(&mut self, font_size: f32) -> Size {
120 if let Some(size) = self.cached_size {
121 return size;
122 }
123
124 let mut max_width = 0.0f32;
126 let layout_runs = self.buffer.layout_runs();
127 for run in layout_runs {
128 max_width = max_width.max(run.line_w);
129 }
130 let total_height = self.buffer.lines.len() as f32 * font_size * 1.4;
131
132 let size = Size {
133 width: max_width,
134 height: total_height,
135 };
136
137 self.cached_size = Some(size);
138 size
139 }
140}
141
142pub(crate) type SharedTextCache = Arc<Mutex<HashMap<TextCacheKey, SharedTextBuffer>>>;
144
145pub(crate) fn trim_text_cache(cache: &mut HashMap<TextCacheKey, SharedTextBuffer>) {
148 if cache.len() > MAX_CACHE_ITEMS {
149 let target_size = MAX_CACHE_ITEMS / 2;
150 let to_remove = cache.len() - target_size;
151
152 let keys_to_remove: Vec<TextCacheKey> = cache.keys().take(to_remove).cloned().collect();
154
155 for key in keys_to_remove {
156 cache.remove(&key);
157 }
158
159 log::debug!(
160 "Trimmed text cache from {} to {} entries",
161 cache.len() + to_remove,
162 cache.len()
163 );
164 }
165}
166
167const MAX_CACHE_ITEMS: usize = 256;
169
170pub struct WgpuRenderer {
178 scene: Scene,
179 gpu_renderer: Option<GpuRenderer>,
180 font_system: Arc<Mutex<FontSystem>>,
181 text_cache: SharedTextCache,
183 root_scale: f32,
185}
186
187impl WgpuRenderer {
188 pub fn new_with_fonts(fonts: &[&[u8]]) -> Self {
201 let mut font_system = FontSystem::new();
202
203 #[cfg(target_os = "android")]
208 {
209 log::info!("Skipping Android system fonts - using application-provided fonts");
210 }
212
213 for (i, font_data) in fonts.iter().enumerate() {
215 log::info!("Loading font #{}, size: {} bytes", i, font_data.len());
216 font_system.db_mut().load_font_data(font_data.to_vec());
217 }
218
219 let face_count = font_system.db().faces().count();
220 log::info!("Total font faces loaded: {}", face_count);
221
222 if face_count == 0 {
223 log::error!("No fonts loaded! Text rendering will fail!");
224 }
225
226 let font_system = Arc::new(Mutex::new(font_system));
227
228 let text_cache = Arc::new(Mutex::new(HashMap::new()));
230
231 let text_measurer = WgpuTextMeasurer::new(font_system.clone(), text_cache.clone());
232 set_text_measurer(text_measurer.clone());
233
234 Self {
235 scene: Scene::new(),
236 gpu_renderer: None,
237 font_system,
238 text_cache,
239 root_scale: 1.0,
240 }
241 }
242
243 pub fn new() -> Self {
248 let font_system = FontSystem::new();
249 let font_system = Arc::new(Mutex::new(font_system));
250 let text_cache = Arc::new(Mutex::new(HashMap::new()));
251
252 let text_measurer = WgpuTextMeasurer::new(font_system.clone(), text_cache.clone());
253 set_text_measurer(text_measurer.clone());
254
255 Self {
256 scene: Scene::new(),
257 gpu_renderer: None,
258 font_system,
259 text_cache,
260 root_scale: 1.0,
261 }
262 }
263
264 pub fn init_gpu(
266 &mut self,
267 device: Arc<wgpu::Device>,
268 queue: Arc<wgpu::Queue>,
269 surface_format: wgpu::TextureFormat,
270 ) {
271 self.gpu_renderer = Some(GpuRenderer::new(
272 device,
273 queue,
274 surface_format,
275 self.font_system.clone(),
276 self.text_cache.clone(),
277 ));
278 }
279
280 pub fn set_root_scale(&mut self, scale: f32) {
282 self.root_scale = scale;
283 }
284
285 pub fn render(
287 &mut self,
288 view: &wgpu::TextureView,
289 width: u32,
290 height: u32,
291 ) -> Result<(), WgpuRendererError> {
292 if let Some(gpu_renderer) = &mut self.gpu_renderer {
293 gpu_renderer
294 .render(
295 view,
296 &self.scene.shapes,
297 &self.scene.images,
298 &self.scene.texts,
299 width,
300 height,
301 self.root_scale,
302 )
303 .map_err(WgpuRendererError::Wgpu)
304 } else {
305 Err(WgpuRendererError::Wgpu(
306 "GPU renderer not initialized. Call init_gpu() first.".to_string(),
307 ))
308 }
309 }
310
311 pub fn device(&self) -> &wgpu::Device {
313 self.gpu_renderer
314 .as_ref()
315 .map(|r| &*r.device)
316 .expect("GPU renderer not initialized")
317 }
318}
319
320impl Default for WgpuRenderer {
321 fn default() -> Self {
322 Self::new()
323 }
324}
325
326impl Renderer for WgpuRenderer {
327 type Scene = Scene;
328 type Error = WgpuRendererError;
329
330 fn scene(&self) -> &Self::Scene {
331 &self.scene
332 }
333
334 fn scene_mut(&mut self) -> &mut Self::Scene {
335 &mut self.scene
336 }
337
338 fn rebuild_scene(
339 &mut self,
340 layout_tree: &LayoutTree,
341 _viewport: Size,
342 ) -> Result<(), Self::Error> {
343 self.scene.clear();
344 pipeline::render_layout_tree(layout_tree.root(), &mut self.scene);
346 Ok(())
347 }
348
349 fn rebuild_scene_from_applier(
350 &mut self,
351 applier: &mut MemoryApplier,
352 root: NodeId,
353 _viewport: Size,
354 ) -> Result<(), Self::Error> {
355 self.scene.clear();
356 pipeline::render_from_applier(applier, root, &mut self.scene, 1.0);
359 Ok(())
360 }
361
362 fn draw_dev_overlay(&mut self, text: &str, viewport: Size) {
363 use cranpose_ui_graphics::{Brush, Color, Rect, RoundedCornerShape};
364
365 let padding = 8.0;
368 let font_size = 14.0;
369
370 let char_width = 7.0;
372 let text_width = text.len() as f32 * char_width;
373 let text_height = font_size * 1.4;
374
375 let x = viewport.width - text_width - padding * 2.0;
376 let y = padding;
377
378 let bg_rect = Rect {
380 x,
381 y,
382 width: text_width + padding,
383 height: text_height + padding / 2.0,
384 };
385 self.scene.push_shape(
386 bg_rect,
387 Brush::Solid(Color(0.0, 0.0, 0.0, 0.7)),
388 Some(RoundedCornerShape::uniform(4.0)),
389 None,
390 );
391
392 let text_rect = Rect {
394 x: x + padding / 2.0,
395 y: y + padding / 4.0,
396 width: text_width,
397 height: text_height,
398 };
399 self.scene.push_text(
400 NodeId::MAX,
401 text_rect,
402 Rc::from(text),
403 Color(0.0, 1.0, 0.0, 1.0), font_size,
405 1.0,
406 None,
407 );
408 }
409}
410
411fn resolve_font_size(style: &cranpose_ui::text::TextStyle) -> f32 {
412 match style.font_size {
413 cranpose_ui::text::TextUnit::Sp(v) => v,
414 cranpose_ui::text::TextUnit::Em(v) => v * 14.0,
415 cranpose_ui::text::TextUnit::Unspecified => 14.0,
416 }
417}
418
419#[derive(Clone)]
424struct WgpuTextMeasurer {
425 font_system: Arc<Mutex<FontSystem>>,
426 size_cache: TextSizeCache,
427 text_cache: SharedTextCache,
429}
430
431impl WgpuTextMeasurer {
432 fn new(font_system: Arc<Mutex<FontSystem>>, text_cache: SharedTextCache) -> Self {
433 Self {
434 font_system,
435 size_cache: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(1024).unwrap()))),
437 text_cache,
438 }
439 }
440}
441
442impl TextMeasurer for WgpuTextMeasurer {
445 fn measure(
446 &self,
447 text: &str,
448 style: &cranpose_ui::text::TextStyle,
449 ) -> cranpose_ui::TextMetrics {
450 let font_size = resolve_font_size(style);
451 let size_int = (font_size * 100.0) as i32;
452
453 let mut hasher = FxHasher::default();
456 text.hash(&mut hasher);
457 let text_hash = hasher.finish();
458 let cache_key = (text_hash, size_int);
459
460 {
462 let mut cache = self.size_cache.lock().unwrap();
463 if let Some((cached_text, size)) = cache.get(&cache_key) {
464 if cached_text == text {
466 return cranpose_ui::TextMetrics {
467 width: size.width,
468 height: size.height,
469 line_height: font_size * 1.4,
470 line_count: 1, };
472 }
473 }
474 }
475
476 let text_buffer_key = TextCacheKey::new(text, font_size);
478 let mut font_system = self.font_system.lock().unwrap();
479 let mut text_cache = self.text_cache.lock().unwrap();
480
481 let size = {
483 let buffer = text_cache.entry(text_buffer_key).or_insert_with(|| {
484 let buffer =
485 Buffer::new(&mut font_system, Metrics::new(font_size, font_size * 1.4));
486 SharedTextBuffer {
487 buffer,
488 text: String::new(),
489 font_size: 0.0,
490 cached_size: None,
491 }
492 });
493
494 buffer.ensure(&mut font_system, text, font_size, Attrs::new());
496
497 buffer.size(font_size)
499 };
500
501 trim_text_cache(&mut text_cache);
503
504 drop(font_system);
505 drop(text_cache);
506
507 let mut size_cache = self.size_cache.lock().unwrap();
509 size_cache.put(cache_key, (text.to_string(), size));
511
512 let line_height = font_size * 1.4;
514 let line_count = text.split('\n').count().max(1);
515
516 cranpose_ui::TextMetrics {
517 width: size.width,
518 height: size.height,
519 line_height,
520 line_count,
521 }
522 }
523
524 fn get_offset_for_position(
525 &self,
526 text: &str,
527 style: &cranpose_ui::text::TextStyle,
528 x: f32,
529 y: f32,
530 ) -> usize {
531 let font_size = resolve_font_size(style);
532 if text.is_empty() {
533 return 0;
534 }
535
536 let line_height = font_size * 1.4;
537
538 let line_index = (y / line_height).floor().max(0.0) as usize;
540 let lines: Vec<&str> = text.split('\n').collect();
541 let target_line = line_index.min(lines.len().saturating_sub(1));
542
543 let mut line_start_byte = 0;
545 for line in lines.iter().take(target_line) {
546 line_start_byte += line.len() + 1; }
548
549 let line_text = lines.get(target_line).unwrap_or(&"");
551
552 if line_text.is_empty() {
553 return line_start_byte;
554 }
555
556 let cache_key = TextCacheKey::new(line_text, font_size);
558 let mut font_system = self.font_system.lock().unwrap();
559 let mut text_cache = self.text_cache.lock().unwrap();
560
561 let buffer = text_cache.entry(cache_key).or_insert_with(|| {
562 let buffer = Buffer::new(&mut font_system, Metrics::new(font_size, font_size * 1.4));
563 SharedTextBuffer {
564 buffer,
565 text: String::new(),
566 font_size: 0.0,
567 cached_size: None,
568 }
569 });
570
571 buffer.ensure(&mut font_system, line_text, font_size, Attrs::new());
572
573 let mut best_offset = 0;
575 let mut best_distance = f32::INFINITY;
576
577 for run in buffer.buffer.layout_runs() {
578 let mut glyph_x = 0.0f32;
579 for glyph in run.glyphs.iter() {
580 let left_dist = (x - glyph_x).abs();
582 if left_dist < best_distance {
583 best_distance = left_dist;
584 best_offset = glyph.start;
586 }
587
588 glyph_x += glyph.w;
590
591 let right_dist = (x - glyph_x).abs();
593 if right_dist < best_distance {
594 best_distance = right_dist;
595 best_offset = glyph.end;
596 }
597 }
598 }
599
600 line_start_byte + best_offset.min(line_text.len())
602 }
603
604 fn get_cursor_x_for_offset(
605 &self,
606 text: &str,
607 style: &cranpose_ui::text::TextStyle,
608 offset: usize,
609 ) -> f32 {
610 let clamped_offset = offset.min(text.len());
611 if clamped_offset == 0 {
612 return 0.0;
613 }
614
615 let prefix = &text[..clamped_offset];
617 self.measure(prefix, style).width
618 }
619
620 fn layout(
621 &self,
622 text: &str,
623 style: &cranpose_ui::text::TextStyle,
624 ) -> cranpose_ui::text_layout_result::TextLayoutResult {
625 use cranpose_ui::text_layout_result::{LineLayout, TextLayoutResult};
626
627 let font_size = resolve_font_size(style);
628 let line_height = font_size * 1.4;
629
630 let cache_key = TextCacheKey::new(text, font_size);
632 let mut font_system = self.font_system.lock().unwrap();
633 let mut text_cache = self.text_cache.lock().unwrap();
634
635 let buffer = text_cache.entry(cache_key.clone()).or_insert_with(|| {
636 let buffer = Buffer::new(&mut font_system, Metrics::new(font_size, line_height));
637 SharedTextBuffer {
638 buffer,
639 text: String::new(),
640 font_size: 0.0,
641 cached_size: None,
642 }
643 });
644 buffer.ensure(&mut font_system, text, font_size, Attrs::new());
645
646 let mut glyph_x_positions = Vec::new();
648 let mut char_to_byte = Vec::new();
649 let mut lines = Vec::new();
650
651 let mut current_line_y = 0.0f32;
652 let mut line_start_offset = 0usize;
653
654 for run in buffer.buffer.layout_runs() {
655 let line_idx = run.line_i;
656 let line_y = line_idx as f32 * line_height;
657
658 if lines.is_empty() || line_y != current_line_y {
660 if !lines.is_empty() {
661 if let Some(_last) = lines.last_mut() {
663 }
665 }
666 current_line_y = line_y;
667 }
668
669 for glyph in run.glyphs.iter() {
670 glyph_x_positions.push(glyph.x);
671 char_to_byte.push(glyph.start);
672
673 if glyph.end > line_start_offset {
675 line_start_offset = glyph.end;
676 }
677 }
678 }
679
680 let total_width = self.measure(text, style).width;
682 glyph_x_positions.push(total_width);
683 char_to_byte.push(text.len());
684
685 let mut y = 0.0f32;
687 let mut line_start = 0usize;
688 for (i, line_text) in text.split('\n').enumerate() {
689 let line_end = if i == text.split('\n').count() - 1 {
690 text.len()
691 } else {
692 line_start + line_text.len()
693 };
694
695 lines.push(LineLayout {
696 start_offset: line_start,
697 end_offset: line_end,
698 y,
699 height: line_height,
700 });
701
702 line_start = line_end + 1;
703 y += line_height;
704 }
705
706 if lines.is_empty() {
707 lines.push(LineLayout {
708 start_offset: 0,
709 end_offset: 0,
710 y: 0.0,
711 height: line_height,
712 });
713 }
714
715 let metrics = self.measure(text, style);
716 TextLayoutResult::new(
717 metrics.width,
718 metrics.height,
719 line_height,
720 glyph_x_positions,
721 char_to_byte,
722 lines,
723 text,
724 )
725 }
726}