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