1mod effect_renderer;
7mod offscreen;
8mod pipeline;
9mod render;
10mod scene;
11mod shader_cache;
12mod shaders;
13
14pub use scene::{BackdropLayer, ClickAction, DrawShape, HitRegion, ImageDraw, Scene, TextDraw};
15
16use cranpose_core::{MemoryApplier, NodeId};
17use cranpose_render_common::{RenderScene, Renderer};
18use cranpose_ui::{set_text_measurer, LayoutTree, TextMeasurer};
19use cranpose_ui_graphics::Size;
20use glyphon::{Attrs, Buffer, FontSystem, Metrics, Shaping};
21use lru::LruCache;
22use render::GpuRenderer;
23use rustc_hash::FxHasher;
24use std::collections::HashMap;
25use std::hash::{Hash, Hasher};
26use std::num::NonZeroUsize;
27use std::rc::Rc;
28use std::sync::{Arc, Mutex};
29
30type TextSizeCache = Arc<Mutex<LruCache<(u64, i32), (String, Size)>>>;
34
35#[derive(Debug)]
36pub enum WgpuRendererError {
37 Layout(String),
38 Wgpu(String),
39}
40
41#[derive(Debug, Clone)]
43pub struct CapturedFrame {
44 pub width: u32,
45 pub height: u32,
46 pub pixels: Vec<u8>,
47}
48
49#[derive(Clone, PartialEq, Eq, Hash)]
51pub(crate) enum TextKey {
52 Content(String),
53 Node(NodeId),
54}
55
56#[derive(Clone, PartialEq, Eq)]
57pub(crate) struct TextCacheKey {
58 key: TextKey,
59 scale_bits: u32, }
61
62impl TextCacheKey {
63 fn new(text: &str, font_size: f32) -> Self {
64 Self {
65 key: TextKey::Content(text.to_string()),
66 scale_bits: font_size.to_bits(),
67 }
68 }
69
70 fn for_node(node_id: NodeId, font_size: f32) -> Self {
71 Self {
72 key: TextKey::Node(node_id),
73 scale_bits: font_size.to_bits(),
74 }
75 }
76}
77
78impl Hash for TextCacheKey {
79 fn hash<H: Hasher>(&self, state: &mut H) {
80 self.key.hash(state);
81 self.scale_bits.hash(state);
82 }
83}
84
85pub(crate) struct SharedTextBuffer {
87 pub(crate) buffer: Buffer,
88 text: String,
89 font_size: f32,
90 cached_size: Option<Size>,
92}
93
94impl SharedTextBuffer {
95 pub(crate) fn ensure(
97 &mut self,
98 font_system: &mut FontSystem,
99 text: &str,
100 font_size: f32,
101 attrs: Attrs,
102 ) {
103 let text_changed = self.text != text;
104 let font_changed = (self.font_size - font_size).abs() > 0.1;
105
106 if !text_changed && !font_changed {
108 return; }
110
111 let metrics = Metrics::new(font_size, font_size * 1.4);
113 self.buffer.set_metrics(font_system, metrics);
114 self.buffer
115 .set_size(font_system, Some(f32::MAX), Some(f32::MAX));
116
117 self.buffer
119 .set_text(font_system, text, &attrs, Shaping::Advanced);
120 self.buffer.shape_until_scroll(font_system, false);
121
122 self.text.clear();
124 self.text.push_str(text);
125 self.font_size = font_size;
126 self.cached_size = None; }
128
129 pub(crate) fn size(&mut self, font_size: f32) -> Size {
131 if let Some(size) = self.cached_size {
132 return size;
133 }
134
135 let mut max_width = 0.0f32;
137 let layout_runs = self.buffer.layout_runs();
138 for run in layout_runs {
139 max_width = max_width.max(run.line_w);
140 }
141 let total_height = self.buffer.lines.len() as f32 * font_size * 1.4;
142
143 let size = Size {
144 width: max_width,
145 height: total_height,
146 };
147
148 self.cached_size = Some(size);
149 size
150 }
151}
152
153pub(crate) type SharedTextCache = Arc<Mutex<HashMap<TextCacheKey, SharedTextBuffer>>>;
155
156pub(crate) fn trim_text_cache(cache: &mut HashMap<TextCacheKey, SharedTextBuffer>) {
159 if cache.len() > MAX_CACHE_ITEMS {
160 let target_size = MAX_CACHE_ITEMS / 2;
161 let to_remove = cache.len() - target_size;
162
163 let keys_to_remove: Vec<TextCacheKey> = cache.keys().take(to_remove).cloned().collect();
165
166 for key in keys_to_remove {
167 cache.remove(&key);
168 }
169
170 log::debug!(
171 "Trimmed text cache from {} to {} entries",
172 cache.len() + to_remove,
173 cache.len()
174 );
175 }
176}
177
178const MAX_CACHE_ITEMS: usize = 256;
180
181pub struct WgpuRenderer {
189 scene: Scene,
190 gpu_renderer: Option<GpuRenderer>,
191 font_system: Arc<Mutex<FontSystem>>,
192 text_cache: SharedTextCache,
194 root_scale: f32,
196}
197
198impl WgpuRenderer {
199 pub fn new_with_fonts(fonts: &[&[u8]]) -> Self {
212 let mut font_system = FontSystem::new();
213
214 #[cfg(target_os = "android")]
219 {
220 log::info!("Skipping Android system fonts - using application-provided fonts");
221 }
223
224 for (i, font_data) in fonts.iter().enumerate() {
226 log::info!("Loading font #{}, size: {} bytes", i, font_data.len());
227 font_system.db_mut().load_font_data(font_data.to_vec());
228 }
229
230 let face_count = font_system.db().faces().count();
231 log::info!("Total font faces loaded: {}", face_count);
232
233 if face_count == 0 {
234 log::error!("No fonts loaded! Text rendering will fail!");
235 }
236
237 let font_system = Arc::new(Mutex::new(font_system));
238
239 let text_cache = Arc::new(Mutex::new(HashMap::new()));
241
242 let text_measurer = WgpuTextMeasurer::new(font_system.clone(), text_cache.clone());
243 set_text_measurer(text_measurer.clone());
244
245 Self {
246 scene: Scene::new(),
247 gpu_renderer: None,
248 font_system,
249 text_cache,
250 root_scale: 1.0,
251 }
252 }
253
254 pub fn new() -> Self {
259 let font_system = FontSystem::new();
260 let font_system = Arc::new(Mutex::new(font_system));
261 let text_cache = Arc::new(Mutex::new(HashMap::new()));
262
263 let text_measurer = WgpuTextMeasurer::new(font_system.clone(), text_cache.clone());
264 set_text_measurer(text_measurer.clone());
265
266 Self {
267 scene: Scene::new(),
268 gpu_renderer: None,
269 font_system,
270 text_cache,
271 root_scale: 1.0,
272 }
273 }
274
275 pub fn init_gpu(
277 &mut self,
278 device: Arc<wgpu::Device>,
279 queue: Arc<wgpu::Queue>,
280 surface_format: wgpu::TextureFormat,
281 ) {
282 self.gpu_renderer = Some(GpuRenderer::new(
283 device,
284 queue,
285 surface_format,
286 self.font_system.clone(),
287 self.text_cache.clone(),
288 ));
289 }
290
291 pub fn set_root_scale(&mut self, scale: f32) {
293 self.root_scale = scale;
294 }
295
296 pub fn render(
298 &mut self,
299 view: &wgpu::TextureView,
300 width: u32,
301 height: u32,
302 ) -> Result<(), WgpuRendererError> {
303 if let Some(gpu_renderer) = &mut self.gpu_renderer {
304 gpu_renderer
305 .render(
306 view,
307 &self.scene.shapes,
308 &self.scene.images,
309 &self.scene.texts,
310 &self.scene.shadow_draws,
311 &self.scene.effect_layers,
312 &self.scene.backdrop_layers,
313 width,
314 height,
315 self.root_scale,
316 )
317 .map_err(WgpuRendererError::Wgpu)
318 } else {
319 Err(WgpuRendererError::Wgpu(
320 "GPU renderer not initialized. Call init_gpu() first.".to_string(),
321 ))
322 }
323 }
324
325 pub fn capture_frame(
329 &mut self,
330 width: u32,
331 height: u32,
332 ) -> Result<CapturedFrame, WgpuRendererError> {
333 self.capture_frame_with_scale(width, height, self.root_scale)
334 }
335
336 pub fn capture_frame_with_scale(
338 &mut self,
339 width: u32,
340 height: u32,
341 root_scale: f32,
342 ) -> Result<CapturedFrame, WgpuRendererError> {
343 if let Some(gpu_renderer) = &mut self.gpu_renderer {
344 let pixels = gpu_renderer
345 .render_to_rgba_pixels(
346 &self.scene.shapes,
347 &self.scene.images,
348 &self.scene.texts,
349 &self.scene.shadow_draws,
350 &self.scene.effect_layers,
351 &self.scene.backdrop_layers,
352 width,
353 height,
354 root_scale,
355 )
356 .map_err(WgpuRendererError::Wgpu)?;
357 Ok(CapturedFrame {
358 width,
359 height,
360 pixels,
361 })
362 } else {
363 Err(WgpuRendererError::Wgpu(
364 "GPU renderer not initialized. Call init_gpu() first.".to_string(),
365 ))
366 }
367 }
368
369 pub fn device(&self) -> &wgpu::Device {
371 self.gpu_renderer
372 .as_ref()
373 .map(|r| &*r.device)
374 .expect("GPU renderer not initialized")
375 }
376}
377
378impl Default for WgpuRenderer {
379 fn default() -> Self {
380 Self::new()
381 }
382}
383
384impl Renderer for WgpuRenderer {
385 type Scene = Scene;
386 type Error = WgpuRendererError;
387
388 fn scene(&self) -> &Self::Scene {
389 &self.scene
390 }
391
392 fn scene_mut(&mut self) -> &mut Self::Scene {
393 &mut self.scene
394 }
395
396 fn rebuild_scene(
397 &mut self,
398 layout_tree: &LayoutTree,
399 _viewport: Size,
400 ) -> Result<(), Self::Error> {
401 self.scene.clear();
402 pipeline::render_layout_tree(layout_tree.root(), &mut self.scene);
404 Ok(())
405 }
406
407 fn rebuild_scene_from_applier(
408 &mut self,
409 applier: &mut MemoryApplier,
410 root: NodeId,
411 _viewport: Size,
412 ) -> Result<(), Self::Error> {
413 self.scene.clear();
414 pipeline::render_from_applier(applier, root, &mut self.scene, 1.0);
417 Ok(())
418 }
419
420 fn draw_dev_overlay(&mut self, text: &str, viewport: Size) {
421 use cranpose_ui_graphics::{BlendMode, Brush, Color, Rect, RoundedCornerShape};
422
423 let padding = 8.0;
426 let font_size = 14.0;
427
428 let char_width = 7.0;
430 let text_width = text.len() as f32 * char_width;
431 let text_height = font_size * 1.4;
432
433 let x = viewport.width - text_width - padding * 2.0;
434 let y = padding;
435
436 let bg_rect = Rect {
438 x,
439 y,
440 width: text_width + padding,
441 height: text_height + padding / 2.0,
442 };
443 self.scene.push_shape(
444 bg_rect,
445 Brush::Solid(Color(0.0, 0.0, 0.0, 0.7)),
446 Some(RoundedCornerShape::uniform(4.0)),
447 None,
448 BlendMode::SrcOver,
449 );
450
451 let text_rect = Rect {
453 x: x + padding / 2.0,
454 y: y + padding / 4.0,
455 width: text_width,
456 height: text_height,
457 };
458 self.scene.push_text(
459 NodeId::MAX,
460 text_rect,
461 Rc::from(text),
462 Color(0.0, 1.0, 0.0, 1.0), font_size,
464 1.0,
465 None,
466 );
467 }
468}
469
470fn resolve_font_size(style: &cranpose_ui::text::TextStyle) -> f32 {
471 match style.font_size {
472 cranpose_ui::text::TextUnit::Sp(v) => v,
473 cranpose_ui::text::TextUnit::Em(v) => v * 14.0,
474 cranpose_ui::text::TextUnit::Unspecified => 14.0,
475 }
476}
477
478#[derive(Clone)]
483struct WgpuTextMeasurer {
484 font_system: Arc<Mutex<FontSystem>>,
485 size_cache: TextSizeCache,
486 text_cache: SharedTextCache,
488}
489
490impl WgpuTextMeasurer {
491 fn new(font_system: Arc<Mutex<FontSystem>>, text_cache: SharedTextCache) -> Self {
492 Self {
493 font_system,
494 size_cache: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(1024).unwrap()))),
496 text_cache,
497 }
498 }
499}
500
501impl TextMeasurer for WgpuTextMeasurer {
504 fn measure(
505 &self,
506 text: &str,
507 style: &cranpose_ui::text::TextStyle,
508 ) -> cranpose_ui::TextMetrics {
509 let font_size = resolve_font_size(style);
510 let size_int = (font_size * 100.0) as i32;
511
512 let mut hasher = FxHasher::default();
515 text.hash(&mut hasher);
516 let text_hash = hasher.finish();
517 let cache_key = (text_hash, size_int);
518
519 {
521 let mut cache = self.size_cache.lock().unwrap();
522 if let Some((cached_text, size)) = cache.get(&cache_key) {
523 if cached_text == text {
525 return cranpose_ui::TextMetrics {
526 width: size.width,
527 height: size.height,
528 line_height: font_size * 1.4,
529 line_count: 1, };
531 }
532 }
533 }
534
535 let text_buffer_key = TextCacheKey::new(text, font_size);
537 let mut font_system = self.font_system.lock().unwrap();
538 let mut text_cache = self.text_cache.lock().unwrap();
539
540 let size = {
542 let buffer = text_cache.entry(text_buffer_key).or_insert_with(|| {
543 let buffer =
544 Buffer::new(&mut font_system, Metrics::new(font_size, font_size * 1.4));
545 SharedTextBuffer {
546 buffer,
547 text: String::new(),
548 font_size: 0.0,
549 cached_size: None,
550 }
551 });
552
553 buffer.ensure(&mut font_system, text, font_size, Attrs::new());
555
556 buffer.size(font_size)
558 };
559
560 trim_text_cache(&mut text_cache);
562
563 drop(font_system);
564 drop(text_cache);
565
566 let mut size_cache = self.size_cache.lock().unwrap();
568 size_cache.put(cache_key, (text.to_string(), size));
570
571 let line_height = font_size * 1.4;
573 let line_count = text.split('\n').count().max(1);
574
575 cranpose_ui::TextMetrics {
576 width: size.width,
577 height: size.height,
578 line_height,
579 line_count,
580 }
581 }
582
583 fn get_offset_for_position(
584 &self,
585 text: &str,
586 style: &cranpose_ui::text::TextStyle,
587 x: f32,
588 y: f32,
589 ) -> usize {
590 let font_size = resolve_font_size(style);
591 if text.is_empty() {
592 return 0;
593 }
594
595 let line_height = font_size * 1.4;
596
597 let line_index = (y / line_height).floor().max(0.0) as usize;
599 let lines: Vec<&str> = text.split('\n').collect();
600 let target_line = line_index.min(lines.len().saturating_sub(1));
601
602 let mut line_start_byte = 0;
604 for line in lines.iter().take(target_line) {
605 line_start_byte += line.len() + 1; }
607
608 let line_text = lines.get(target_line).unwrap_or(&"");
610
611 if line_text.is_empty() {
612 return line_start_byte;
613 }
614
615 let cache_key = TextCacheKey::new(line_text, font_size);
617 let mut font_system = self.font_system.lock().unwrap();
618 let mut text_cache = self.text_cache.lock().unwrap();
619
620 let buffer = text_cache.entry(cache_key).or_insert_with(|| {
621 let buffer = Buffer::new(&mut font_system, Metrics::new(font_size, font_size * 1.4));
622 SharedTextBuffer {
623 buffer,
624 text: String::new(),
625 font_size: 0.0,
626 cached_size: None,
627 }
628 });
629
630 buffer.ensure(&mut font_system, line_text, font_size, Attrs::new());
631
632 let mut best_offset = 0;
634 let mut best_distance = f32::INFINITY;
635
636 for run in buffer.buffer.layout_runs() {
637 let mut glyph_x = 0.0f32;
638 for glyph in run.glyphs.iter() {
639 let left_dist = (x - glyph_x).abs();
641 if left_dist < best_distance {
642 best_distance = left_dist;
643 best_offset = glyph.start;
645 }
646
647 glyph_x += glyph.w;
649
650 let right_dist = (x - glyph_x).abs();
652 if right_dist < best_distance {
653 best_distance = right_dist;
654 best_offset = glyph.end;
655 }
656 }
657 }
658
659 line_start_byte + best_offset.min(line_text.len())
661 }
662
663 fn get_cursor_x_for_offset(
664 &self,
665 text: &str,
666 style: &cranpose_ui::text::TextStyle,
667 offset: usize,
668 ) -> f32 {
669 let clamped_offset = offset.min(text.len());
670 if clamped_offset == 0 {
671 return 0.0;
672 }
673
674 let prefix = &text[..clamped_offset];
676 self.measure(prefix, style).width
677 }
678
679 fn layout(
680 &self,
681 text: &str,
682 style: &cranpose_ui::text::TextStyle,
683 ) -> cranpose_ui::text_layout_result::TextLayoutResult {
684 use cranpose_ui::text_layout_result::{LineLayout, TextLayoutResult};
685
686 let font_size = resolve_font_size(style);
687 let line_height = font_size * 1.4;
688
689 let cache_key = TextCacheKey::new(text, font_size);
691 let mut font_system = self.font_system.lock().unwrap();
692 let mut text_cache = self.text_cache.lock().unwrap();
693
694 let buffer = text_cache.entry(cache_key.clone()).or_insert_with(|| {
695 let buffer = Buffer::new(&mut font_system, Metrics::new(font_size, line_height));
696 SharedTextBuffer {
697 buffer,
698 text: String::new(),
699 font_size: 0.0,
700 cached_size: None,
701 }
702 });
703 buffer.ensure(&mut font_system, text, font_size, Attrs::new());
704
705 let mut glyph_x_positions = Vec::new();
707 let mut char_to_byte = Vec::new();
708 let mut lines = Vec::new();
709
710 let mut current_line_y = 0.0f32;
711 let mut line_start_offset = 0usize;
712
713 for run in buffer.buffer.layout_runs() {
714 let line_idx = run.line_i;
715 let line_y = line_idx as f32 * line_height;
716
717 if lines.is_empty() || line_y != current_line_y {
719 if !lines.is_empty() {
720 if let Some(_last) = lines.last_mut() {
722 }
724 }
725 current_line_y = line_y;
726 }
727
728 for glyph in run.glyphs.iter() {
729 glyph_x_positions.push(glyph.x);
730 char_to_byte.push(glyph.start);
731
732 if glyph.end > line_start_offset {
734 line_start_offset = glyph.end;
735 }
736 }
737 }
738
739 let total_width = self.measure(text, style).width;
741 glyph_x_positions.push(total_width);
742 char_to_byte.push(text.len());
743
744 let mut y = 0.0f32;
746 let mut line_start = 0usize;
747 for (i, line_text) in text.split('\n').enumerate() {
748 let line_end = if i == text.split('\n').count() - 1 {
749 text.len()
750 } else {
751 line_start + line_text.len()
752 };
753
754 lines.push(LineLayout {
755 start_offset: line_start,
756 end_offset: line_end,
757 y,
758 height: line_height,
759 });
760
761 line_start = line_end + 1;
762 y += line_height;
763 }
764
765 if lines.is_empty() {
766 lines.push(LineLayout {
767 start_offset: 0,
768 end_offset: 0,
769 y: 0.0,
770 height: line_height,
771 });
772 }
773
774 let metrics = self.measure(text, style);
775 TextLayoutResult::new(
776 metrics.width,
777 metrics.height,
778 line_height,
779 glyph_x_positions,
780 char_to_byte,
781 lines,
782 text,
783 )
784 }
785}