1use blinc_core::{
6 Brush, Color, CornerRadius, DrawCommand, DrawContext, DrawContextExt, Rect, Stroke,
7};
8use blinc_gpu::{
9 FontRegistry, GenericFont as GpuGenericFont, GpuGlyph, GpuImage, GpuImageInstance,
10 GpuPaintContext, GpuPrimitive, GpuRenderer, ImageRenderingContext, PrimitiveBatch,
11 TextAlignment, TextAnchor, TextRenderingContext,
12};
13use blinc_layout::div::{FontFamily, FontWeight, GenericFont, TextAlign, TextVerticalAlign};
14use blinc_layout::prelude::*;
15use blinc_layout::render_state::Overlay;
16use blinc_layout::renderer::ElementType;
17use blinc_svg::{RasterizedSvg, SvgDocument};
18use lru::LruCache;
19use std::collections::hash_map::DefaultHasher;
20use std::hash::{Hash, Hasher};
21use std::num::NonZeroUsize;
22use std::sync::{Arc, Mutex};
23
24use crate::error::Result;
25use crate::svg_atlas::SvgAtlas;
26
27const IMAGE_CACHE_CAPACITY: usize = 32;
29
30const SVG_CACHE_CAPACITY: usize = 128;
32
33fn intersect_clip_rects(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
35 let x1 = a[0].max(b[0]);
36 let y1 = a[1].max(b[1]);
37 let x2 = (a[0] + a[2]).min(b[0] + b[2]);
38 let y2 = (a[1] + a[3]).min(b[1] + b[3]);
39 [x1, y1, (x2 - x1).max(0.0), (y2 - y1).max(0.0)]
40}
41
42fn merge_scroll_clip(new_clip: [f32; 4], existing: Option<[f32; 4]>) -> Option<[f32; 4]> {
44 match existing {
45 Some(ex) => Some(intersect_clip_rects(new_clip, ex)),
46 None => Some(new_clip),
47 }
48}
49
50fn effective_single_clip(primary: Option<[f32; 4]>, scroll: Option<[f32; 4]>) -> Option<[f32; 4]> {
53 match (primary, scroll) {
54 (Some(c), Some(s)) => Some(intersect_clip_rects(c, s)),
55 (c, s) => c.or(s),
56 }
57}
58
59pub struct RenderContext {
63 renderer: GpuRenderer,
64 text_ctx: TextRenderingContext,
65 image_ctx: ImageRenderingContext,
66 device: Arc<wgpu::Device>,
67 queue: Arc<wgpu::Queue>,
68 sample_count: u32,
69 backdrop_texture: Option<CachedTexture>,
71 msaa_texture: Option<CachedTexture>,
73 image_cache: LruCache<String, GpuImage>,
75 svg_cache: LruCache<u64, SvgDocument>,
77 svg_atlas: SvgAtlas,
79 scratch_glyphs: Vec<GpuGlyph>,
81 scratch_texts: Vec<TextElement>,
82 scratch_svgs: Vec<SvgElement>,
83 scratch_images: Vec<ImageElement>,
84 cursor_pos: [f32; 2],
86 has_active_flows: bool,
88 frame_count: u64,
90}
91
92struct CachedTexture {
93 texture: wgpu::Texture,
94 view: wgpu::TextureView,
95 width: u32,
96 height: u32,
97}
98
99#[derive(Clone, Debug)]
103struct Transform3DLayerInfo {
104 node_id: LayoutNodeId,
106 layer_bounds: [f32; 4],
108 transform_3d: blinc_core::Transform3DParams,
110 opacity: f32,
112}
113
114#[derive(Clone)]
116struct TextElement {
117 content: String,
118 x: f32,
119 y: f32,
120 width: f32,
121 height: f32,
122 font_size: f32,
123 color: [f32; 4],
124 align: TextAlign,
125 weight: FontWeight,
126 italic: bool,
128 v_align: TextVerticalAlign,
130 clip_bounds: Option<[f32; 4]>,
132 motion_opacity: f32,
134 wrap: bool,
136 line_height: f32,
138 measured_width: f32,
140 font_family: FontFamily,
142 word_spacing: f32,
144 letter_spacing: f32,
146 z_index: u32,
148 ascender: f32,
150 strikethrough: bool,
152 underline: bool,
154 decoration_color: Option<[f32; 4]>,
156 decoration_thickness: Option<f32>,
158 css_affine: Option<[f32; 6]>,
161 text_shadow: Option<blinc_core::Shadow>,
163 transform_3d_layer: Option<Transform3DLayerInfo>,
165 is_foreground: bool,
167}
168
169#[derive(Clone)]
171struct ImageElement {
172 source: String,
173 x: f32,
174 y: f32,
175 width: f32,
176 height: f32,
177 object_fit: u8,
178 object_position: [f32; 2],
179 opacity: f32,
180 border_radius: f32,
181 tint: [f32; 4],
182 clip_bounds: Option<[f32; 4]>,
184 clip_radius: [f32; 4],
186 layer: RenderLayer,
188 loading_strategy: u8,
190 placeholder_type: u8,
192 placeholder_color: [f32; 4],
194 z_index: u32,
196 border_width: f32,
198 border_color: blinc_core::Color,
200 css_affine: Option<[f32; 6]>,
202 shadow: Option<blinc_core::Shadow>,
204 filter_a: [f32; 4],
206 filter_b: [f32; 4],
208 scroll_clip: Option<[f32; 4]>,
212 mask_params: [f32; 4],
214 mask_info: [f32; 4],
216 transform_3d_layer: Option<Transform3DLayerInfo>,
218}
219
220#[derive(Clone)]
222struct SvgElement {
223 source: Arc<str>,
224 x: f32,
225 y: f32,
226 width: f32,
227 height: f32,
228 tint: Option<blinc_core::Color>,
230 fill: Option<blinc_core::Color>,
232 stroke: Option<blinc_core::Color>,
234 stroke_width: Option<f32>,
236 stroke_dasharray: Option<Vec<f32>>,
238 stroke_dashoffset: Option<f32>,
240 svg_path_data: Option<String>,
242 clip_bounds: Option<[f32; 4]>,
244 motion_opacity: f32,
246 css_affine: Option<[f32; 6]>,
249 tag_overrides: std::collections::HashMap<String, blinc_layout::element::SvgTagStyle>,
251 transform_3d_layer: Option<Transform3DLayerInfo>,
253}
254
255#[derive(Clone)]
257struct FlowElement {
258 flow_name: String,
260 flow_graph: Option<std::sync::Arc<blinc_core::FlowGraph>>,
262 x: f32,
264 y: f32,
265 width: f32,
266 height: f32,
267 z_index: u32,
269 corner_radius: f32,
271}
272
273#[derive(Clone)]
275struct DebugBoundsElement {
276 x: f32,
277 y: f32,
278 width: f32,
279 height: f32,
280 element_type: String,
282 depth: u32,
284}
285
286impl RenderContext {
287 pub(crate) fn new(
289 renderer: GpuRenderer,
290 text_ctx: TextRenderingContext,
291 device: Arc<wgpu::Device>,
292 queue: Arc<wgpu::Queue>,
293 sample_count: u32,
294 ) -> Self {
295 let image_ctx = ImageRenderingContext::new(device.clone(), queue.clone());
296 let svg_atlas = SvgAtlas::new(&device);
297 Self {
298 renderer,
299 text_ctx,
300 image_ctx,
301 device,
302 queue,
303 sample_count,
304 backdrop_texture: None,
305 msaa_texture: None,
306 image_cache: LruCache::new(NonZeroUsize::new(IMAGE_CACHE_CAPACITY).unwrap()),
307 svg_cache: LruCache::new(NonZeroUsize::new(SVG_CACHE_CAPACITY).unwrap()),
308 svg_atlas,
309 scratch_glyphs: Vec::with_capacity(1024), scratch_texts: Vec::with_capacity(64), scratch_svgs: Vec::with_capacity(32), scratch_images: Vec::with_capacity(32), cursor_pos: [0.0; 2],
314 has_active_flows: false,
315 frame_count: 0,
316 }
317 }
318
319 pub fn set_cursor_position(&mut self, x: f32, y: f32) {
321 self.cursor_pos = [x, y];
322 }
323
324 pub fn has_active_flows(&self) -> bool {
327 self.has_active_flows
328 }
329
330 pub fn set_blend_target(&mut self, texture: &wgpu::Texture) {
333 self.renderer.set_blend_target(texture);
334 }
335
336 pub fn clear_blend_target(&mut self) {
338 self.renderer.clear_blend_target();
339 }
340
341 pub fn load_font_data_to_registry(&mut self, data: Vec<u8>) -> usize {
346 self.text_ctx.load_font_data_to_registry(data)
347 }
348
349 pub fn render_tree(
353 &mut self,
354 tree: &RenderTree,
355 width: u32,
356 height: u32,
357 target: &wgpu::TextureView,
358 ) -> Result<()> {
359 let scale_factor = tree.scale_factor();
361
362 let mut bg_ctx =
364 GpuPaintContext::with_text_context(width as f32, height as f32, &mut self.text_ctx);
365
366 tree.render_to_layer(&mut bg_ctx, RenderLayer::Background);
368 tree.render_to_layer(&mut bg_ctx, RenderLayer::Glass);
369
370 let mut bg_batch = bg_ctx.take_batch();
372
373 let mut fg_ctx =
375 GpuPaintContext::with_text_context(width as f32, height as f32, &mut self.text_ctx);
376 tree.render_to_layer(&mut fg_ctx, RenderLayer::Foreground);
377
378 let mut fg_batch = fg_ctx.take_batch();
380
381 let (texts, svgs, images, _flows) = self.collect_render_elements(tree);
383
384 self.preload_images(&images, width as f32, height as f32);
386
387 let mut all_glyphs = Vec::new();
389 let mut css_transformed_text_prims: Vec<GpuPrimitive> = Vec::new();
390 for text in &texts {
391 let alignment = match text.align {
393 TextAlign::Left => TextAlignment::Left,
394 TextAlign::Center => TextAlignment::Center,
395 TextAlign::Right => TextAlignment::Right,
396 };
397
398 let (anchor, y_pos, use_layout_height) = match text.v_align {
407 TextVerticalAlign::Center => {
408 (TextAnchor::Center, text.y + text.height / 2.0, false)
409 }
410 TextVerticalAlign::Top => (TextAnchor::Top, text.y, true),
411 TextVerticalAlign::Baseline => {
412 let baseline_y = text.y + text.ascender;
415 (TextAnchor::Baseline, baseline_y, false)
416 }
417 };
418
419 let wrap_width = if text.wrap {
422 if let Some(clip) = text.clip_bounds {
423 clip[2].min(text.width)
425 } else {
426 text.width
427 }
428 } else {
429 text.width
430 };
431
432 let font_name = text.font_family.name.as_deref();
434 let generic = to_gpu_generic_font(text.font_family.generic);
435 let font_weight = text.weight.weight();
436
437 let layout_height = if use_layout_height {
439 Some(text.height)
440 } else {
441 None
442 };
443
444 match self.text_ctx.prepare_text_with_style(
445 &text.content,
446 text.x,
447 y_pos,
448 text.font_size,
449 text.color,
450 anchor,
451 alignment,
452 Some(wrap_width),
453 text.wrap,
454 font_name,
455 generic,
456 font_weight,
457 text.italic,
458 layout_height,
459 text.letter_spacing,
460 ) {
461 Ok(mut glyphs) => {
462 tracing::trace!(
463 "Prepared {} glyphs for text '{}' (font={:?}, generic={:?})",
464 glyphs.len(),
465 text.content,
466 font_name,
467 generic
468 );
469 if let Some(clip) = text.clip_bounds {
471 for glyph in &mut glyphs {
472 glyph.clip_bounds = clip;
473 }
474 }
475
476 if let Some(affine) = text.css_affine {
477 let [a, b, c, d, tx, ty] = affine;
479 let tx_scaled = tx * scale_factor;
480 let ty_scaled = ty * scale_factor;
481 for glyph in &glyphs {
482 let gc_x = glyph.bounds[0] + glyph.bounds[2] / 2.0;
483 let gc_y = glyph.bounds[1] + glyph.bounds[3] / 2.0;
484 let new_gc_x = a * gc_x + c * gc_y + tx_scaled;
485 let new_gc_y = b * gc_x + d * gc_y + ty_scaled;
486 let mut prim = GpuPrimitive::from_glyph(glyph);
487 prim.bounds = [
488 new_gc_x - glyph.bounds[2] / 2.0,
489 new_gc_y - glyph.bounds[3] / 2.0,
490 glyph.bounds[2],
491 glyph.bounds[3],
492 ];
493 prim.local_affine = [a, b, c, d];
494 css_transformed_text_prims.push(prim);
495 }
496 } else {
497 all_glyphs.extend(glyphs);
498 }
499 }
500 Err(e) => {
501 tracing::warn!("Failed to prepare text '{}': {:?}", text.content, e);
502 }
503 }
504 }
505
506 tracing::trace!(
507 "Text rendering: {} texts collected, {} total glyphs prepared",
508 texts.len(),
509 all_glyphs.len()
510 );
511
512 self.renderer.resize(width, height);
516
517 if !css_transformed_text_prims.is_empty() {
520 if let (Some(atlas), Some(color_atlas)) =
521 (self.text_ctx.atlas_view(), self.text_ctx.color_atlas_view())
522 {
523 bg_batch.primitives.append(&mut css_transformed_text_prims);
524 self.renderer.set_glyph_atlas(atlas, color_atlas);
525 }
526 }
527
528 let has_glass = bg_batch.glass_count() > 0;
529
530 if has_glass {
532 self.ensure_glass_textures(width, height);
533 }
534 let use_msaa_overlay = self.sample_count > 1;
535
536 if has_glass {
540 let (bg_images, fg_images): (Vec<_>, Vec<_>) = images
543 .iter()
544 .partition(|img| img.layer == RenderLayer::Background);
545
546 let has_bg_images = !bg_images.is_empty();
548 if has_bg_images {
549 let backdrop_tex = self.backdrop_texture.take().unwrap();
551 self.renderer
552 .clear_target(&backdrop_tex.view, wgpu::Color::TRANSPARENT);
553 self.renderer.clear_target(target, wgpu::Color::BLACK);
554 self.render_images_ref(&backdrop_tex.view, &bg_images);
555 self.render_images_ref(target, &bg_images);
556 self.backdrop_texture = Some(backdrop_tex);
557 }
558
559 {
562 let backdrop = self.backdrop_texture.as_ref().unwrap();
563 self.renderer.render_glass_frame(
564 target,
565 &backdrop.view,
566 (backdrop.width, backdrop.height),
567 &bg_batch,
568 has_bg_images,
569 );
570 }
571
572 if use_msaa_overlay && bg_batch.has_paths() {
575 self.renderer
576 .render_paths_overlay_msaa(target, &bg_batch, self.sample_count);
577 }
578
579 if !has_bg_images {
581 self.render_images_ref(target, &bg_images);
582 }
583
584 self.render_images_ref(target, &fg_images);
586
587 let has_layer_effects = fg_batch.has_layer_effects();
590 if has_layer_effects {
591 fg_batch.convert_glyphs_to_primitives();
593 if !fg_batch.is_empty() {
594 self.preload_mask_images(&fg_batch);
596 self.renderer.render_overlay(target, &fg_batch);
597 }
598 if !svgs.is_empty() {
600 self.render_rasterized_svgs(target, &svgs, scale_factor);
601 }
602 } else if self.renderer.unified_text_rendering() {
603 let unified_primitives = fg_batch.get_unified_foreground_primitives();
605 if !unified_primitives.is_empty() {
606 self.render_unified(target, &unified_primitives);
607 }
608
609 if use_msaa_overlay && fg_batch.has_paths() {
611 self.renderer
612 .render_paths_overlay_msaa(target, &fg_batch, self.sample_count);
613 }
614
615 if !svgs.is_empty() {
617 self.render_rasterized_svgs(target, &svgs, scale_factor);
618 }
619 } else {
620 if !fg_batch.is_empty() {
622 if use_msaa_overlay {
623 self.renderer
624 .render_overlay_msaa(target, &fg_batch, self.sample_count);
625 } else {
626 self.renderer.render_overlay(target, &fg_batch);
627 }
628 }
629
630 if !all_glyphs.is_empty() {
632 self.render_text(target, &all_glyphs);
633 }
634
635 if !svgs.is_empty() {
637 self.render_rasterized_svgs(target, &svgs, scale_factor);
638 }
639 }
640
641 let decorations_by_layer = generate_text_decoration_primitives_by_layer(&texts);
643 for primitives in decorations_by_layer.values() {
644 if !primitives.is_empty() {
645 self.render_unified(target, primitives);
646 }
647 }
648 } else {
649 self.renderer
656 .render_with_clear(target, &bg_batch, [0.0, 0.0, 0.0, 1.0]);
657
658 if use_msaa_overlay && bg_batch.has_paths() {
660 self.renderer
661 .render_paths_overlay_msaa(target, &bg_batch, self.sample_count);
662 }
663
664 self.render_images(target, &images, width as f32, height as f32, scale_factor);
666
667 let has_layer_effects = fg_batch.has_layer_effects();
671 if has_layer_effects {
672 fg_batch.convert_glyphs_to_primitives();
675
676 if !fg_batch.is_empty() {
678 self.renderer.render_overlay(target, &fg_batch);
679 }
680 if !svgs.is_empty() {
682 self.render_rasterized_svgs(target, &svgs, scale_factor);
683 }
684 } else if self.renderer.unified_text_rendering() {
685 let unified_primitives = fg_batch.get_unified_foreground_primitives();
688 if !unified_primitives.is_empty() {
689 self.render_unified(target, &unified_primitives);
690 }
691
692 if use_msaa_overlay && fg_batch.has_paths() {
694 self.renderer
695 .render_paths_overlay_msaa(target, &fg_batch, self.sample_count);
696 }
697
698 if !svgs.is_empty() {
700 self.render_rasterized_svgs(target, &svgs, scale_factor);
701 }
702 } else {
703 if !fg_batch.is_empty() {
705 if use_msaa_overlay {
706 self.renderer
707 .render_overlay_msaa(target, &fg_batch, self.sample_count);
708 } else {
709 self.renderer.render_overlay(target, &fg_batch);
710 }
711 }
712
713 if !all_glyphs.is_empty() {
715 self.render_text(target, &all_glyphs);
716 }
717
718 if !svgs.is_empty() {
720 self.render_rasterized_svgs(target, &svgs, scale_factor);
721 }
722 }
723
724 let decorations_by_layer = generate_text_decoration_primitives_by_layer(&texts);
726 for primitives in decorations_by_layer.values() {
727 if !primitives.is_empty() {
728 self.render_unified(target, primitives);
729 }
730 }
731 }
732
733 self.return_scratch_elements(texts, svgs, images);
735
736 self.renderer.poll();
738
739 Ok(())
740 }
741
742 #[inline]
744 fn return_scratch_elements(
745 &mut self,
746 mut texts: Vec<TextElement>,
747 mut svgs: Vec<SvgElement>,
748 mut images: Vec<ImageElement>,
749 ) {
750 texts.clear();
752 svgs.clear();
753 images.clear();
754 self.scratch_texts = texts;
755 self.scratch_svgs = svgs;
756 self.scratch_images = images;
757 }
758
759 fn log_cache_stats(&mut self) {
762 self.frame_count += 1;
763 if self.frame_count % 300 != 1 {
764 return;
765 }
766 let (aw, ah) = self.text_ctx.atlas_dimensions();
767 let (caw, cah) = self.text_ctx.color_atlas_dimensions();
768 let atlas_glyphs = self.text_ctx.atlas().glyph_count();
769 let atlas_util = self.text_ctx.atlas().utilization();
770 let color_glyphs = self.text_ctx.color_atlas().glyph_count();
771 let color_util = self.text_ctx.color_atlas().utilization();
772 let glyph_cache = self.text_ctx.glyph_cache_len();
773 let glyph_cap = self.text_ctx.glyph_cache_capacity();
774 let color_cache = self.text_ctx.color_glyph_cache_len();
775 let color_cap = self.text_ctx.color_glyph_cache_capacity();
776 let img_cache = self.image_cache.len();
777 let svg_cache = self.svg_cache.len();
778 let svg_atlas_entries = self.svg_atlas.entry_count();
779 let svg_atlas_util = self.svg_atlas.utilization();
780 let (svg_aw, svg_ah) = (self.svg_atlas.width(), self.svg_atlas.height());
781
782 tracing::info!(
783 "Cache stats [frame {}]: \
784 atlas={}x{} ({} glyphs, {:.1}% used), \
785 color_atlas={}x{} ({} glyphs, {:.1}% used), \
786 glyph_lru={}/{}, color_glyph_lru={}/{}, \
787 image={}/{}, svg_doc={}/{}, svg_atlas={}x{} ({} entries, {:.1}% used)",
788 self.frame_count,
789 aw,
790 ah,
791 atlas_glyphs,
792 atlas_util * 100.0,
793 caw,
794 cah,
795 color_glyphs,
796 color_util * 100.0,
797 glyph_cache,
798 glyph_cap,
799 color_cache,
800 color_cap,
801 img_cache,
802 IMAGE_CACHE_CAPACITY,
803 svg_cache,
804 SVG_CACHE_CAPACITY,
805 svg_aw,
806 svg_ah,
807 svg_atlas_entries,
808 svg_atlas_util * 100.0,
809 );
810 }
811
812 fn ensure_glass_textures(&mut self, width: u32, height: u32) {
818 let format = self.renderer.texture_format();
820
821 let backdrop_width = (width / 2).max(1);
824 let backdrop_height = (height / 2).max(1);
825
826 let needs_backdrop = self
827 .backdrop_texture
828 .as_ref()
829 .map(|t| t.width != backdrop_width || t.height != backdrop_height)
830 .unwrap_or(true);
831
832 if needs_backdrop {
833 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
835 label: Some("Glass Backdrop"),
836 size: wgpu::Extent3d {
837 width: backdrop_width,
838 height: backdrop_height,
839 depth_or_array_layers: 1,
840 },
841 mip_level_count: 1,
842 sample_count: 1,
843 dimension: wgpu::TextureDimension::D2,
844 format,
845 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
846 | wgpu::TextureUsages::TEXTURE_BINDING,
847 view_formats: &[],
848 });
849 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
850 self.backdrop_texture = Some(CachedTexture {
851 texture,
852 view,
853 width: backdrop_width,
854 height: backdrop_height,
855 });
856 }
857 }
858
859 fn render_text(&mut self, target: &wgpu::TextureView, glyphs: &[GpuGlyph]) {
861 if let (Some(atlas_view), Some(color_atlas_view)) =
862 (self.text_ctx.atlas_view(), self.text_ctx.color_atlas_view())
863 {
864 self.renderer.render_text(
865 target,
866 glyphs,
867 atlas_view,
868 color_atlas_view,
869 self.text_ctx.sampler(),
870 );
871 }
872 }
873
874 fn render_unified(&mut self, target: &wgpu::TextureView, primitives: &[GpuPrimitive]) {
879 if primitives.is_empty() {
880 return;
881 }
882
883 self.renderer.render_primitives_overlay(target, primitives);
884 }
885
886 fn render_text_decorations_for_layer(
888 &mut self,
889 target: &wgpu::TextureView,
890 decorations_by_layer: &std::collections::HashMap<u32, Vec<GpuPrimitive>>,
891 z_layer: u32,
892 ) {
893 if let Some(primitives) = decorations_by_layer.get(&z_layer) {
894 if !primitives.is_empty() {
895 self.renderer.render_primitives_overlay(target, primitives);
896 }
897 }
898 }
899
900 fn render_text_debug(&mut self, target: &wgpu::TextureView, texts: &[TextElement]) {
908 let debug_primitives = generate_text_debug_primitives(texts);
909 if !debug_primitives.is_empty() {
910 self.renderer
911 .render_primitives_overlay(target, &debug_primitives);
912 }
913 }
914
915 fn render_layout_debug(&mut self, target: &wgpu::TextureView, tree: &RenderTree, scale: f32) {
921 let debug_bounds = collect_debug_bounds(tree, scale);
922 let debug_primitives = generate_layout_debug_primitives(&debug_bounds);
923 if !debug_primitives.is_empty() {
924 self.renderer
925 .render_primitives_overlay(target, &debug_primitives);
926 }
927 }
928
929 fn render_motion_debug(
935 &mut self,
936 target: &wgpu::TextureView,
937 tree: &RenderTree,
938 width: u32,
939 _height: u32,
940 ) {
941 let stats = tree.debug_stats();
942 let mut debug_primitives = Vec::new();
943
944 let panel_width = 200.0;
946 let panel_height = 100.0;
947 let panel_x = width as f32 - panel_width - 10.0;
948 let panel_y = 10.0;
949
950 debug_primitives.push(
952 GpuPrimitive::rect(panel_x, panel_y, panel_width, panel_height)
953 .with_color(0.1, 0.1, 0.15, 0.85)
954 .with_corner_radius(6.0),
955 );
956
957 let has_active = stats.visual_animation_count > 0
959 || stats.layout_animation_count > 0
960 || stats.animated_bounds_count > 0;
961
962 let (r, g, b, a) = if has_active {
963 (0.2, 0.9, 0.3, 1.0) } else {
965 (0.4, 0.4, 0.5, 1.0) };
967
968 debug_primitives.push(
969 GpuPrimitive::rect(panel_x + 10.0, panel_y + 12.0, 10.0, 10.0)
970 .with_color(r, g, b, a)
971 .with_corner_radius(5.0),
972 );
973
974 let bar_x = panel_x + 12.0;
976 let bar_width = panel_width - 24.0;
977 let bar_height = 6.0;
978
979 let visual_ratio = (stats.visual_animation_count as f32).min(10.0) / 10.0;
981 if visual_ratio > 0.0 {
982 debug_primitives.push(
983 GpuPrimitive::rect(bar_x, panel_y + 35.0, bar_width * visual_ratio, bar_height)
984 .with_color(0.0, 0.8, 0.9, 0.9)
985 .with_corner_radius(3.0),
986 );
987 }
988
989 let layout_ratio = (stats.layout_animation_count as f32).min(10.0) / 10.0;
991 if layout_ratio > 0.0 {
992 debug_primitives.push(
993 GpuPrimitive::rect(bar_x, panel_y + 50.0, bar_width * layout_ratio, bar_height)
994 .with_color(0.9, 0.2, 0.8, 0.9)
995 .with_corner_radius(3.0),
996 );
997 }
998
999 let bounds_ratio = (stats.animated_bounds_count as f32).min(50.0) / 50.0;
1001 if bounds_ratio > 0.0 {
1002 debug_primitives.push(
1003 GpuPrimitive::rect(bar_x, panel_y + 65.0, bar_width * bounds_ratio, bar_height)
1004 .with_color(0.95, 0.85, 0.2, 0.9)
1005 .with_corner_radius(3.0),
1006 );
1007 }
1008
1009 let scroll_count = stats.scroll_physics_count.min(8);
1011 for i in 0..scroll_count {
1012 debug_primitives.push(
1013 GpuPrimitive::rect(bar_x + (i as f32 * 14.0), panel_y + 80.0, 8.0, 8.0)
1014 .with_color(1.0, 0.6, 0.2, 0.9)
1015 .with_corner_radius(4.0),
1016 );
1017 }
1018
1019 if !debug_primitives.is_empty() {
1020 self.renderer
1021 .render_primitives_overlay(target, &debug_primitives);
1022 }
1023 }
1024
1025 fn render_images_to_backdrop(&mut self, images: &[&ImageElement]) {
1027 let Some(ref backdrop) = self.backdrop_texture else {
1028 return;
1029 };
1030 let target = backdrop
1032 .texture
1033 .create_view(&wgpu::TextureViewDescriptor::default());
1034 self.render_images_ref(&target, images);
1035 }
1036
1037 fn preload_images(
1042 &mut self,
1043 images: &[ImageElement],
1044 viewport_width: f32,
1045 viewport_height: f32,
1046 ) {
1047 const VISIBILITY_BUFFER: f32 = 100.0;
1049
1050 for image in images {
1051 if self.image_cache.contains(&image.source) {
1053 continue;
1054 }
1055
1056 if image.loading_strategy == 1 {
1058 let is_visible = if let Some([clip_x, clip_y, clip_w, clip_h]) = image.clip_bounds {
1061 let clip_left = clip_x - VISIBILITY_BUFFER;
1063 let clip_top = clip_y - VISIBILITY_BUFFER;
1064 let clip_right = clip_x + clip_w + VISIBILITY_BUFFER;
1065 let clip_bottom = clip_y + clip_h + VISIBILITY_BUFFER;
1066
1067 let image_right = image.x + image.width;
1068 let image_bottom = image.y + image.height;
1069
1070 image.x < clip_right
1071 && image_right > clip_left
1072 && image.y < clip_bottom
1073 && image_bottom > clip_top
1074 } else {
1075 let viewport_left = -VISIBILITY_BUFFER;
1077 let viewport_top = -VISIBILITY_BUFFER;
1078 let viewport_right = viewport_width + VISIBILITY_BUFFER;
1079 let viewport_bottom = viewport_height + VISIBILITY_BUFFER;
1080
1081 let image_right = image.x + image.width;
1082 let image_bottom = image.y + image.height;
1083
1084 image.x < viewport_right
1085 && image_right > viewport_left
1086 && image.y < viewport_bottom
1087 && image_bottom > viewport_top
1088 };
1089
1090 if !is_visible {
1091 continue;
1093 }
1094 }
1095
1096 let source = blinc_image::ImageSource::from_uri(&image.source);
1098 let image_data = match blinc_image::ImageData::load(source) {
1099 Ok(data) => data,
1100 Err(e) => {
1101 tracing::trace!("Failed to load image '{}': {:?}", image.source, e);
1102 continue; }
1104 };
1105
1106 let gpu_image = self.image_ctx.create_image_labeled(
1108 image_data.pixels(),
1109 image_data.width(),
1110 image_data.height(),
1111 &image.source,
1112 );
1113
1114 self.image_cache.put(image.source.clone(), gpu_image);
1116 }
1117 }
1118
1119 fn preload_mask_images(&mut self, batch: &PrimitiveBatch) {
1121 use blinc_core::LayerEffect;
1122 for entry in &batch.layer_commands {
1123 if let blinc_gpu::primitives::LayerCommand::Push { config } = &entry.command {
1124 for effect in &config.effects {
1125 if let LayerEffect::MaskImage { image_url, .. } = effect {
1126 if self.renderer.has_mask_image(image_url) {
1127 continue;
1128 }
1129 let source = blinc_image::ImageSource::from_uri(image_url);
1130 if let Ok(data) = blinc_image::ImageData::load(source) {
1131 self.renderer.load_mask_image_rgba(
1132 image_url,
1133 data.pixels(),
1134 data.width(),
1135 data.height(),
1136 );
1137 }
1138 }
1139 }
1140 }
1141 }
1142 }
1143
1144 fn mask_image_to_arrays(mask: Option<&blinc_core::MaskImage>) -> ([f32; 4], [f32; 4]) {
1149 match mask {
1150 Some(blinc_core::MaskImage::Gradient(gradient)) => match gradient {
1151 blinc_core::Gradient::Linear {
1152 start, end, stops, ..
1153 } => {
1154 let (sa, ea) = Self::extract_mask_alphas_from_stops(stops);
1155 ([start.x, start.y, end.x, end.y], [1.0, sa, ea, 0.0])
1156 }
1157 blinc_core::Gradient::Radial {
1158 center,
1159 radius,
1160 stops,
1161 ..
1162 } => {
1163 let (sa, ea) = Self::extract_mask_alphas_from_stops(stops);
1164 ([center.x, center.y, *radius, 0.0], [2.0, sa, ea, 0.0])
1165 }
1166 blinc_core::Gradient::Conic { center, stops, .. } => {
1167 let (sa, ea) = Self::extract_mask_alphas_from_stops(stops);
1168 ([center.x, center.y, 0.5, 0.0], [2.0, sa, ea, 0.0])
1169 }
1170 },
1171 _ => ([0.0; 4], [0.0; 4]),
1172 }
1173 }
1174
1175 fn extract_mask_alphas_from_stops(stops: &[blinc_core::GradientStop]) -> (f32, f32) {
1176 if stops.is_empty() {
1177 return (1.0, 0.0);
1178 }
1179 (stops[0].color.a, stops[stops.len() - 1].color.a)
1180 }
1181
1182 fn css_filter_to_arrays(
1183 filter: &blinc_layout::element_style::CssFilter,
1184 ) -> ([f32; 4], [f32; 4]) {
1185 (
1186 [
1187 filter.grayscale,
1188 filter.invert,
1189 filter.sepia,
1190 filter.hue_rotate.to_radians(),
1191 ],
1192 [filter.brightness, filter.contrast, filter.saturate, 0.0],
1193 )
1194 }
1195
1196 fn transform_clip_by_affine(
1200 clip: [f32; 4],
1201 clip_radius: [f32; 4],
1202 affine: [f32; 6],
1203 scale_factor: f32,
1204 ) -> ([f32; 4], [f32; 4]) {
1205 let [a, b, c, d, tx, ty] = affine;
1206 let tx_s = tx * scale_factor;
1207 let ty_s = ty * scale_factor;
1208 let ccx = clip[0] + clip[2] * 0.5;
1210 let ccy = clip[1] + clip[3] * 0.5;
1211 let new_cx = a * ccx + c * ccy + tx_s;
1212 let new_cy = b * ccx + d * ccy + ty_s;
1213 let s = (a * d - b * c).abs().sqrt().max(1e-6);
1215 let new_clip = [
1216 new_cx - clip[2] * s * 0.5,
1217 new_cy - clip[3] * s * 0.5,
1218 clip[2] * s,
1219 clip[3] * s,
1220 ];
1221 let new_radius = [
1222 clip_radius[0] * s,
1223 clip_radius[1] * s,
1224 clip_radius[2] * s,
1225 clip_radius[3] * s,
1226 ];
1227 (new_clip, new_radius)
1228 }
1229
1230 fn decompose_image_affine(
1235 x: f32,
1236 y: f32,
1237 w: f32,
1238 h: f32,
1239 affine: [f32; 6],
1240 scale_factor: f32,
1241 ) -> (f32, f32, f32, f32, f32, f32, f32, f32) {
1242 let [a, b, c, d, tx, ty] = affine;
1243 let tx_s = tx * scale_factor;
1245 let ty_s = ty * scale_factor;
1246 let cx = x + w * 0.5;
1248 let cy = y + h * 0.5;
1249 let new_cx = a * cx + c * cy + tx_s;
1250 let new_cy = b * cx + d * cy + ty_s;
1251 (new_cx - w * 0.5, new_cy - h * 0.5, w, h, a, b, c, d)
1253 }
1254
1255 fn render_images(
1257 &mut self,
1258 target: &wgpu::TextureView,
1259 images: &[ImageElement],
1260 viewport_width: f32,
1261 viewport_height: f32,
1262 scale_factor: f32,
1263 ) {
1264 use blinc_image::{calculate_fit_rects, src_rect_to_uv, ObjectFit, ObjectPosition};
1265
1266 for image in images {
1267 let gpu_image = self.image_cache.get(&image.source);
1269
1270 if gpu_image.is_none() && image.placeholder_type != 0 {
1272 if image.placeholder_type == 1 {
1274 let color = blinc_core::Color::rgba(
1276 image.placeholder_color[0],
1277 image.placeholder_color[1],
1278 image.placeholder_color[2],
1279 image.placeholder_color[3],
1280 );
1281
1282 let mut ctx = GpuPaintContext::new(viewport_width, viewport_height);
1284
1285 let rect = blinc_core::Rect::new(image.x, image.y, image.width, image.height);
1286
1287 ctx.fill_rounded_rect(
1288 rect,
1289 blinc_core::CornerRadius::uniform(image.border_radius),
1290 color,
1291 );
1292
1293 let batch = ctx.take_batch();
1294 self.renderer.render_overlay(target, &batch);
1295 }
1296 continue;
1298 }
1299
1300 let Some(gpu_image) = gpu_image else {
1301 continue; };
1303
1304 let object_fit = match image.object_fit {
1306 0 => ObjectFit::Cover,
1307 1 => ObjectFit::Contain,
1308 2 => ObjectFit::Fill,
1309 3 => ObjectFit::ScaleDown,
1310 4 => ObjectFit::None,
1311 _ => ObjectFit::Cover,
1312 };
1313
1314 let object_position =
1316 ObjectPosition::new(image.object_position[0], image.object_position[1]);
1317
1318 let (src_rect, dst_rect) = calculate_fit_rects(
1320 gpu_image.width(),
1321 gpu_image.height(),
1322 image.width,
1323 image.height,
1324 object_fit,
1325 object_position,
1326 );
1327
1328 let src_uv = src_rect_to_uv(src_rect, gpu_image.width(), gpu_image.height());
1330
1331 let base_x = image.x + dst_rect[0];
1333 let base_y = image.y + dst_rect[1];
1334 let base_w = dst_rect[2];
1335 let base_h = dst_rect[3];
1336
1337 let (draw_x, draw_y, draw_w, draw_h, ta, tb, tc, td) = if let Some(affine) =
1338 image.css_affine
1339 {
1340 Self::decompose_image_affine(base_x, base_y, base_w, base_h, affine, scale_factor)
1341 } else {
1342 (base_x, base_y, base_w, base_h, 1.0, 0.0, 0.0, 1.0)
1343 };
1344
1345 let effective_clip = image.clip_bounds.map(|clip| {
1347 if let Some(affine) = image.css_affine {
1348 Self::transform_clip_by_affine(clip, image.clip_radius, affine, scale_factor)
1349 } else {
1350 (clip, image.clip_radius)
1351 }
1352 });
1353
1354 if let Some(ref shadow) = image.shadow {
1356 let mut shadow_ctx = GpuPaintContext::new(viewport_width, viewport_height);
1357 if let Some(clip) = image.clip_bounds {
1359 shadow_ctx.push_clip(blinc_core::ClipShape::RoundedRect {
1360 rect: blinc_core::Rect::new(clip[0], clip[1], clip[2], clip[3]),
1361 corner_radius: blinc_core::CornerRadius {
1362 top_left: image.clip_radius[0],
1363 top_right: image.clip_radius[1],
1364 bottom_right: image.clip_radius[2],
1365 bottom_left: image.clip_radius[3],
1366 },
1367 });
1368 }
1369 let shadow_rect =
1370 blinc_core::Rect::new(image.x, image.y, image.width, image.height);
1371 let shadow_radius = blinc_core::CornerRadius::uniform(image.border_radius);
1372 shadow_ctx.draw_shadow(shadow_rect, shadow_radius, *shadow);
1373 let shadow_batch = shadow_ctx.take_batch();
1374 self.renderer.render_overlay(target, &shadow_batch);
1375 }
1376
1377 let mut instance = GpuImageInstance::new(draw_x, draw_y, draw_w, draw_h)
1379 .with_src_uv(src_uv[0], src_uv[1], src_uv[2], src_uv[3])
1380 .with_tint(image.tint[0], image.tint[1], image.tint[2], image.tint[3])
1381 .with_border_radius(image.border_radius)
1382 .with_opacity(image.opacity)
1383 .with_transform(ta, tb, tc, td)
1384 .with_filter(image.filter_a, image.filter_b);
1385
1386 if image.border_width > 0.0 {
1388 instance = instance.with_image_border(
1389 image.border_width,
1390 image.border_color.r,
1391 image.border_color.g,
1392 image.border_color.b,
1393 image.border_color.a,
1394 );
1395 }
1396
1397 if image.mask_info[0] > 0.5 {
1399 instance.mask_params = image.mask_params;
1400 instance.mask_info = image.mask_info;
1401 }
1402
1403 if let Some((clip, clip_r)) = effective_clip {
1405 instance = instance.with_clip_rounded_rect_corners(
1406 clip[0], clip[1], clip[2], clip[3], clip_r[0], clip_r[1], clip_r[2], clip_r[3],
1407 );
1408 }
1409 if let Some(sc) = image.scroll_clip {
1411 instance = instance.with_clip2_rect(sc[0], sc[1], sc[2], sc[3]);
1412 }
1413
1414 self.renderer
1416 .render_images(target, gpu_image.view(), &[instance]);
1417 }
1418 }
1419
1420 fn render_images_ref(&mut self, target: &wgpu::TextureView, images: &[&ImageElement]) {
1422 use blinc_image::{calculate_fit_rects, src_rect_to_uv, ObjectFit, ObjectPosition};
1423
1424 for image in images {
1425 let Some(gpu_image) = self.image_cache.get(&image.source) else {
1427 continue; };
1429
1430 let object_fit = match image.object_fit {
1432 0 => ObjectFit::Cover,
1433 1 => ObjectFit::Contain,
1434 2 => ObjectFit::Fill,
1435 3 => ObjectFit::ScaleDown,
1436 4 => ObjectFit::None,
1437 _ => ObjectFit::Cover,
1438 };
1439
1440 let object_position =
1442 ObjectPosition::new(image.object_position[0], image.object_position[1]);
1443
1444 let (src_rect, dst_rect) = calculate_fit_rects(
1446 gpu_image.width(),
1447 gpu_image.height(),
1448 image.width,
1449 image.height,
1450 object_fit,
1451 object_position,
1452 );
1453
1454 let src_uv = src_rect_to_uv(src_rect, gpu_image.width(), gpu_image.height());
1456
1457 let base_x = image.x + dst_rect[0];
1459 let base_y = image.y + dst_rect[1];
1460 let base_w = dst_rect[2];
1461 let base_h = dst_rect[3];
1462
1463 let (draw_x, draw_y, draw_w, draw_h, ta, tb, tc, td) =
1466 if let Some(affine) = image.css_affine {
1467 Self::decompose_image_affine(base_x, base_y, base_w, base_h, affine, 1.0)
1468 } else {
1469 (base_x, base_y, base_w, base_h, 1.0, 0.0, 0.0, 1.0)
1470 };
1471
1472 let effective_clip = image.clip_bounds.map(|clip| {
1474 if let Some(affine) = image.css_affine {
1475 Self::transform_clip_by_affine(clip, image.clip_radius, affine, 1.0)
1476 } else {
1477 (clip, image.clip_radius)
1478 }
1479 });
1480
1481 let mut instance = GpuImageInstance::new(draw_x, draw_y, draw_w, draw_h)
1483 .with_src_uv(src_uv[0], src_uv[1], src_uv[2], src_uv[3])
1484 .with_tint(image.tint[0], image.tint[1], image.tint[2], image.tint[3])
1485 .with_border_radius(image.border_radius)
1486 .with_opacity(image.opacity)
1487 .with_transform(ta, tb, tc, td)
1488 .with_filter(image.filter_a, image.filter_b);
1489
1490 if image.border_width > 0.0 {
1492 instance = instance.with_image_border(
1493 image.border_width,
1494 image.border_color.r,
1495 image.border_color.g,
1496 image.border_color.b,
1497 image.border_color.a,
1498 );
1499 }
1500
1501 if image.mask_info[0] > 0.5 {
1503 instance.mask_params = image.mask_params;
1504 instance.mask_info = image.mask_info;
1505 }
1506
1507 if let Some((clip, clip_r)) = effective_clip {
1509 instance = instance.with_clip_rounded_rect_corners(
1510 clip[0], clip[1], clip[2], clip[3], clip_r[0], clip_r[1], clip_r[2], clip_r[3],
1511 );
1512 }
1513 if let Some(sc) = image.scroll_clip {
1515 instance = instance.with_clip2_rect(sc[0], sc[1], sc[2], sc[3]);
1516 }
1517
1518 self.renderer
1520 .render_images(target, gpu_image.view(), &[instance]);
1521 }
1522 }
1523
1524 fn render_svg_element(&mut self, ctx: &mut GpuPaintContext, svg: &SvgElement) {
1526 if svg.motion_opacity <= 0.001 {
1528 return;
1529 }
1530
1531 if let Some([clip_x, clip_y, clip_w, clip_h]) = svg.clip_bounds {
1533 let svg_right = svg.x + svg.width;
1534 let svg_bottom = svg.y + svg.height;
1535 let clip_right = clip_x + clip_w;
1536 let clip_bottom = clip_y + clip_h;
1537
1538 if svg.x >= clip_right
1540 || svg_right <= clip_x
1541 || svg.y >= clip_bottom
1542 || svg_bottom <= clip_y
1543 {
1544 return;
1545 }
1546 }
1547
1548 let svg_hash = {
1550 let mut hasher = DefaultHasher::new();
1551 svg.source.hash(&mut hasher);
1552 hasher.finish()
1553 };
1554
1555 let doc = if let Some(cached) = self.svg_cache.get(&svg_hash) {
1557 cached.clone()
1558 } else {
1559 let Ok(parsed) = SvgDocument::from_str(&svg.source) else {
1560 return;
1561 };
1562 self.svg_cache.put(svg_hash, parsed.clone());
1563 parsed
1564 };
1565
1566 if let Some([clip_x, clip_y, clip_w, clip_h]) = svg.clip_bounds {
1568 ctx.push_clip(blinc_core::ClipShape::rect(Rect::new(
1569 clip_x, clip_y, clip_w, clip_h,
1570 )));
1571 }
1572
1573 if svg.motion_opacity < 1.0 {
1575 ctx.push_opacity(svg.motion_opacity);
1576 }
1577
1578 let has_css_overrides = svg.tint.is_some()
1580 || svg.fill.is_some()
1581 || svg.stroke.is_some()
1582 || svg.stroke_width.is_some();
1583 if has_css_overrides {
1584 self.render_svg_with_overrides(
1585 ctx,
1586 &doc,
1587 svg.x,
1588 svg.y,
1589 svg.width,
1590 svg.height,
1591 svg.tint,
1592 svg.fill,
1593 svg.stroke,
1594 svg.stroke_width,
1595 );
1596 } else {
1597 doc.render_fit(ctx, Rect::new(svg.x, svg.y, svg.width, svg.height));
1598 }
1599
1600 if svg.motion_opacity < 1.0 {
1602 ctx.pop_opacity();
1603 }
1604
1605 if svg.clip_bounds.is_some() {
1607 ctx.pop_clip();
1608 }
1609 }
1610
1611 #[allow(clippy::too_many_arguments)]
1613 fn render_svg_with_overrides(
1614 &self,
1615 ctx: &mut GpuPaintContext,
1616 doc: &SvgDocument,
1617 x: f32,
1618 y: f32,
1619 width: f32,
1620 height: f32,
1621 tint: Option<blinc_core::Color>,
1622 fill: Option<blinc_core::Color>,
1623 stroke: Option<blinc_core::Color>,
1624 stroke_width: Option<f32>,
1625 ) {
1626 use blinc_svg::SvgDrawCommand;
1627
1628 let scale_x = width / doc.width;
1630 let scale_y = height / doc.height;
1631 let scale = scale_x.min(scale_y);
1632
1633 let scaled_width = doc.width * scale;
1635 let scaled_height = doc.height * scale;
1636 let offset_x = x + (width - scaled_width) / 2.0;
1637 let offset_y = y + (height - scaled_height) / 2.0;
1638
1639 let commands = doc.commands();
1640
1641 for cmd in commands {
1642 match cmd {
1643 SvgDrawCommand::FillPath { path, brush } => {
1644 let scaled = scale_and_translate_path(&path, offset_x, offset_y, scale);
1645 let fill_brush = if let Some(f) = fill {
1647 Brush::Solid(f)
1648 } else if let Some(t) = tint {
1649 Brush::Solid(t)
1650 } else {
1651 brush.clone()
1652 };
1653 ctx.fill_path(&scaled, fill_brush);
1654 }
1655 SvgDrawCommand::StrokePath {
1656 path,
1657 stroke: orig_stroke,
1658 brush,
1659 } => {
1660 let scaled = scale_and_translate_path(&path, offset_x, offset_y, scale);
1661 let sw = stroke_width.unwrap_or(orig_stroke.width) * scale;
1663 let scaled_stroke = Stroke::new(sw)
1664 .with_cap(orig_stroke.cap)
1665 .with_join(orig_stroke.join);
1666 let stroke_brush = if let Some(s) = stroke {
1668 Brush::Solid(s)
1669 } else if let Some(t) = tint {
1670 Brush::Solid(t)
1671 } else {
1672 brush.clone()
1673 };
1674 ctx.stroke_path(&scaled, &scaled_stroke, stroke_brush);
1675 }
1676 }
1677 }
1678 }
1679
1680 fn render_rasterized_svgs(
1688 &mut self,
1689 target: &wgpu::TextureView,
1690 svgs: &[SvgElement],
1691 scale_factor: f32,
1692 ) {
1693 let mut instances: Vec<GpuImageInstance> = Vec::with_capacity(svgs.len());
1695
1696 for svg in svgs {
1697 if svg.motion_opacity <= 0.001 {
1699 continue;
1700 }
1701
1702 if let Some([clip_x, clip_y, clip_w, clip_h]) = svg.clip_bounds {
1704 let svg_right = svg.x + svg.width;
1705 let svg_bottom = svg.y + svg.height;
1706 let clip_right = clip_x + clip_w;
1707 let clip_bottom = clip_y + clip_h;
1708
1709 if svg.x >= clip_right
1710 || svg_right <= clip_x
1711 || svg.y >= clip_bottom
1712 || svg_bottom <= clip_y
1713 {
1714 continue;
1715 }
1716 }
1717
1718 let raster_width = ((svg.width * scale_factor).ceil() as u32).max(1);
1721 let raster_height = ((svg.height * scale_factor).ceil() as u32).max(1);
1722
1723 let is_tintable = svg.tint.is_some()
1727 && svg.fill.is_none()
1728 && svg.stroke.is_none()
1729 && svg.stroke_width.is_none()
1730 && svg.stroke_dasharray.is_none()
1731 && svg.stroke_dashoffset.is_none()
1732 && svg.svg_path_data.is_none()
1733 && svg.tag_overrides.is_empty()
1734 && svg.source.contains("currentColor");
1735
1736 let cache_key = {
1739 let mut hasher = DefaultHasher::new();
1740 svg.source.hash(&mut hasher);
1741 raster_width.hash(&mut hasher);
1742 raster_height.hash(&mut hasher);
1743 if is_tintable {
1744 255u8.hash(&mut hasher);
1746 } else if let Some(tint) = &svg.tint {
1747 tint.r.to_bits().hash(&mut hasher);
1748 tint.g.to_bits().hash(&mut hasher);
1749 tint.b.to_bits().hash(&mut hasher);
1750 tint.a.to_bits().hash(&mut hasher);
1751 }
1752 if let Some(fill) = &svg.fill {
1753 1u8.hash(&mut hasher);
1754 fill.r.to_bits().hash(&mut hasher);
1755 fill.g.to_bits().hash(&mut hasher);
1756 fill.b.to_bits().hash(&mut hasher);
1757 fill.a.to_bits().hash(&mut hasher);
1758 }
1759 if let Some(stroke) = &svg.stroke {
1760 2u8.hash(&mut hasher);
1761 stroke.r.to_bits().hash(&mut hasher);
1762 stroke.g.to_bits().hash(&mut hasher);
1763 stroke.b.to_bits().hash(&mut hasher);
1764 stroke.a.to_bits().hash(&mut hasher);
1765 }
1766 if let Some(sw) = &svg.stroke_width {
1767 3u8.hash(&mut hasher);
1768 sw.to_bits().hash(&mut hasher);
1769 }
1770 if let Some(ref da) = svg.stroke_dasharray {
1771 4u8.hash(&mut hasher);
1772 for v in da {
1773 v.to_bits().hash(&mut hasher);
1774 }
1775 }
1776 if let Some(offset) = &svg.stroke_dashoffset {
1777 5u8.hash(&mut hasher);
1778 offset.to_bits().hash(&mut hasher);
1779 }
1780 if let Some(ref path_data) = svg.svg_path_data {
1781 6u8.hash(&mut hasher);
1782 path_data.hash(&mut hasher);
1783 }
1784 if !svg.tag_overrides.is_empty() {
1786 7u8.hash(&mut hasher);
1787 let mut keys: Vec<&String> = svg.tag_overrides.keys().collect();
1789 keys.sort();
1790 for key in keys {
1791 key.hash(&mut hasher);
1792 if let Some(ts) = svg.tag_overrides.get(key) {
1793 if let Some(f) = &ts.fill {
1794 for v in f {
1795 v.to_bits().hash(&mut hasher);
1796 }
1797 }
1798 if let Some(s) = &ts.stroke {
1799 for v in s {
1800 v.to_bits().hash(&mut hasher);
1801 }
1802 }
1803 if let Some(sw) = &ts.stroke_width {
1804 sw.to_bits().hash(&mut hasher);
1805 }
1806 if let Some(op) = &ts.opacity {
1807 op.to_bits().hash(&mut hasher);
1808 }
1809 }
1810 }
1811 }
1812 hasher.finish()
1813 };
1814
1815 if self.svg_atlas.get(cache_key).is_none() {
1817 let has_overrides = svg.tint.is_some()
1819 || svg.fill.is_some()
1820 || svg.stroke.is_some()
1821 || svg.stroke_width.is_some()
1822 || svg.stroke_dasharray.is_some()
1823 || svg.stroke_dashoffset.is_some()
1824 || svg.svg_path_data.is_some()
1825 || !svg.tag_overrides.is_empty();
1826
1827 fn color_val(c: blinc_core::Color) -> String {
1828 if c.a < 1.0 {
1829 format!(
1830 "rgba({},{},{},{})",
1831 (c.r * 255.0) as u8,
1832 (c.g * 255.0) as u8,
1833 (c.b * 255.0) as u8,
1834 c.a
1835 )
1836 } else {
1837 format!(
1838 "#{:02x}{:02x}{:02x}",
1839 (c.r * 255.0) as u8,
1840 (c.g * 255.0) as u8,
1841 (c.b * 255.0) as u8
1842 )
1843 }
1844 }
1845
1846 let effective_source = if has_overrides {
1847 let mut svg_attrs = String::new();
1849 if let Some(fill) = svg.fill {
1850 svg_attrs.push_str(&format!(r#" fill="{}""#, color_val(fill)));
1851 }
1852 if let Some(stroke) = svg.stroke {
1853 svg_attrs.push_str(&format!(r#" stroke="{}""#, color_val(stroke)));
1854 }
1855 if let Some(sw) = svg.stroke_width {
1856 svg_attrs.push_str(&format!(r#" stroke-width="{}""#, sw));
1857 }
1858 if let Some(ref da) = svg.stroke_dasharray {
1859 let da_str = da
1860 .iter()
1861 .map(|v| v.to_string())
1862 .collect::<Vec<_>>()
1863 .join(",");
1864 svg_attrs.push_str(&format!(r#" stroke-dasharray="{}""#, da_str));
1865 }
1866 if let Some(offset) = svg.stroke_dashoffset {
1867 svg_attrs.push_str(&format!(r#" stroke-dashoffset="{}""#, offset));
1868 }
1869
1870 fn strip_attr(s: &mut String, tag_start: usize, tag_end: usize, attr: &str) {
1872 let region = &s[tag_start..tag_end];
1873 let attr_eq = format!("{}=", attr);
1874 if let Some(attr_offset) = region.find(&attr_eq) {
1875 let abs_attr = tag_start + attr_offset;
1876 let after_eq = abs_attr + attr.len() + 1;
1877 if after_eq < s.len() {
1878 let quote = s.as_bytes()[after_eq];
1879 if quote == b'"' || quote == b'\'' {
1880 if let Some(end_quote) = s[after_eq + 1..].find(quote as char) {
1881 let remove_end = after_eq + 1 + end_quote + 1;
1882 let remove_start =
1883 if abs_attr > 0 && s.as_bytes()[abs_attr - 1] == b' ' {
1884 abs_attr - 1
1885 } else {
1886 abs_attr
1887 };
1888 s.replace_range(remove_start..remove_end, "");
1889 }
1890 }
1891 }
1892 }
1893 }
1894
1895 let mut modified = String::from(&*svg.source);
1896
1897 if let Some(svg_close) = modified.find('>') {
1899 if svg.stroke.is_some() {
1900 strip_attr(&mut modified, 0, svg_close, "stroke");
1901 }
1902 if svg.fill.is_some() {
1903 let svg_close = modified.find('>').unwrap_or(0);
1904 strip_attr(&mut modified, 0, svg_close, "fill");
1905 }
1906 if svg.stroke_width.is_some() {
1907 let svg_close = modified.find('>').unwrap_or(0);
1908 strip_attr(&mut modified, 0, svg_close, "stroke-width");
1909 }
1910 if svg.stroke_dasharray.is_some() {
1911 let svg_close = modified.find('>').unwrap_or(0);
1912 strip_attr(&mut modified, 0, svg_close, "stroke-dasharray");
1913 }
1914 if svg.stroke_dashoffset.is_some() {
1915 let svg_close = modified.find('>').unwrap_or(0);
1916 strip_attr(&mut modified, 0, svg_close, "stroke-dashoffset");
1917 }
1918 }
1919
1920 if !svg_attrs.is_empty() {
1922 if let Some(pos) = modified.find('>') {
1923 let insert_pos = if pos > 0 && modified.as_bytes()[pos - 1] == b'/' {
1924 pos - 1
1925 } else {
1926 pos
1927 };
1928 modified.insert_str(insert_pos, &svg_attrs);
1929 }
1930 }
1931
1932 let shape_tags = [
1934 "<path",
1935 "<circle",
1936 "<rect",
1937 "<polygon",
1938 "<line",
1939 "<ellipse",
1940 "<polyline",
1941 ];
1942 for tag in &shape_tags {
1943 let tag_name = tag.trim_start_matches('<');
1944 let tag_style = svg.tag_overrides.get(tag_name);
1945
1946 let effective_fill: Option<blinc_core::Color> = tag_style
1948 .and_then(|ts| ts.fill)
1949 .map(|c| blinc_core::Color::rgba(c[0], c[1], c[2], c[3]))
1950 .or(svg.fill);
1951 let effective_stroke: Option<blinc_core::Color> = tag_style
1952 .and_then(|ts| ts.stroke)
1953 .map(|c| blinc_core::Color::rgba(c[0], c[1], c[2], c[3]))
1954 .or(svg.stroke);
1955 let effective_stroke_width: Option<f32> = tag_style
1956 .and_then(|ts| ts.stroke_width)
1957 .or(svg.stroke_width);
1958 let effective_dasharray: Option<Vec<f32>> = tag_style
1959 .and_then(|ts| ts.stroke_dasharray.clone())
1960 .or_else(|| svg.stroke_dasharray.clone());
1961 let effective_dashoffset: Option<f32> = tag_style
1962 .and_then(|ts| ts.stroke_dashoffset)
1963 .or(svg.stroke_dashoffset);
1964 let effective_opacity: Option<f32> = tag_style.and_then(|ts| ts.opacity);
1965
1966 let mut search_from = 0;
1967 while let Some(tag_start) = modified[search_from..].find(tag) {
1968 let abs_tag = search_from + tag_start;
1969 let abs_start = abs_tag + tag.len();
1970 if let Some(close) = modified[abs_start..].find('>') {
1971 let abs_close = abs_start + close;
1972
1973 if effective_stroke.is_some() {
1974 strip_attr(&mut modified, abs_tag, abs_close, "stroke-width");
1975 let new_close = abs_start
1976 + modified[abs_start..].find('>').unwrap_or(close);
1977 strip_attr(&mut modified, abs_tag, new_close, "stroke");
1978 }
1979 if effective_fill.is_some() {
1980 let new_close = abs_start
1981 + modified[abs_start..].find('>').unwrap_or(close);
1982 strip_attr(&mut modified, abs_tag, new_close, "fill");
1983 }
1984 if effective_stroke_width.is_some() {
1985 let new_close = abs_start
1986 + modified[abs_start..].find('>').unwrap_or(close);
1987 strip_attr(&mut modified, abs_tag, new_close, "stroke-width");
1988 }
1989 if effective_dasharray.is_some() {
1990 let new_close = abs_start
1991 + modified[abs_start..].find('>').unwrap_or(close);
1992 strip_attr(
1993 &mut modified,
1994 abs_tag,
1995 new_close,
1996 "stroke-dasharray",
1997 );
1998 }
1999 if effective_dashoffset.is_some() {
2000 let new_close = abs_start
2001 + modified[abs_start..].find('>').unwrap_or(close);
2002 strip_attr(
2003 &mut modified,
2004 abs_tag,
2005 new_close,
2006 "stroke-dashoffset",
2007 );
2008 }
2009 if effective_opacity.is_some() {
2010 let new_close = abs_start
2011 + modified[abs_start..].find('>').unwrap_or(close);
2012 strip_attr(&mut modified, abs_tag, new_close, "opacity");
2013 }
2014 if svg.svg_path_data.is_some() && *tag == "<path" {
2015 let new_close = abs_start
2016 + modified[abs_start..].find('>').unwrap_or(close);
2017 strip_attr(&mut modified, abs_tag, new_close, "d");
2018 }
2019
2020 let abs_close =
2022 abs_start + modified[abs_start..].find('>').unwrap_or(0);
2023 let is_self_close =
2024 abs_close > 0 && modified.as_bytes()[abs_close - 1] == b'/';
2025 let insert_at = if is_self_close {
2026 abs_close - 1
2027 } else {
2028 abs_close
2029 };
2030 let mut elem_attrs = String::new();
2031 if let Some(fill) = effective_fill {
2032 elem_attrs.push_str(&format!(r#" fill="{}""#, color_val(fill)));
2033 }
2034 if let Some(stroke) = effective_stroke {
2035 elem_attrs
2036 .push_str(&format!(r#" stroke="{}""#, color_val(stroke)));
2037 }
2038 if let Some(sw) = effective_stroke_width {
2039 elem_attrs.push_str(&format!(r#" stroke-width="{}""#, sw));
2040 }
2041 if let Some(ref da) = effective_dasharray {
2042 let da_str = da
2043 .iter()
2044 .map(|v| v.to_string())
2045 .collect::<Vec<_>>()
2046 .join(",");
2047 elem_attrs
2048 .push_str(&format!(r#" stroke-dasharray="{}""#, da_str));
2049 }
2050 if let Some(offset) = effective_dashoffset {
2051 elem_attrs
2052 .push_str(&format!(r#" stroke-dashoffset="{}""#, offset));
2053 }
2054 if let Some(opacity) = effective_opacity {
2055 elem_attrs.push_str(&format!(r#" opacity="{}""#, opacity));
2056 }
2057 if let Some(ref path_data) = svg.svg_path_data {
2058 if *tag == "<path" {
2059 elem_attrs.push_str(&format!(r#" d="{}""#, path_data));
2060 }
2061 }
2062 modified.insert_str(insert_at, &elem_attrs);
2063 search_from = insert_at + elem_attrs.len() + 1;
2064 } else {
2065 break;
2066 }
2067 }
2068 }
2069
2070 std::borrow::Cow::Owned(modified)
2071 } else {
2072 std::borrow::Cow::Borrowed(&*svg.source)
2073 };
2074
2075 let final_source = if is_tintable {
2079 std::borrow::Cow::Owned(effective_source.replace("currentColor", "#ffffff"))
2080 } else if let Some(tint) = svg.tint {
2081 if effective_source.contains("currentColor") {
2082 std::borrow::Cow::Owned(
2083 effective_source.replace("currentColor", &color_val(tint)),
2084 )
2085 } else {
2086 effective_source
2087 }
2088 } else {
2089 effective_source
2090 };
2091
2092 let rasterized =
2093 RasterizedSvg::from_str(&final_source, raster_width, raster_height);
2094
2095 let rasterized = match rasterized {
2096 Ok(r) => r,
2097 Err(e) => {
2098 tracing::warn!("Failed to rasterize SVG: {}", e);
2099 continue;
2100 }
2101 };
2102
2103 if self
2105 .svg_atlas
2106 .insert(
2107 cache_key,
2108 rasterized.width,
2109 rasterized.height,
2110 rasterized.data(),
2111 &self.device,
2112 )
2113 .is_none()
2114 {
2115 tracing::warn!(
2116 "SVG atlas full, could not allocate {}x{}",
2117 raster_width,
2118 raster_height
2119 );
2120 continue;
2121 }
2122 }
2123
2124 let Some(region) = self.svg_atlas.get(cache_key) else {
2126 continue;
2127 };
2128 let src_uv = region.uv_bounds(self.svg_atlas.width(), self.svg_atlas.height());
2129
2130 let (draw_x, draw_y, draw_w, draw_h, ta, tb, tc, td) =
2133 if let Some([a, b, c, d, tx, ty]) = svg.css_affine {
2134 let tx_s = tx * scale_factor;
2136 let ty_s = ty * scale_factor;
2137
2138 let cx = svg.x + svg.width * 0.5;
2140 let cy = svg.y + svg.height * 0.5;
2141 let new_cx = a * cx + c * cy + tx_s;
2142 let new_cy = b * cx + d * cy + ty_s;
2143
2144 (
2146 new_cx - svg.width * 0.5,
2147 new_cy - svg.height * 0.5,
2148 svg.width,
2149 svg.height,
2150 a,
2151 b,
2152 c,
2153 d,
2154 )
2155 } else {
2156 (svg.x, svg.y, svg.width, svg.height, 1.0, 0.0, 0.0, 1.0)
2157 };
2158
2159 let mut instance = GpuImageInstance::new(draw_x, draw_y, draw_w, draw_h)
2161 .with_src_uv(src_uv[0], src_uv[1], src_uv[2], src_uv[3])
2162 .with_opacity(svg.motion_opacity)
2163 .with_transform(ta, tb, tc, td);
2164
2165 if is_tintable {
2168 if let Some(tint) = svg.tint {
2169 instance = instance.with_tint(tint.r, tint.g, tint.b, tint.a);
2170 }
2171 }
2172
2173 if let Some([clip_x, clip_y, clip_w, clip_h]) = svg.clip_bounds {
2175 instance = instance.with_clip_rect(clip_x, clip_y, clip_w, clip_h);
2176 }
2177
2178 instances.push(instance);
2179 }
2180
2181 if !instances.is_empty() {
2183 self.svg_atlas.upload(&self.queue);
2184 self.renderer
2185 .render_images(target, self.svg_atlas.view(), &instances);
2186 }
2187 }
2188
2189 fn collect_render_elements(
2191 &mut self,
2192 tree: &RenderTree,
2193 ) -> (
2194 Vec<TextElement>,
2195 Vec<SvgElement>,
2196 Vec<ImageElement>,
2197 Vec<FlowElement>,
2198 ) {
2199 self.collect_render_elements_with_state(tree, None)
2200 }
2201
2202 fn collect_render_elements_with_state(
2204 &mut self,
2205 tree: &RenderTree,
2206 render_state: Option<&blinc_layout::RenderState>,
2207 ) -> (
2208 Vec<TextElement>,
2209 Vec<SvgElement>,
2210 Vec<ImageElement>,
2211 Vec<FlowElement>,
2212 ) {
2213 let mut texts = std::mem::take(&mut self.scratch_texts);
2216 let mut svgs = std::mem::take(&mut self.scratch_svgs);
2217 let mut images = std::mem::take(&mut self.scratch_images);
2218 let mut flows = Vec::new();
2219 texts.clear();
2220 svgs.clear();
2221 images.clear();
2222
2223 let scale = tree.scale_factor();
2225
2226 if let Some(root) = tree.root() {
2227 let mut z_layer = 0u32;
2228 self.collect_elements_recursive(
2229 tree,
2230 root,
2231 (0.0, 0.0),
2232 false, false, None, None, 1.0, (0.0, 0.0), (1.0, 1.0), None, render_state,
2241 scale,
2242 &mut z_layer,
2243 &mut texts,
2244 &mut svgs,
2245 &mut images,
2246 &mut flows,
2247 None, 1.0, None, None, None, );
2253 }
2254
2255 texts.sort_by_key(|t| t.z_index);
2257
2258 (texts, svgs, images, flows)
2259 }
2260
2261 #[allow(clippy::too_many_arguments, clippy::only_used_in_recursion)]
2262 fn collect_elements_recursive(
2263 &self,
2264 tree: &RenderTree,
2265 node: LayoutNodeId,
2266 parent_offset: (f32, f32),
2267 inside_glass: bool,
2268 inside_foreground: bool,
2269 current_clip: Option<[f32; 4]>,
2270 current_clip_radius: Option<[f32; 4]>,
2271 inherited_motion_opacity: f32,
2272 inherited_motion_translate: (f32, f32),
2273 inherited_motion_scale: (f32, f32),
2274 inherited_motion_scale_center: Option<(f32, f32)>,
2277 render_state: Option<&blinc_layout::RenderState>,
2278 scale: f32,
2279 z_layer: &mut u32,
2280 texts: &mut Vec<TextElement>,
2281 svgs: &mut Vec<SvgElement>,
2282 images: &mut Vec<ImageElement>,
2283 flows: &mut Vec<FlowElement>,
2284 inherited_css_affine: Option<[f32; 6]>,
2287 inherited_css_opacity: f32,
2290 parent_node: Option<LayoutNodeId>,
2293 current_scroll_clip: Option<[f32; 4]>,
2297 inside_3d_layer: Option<Transform3DLayerInfo>,
2301 ) {
2302 use blinc_layout::Material;
2303
2304 let Some(bounds) = tree.get_render_bounds(node, parent_offset) else {
2307 return;
2308 };
2309
2310 let abs_x = bounds.x;
2311 let abs_y = bounds.y;
2312
2313 let motion_values = render_state.and_then(|rs| {
2315 if let Some(render_node) = tree.get_render_node(node) {
2317 if let Some(ref stable_key) = render_node.props.motion_stable_id {
2318 return rs.get_stable_motion_values(stable_key);
2319 }
2320 }
2321 rs.get_motion_values(node)
2322 });
2323
2324 let binding_scale = tree.get_motion_scale(node);
2329 let binding_opacity = tree.get_motion_opacity(node);
2330
2331 let node_motion_opacity = motion_values
2333 .and_then(|m| m.opacity)
2334 .unwrap_or_else(|| binding_opacity.unwrap_or(1.0));
2335
2336 let node_motion_translate = motion_values
2339 .map(|m| m.resolved_translate())
2340 .unwrap_or((0.0, 0.0));
2341
2342 let node_motion_scale = motion_values
2344 .map(|m| m.resolved_scale())
2345 .unwrap_or((1.0, 1.0));
2346
2347 let binding_scale_values = binding_scale.unwrap_or((1.0, 1.0));
2349
2350 let effective_motion_opacity = inherited_motion_opacity * node_motion_opacity;
2354 let effective_motion_translate = (
2355 inherited_motion_translate.0 + node_motion_translate.0,
2356 inherited_motion_translate.1 + node_motion_translate.1,
2357 );
2358 let effective_motion_scale = (
2360 inherited_motion_scale.0 * node_motion_scale.0 * binding_scale_values.0,
2361 inherited_motion_scale.1 * node_motion_scale.1 * binding_scale_values.1,
2362 );
2363
2364 let this_node_has_scale = (node_motion_scale.0 - 1.0).abs() > 0.001
2368 || (node_motion_scale.1 - 1.0).abs() > 0.001
2369 || (binding_scale_values.0 - 1.0).abs() > 0.001
2370 || (binding_scale_values.1 - 1.0).abs() > 0.001;
2371
2372 let effective_motion_scale_center = if this_node_has_scale {
2373 let center_x = abs_x + bounds.width / 2.0;
2375 let center_y = abs_y + bounds.height / 2.0;
2376 Some((center_x, center_y))
2377 } else {
2378 inherited_motion_scale_center
2380 };
2381
2382 if effective_motion_opacity <= 0.001 {
2384 return;
2385 }
2386
2387 if let Some(render_node) = tree.get_render_node(node) {
2389 if !render_node.props.visible {
2390 return;
2391 }
2392 }
2393
2394 let is_glass = tree
2396 .get_render_node(node)
2397 .map(|n| matches!(n.props.material, Some(Material::Glass(_))))
2398 .unwrap_or(false);
2399
2400 let children_inside_glass = inside_glass || is_glass;
2402
2403 let is_foreground_node = tree
2405 .get_render_node(node)
2406 .map(|n| n.props.layer == RenderLayer::Foreground)
2407 .unwrap_or(false);
2408 let children_inside_foreground = inside_foreground || is_foreground_node;
2409
2410 let clips_content = tree
2412 .get_render_node(node)
2413 .map(|n| n.props.clips_content)
2414 .unwrap_or(false);
2415
2416 let has_layout_animation = tree.is_layout_animating(node);
2419
2420 let is_stack_layer = tree
2422 .get_render_node(node)
2423 .map(|n| n.props.is_stack_layer)
2424 .unwrap_or(false);
2425 if is_stack_layer {
2426 *z_layer += 1;
2427 }
2428
2429 let saved_z_layer = *z_layer;
2431 let node_z_index = tree
2432 .get_render_node(node)
2433 .map(|n| n.props.z_index)
2434 .unwrap_or(0);
2435 if node_z_index > 0 {
2436 *z_layer = node_z_index as u32;
2437 }
2438
2439 let should_clip = clips_content || has_layout_animation;
2443 let (child_clip, child_clip_radius, child_scroll_clip) = if should_clip {
2444 let clip_bounds = if has_layout_animation {
2447 tree.get_render_bounds(node, parent_offset)
2449 .map(|b| [b.x, b.y, b.width, b.height])
2450 .unwrap_or([abs_x, abs_y, bounds.width, bounds.height])
2451 } else {
2452 [abs_x, abs_y, bounds.width, bounds.height]
2453 };
2454 let bw = tree
2458 .get_render_node(node)
2459 .map(|n| n.props.border_width)
2460 .unwrap_or(0.0);
2461 let this_clip = [
2462 clip_bounds[0] + bw,
2463 clip_bounds[1] + bw,
2464 (clip_bounds[2] - bw * 2.0).max(0.0),
2465 (clip_bounds[3] - bw * 2.0).max(0.0),
2466 ];
2467
2468 let this_clip_radius = tree.get_render_node(node).map(|n| {
2471 let r = &n.props.border_radius;
2472 [
2473 (r.top_left - bw).max(0.0),
2474 (r.top_right - bw).max(0.0),
2475 (r.bottom_right - bw).max(0.0),
2476 (r.bottom_left - bw).max(0.0),
2477 ]
2478 });
2479
2480 let this_has_radius = this_clip_radius
2481 .map(|r| r.iter().any(|&v| v > 0.5))
2482 .unwrap_or(false);
2483 let parent_has_radius = current_clip_radius
2484 .map(|r| r.iter().any(|&v| v > 0.5))
2485 .unwrap_or(false);
2486
2487 if let Some(parent_clip) = current_clip {
2488 if this_has_radius && !parent_has_radius {
2489 (
2494 Some(this_clip),
2495 this_clip_radius,
2496 merge_scroll_clip(parent_clip, current_scroll_clip),
2497 )
2498 } else if !this_has_radius && parent_has_radius {
2499 (
2503 current_clip,
2504 current_clip_radius,
2505 merge_scroll_clip(this_clip, current_scroll_clip),
2506 )
2507 } else {
2508 let x1 = parent_clip[0].max(this_clip[0]);
2510 let y1 = parent_clip[1].max(this_clip[1]);
2511 let parent_right = parent_clip[0] + parent_clip[2];
2512 let parent_bottom = parent_clip[1] + parent_clip[3];
2513 let this_right = this_clip[0] + this_clip[2];
2514 let this_bottom = this_clip[1] + this_clip[3];
2515 let x2 = parent_right.min(this_right);
2516 let y2 = parent_bottom.min(this_bottom);
2517 let w = (x2 - x1).max(0.0);
2518 let h = (y2 - y1).max(0.0);
2519 let clip = Some([x1, y1, w, h]);
2520
2521 let child_r = this_clip_radius.unwrap_or([0.0; 4]);
2522 let parent_r = current_clip_radius.unwrap_or([0.0; 4]);
2523 let radius = Some([
2524 child_r[0].max(parent_r[0]),
2525 child_r[1].max(parent_r[1]),
2526 child_r[2].max(parent_r[2]),
2527 child_r[3].max(parent_r[3]),
2528 ]);
2529
2530 (clip, radius, current_scroll_clip)
2531 }
2532 } else {
2533 if this_has_radius {
2535 (Some(this_clip), this_clip_radius, current_scroll_clip)
2537 } else {
2538 let new_scroll_clip = if let Some(existing) = current_scroll_clip {
2541 let x1 = existing[0].max(this_clip[0]);
2542 let y1 = existing[1].max(this_clip[1]);
2543 let x2 = (existing[0] + existing[2]).min(this_clip[0] + this_clip[2]);
2544 let y2 = (existing[1] + existing[3]).min(this_clip[1] + this_clip[3]);
2545 [x1, y1, (x2 - x1).max(0.0), (y2 - y1).max(0.0)]
2546 } else {
2547 this_clip
2548 };
2549 (None, None, Some(new_scroll_clip))
2550 }
2551 }
2552 } else {
2553 (current_clip, current_clip_radius, current_scroll_clip)
2554 };
2555
2556 let node_css_affine = if let Some(render_node) = tree.get_render_node(node) {
2564 let has_non_identity = if let Some(blinc_core::Transform::Affine2D(affine)) =
2565 &render_node.props.transform
2566 {
2567 let [a, b, c, d, tx, ty] = affine.elements;
2568 !((a - 1.0).abs() < 0.0001
2569 && b.abs() < 0.0001
2570 && c.abs() < 0.0001
2571 && (d - 1.0).abs() < 0.0001
2572 && tx.abs() < 0.0001
2573 && ty.abs() < 0.0001)
2574 } else {
2575 false
2576 };
2577
2578 if has_non_identity {
2579 let affine = match &render_node.props.transform {
2580 Some(blinc_core::Transform::Affine2D(a)) => a.elements,
2581 _ => unreachable!(),
2582 };
2583 let [a, b, c, d, tx, ty] = affine;
2584 let (cx, cy) = if let Some([ox_pct, oy_pct]) = render_node.props.transform_origin {
2586 (
2587 abs_x + bounds.width * ox_pct / 100.0,
2588 abs_y + bounds.height * oy_pct / 100.0,
2589 )
2590 } else {
2591 (abs_x + bounds.width / 2.0, abs_y + bounds.height / 2.0)
2592 };
2593 let this_affine = [
2596 a,
2597 b,
2598 c,
2599 d,
2600 cx * (1.0 - a) - cy * c + tx,
2601 cy * (1.0 - d) - cx * b + ty,
2602 ];
2603 match inherited_css_affine {
2604 Some(parent) => {
2605 let [pa, pb, pc, pd, ptx, pty] = parent;
2606 Some([
2607 a * pa + c * pb,
2608 b * pa + d * pb,
2609 a * pc + c * pd,
2610 b * pc + d * pd,
2611 a * ptx + c * pty + this_affine[4],
2612 b * ptx + d * pty + this_affine[5],
2613 ])
2614 }
2615 None => Some(this_affine),
2616 }
2617 } else {
2618 inherited_css_affine
2619 }
2620 } else {
2621 inherited_css_affine
2622 };
2623
2624 if let Some(render_node) = tree.get_render_node(node) {
2625 let effective_layer = if inside_glass && !is_glass {
2627 RenderLayer::Foreground
2628 } else if is_glass {
2629 RenderLayer::Glass
2630 } else {
2631 render_node.props.layer
2632 };
2633
2634 match &render_node.element_type {
2635 ElementType::Text(text_data) => {
2636 let base_x = abs_x * scale;
2640 let base_y = abs_y * scale;
2641 let base_width = bounds.width * scale;
2642 let base_height = bounds.height * scale;
2643
2644 let scaled_motion_tx = effective_motion_translate.0 * scale;
2646 let scaled_motion_ty = effective_motion_translate.1 * scale;
2647
2648 let (scaled_x, scaled_y, scaled_width, scaled_height) =
2655 if let Some((motion_center_x, motion_center_y)) =
2656 effective_motion_scale_center
2657 {
2658 let motion_center_x_scaled = motion_center_x * scale;
2660 let motion_center_y_scaled = motion_center_y * scale;
2661
2662 let rel_x = base_x - motion_center_x_scaled;
2664 let rel_y = base_y - motion_center_y_scaled;
2665
2666 let scaled_rel_x = rel_x * effective_motion_scale.0;
2668 let scaled_rel_y = rel_y * effective_motion_scale.1;
2669 let scaled_w = base_width * effective_motion_scale.0;
2670 let scaled_h = base_height * effective_motion_scale.1;
2671
2672 let final_x = motion_center_x_scaled + scaled_rel_x + scaled_motion_tx;
2674 let final_y = motion_center_y_scaled + scaled_rel_y + scaled_motion_ty;
2675
2676 (final_x, final_y, scaled_w, scaled_h)
2677 } else {
2678 let final_x = base_x + scaled_motion_tx;
2680 let final_y = base_y + scaled_motion_ty;
2681 (final_x, final_y, base_width, base_height)
2682 };
2683
2684 let base_font_size = render_node.props.font_size.unwrap_or(text_data.font_size);
2686 let scaled_font_size = base_font_size * effective_motion_scale.1 * scale;
2687 let scaled_measured_width =
2688 text_data.measured_width * effective_motion_scale.0 * scale;
2689
2690 let effective_clip = effective_single_clip(current_clip, current_scroll_clip);
2693 let scaled_clip = effective_clip
2694 .map(|[cx, cy, cw, ch]| [cx * scale, cy * scale, cw * scale, ch * scale]);
2695
2696 if effective_motion_translate.0.abs() > 0.1
2698 || effective_motion_translate.1.abs() > 0.1
2699 || (effective_motion_scale.0 - 1.0).abs() > 0.01
2700 || (effective_motion_scale.1 - 1.0).abs() > 0.01
2701 {
2702 tracing::trace!(
2703 "Text '{}': motion_translate=({:.1}, {:.1}), motion_scale=({:.2}, {:.2}), base=({:.1}, {:.1}), final=({:.1}, {:.1})",
2704 text_data.content,
2705 effective_motion_translate.0,
2706 effective_motion_translate.1,
2707 effective_motion_scale.0,
2708 effective_motion_scale.1,
2709 base_x,
2710 base_y,
2711 scaled_x,
2712 scaled_y,
2713 );
2714 }
2715 tracing::trace!(
2716 "Text '{}': abs=({:.1}, {:.1}), size=({:.1}x{:.1}), font={:.1}, align={:?}, v_align={:?}, z_layer={}",
2717 text_data.content,
2718 scaled_x,
2719 scaled_y,
2720 scaled_width,
2721 scaled_height,
2722 scaled_font_size,
2723 text_data.align,
2724 text_data.v_align,
2725 *z_layer
2726 );
2727
2728 let is_nowrap = !text_data.wrap
2732 || matches!(
2733 render_node.props.white_space,
2734 Some(blinc_layout::element_style::WhiteSpace::Nowrap)
2735 | Some(blinc_layout::element_style::WhiteSpace::Pre)
2736 );
2737 let content = if is_nowrap
2738 && matches!(
2739 render_node.props.text_overflow,
2740 Some(blinc_layout::element_style::TextOverflow::Ellipsis)
2741 )
2742 && scaled_measured_width > scaled_width
2743 && scaled_width > 0.0
2744 {
2745 let mut options = blinc_layout::text_measure::TextLayoutOptions::new();
2747 options.font_name = text_data.font_family.name.clone();
2748 options.generic_font = text_data.font_family.generic;
2749 options.font_weight =
2750 match render_node.props.font_weight.unwrap_or(text_data.weight) {
2751 FontWeight::Bold => 700,
2752 FontWeight::Normal => 400,
2753 FontWeight::Light => 300,
2754 _ => 400,
2755 };
2756 options.letter_spacing = render_node
2757 .props
2758 .letter_spacing
2759 .unwrap_or(text_data.letter_spacing);
2760
2761 let ellipsis = "\u{2026}";
2763 let ellipsis_w = blinc_layout::text_measure::measure_text_with_options(
2764 ellipsis,
2765 scaled_font_size / scale,
2766 &options,
2767 )
2768 .width
2769 * scale;
2770 let target_width = scaled_width - ellipsis_w;
2771
2772 if target_width > 0.0 {
2773 let chars: Vec<char> = text_data.content.chars().collect();
2775 let mut lo = 0usize;
2776 let mut hi = chars.len();
2777 while lo < hi {
2778 #[allow(clippy::manual_div_ceil)]
2779 let mid = (lo + hi + 1) / 2;
2780 let sub: String = chars[..mid].iter().collect();
2781 let w = blinc_layout::text_measure::measure_text_with_options(
2782 &sub,
2783 scaled_font_size / scale,
2784 &options,
2785 )
2786 .width
2787 * scale;
2788 if w <= target_width {
2789 lo = mid;
2790 } else {
2791 hi = mid - 1;
2792 }
2793 }
2794 let truncated: String = chars[..lo].iter().collect();
2795 format!("{}{}", truncated.trim_end(), ellipsis)
2796 } else {
2797 ellipsis.to_string()
2798 }
2799 } else {
2800 text_data.content.clone()
2801 };
2802
2803 texts.push(TextElement {
2804 content,
2805 x: scaled_x,
2806 y: scaled_y,
2807 width: scaled_width,
2808 height: scaled_height,
2809 font_size: scaled_font_size,
2810 color: render_node.props.text_color.unwrap_or(text_data.color),
2811 align: text_data.align,
2812 weight: render_node.props.font_weight.unwrap_or(text_data.weight),
2813 italic: text_data.italic,
2814 v_align: text_data.v_align,
2815 clip_bounds: scaled_clip,
2816 motion_opacity: effective_motion_opacity
2817 * render_node.props.opacity
2818 * inherited_css_opacity,
2819 wrap: !is_nowrap && text_data.wrap,
2820 line_height: text_data.line_height,
2821 measured_width: scaled_measured_width,
2822 font_family: text_data.font_family.clone(),
2823 word_spacing: text_data.word_spacing,
2824 letter_spacing: render_node
2825 .props
2826 .letter_spacing
2827 .unwrap_or(text_data.letter_spacing),
2828 z_index: *z_layer,
2829 ascender: text_data.ascender * effective_motion_scale.1 * scale,
2830 strikethrough: render_node.props.text_decoration.map_or(
2831 text_data.strikethrough,
2832 |td| {
2833 matches!(
2834 td,
2835 blinc_layout::element_style::TextDecoration::LineThrough
2836 )
2837 },
2838 ),
2839 underline: render_node.props.text_decoration.map_or(
2840 text_data.underline,
2841 |td| {
2842 matches!(td, blinc_layout::element_style::TextDecoration::Underline)
2843 },
2844 ),
2845 decoration_color: render_node.props.text_decoration_color,
2846 decoration_thickness: render_node.props.text_decoration_thickness,
2847 css_affine: node_css_affine,
2848 text_shadow: render_node.props.text_shadow,
2849 transform_3d_layer: inside_3d_layer.clone(),
2850 is_foreground: children_inside_foreground,
2851 });
2852 }
2853 ElementType::Svg(svg_data) => {
2854 let base_x = abs_x * scale;
2856 let base_y = abs_y * scale;
2857 let base_width = bounds.width * scale;
2858 let base_height = bounds.height * scale;
2859
2860 let scaled_motion_tx = effective_motion_translate.0 * scale;
2862 let scaled_motion_ty = effective_motion_translate.1 * scale;
2863
2864 let (scaled_x, scaled_y, scaled_width, scaled_height) =
2866 if let Some((motion_center_x, motion_center_y)) =
2867 effective_motion_scale_center
2868 {
2869 let motion_center_x_scaled = motion_center_x * scale;
2870 let motion_center_y_scaled = motion_center_y * scale;
2871
2872 let rel_x = base_x - motion_center_x_scaled;
2873 let rel_y = base_y - motion_center_y_scaled;
2874
2875 let scaled_rel_x = rel_x * effective_motion_scale.0;
2876 let scaled_rel_y = rel_y * effective_motion_scale.1;
2877 let scaled_w = base_width * effective_motion_scale.0;
2878 let scaled_h = base_height * effective_motion_scale.1;
2879
2880 let final_x = motion_center_x_scaled + scaled_rel_x + scaled_motion_tx;
2881 let final_y = motion_center_y_scaled + scaled_rel_y + scaled_motion_ty;
2882
2883 (final_x, final_y, scaled_w, scaled_h)
2884 } else {
2885 let final_x = base_x + scaled_motion_tx;
2886 let final_y = base_y + scaled_motion_ty;
2887 (final_x, final_y, base_width, base_height)
2888 };
2889
2890 let effective_clip = effective_single_clip(current_clip, current_scroll_clip);
2893 let scaled_clip = effective_clip
2894 .map(|[cx, cy, cw, ch]| [cx * scale, cy * scale, cw * scale, ch * scale]);
2895
2896 svgs.push(SvgElement {
2900 source: svg_data.source.clone(),
2901 x: scaled_x,
2902 y: scaled_y,
2903 width: scaled_width,
2904 height: scaled_height,
2905 tint: svg_data.tint.or_else(|| {
2906 render_node
2907 .props
2908 .text_color
2909 .map(|c| blinc_core::Color::rgba(c[0], c[1], c[2], c[3]))
2910 }),
2911 fill: render_node
2912 .props
2913 .fill
2914 .map(|c| blinc_core::Color::rgba(c[0], c[1], c[2], c[3]))
2915 .or(svg_data.fill),
2916 stroke: render_node
2917 .props
2918 .stroke
2919 .map(|c| blinc_core::Color::rgba(c[0], c[1], c[2], c[3]))
2920 .or(svg_data.stroke),
2921 stroke_width: render_node.props.stroke_width.or(svg_data.stroke_width),
2922 stroke_dasharray: render_node.props.stroke_dasharray.clone(),
2923 stroke_dashoffset: render_node.props.stroke_dashoffset,
2924 svg_path_data: render_node.props.svg_path_data.clone(),
2925 clip_bounds: scaled_clip,
2926 motion_opacity: effective_motion_opacity
2927 * render_node.props.opacity
2928 * inherited_css_opacity,
2929 css_affine: node_css_affine,
2930 tag_overrides: render_node.props.svg_tag_styles.clone(),
2931 transform_3d_layer: inside_3d_layer.clone(),
2932 });
2933 }
2934 ElementType::Image(image_data) => {
2935 let scaled_clip = current_clip
2937 .map(|[cx, cy, cw, ch]| [cx * scale, cy * scale, cw * scale, ch * scale]);
2938
2939 let scaled_clip_radius = current_clip_radius
2941 .map(|[tl, tr, br, bl]| [tl * scale, tr * scale, br * scale, bl * scale])
2942 .unwrap_or([0.0; 4]);
2943
2944 let scaled_scroll_clip = current_scroll_clip
2946 .map(|[cx, cy, cw, ch]| [cx * scale, cy * scale, cw * scale, ch * scale]);
2947
2948 let parent_props = parent_node
2952 .and_then(|pid| tree.get_render_node(pid))
2953 .map(|pn| &pn.props);
2954
2955 let own_css_opacity = render_node.props.opacity;
2957 let final_opacity = image_data.opacity
2958 * own_css_opacity
2959 * inherited_css_opacity
2960 * effective_motion_opacity;
2961
2962 let own_br = render_node.props.border_radius.top_left;
2965 let final_border_radius = if own_br > 0.0 {
2966 own_br * scale
2967 } else {
2968 image_data.border_radius * scale
2969 };
2970
2971 let border_width = render_node.props.border_width * scale;
2974 let border_color = render_node
2975 .props
2976 .border_color
2977 .unwrap_or(blinc_core::Color::TRANSPARENT);
2978
2979 let shadow = render_node.props.shadow;
2981
2982 let own_filter = &render_node.props.filter;
2984 let parent_filter = parent_props.and_then(|p| p.filter.as_ref());
2985 let effective_filter = own_filter.as_ref().or(parent_filter);
2986 let filter_a = effective_filter
2987 .map(|f| Self::css_filter_to_arrays(f).0)
2988 .unwrap_or([0.0, 0.0, 0.0, 0.0]);
2989 let filter_b = effective_filter
2990 .map(|f| Self::css_filter_to_arrays(f).1)
2991 .unwrap_or([1.0, 1.0, 1.0, 0.0]);
2992
2993 let final_object_fit = render_node
2995 .props
2996 .object_fit
2997 .unwrap_or(image_data.object_fit);
2998 let final_object_position = render_node
2999 .props
3000 .object_position
3001 .unwrap_or(image_data.object_position);
3002
3003 let own_mask = render_node.props.mask_image.as_ref();
3005 let parent_mask = parent_props.and_then(|p| p.mask_image.as_ref());
3006 let effective_mask = own_mask.or(parent_mask);
3007 let (mask_params, mask_info) = Self::mask_image_to_arrays(effective_mask);
3008
3009 images.push(ImageElement {
3010 source: image_data.source.clone(),
3011 x: abs_x * scale,
3012 y: abs_y * scale,
3013 width: bounds.width * scale,
3014 height: bounds.height * scale,
3015 object_fit: final_object_fit,
3016 object_position: final_object_position,
3017 opacity: final_opacity,
3018 border_radius: final_border_radius,
3019 tint: image_data.tint,
3020 clip_bounds: scaled_clip,
3021 clip_radius: scaled_clip_radius,
3022 layer: effective_layer,
3023 loading_strategy: image_data.loading_strategy,
3024 placeholder_type: image_data.placeholder_type,
3025 placeholder_color: image_data.placeholder_color,
3026 z_index: *z_layer,
3027 border_width,
3028 border_color,
3029 css_affine: node_css_affine,
3030 shadow,
3031 filter_a,
3032 filter_b,
3033 scroll_clip: scaled_scroll_clip,
3034 mask_params,
3035 mask_info,
3036 transform_3d_layer: inside_3d_layer.clone(),
3037 });
3038 }
3039 ElementType::Canvas(_) => {}
3041 ElementType::Div => {
3042 if let Some(blinc_core::Brush::Image(ref img_brush)) =
3044 render_node.props.background
3045 {
3046 let scaled_clip = current_clip.map(|[cx, cy, cw, ch]| {
3047 [cx * scale, cy * scale, cw * scale, ch * scale]
3048 });
3049 let scaled_clip_radius = current_clip_radius
3050 .map(|[tl, tr, br, bl]| {
3051 [tl * scale, tr * scale, br * scale, bl * scale]
3052 })
3053 .unwrap_or([0.0; 4]);
3054 let scaled_scroll_clip_bg = current_scroll_clip.map(|[cx, cy, cw, ch]| {
3055 [cx * scale, cy * scale, cw * scale, ch * scale]
3056 });
3057
3058 images.push(ImageElement {
3059 source: img_brush.source.clone(),
3060 x: abs_x * scale,
3061 y: abs_y * scale,
3062 width: bounds.width * scale,
3063 height: bounds.height * scale,
3064 object_fit: match img_brush.fit {
3065 blinc_core::ImageFit::Cover => 0,
3066 blinc_core::ImageFit::Contain => 1,
3067 blinc_core::ImageFit::Fill => 2,
3068 blinc_core::ImageFit::Tile => 0,
3069 },
3070 object_position: [img_brush.position.x, img_brush.position.y],
3071 opacity: img_brush.opacity
3072 * render_node.props.opacity
3073 * inherited_css_opacity
3074 * effective_motion_opacity,
3075 border_radius: render_node.props.border_radius.top_left * scale,
3076 tint: [
3077 img_brush.tint.r,
3078 img_brush.tint.g,
3079 img_brush.tint.b,
3080 img_brush.tint.a,
3081 ],
3082 clip_bounds: scaled_clip,
3083 clip_radius: scaled_clip_radius,
3084 layer: effective_layer,
3085 loading_strategy: 0, placeholder_type: 0, placeholder_color: [0.0; 4],
3088 z_index: *z_layer,
3089 border_width: 0.0,
3090 border_color: blinc_core::Color::TRANSPARENT,
3091 css_affine: node_css_affine,
3092 shadow: render_node.props.shadow,
3093 filter_a: render_node
3094 .props
3095 .filter
3096 .as_ref()
3097 .map(|f| Self::css_filter_to_arrays(f).0)
3098 .unwrap_or([0.0, 0.0, 0.0, 0.0]),
3099 filter_b: render_node
3100 .props
3101 .filter
3102 .as_ref()
3103 .map(|f| Self::css_filter_to_arrays(f).1)
3104 .unwrap_or([1.0, 1.0, 1.0, 0.0]),
3105 scroll_clip: scaled_scroll_clip_bg,
3106 mask_params: {
3107 let (mp, _) = Self::mask_image_to_arrays(
3108 render_node.props.mask_image.as_ref(),
3109 );
3110 mp
3111 },
3112 mask_info: {
3113 let (_, mi) = Self::mask_image_to_arrays(
3114 render_node.props.mask_image.as_ref(),
3115 );
3116 mi
3117 },
3118 transform_3d_layer: inside_3d_layer.clone(),
3119 });
3120 }
3121 }
3122 ElementType::StyledText(styled_data) => {
3124 let base_x = abs_x * scale;
3126 let base_y = abs_y * scale;
3127 let base_width = bounds.width * scale;
3128 let base_height = bounds.height * scale;
3129
3130 let scaled_motion_tx = effective_motion_translate.0 * scale;
3132 let scaled_motion_ty = effective_motion_translate.1 * scale;
3133
3134 let (scaled_x, scaled_y, scaled_width, scaled_height) =
3136 if let Some((motion_center_x, motion_center_y)) =
3137 effective_motion_scale_center
3138 {
3139 let motion_center_x_scaled = motion_center_x * scale;
3140 let motion_center_y_scaled = motion_center_y * scale;
3141
3142 let rel_x = base_x - motion_center_x_scaled;
3143 let rel_y = base_y - motion_center_y_scaled;
3144
3145 let scaled_rel_x = rel_x * effective_motion_scale.0;
3146 let scaled_rel_y = rel_y * effective_motion_scale.1;
3147 let scaled_w = base_width * effective_motion_scale.0;
3148 let scaled_h = base_height * effective_motion_scale.1;
3149
3150 let final_x = motion_center_x_scaled + scaled_rel_x + scaled_motion_tx;
3151 let final_y = motion_center_y_scaled + scaled_rel_y + scaled_motion_ty;
3152
3153 (final_x, final_y, scaled_w, scaled_h)
3154 } else {
3155 let final_x = base_x + scaled_motion_tx;
3156 let final_y = base_y + scaled_motion_ty;
3157 (final_x, final_y, base_width, base_height)
3158 };
3159
3160 let base_styled_font_size =
3162 render_node.props.font_size.unwrap_or(styled_data.font_size);
3163 let scaled_font_size = base_styled_font_size * effective_motion_scale.1 * scale;
3164 let effective_clip = effective_single_clip(current_clip, current_scroll_clip);
3166 let scaled_clip = effective_clip
3167 .map(|[cx, cy, cw, ch]| [cx * scale, cy * scale, cw * scale, ch * scale]);
3168
3169 let content = &styled_data.content;
3172 let content_len = content.len();
3173
3174 let default_bold = styled_data.weight == FontWeight::Bold;
3176 let default_italic = styled_data.italic;
3177
3178 let mut boundaries: Vec<usize> = vec![0, content_len];
3180 for span in &styled_data.spans {
3181 if span.start < content_len {
3182 boundaries.push(span.start);
3183 }
3184 if span.end <= content_len {
3185 boundaries.push(span.end);
3186 }
3187 }
3188 boundaries.sort();
3189 boundaries.dedup();
3190
3191 #[allow(clippy::type_complexity)]
3193 let mut segments: Vec<(
3194 usize,
3195 usize,
3196 [f32; 4],
3197 bool,
3198 bool,
3199 bool,
3200 bool,
3201 )> = Vec::new();
3202
3203 for window in boundaries.windows(2) {
3204 let seg_start = window[0];
3205 let seg_end = window[1];
3206 if seg_start >= seg_end {
3207 continue;
3208 }
3209
3210 let mut color: Option<[f32; 4]> = None;
3212 let mut bold = default_bold;
3213 let mut italic = default_italic;
3214 let mut underline = false;
3215 let mut strikethrough = false;
3216
3217 for span in &styled_data.spans {
3218 if span.start <= seg_start && span.end >= seg_end {
3220 if span.bold {
3222 bold = true;
3223 }
3224 if span.italic {
3225 italic = true;
3226 }
3227 if span.underline {
3228 underline = true;
3229 }
3230 if span.strikethrough {
3231 strikethrough = true;
3232 }
3233 if span.color[3] > 0.0 {
3235 color = Some(span.color);
3236 }
3237 }
3238 }
3239
3240 let default_color = render_node
3242 .props
3243 .text_color
3244 .unwrap_or(styled_data.default_color);
3245 let final_color = color.unwrap_or(default_color);
3246 segments.push((
3247 seg_start,
3248 seg_end,
3249 final_color,
3250 bold,
3251 italic,
3252 underline,
3253 strikethrough,
3254 ));
3255 }
3256
3257 let scaled_ascender = styled_data.ascender * scale;
3259
3260 let mut x_offset = 0.0f32;
3262 for (start, end, color, bold, italic, underline, strikethrough) in segments {
3263 if start >= end || start >= content.len() {
3264 continue;
3265 }
3266 let segment_text = &content[start..end.min(content.len())];
3267 if segment_text.is_empty() {
3268 continue;
3269 }
3270
3271 let mut options = blinc_layout::text_measure::TextLayoutOptions::new();
3273 options.font_name = styled_data.font_family.name.clone();
3274 options.generic_font = styled_data.font_family.generic;
3275 options.font_weight = if bold { 700 } else { 400 };
3276 options.italic = italic;
3277 let metrics = blinc_layout::text_measure::measure_text_with_options(
3278 segment_text,
3279 styled_data.font_size,
3280 &options,
3281 );
3282 let segment_width = metrics.width * scale * effective_motion_scale.0;
3284
3285 texts.push(TextElement {
3286 content: segment_text.to_string(),
3287 x: scaled_x + x_offset,
3288 y: scaled_y,
3289 width: segment_width,
3290 height: scaled_height,
3291 font_size: scaled_font_size,
3292 color,
3293 align: TextAlign::Left, weight: if bold {
3295 FontWeight::Bold
3296 } else {
3297 FontWeight::Normal
3298 },
3299 italic,
3300 v_align: styled_data.v_align,
3301 clip_bounds: scaled_clip,
3302 motion_opacity: effective_motion_opacity
3303 * render_node.props.opacity
3304 * inherited_css_opacity,
3305 wrap: false, line_height: styled_data.line_height,
3307 measured_width: segment_width,
3308 font_family: styled_data.font_family.clone(),
3309 word_spacing: 0.0,
3310 letter_spacing: render_node.props.letter_spacing.unwrap_or(0.0),
3311 z_index: *z_layer,
3312 ascender: scaled_ascender * effective_motion_scale.1, strikethrough,
3314 underline,
3315 decoration_color: render_node.props.text_decoration_color,
3316 decoration_thickness: render_node.props.text_decoration_thickness,
3317 css_affine: node_css_affine,
3318 text_shadow: render_node.props.text_shadow,
3319 transform_3d_layer: inside_3d_layer.clone(),
3320 is_foreground: children_inside_foreground,
3321 });
3322
3323 x_offset += segment_width;
3324 }
3325 }
3326 }
3327
3328 if let Some(ref flow_name) = render_node.props.flow {
3331 flows.push(FlowElement {
3332 flow_name: flow_name.clone(),
3333 flow_graph: render_node.props.flow_graph.clone(),
3334 x: abs_x * scale,
3335 y: abs_y * scale,
3336 width: bounds.width * scale,
3337 height: bounds.height * scale,
3338 z_index: *z_layer,
3339 corner_radius: render_node.props.border_radius.top_left * scale,
3340 });
3341 }
3342 }
3343
3344 let scroll_offset = tree.get_scroll_offset(node);
3346 let static_motion_offset = tree
3347 .get_motion_transform(node)
3348 .map(|t| match t {
3349 blinc_core::Transform::Affine2D(a) => (a.elements[4], a.elements[5]),
3350 _ => (0.0, 0.0),
3351 })
3352 .unwrap_or((0.0, 0.0));
3353
3354 let new_offset = (
3355 abs_x + scroll_offset.0 + static_motion_offset.0,
3356 abs_y + scroll_offset.1 + static_motion_offset.1,
3357 );
3358
3359 let child_css_opacity = if let Some(rn) = tree.get_render_node(node) {
3362 inherited_css_opacity * rn.props.opacity
3363 } else {
3364 inherited_css_opacity
3365 };
3366
3367 let child_3d_layer = if let Some(rn) = tree.get_render_node(node) {
3370 let has_3d = rn.props.rotate_x.is_some()
3371 || rn.props.rotate_y.is_some()
3372 || rn.props.perspective.is_some();
3373 if has_3d {
3374 let rx = rn.props.rotate_x.unwrap_or(0.0).to_radians();
3375 let ry = rn.props.rotate_y.unwrap_or(0.0).to_radians();
3376 let d = rn.props.perspective.unwrap_or(800.0) * scale;
3377 Some(Transform3DLayerInfo {
3378 node_id: node,
3379 layer_bounds: [
3380 abs_x * scale,
3381 abs_y * scale,
3382 bounds.width * scale,
3383 bounds.height * scale,
3384 ],
3385 transform_3d: blinc_core::Transform3DParams {
3386 sin_rx: rx.sin(),
3387 cos_rx: rx.cos(),
3388 sin_ry: ry.sin(),
3389 cos_ry: ry.cos(),
3390 perspective_d: d,
3391 },
3392 opacity: rn.props.opacity,
3393 })
3394 } else {
3395 inside_3d_layer.clone()
3396 }
3397 } else {
3398 inside_3d_layer.clone()
3399 };
3400
3401 for child_id in tree.layout().children(node) {
3402 self.collect_elements_recursive(
3403 tree,
3404 child_id,
3405 new_offset,
3406 children_inside_glass,
3407 children_inside_foreground,
3408 child_clip,
3409 child_clip_radius,
3410 effective_motion_opacity,
3411 effective_motion_translate,
3412 effective_motion_scale,
3413 effective_motion_scale_center,
3414 render_state,
3415 scale,
3416 z_layer,
3417 texts,
3418 svgs,
3419 images,
3420 flows,
3421 node_css_affine,
3422 child_css_opacity,
3423 Some(node), child_scroll_clip,
3425 child_3d_layer.clone(),
3426 );
3427 }
3428
3429 if node_z_index > 0 {
3431 *z_layer = saved_z_layer;
3432 }
3433 }
3434
3435 pub fn device(&self) -> &Arc<wgpu::Device> {
3437 &self.device
3438 }
3439
3440 pub fn queue(&self) -> &Arc<wgpu::Queue> {
3442 &self.queue
3443 }
3444
3445 pub fn font_registry(&self) -> Arc<Mutex<FontRegistry>> {
3450 self.text_ctx.font_registry()
3451 }
3452
3453 pub fn texture_format(&self) -> wgpu::TextureFormat {
3455 self.renderer.texture_format()
3456 }
3457
3458 pub fn render_tree_with_state(
3467 &mut self,
3468 tree: &RenderTree,
3469 render_state: &blinc_layout::RenderState,
3470 width: u32,
3471 height: u32,
3472 target: &wgpu::TextureView,
3473 ) -> Result<()> {
3474 self.render_tree(tree, width, height, target)?;
3476
3477 self.render_overlays(render_state, width, height, target);
3479
3480 Ok(())
3481 }
3482
3483 pub fn render_tree_with_motion(
3492 &mut self,
3493 tree: &RenderTree,
3494 render_state: &blinc_layout::RenderState,
3495 width: u32,
3496 height: u32,
3497 target: &wgpu::TextureView,
3498 ) -> Result<()> {
3499 let scale_factor = tree.scale_factor();
3501
3502 let mut ctx =
3504 GpuPaintContext::with_text_context(width as f32, height as f32, &mut self.text_ctx);
3505
3506 tree.render_with_motion(&mut ctx, render_state);
3508
3509 let mut batch = ctx.take_batch();
3511
3512 let (all_texts, all_svgs, all_images, flow_elements) =
3514 self.collect_render_elements_with_state(tree, Some(render_state));
3515
3516 let mut texts = Vec::new();
3520 let mut fg_texts = Vec::new();
3521 let mut layer_3d_texts: std::collections::HashMap<
3522 LayoutNodeId,
3523 (Transform3DLayerInfo, Vec<TextElement>),
3524 > = std::collections::HashMap::new();
3525 for text in all_texts {
3526 if let Some(ref info) = text.transform_3d_layer {
3527 layer_3d_texts
3528 .entry(info.node_id)
3529 .or_insert_with(|| (info.clone(), Vec::new()))
3530 .1
3531 .push(text);
3532 } else if text.is_foreground {
3533 fg_texts.push(text);
3534 } else {
3535 texts.push(text);
3536 }
3537 }
3538
3539 let mut svgs = Vec::new();
3540 let mut layer_3d_svgs: std::collections::HashMap<LayoutNodeId, Vec<SvgElement>> =
3541 std::collections::HashMap::new();
3542 for svg in all_svgs {
3543 if let Some(ref info) = svg.transform_3d_layer {
3544 layer_3d_svgs.entry(info.node_id).or_default().push(svg);
3545 } else {
3546 svgs.push(svg);
3547 }
3548 }
3549
3550 let mut images = Vec::new();
3551 let mut layer_3d_images: std::collections::HashMap<LayoutNodeId, Vec<ImageElement>> =
3552 std::collections::HashMap::new();
3553 for image in all_images {
3554 if let Some(ref info) = image.transform_3d_layer {
3555 layer_3d_images.entry(info.node_id).or_default().push(image);
3556 } else {
3557 images.push(image);
3558 }
3559 }
3560
3561 let layer_3d_ids: Vec<LayoutNodeId> = layer_3d_texts.keys().cloned().collect();
3563
3564 self.preload_images(&images, width as f32, height as f32);
3566 for layer_imgs in layer_3d_images.values() {
3567 self.preload_images(layer_imgs, width as f32, height as f32);
3568 }
3569
3570 let mut glyphs_by_layer: std::collections::BTreeMap<u32, Vec<GpuGlyph>> =
3573 std::collections::BTreeMap::new();
3574 let mut css_transformed_text_prims: Vec<GpuPrimitive> = Vec::new();
3575 for text in &texts {
3576 if let Some([clip_x, clip_y, clip_w, clip_h]) = text.clip_bounds {
3579 let text_right = text.x + text.width;
3580 let text_bottom = text.y + text.height;
3581 let clip_right = clip_x + clip_w;
3582 let clip_bottom = clip_y + clip_h;
3583
3584 if text.x >= clip_right
3586 || text_right <= clip_x
3587 || text.y >= clip_bottom
3588 || text_bottom <= clip_y
3589 {
3590 continue;
3592 }
3593 }
3594
3595 let alignment = match text.align {
3596 TextAlign::Left => TextAlignment::Left,
3597 TextAlign::Center => TextAlignment::Center,
3598 TextAlign::Right => TextAlignment::Right,
3599 };
3600
3601 let color = if text.motion_opacity < 1.0 {
3603 [
3604 text.color[0],
3605 text.color[1],
3606 text.color[2],
3607 text.color[3] * text.motion_opacity,
3608 ]
3609 } else {
3610 text.color
3611 };
3612
3613 let effective_width = if let Some(clip) = text.clip_bounds {
3619 clip[2].min(text.width)
3621 } else {
3622 text.width
3623 };
3624
3625 let needs_wrap = text.wrap && effective_width < text.measured_width - 2.0;
3627
3628 let wrap_width = Some(text.width);
3631
3632 let font_name = text.font_family.name.as_deref();
3634 let generic = to_gpu_generic_font(text.font_family.generic);
3635 let font_weight = text.weight.weight();
3636
3637 let (anchor, y_pos, use_layout_height) = match text.v_align {
3639 TextVerticalAlign::Center => {
3640 (TextAnchor::Center, text.y + text.height / 2.0, false)
3641 }
3642 TextVerticalAlign::Top => (TextAnchor::Top, text.y, true),
3643 TextVerticalAlign::Baseline => {
3644 let baseline_y = text.y + text.ascender;
3645 (TextAnchor::Baseline, baseline_y, false)
3646 }
3647 };
3648 let layout_height = if use_layout_height {
3649 Some(text.height)
3650 } else {
3651 None
3652 };
3653
3654 if let Some(shadow) = &text.text_shadow {
3656 let shadow_color = [
3657 shadow.color.r,
3658 shadow.color.g,
3659 shadow.color.b,
3660 shadow.color.a * text.motion_opacity,
3661 ];
3662 let shadow_x = text.x + shadow.offset_x * scale_factor;
3663 let shadow_y = y_pos + shadow.offset_y * scale_factor;
3664 if let Ok(mut shadow_glyphs) = self.text_ctx.prepare_text_with_style(
3665 &text.content,
3666 shadow_x,
3667 shadow_y,
3668 text.font_size,
3669 shadow_color,
3670 anchor,
3671 alignment,
3672 wrap_width,
3673 needs_wrap,
3674 font_name,
3675 generic,
3676 font_weight,
3677 text.italic,
3678 layout_height,
3679 text.letter_spacing,
3680 ) {
3681 if let Some(clip) = text.clip_bounds {
3682 for glyph in &mut shadow_glyphs {
3683 glyph.clip_bounds = clip;
3684 }
3685 }
3686 if let Some(affine) = text.css_affine {
3687 let [a, b, c, d, tx, ty] = affine;
3688 let tx_scaled = tx * scale_factor;
3689 let ty_scaled = ty * scale_factor;
3690 for glyph in &shadow_glyphs {
3691 let gc_x = glyph.bounds[0] + glyph.bounds[2] / 2.0;
3692 let gc_y = glyph.bounds[1] + glyph.bounds[3] / 2.0;
3693 let new_gc_x = a * gc_x + c * gc_y + tx_scaled;
3694 let new_gc_y = b * gc_x + d * gc_y + ty_scaled;
3695 let mut prim = GpuPrimitive::from_glyph(glyph);
3696 prim.bounds = [
3697 new_gc_x - glyph.bounds[2] / 2.0,
3698 new_gc_y - glyph.bounds[3] / 2.0,
3699 glyph.bounds[2],
3700 glyph.bounds[3],
3701 ];
3702 prim.local_affine = [a, b, c, d];
3703 prim.set_z_layer(text.z_index);
3704 css_transformed_text_prims.push(prim);
3705 }
3706 } else {
3707 glyphs_by_layer
3708 .entry(text.z_index)
3709 .or_default()
3710 .extend(shadow_glyphs);
3711 }
3712 }
3713 }
3714
3715 match self.text_ctx.prepare_text_with_style(
3716 &text.content,
3717 text.x,
3718 y_pos,
3719 text.font_size,
3720 color,
3721 anchor,
3722 alignment,
3723 wrap_width,
3724 needs_wrap,
3725 font_name,
3726 generic,
3727 font_weight,
3728 text.italic,
3729 layout_height,
3730 text.letter_spacing,
3731 ) {
3732 Ok(mut glyphs) => {
3733 tracing::trace!(
3734 "render_tree_with_motion: prepared {} glyphs for '{}' (font={:?})",
3735 glyphs.len(),
3736 text.content,
3737 font_name
3738 );
3739 if let Some(clip) = text.clip_bounds {
3741 for glyph in &mut glyphs {
3742 glyph.clip_bounds = clip;
3743 }
3744 }
3745
3746 if let Some(affine) = text.css_affine {
3747 let [a, b, c, d, tx, ty] = affine;
3749 let tx_scaled = tx * scale_factor;
3750 let ty_scaled = ty * scale_factor;
3751 for glyph in &glyphs {
3752 let gc_x = glyph.bounds[0] + glyph.bounds[2] / 2.0;
3754 let gc_y = glyph.bounds[1] + glyph.bounds[3] / 2.0;
3755 let new_gc_x = a * gc_x + c * gc_y + tx_scaled;
3756 let new_gc_y = b * gc_x + d * gc_y + ty_scaled;
3757 let mut prim = GpuPrimitive::from_glyph(glyph);
3758 prim.bounds = [
3759 new_gc_x - glyph.bounds[2] / 2.0,
3760 new_gc_y - glyph.bounds[3] / 2.0,
3761 glyph.bounds[2],
3762 glyph.bounds[3],
3763 ];
3764 prim.local_affine = [a, b, c, d];
3765 prim.set_z_layer(text.z_index);
3766 css_transformed_text_prims.push(prim);
3767 }
3768 } else {
3769 glyphs_by_layer
3771 .entry(text.z_index)
3772 .or_default()
3773 .extend(glyphs);
3774 }
3775 }
3776 Err(e) => {
3777 tracing::warn!(
3778 "render_tree_with_motion: failed to prepare text '{}': {:?}",
3779 text.content,
3780 e
3781 );
3782 }
3783 }
3784 }
3785
3786 let mut fg_glyphs: Vec<GpuGlyph> = Vec::new();
3788 for text in &fg_texts {
3789 if let Some([clip_x, clip_y, clip_w, clip_h]) = text.clip_bounds {
3790 let text_right = text.x + text.width;
3791 let text_bottom = text.y + text.height;
3792 let clip_right = clip_x + clip_w;
3793 let clip_bottom = clip_y + clip_h;
3794 if text.x >= clip_right
3795 || text_right <= clip_x
3796 || text.y >= clip_bottom
3797 || text_bottom <= clip_y
3798 {
3799 continue;
3800 }
3801 }
3802
3803 let alignment = match text.align {
3804 TextAlign::Left => TextAlignment::Left,
3805 TextAlign::Center => TextAlignment::Center,
3806 TextAlign::Right => TextAlignment::Right,
3807 };
3808
3809 let color = if text.motion_opacity < 1.0 {
3810 [
3811 text.color[0],
3812 text.color[1],
3813 text.color[2],
3814 text.color[3] * text.motion_opacity,
3815 ]
3816 } else {
3817 text.color
3818 };
3819
3820 let effective_width = if let Some(clip) = text.clip_bounds {
3821 clip[2].min(text.width)
3822 } else {
3823 text.width
3824 };
3825 let needs_wrap = text.wrap && effective_width < text.measured_width - 2.0;
3826 let wrap_width = Some(text.width);
3827 let font_name = text.font_family.name.as_deref();
3828 let generic = to_gpu_generic_font(text.font_family.generic);
3829 let font_weight = text.weight.weight();
3830
3831 let (anchor, y_pos, use_layout_height) = match text.v_align {
3832 TextVerticalAlign::Center => {
3833 (TextAnchor::Center, text.y + text.height / 2.0, false)
3834 }
3835 TextVerticalAlign::Top => (TextAnchor::Top, text.y, true),
3836 TextVerticalAlign::Baseline => {
3837 let baseline_y = text.y + text.ascender;
3838 (TextAnchor::Baseline, baseline_y, false)
3839 }
3840 };
3841 let layout_height = if use_layout_height {
3842 Some(text.height)
3843 } else {
3844 None
3845 };
3846
3847 if let Ok(mut glyphs) = self.text_ctx.prepare_text_with_style(
3848 &text.content,
3849 text.x,
3850 y_pos,
3851 text.font_size,
3852 color,
3853 anchor,
3854 alignment,
3855 wrap_width,
3856 needs_wrap,
3857 font_name,
3858 generic,
3859 font_weight,
3860 text.italic,
3861 layout_height,
3862 text.letter_spacing,
3863 ) {
3864 if let Some(clip) = text.clip_bounds {
3865 for glyph in &mut glyphs {
3866 glyph.clip_bounds = clip;
3867 }
3868 }
3869 fg_glyphs.extend(glyphs);
3870 }
3871 }
3872
3873 tracing::trace!(
3874 "render_tree_with_motion: {} texts, {} fg texts, {} z-layers with glyphs, {} css-transformed",
3875 texts.len(),
3876 fg_texts.len(),
3877 glyphs_by_layer.len(),
3878 css_transformed_text_prims.len()
3879 );
3880
3881 self.renderer.resize(width, height);
3885
3886 if !css_transformed_text_prims.is_empty() {
3889 if let (Some(atlas), Some(color_atlas)) =
3890 (self.text_ctx.atlas_view(), self.text_ctx.color_atlas_view())
3891 {
3892 batch.primitives.append(&mut css_transformed_text_prims);
3893 self.renderer.set_glyph_atlas(atlas, color_atlas);
3894 }
3895 }
3896
3897 let has_glass = batch.glass_count() > 0;
3898 let has_layer_effects_in_batch = batch.has_layer_effects();
3899
3900 if has_glass {
3902 self.ensure_glass_textures(width, height);
3903 }
3904 let use_msaa_overlay = self.sample_count > 1;
3905
3906 if has_glass {
3907 let (bg_images, fg_images): (Vec<_>, Vec<_>) = images
3909 .iter()
3910 .partition(|img| img.layer == RenderLayer::Background);
3911
3912 let has_bg_images = !bg_images.is_empty();
3914 if has_bg_images {
3915 let backdrop_tex = self.backdrop_texture.take().unwrap();
3916 self.renderer
3917 .clear_target(&backdrop_tex.view, wgpu::Color::TRANSPARENT);
3918 self.renderer.clear_target(target, wgpu::Color::BLACK);
3919 self.render_images_ref(&backdrop_tex.view, &bg_images);
3920 self.render_images_ref(target, &bg_images);
3921 self.backdrop_texture = Some(backdrop_tex);
3922 }
3923
3924 if has_layer_effects_in_batch {
3925 {
3931 let backdrop = self.backdrop_texture.as_ref().unwrap();
3932 self.renderer.render_to_backdrop(
3933 &backdrop.view,
3934 (backdrop.width, backdrop.height),
3935 &batch,
3936 has_bg_images,
3937 );
3938 }
3939
3940 self.renderer
3942 .render_with_clear(target, &batch, [0.0, 0.0, 0.0, 1.0]);
3943
3944 if has_bg_images {
3946 self.render_images_ref(target, &bg_images);
3947 }
3948
3949 if batch.glass_count() > 0 {
3951 let backdrop = self.backdrop_texture.as_ref().unwrap();
3952 self.renderer.render_glass(target, &backdrop.view, &batch);
3953 }
3954 } else {
3955 let backdrop = self.backdrop_texture.as_ref().unwrap();
3957 self.renderer.render_glass_frame(
3958 target,
3959 &backdrop.view,
3960 (backdrop.width, backdrop.height),
3961 &batch,
3962 has_bg_images,
3963 );
3964 }
3965
3966 if use_msaa_overlay && batch.has_paths() {
3969 self.renderer
3970 .render_paths_overlay_msaa(target, &batch, self.sample_count);
3971 }
3972
3973 if !has_bg_images {
3975 self.render_images_ref(target, &bg_images);
3976 }
3977 self.render_images_ref(target, &fg_images);
3978
3979 let max_z = batch.max_z_layer();
3981 let max_text_z = glyphs_by_layer.keys().cloned().max().unwrap_or(0);
3982 let decorations_by_layer = generate_text_decoration_primitives_by_layer(&texts);
3983 let max_decoration_z = decorations_by_layer.keys().cloned().max().unwrap_or(0);
3984 let max_glass_layer = max_z.max(max_text_z).max(max_decoration_z);
3985
3986 {
3988 let mut scratch = std::mem::take(&mut self.scratch_glyphs);
3989 scratch.clear();
3990 if let Some(glyphs) = glyphs_by_layer.get(&0) {
3991 scratch.extend_from_slice(glyphs);
3992 }
3993 if !scratch.is_empty() {
3994 self.render_text(target, &scratch);
3995 }
3996 self.scratch_glyphs = scratch;
3997 }
3998 self.render_text_decorations_for_layer(target, &decorations_by_layer, 0);
3999
4000 if max_glass_layer > 0 {
4001 let effect_indices = batch.effect_layer_indices();
4002 for z in 1..=max_glass_layer {
4003 let layer_primitives = if effect_indices.is_empty() {
4005 batch.primitives_for_layer(z)
4006 } else {
4007 batch.primitives_for_layer_excluding_effects(z, &effect_indices)
4008 };
4009 if !layer_primitives.is_empty() {
4010 self.renderer
4011 .render_primitives_overlay(target, &layer_primitives);
4012 }
4013
4014 {
4016 let mut scratch = std::mem::take(&mut self.scratch_glyphs);
4017 scratch.clear();
4018 if let Some(glyphs) = glyphs_by_layer.get(&z) {
4019 scratch.extend_from_slice(glyphs);
4020 }
4021 if !scratch.is_empty() {
4022 self.render_text(target, &scratch);
4023 }
4024 self.scratch_glyphs = scratch;
4025 }
4026 self.render_text_decorations_for_layer(target, &decorations_by_layer, z);
4027 }
4028 }
4029
4030 if !svgs.is_empty() {
4032 self.render_rasterized_svgs(target, &svgs, scale_factor);
4033 }
4034
4035 if !fg_glyphs.is_empty() {
4037 self.render_text(target, &fg_glyphs);
4038 }
4039 } else {
4040 let decorations_by_layer = generate_text_decoration_primitives_by_layer(&texts);
4043
4044 let max_z = batch.max_z_layer();
4045 let max_text_z = glyphs_by_layer.keys().cloned().max().unwrap_or(0);
4046 let max_decoration_z = decorations_by_layer.keys().cloned().max().unwrap_or(0);
4047 let max_layer = max_z.max(max_text_z).max(max_decoration_z);
4048 let has_layer_effects = batch.has_layer_effects();
4049
4050 if max_layer > 0 && !has_layer_effects {
4051 let mut images_by_layer: std::collections::BTreeMap<u32, Vec<&ImageElement>> =
4054 std::collections::BTreeMap::new();
4055 for img in &images {
4056 images_by_layer.entry(img.z_index).or_default().push(img);
4057 }
4058 let max_image_z = images_by_layer.keys().cloned().max().unwrap_or(0);
4059 let max_layer = max_layer.max(max_image_z);
4060
4061 let z0_primitives = batch.primitives_for_layer(0);
4063 let mut z0_batch = PrimitiveBatch::new();
4065 z0_batch.primitives = z0_primitives;
4066 z0_batch.paths = batch.paths.clone();
4067 self.renderer
4068 .render_with_clear(target, &z0_batch, [0.0, 0.0, 0.0, 1.0]);
4069
4070 if use_msaa_overlay && z0_batch.has_paths() {
4072 self.renderer
4073 .render_paths_overlay_msaa(target, &z0_batch, self.sample_count);
4074 }
4075
4076 if let Some(z0_images) = images_by_layer.get(&0) {
4078 self.render_images_ref(target, z0_images);
4079 }
4080
4081 if let Some(glyphs) = glyphs_by_layer.get(&0) {
4083 if !glyphs.is_empty() {
4084 self.render_text(target, glyphs);
4085 }
4086 }
4087 self.render_text_decorations_for_layer(target, &decorations_by_layer, 0);
4088
4089 for z in 1..=max_layer {
4091 let layer_primitives = batch.primitives_for_layer(z);
4093 if !layer_primitives.is_empty() {
4094 self.renderer
4095 .render_primitives_overlay(target, &layer_primitives);
4096 }
4097
4098 if let Some(layer_images) = images_by_layer.get(&z) {
4100 self.render_images_ref(target, layer_images);
4101 }
4102
4103 if let Some(glyphs) = glyphs_by_layer.get(&z) {
4105 if !glyphs.is_empty() {
4106 self.render_text(target, glyphs);
4107 }
4108 }
4109 self.render_text_decorations_for_layer(target, &decorations_by_layer, z);
4110 }
4111
4112 if !svgs.is_empty() {
4114 self.render_rasterized_svgs(target, &svgs, scale_factor);
4115 }
4116
4117 if !batch.foreground_primitives.is_empty() {
4119 self.renderer
4120 .render_primitives_overlay(target, &batch.foreground_primitives);
4121 }
4122
4123 if !fg_glyphs.is_empty() {
4125 self.render_text(target, &fg_glyphs);
4126 }
4127 } else {
4128 self.renderer
4130 .render_with_clear(target, &batch, [0.0, 0.0, 0.0, 1.0]);
4131
4132 if use_msaa_overlay && batch.has_paths() {
4134 self.renderer
4135 .render_paths_overlay_msaa(target, &batch, self.sample_count);
4136 }
4137
4138 self.render_images(target, &images, width as f32, height as f32, scale_factor);
4139
4140 if !batch.foreground_primitives.is_empty() {
4142 self.renderer
4143 .render_primitives_overlay(target, &batch.foreground_primitives);
4144 }
4145
4146 if !svgs.is_empty() {
4148 self.render_rasterized_svgs(target, &svgs, scale_factor);
4149 }
4150
4151 if let Some(glyphs) = glyphs_by_layer.get(&0) {
4154 if !glyphs.is_empty() {
4155 self.render_text(target, glyphs);
4156 }
4157 }
4158 self.render_text_decorations_for_layer(target, &decorations_by_layer, 0);
4159
4160 if max_layer > 0 {
4161 let effect_indices = batch.effect_layer_indices();
4162 for z in 1..=max_layer {
4163 let layer_primitives = if effect_indices.is_empty() {
4165 batch.primitives_for_layer(z)
4166 } else {
4167 batch.primitives_for_layer_excluding_effects(z, &effect_indices)
4168 };
4169 if !layer_primitives.is_empty() {
4170 self.renderer
4171 .render_primitives_overlay(target, &layer_primitives);
4172 }
4173
4174 if let Some(glyphs) = glyphs_by_layer.get(&z) {
4176 if !glyphs.is_empty() {
4177 self.render_text(target, glyphs);
4178 }
4179 }
4180 self.render_text_decorations_for_layer(target, &decorations_by_layer, z);
4181 }
4182 }
4183
4184 if !fg_glyphs.is_empty() {
4186 self.render_text(target, &fg_glyphs);
4187 }
4188 }
4189 }
4190
4191 for layer_id in &layer_3d_ids {
4194 if let Some((info, layer_texts)) = layer_3d_texts.get(layer_id) {
4195 let layer_svgs_vec = layer_3d_svgs.get(layer_id);
4196 let layer_images_vec = layer_3d_images.get(layer_id);
4197 self.render_3d_layer_elements(
4198 target,
4199 info,
4200 layer_texts,
4201 layer_svgs_vec.map(|v| v.as_slice()).unwrap_or(&[]),
4202 layer_images_vec.map(|v| v.as_slice()).unwrap_or(&[]),
4203 scale_factor,
4204 );
4205 }
4206 }
4207
4208 self.has_active_flows = !flow_elements.is_empty();
4210 if !flow_elements.is_empty() {
4211 let stylesheet = tree.stylesheet();
4212
4213 static START_TIME: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
4215 let start = START_TIME.get_or_init(std::time::Instant::now);
4216 let elapsed_secs = start.elapsed().as_secs_f32();
4217
4218 for flow_el in &flow_elements {
4219 let graph = flow_el
4221 .flow_graph
4222 .as_deref()
4223 .or_else(|| stylesheet.and_then(|s| s.get_flow(&flow_el.flow_name)));
4224
4225 if let Some(graph) = graph {
4226 if let Err(e) = self.renderer.flow_pipeline_cache().compile(graph) {
4228 tracing::warn!("@flow '{}' compile error: {}", flow_el.flow_name, e);
4229 continue;
4230 }
4231
4232 let uniforms = blinc_gpu::FlowUniformData {
4233 viewport_size: [width as f32, height as f32],
4234 time: elapsed_secs,
4235 frame_index: 0.0, element_bounds: [flow_el.x, flow_el.y, flow_el.width, flow_el.height],
4237 pointer: [
4238 (self.cursor_pos[0] - flow_el.x) / flow_el.width.max(1.0),
4239 (self.cursor_pos[1] - flow_el.y) / flow_el.height.max(1.0),
4240 ],
4241 corner_radius: flow_el.corner_radius,
4242 _padding: 0.0,
4243 };
4244
4245 let viewport = [flow_el.x, flow_el.y, flow_el.width, flow_el.height];
4246 if !self.renderer.render_flow(
4247 target,
4248 &flow_el.flow_name,
4249 &uniforms,
4250 Some(viewport),
4251 ) {
4252 tracing::warn!("@flow '{}' render failed", flow_el.flow_name);
4253 }
4254 }
4255 }
4256 }
4257
4258 self.renderer.poll();
4260
4261 self.render_overlays(render_state, width, height, target);
4263
4264 let debug = DebugMode::from_env();
4266 if debug.text {
4267 self.render_text_debug(target, &texts);
4268 }
4269 if debug.layout {
4270 let scale = tree.scale_factor();
4271 self.render_layout_debug(target, tree, scale);
4272 }
4273 if debug.motion {
4274 self.render_motion_debug(target, tree, width, height);
4275 }
4276
4277 self.return_scratch_elements(texts, svgs, images);
4279
4280 self.log_cache_stats();
4282
4283 Ok(())
4284 }
4285
4286 fn render_3d_layer_elements(
4292 &mut self,
4293 target: &wgpu::TextureView,
4294 info: &Transform3DLayerInfo,
4295 texts: &[TextElement],
4296 svgs: &[SvgElement],
4297 images: &[ImageElement],
4298 scale_factor: f32,
4299 ) {
4300 let [lx, ly, lw, lh] = info.layer_bounds;
4301 if lw <= 0.0 || lh <= 0.0 {
4302 return;
4303 }
4304
4305 let tex_w = (lw.ceil() as u32).max(1);
4306 let tex_h = (lh.ceil() as u32).max(1);
4307
4308 let layer_tex = self.renderer.acquire_layer_texture((tex_w, tex_h), false);
4310 self.renderer
4311 .clear_target(&layer_tex.view, wgpu::Color::TRANSPARENT);
4312
4313 self.renderer.set_viewport_override((tex_w, tex_h));
4315
4316 if !texts.is_empty() {
4318 let mut layer_glyphs: Vec<GpuGlyph> = Vec::new();
4319 for text in texts {
4320 let alignment = match text.align {
4321 TextAlign::Left => TextAlignment::Left,
4322 TextAlign::Center => TextAlignment::Center,
4323 TextAlign::Right => TextAlignment::Right,
4324 };
4325
4326 let color = if text.motion_opacity < 1.0 {
4327 [
4328 text.color[0],
4329 text.color[1],
4330 text.color[2],
4331 text.color[3] * text.motion_opacity,
4332 ]
4333 } else {
4334 text.color
4335 };
4336
4337 let effective_width = if let Some(clip) = text.clip_bounds {
4338 clip[2].min(text.width)
4339 } else {
4340 text.width
4341 };
4342 let needs_wrap = text.wrap && effective_width < text.measured_width - 2.0;
4343 let wrap_width = Some(text.width);
4344 let font_name = text.font_family.name.as_deref();
4345 let generic = to_gpu_generic_font(text.font_family.generic);
4346 let font_weight = text.weight.weight();
4347
4348 let (anchor, y_pos, use_layout_height) = match text.v_align {
4349 TextVerticalAlign::Center => {
4350 (TextAnchor::Center, text.y + text.height / 2.0, false)
4351 }
4352 TextVerticalAlign::Top => (TextAnchor::Top, text.y, true),
4353 TextVerticalAlign::Baseline => {
4354 let baseline_y = text.y + text.ascender;
4355 (TextAnchor::Baseline, baseline_y, false)
4356 }
4357 };
4358 let layout_height = if use_layout_height {
4359 Some(text.height)
4360 } else {
4361 None
4362 };
4363
4364 if let Ok(mut glyphs) = self.text_ctx.prepare_text_with_style(
4365 &text.content,
4366 text.x - lx,
4367 y_pos - ly,
4368 text.font_size,
4369 color,
4370 anchor,
4371 alignment,
4372 wrap_width,
4373 needs_wrap,
4374 font_name,
4375 generic,
4376 font_weight,
4377 text.italic,
4378 layout_height,
4379 text.letter_spacing,
4380 ) {
4381 if let Some(clip) = text.clip_bounds {
4383 for glyph in &mut glyphs {
4384 glyph.clip_bounds = [clip[0] - lx, clip[1] - ly, clip[2], clip[3]];
4385 }
4386 }
4387 layer_glyphs.extend(glyphs);
4388 }
4389 }
4390
4391 if !layer_glyphs.is_empty() {
4392 self.render_text(&layer_tex.view, &layer_glyphs);
4393 }
4394 }
4395
4396 if !images.is_empty() {
4398 let mut offset_images = images.to_vec();
4399 for img in &mut offset_images {
4400 img.x -= lx;
4401 img.y -= ly;
4402 if let Some(ref mut clip) = img.clip_bounds {
4403 clip[0] -= lx;
4404 clip[1] -= ly;
4405 }
4406 if let Some(ref mut scroll) = img.scroll_clip {
4407 scroll[0] -= lx;
4408 scroll[1] -= ly;
4409 }
4410 }
4411 self.render_images(&layer_tex.view, &offset_images, lw, lh, scale_factor);
4412 }
4413
4414 if !svgs.is_empty() {
4416 let mut offset_svgs = svgs.to_vec();
4417 for svg in &mut offset_svgs {
4418 svg.x -= lx;
4419 svg.y -= ly;
4420 if let Some(ref mut clip) = svg.clip_bounds {
4421 clip[0] -= lx;
4422 clip[1] -= ly;
4423 }
4424 }
4425 self.render_rasterized_svgs(&layer_tex.view, &offset_svgs, scale_factor);
4426 }
4427
4428 self.renderer.restore_viewport();
4430
4431 self.renderer.blit_tight_texture_to_target(
4433 &layer_tex.view,
4434 (tex_w, tex_h),
4435 target,
4436 (lx, ly),
4437 (lw, lh),
4438 info.opacity,
4439 blinc_core::BlendMode::Normal,
4440 None,
4441 Some(info.transform_3d),
4442 );
4443
4444 self.renderer.release_layer_texture(layer_tex);
4445 }
4446
4447 pub fn render_overlay_tree_with_motion(
4452 &mut self,
4453 tree: &RenderTree,
4454 render_state: &blinc_layout::RenderState,
4455 width: u32,
4456 height: u32,
4457 target: &wgpu::TextureView,
4458 ) -> Result<()> {
4459 let scale_factor = tree.scale_factor();
4461
4462 let mut ctx =
4464 GpuPaintContext::with_text_context(width as f32, height as f32, &mut self.text_ctx);
4465
4466 tree.render_with_motion(&mut ctx, render_state);
4468
4469 let mut batch = ctx.take_batch();
4471
4472 let (texts, svgs, images, _flows) =
4474 self.collect_render_elements_with_state(tree, Some(render_state));
4475
4476 self.preload_images(&images, width as f32, height as f32);
4478
4479 let mut glyphs_by_layer: std::collections::BTreeMap<u32, Vec<GpuGlyph>> =
4481 std::collections::BTreeMap::new();
4482 let mut css_transformed_text_prims: Vec<GpuPrimitive> = Vec::new();
4483 for text in &texts {
4484 let alignment = match text.align {
4485 TextAlign::Left => TextAlignment::Left,
4486 TextAlign::Center => TextAlignment::Center,
4487 TextAlign::Right => TextAlignment::Right,
4488 };
4489
4490 let color = if text.motion_opacity < 1.0 {
4492 [
4493 text.color[0],
4494 text.color[1],
4495 text.color[2],
4496 text.color[3] * text.motion_opacity,
4497 ]
4498 } else {
4499 text.color
4500 };
4501
4502 let effective_width = if let Some(clip) = text.clip_bounds {
4504 clip[2].min(text.width)
4505 } else {
4506 text.width
4507 };
4508
4509 let needs_wrap = text.wrap && effective_width < text.measured_width - 2.0;
4510 let wrap_width = Some(text.width);
4511 let font_name = text.font_family.name.as_deref();
4512 let generic = to_gpu_generic_font(text.font_family.generic);
4513 let font_weight = text.weight.weight();
4514
4515 let (anchor, y_pos, use_layout_height) = match text.v_align {
4516 TextVerticalAlign::Center => {
4517 (TextAnchor::Center, text.y + text.height / 2.0, false)
4518 }
4519 TextVerticalAlign::Top => (TextAnchor::Top, text.y, true),
4520 TextVerticalAlign::Baseline => {
4521 let baseline_y = text.y + text.ascender;
4522 (TextAnchor::Baseline, baseline_y, false)
4523 }
4524 };
4525 let layout_height = if use_layout_height {
4526 Some(text.height)
4527 } else {
4528 None
4529 };
4530
4531 if let Ok(glyphs) = self.text_ctx.prepare_text_with_style(
4532 &text.content,
4533 text.x,
4534 y_pos,
4535 text.font_size,
4536 color,
4537 anchor,
4538 alignment,
4539 wrap_width,
4540 needs_wrap,
4541 font_name,
4542 generic,
4543 font_weight,
4544 text.italic,
4545 layout_height,
4546 text.letter_spacing,
4547 ) {
4548 let mut glyphs = glyphs;
4549 if let Some(clip) = text.clip_bounds {
4550 for glyph in &mut glyphs {
4551 glyph.clip_bounds = clip;
4552 }
4553 }
4554
4555 if let Some(affine) = text.css_affine {
4556 let [a, b, c, d, tx, ty] = affine;
4559 let tx_scaled = tx * scale_factor;
4560 let ty_scaled = ty * scale_factor;
4561 for glyph in &glyphs {
4562 let gc_x = glyph.bounds[0] + glyph.bounds[2] / 2.0;
4563 let gc_y = glyph.bounds[1] + glyph.bounds[3] / 2.0;
4564 let new_gc_x = a * gc_x + c * gc_y + tx_scaled;
4565 let new_gc_y = b * gc_x + d * gc_y + ty_scaled;
4566 let mut prim = GpuPrimitive::from_glyph(glyph);
4567 prim.bounds = [
4568 new_gc_x - glyph.bounds[2] / 2.0,
4569 new_gc_y - glyph.bounds[3] / 2.0,
4570 glyph.bounds[2],
4571 glyph.bounds[3],
4572 ];
4573 prim.local_affine = [a, b, c, d];
4574 prim.set_z_layer(text.z_index);
4575 css_transformed_text_prims.push(prim);
4576 }
4577 } else {
4578 glyphs_by_layer
4579 .entry(text.z_index)
4580 .or_default()
4581 .extend(glyphs);
4582 }
4583 }
4584 }
4585
4586 self.renderer.resize(width, height);
4590
4591 if !css_transformed_text_prims.is_empty() {
4594 if let (Some(atlas), Some(color_atlas)) =
4595 (self.text_ctx.atlas_view(), self.text_ctx.color_atlas_view())
4596 {
4597 batch.primitives.append(&mut css_transformed_text_prims);
4598 self.renderer.set_glyph_atlas(atlas, color_atlas);
4599 }
4600 }
4601
4602 let max_z = batch.max_z_layer();
4605 let max_text_z = glyphs_by_layer.keys().cloned().max().unwrap_or(0);
4606 let max_layer = max_z.max(max_text_z);
4607
4608 tracing::trace!(
4609 "render_overlay_tree: {} primitives, {} text layers, max_layer={}",
4610 batch.primitives.len(),
4611 glyphs_by_layer.len(),
4612 max_layer
4613 );
4614
4615 for z in 0..=max_layer {
4617 let layer_primitives = batch.primitives_for_layer(z);
4618 if !layer_primitives.is_empty() {
4619 tracing::trace!(
4620 "render_overlay_tree: rendering {} primitives at z={}",
4621 layer_primitives.len(),
4622 z
4623 );
4624 self.renderer
4625 .render_primitives_overlay(target, &layer_primitives);
4626 }
4627
4628 if let Some(glyphs) = glyphs_by_layer.get(&z) {
4629 if !glyphs.is_empty() {
4630 tracing::trace!(
4631 "render_overlay_tree: rendering {} glyphs at z={}",
4632 glyphs.len(),
4633 z
4634 );
4635 self.render_text(target, glyphs);
4636 }
4637 }
4638 }
4639
4640 self.render_images(target, &images, width as f32, height as f32, scale_factor);
4642
4643 if !batch.foreground_primitives.is_empty() {
4645 self.renderer
4646 .render_primitives_overlay(target, &batch.foreground_primitives);
4647 }
4648
4649 self.renderer.poll();
4651
4652 let debug = DebugMode::from_env();
4654 if debug.layout {
4655 let scale = tree.scale_factor();
4656 self.render_layout_debug(target, tree, scale);
4657 }
4658 if debug.motion {
4659 self.render_motion_debug(target, tree, width, height);
4660 }
4661
4662 self.return_scratch_elements(texts, svgs, images);
4664
4665 Ok(())
4666 }
4667
4668 fn render_overlays(
4670 &mut self,
4671 render_state: &blinc_layout::RenderState,
4672 width: u32,
4673 height: u32,
4674 target: &wgpu::TextureView,
4675 ) {
4676 let overlays = render_state.overlays();
4677 if overlays.is_empty() {
4678 return;
4679 }
4680
4681 let mut overlay_ctx = GpuPaintContext::new(width as f32, height as f32);
4683
4684 for overlay in overlays {
4685 match overlay {
4686 Overlay::Cursor {
4687 position,
4688 size,
4689 color,
4690 opacity,
4691 } => {
4692 if *opacity > 0.0 {
4693 let cursor_color =
4695 Color::rgba(color.r, color.g, color.b, color.a * opacity);
4696 overlay_ctx.execute_command(&DrawCommand::FillRect {
4697 rect: Rect::new(position.0, position.1, size.0, size.1),
4698 corner_radius: CornerRadius::default(),
4699 brush: Brush::Solid(cursor_color),
4700 });
4701 }
4702 }
4703 Overlay::Selection { rects: _, color: _ } => {
4704 }
4707 Overlay::FocusRing {
4708 position,
4709 size,
4710 radius,
4711 color,
4712 thickness,
4713 } => {
4714 overlay_ctx.execute_command(&DrawCommand::StrokeRect {
4715 rect: Rect::new(position.0, position.1, size.0, size.1),
4716 corner_radius: CornerRadius::uniform(*radius),
4717 stroke: Stroke::new(*thickness),
4718 brush: Brush::Solid(*color),
4719 });
4720 }
4721 }
4722 }
4723
4724 let overlay_batch = overlay_ctx.take_batch();
4726 if !overlay_batch.is_empty() {
4727 self.renderer.render_overlay(target, &overlay_batch);
4728 }
4729 }
4730}
4731
4732fn to_gpu_generic_font(generic: GenericFont) -> GpuGenericFont {
4734 match generic {
4735 GenericFont::System => GpuGenericFont::System,
4736 GenericFont::Monospace => GpuGenericFont::Monospace,
4737 GenericFont::Serif => GpuGenericFont::Serif,
4738 GenericFont::SansSerif => GpuGenericFont::SansSerif,
4739 }
4740}
4741
4742#[derive(Clone, Copy)]
4750pub struct DebugMode {
4751 pub text: bool,
4753 pub layout: bool,
4755 pub motion: bool,
4757}
4758
4759impl DebugMode {
4760 pub fn from_env() -> Self {
4762 let debug_value = std::env::var("BLINC_DEBUG")
4763 .map(|v| v.to_lowercase())
4764 .unwrap_or_default();
4765
4766 let all = debug_value == "all" || debug_value == "1" || debug_value == "true";
4767 let text = all || debug_value == "text";
4768 let layout = all || debug_value == "layout";
4769 let motion = all || debug_value == "motion";
4770
4771 Self {
4772 text,
4773 layout,
4774 motion,
4775 }
4776 }
4777
4778 pub fn any_enabled(&self) -> bool {
4780 self.text || self.layout || self.motion
4781 }
4782}
4783
4784fn generate_text_decoration_primitives_by_layer(
4792 texts: &[TextElement],
4793) -> std::collections::HashMap<u32, Vec<GpuPrimitive>> {
4794 let mut primitives_by_layer: std::collections::HashMap<u32, Vec<GpuPrimitive>> =
4795 std::collections::HashMap::new();
4796
4797 for text in texts {
4798 if !text.strikethrough && !text.underline {
4799 continue;
4800 }
4801
4802 let decoration_width = if text.wrap && text.measured_width > text.width {
4804 text.width
4805 } else {
4806 text.measured_width.min(text.width)
4807 };
4808
4809 if decoration_width <= 0.0 {
4811 continue;
4812 }
4813
4814 let line_thickness = text
4816 .decoration_thickness
4817 .unwrap_or_else(|| (text.font_size / 14.0).clamp(1.0, 3.0));
4818
4819 let dec_color = text.decoration_color.unwrap_or(text.color);
4821
4822 let layer_primitives = primitives_by_layer.entry(text.z_index).or_default();
4823
4824 let descender_approx = -text.ascender * 0.2;
4830 let glyph_extent = text.ascender - descender_approx;
4831
4832 let baseline_y = match text.v_align {
4833 TextVerticalAlign::Center => {
4834 let glyph_top = text.y + text.height / 2.0 - glyph_extent / 2.0;
4838 glyph_top + text.ascender
4839 }
4840 TextVerticalAlign::Top => {
4841 let glyph_top = text.y + (text.height - glyph_extent) / 2.0;
4845 glyph_top + text.ascender
4846 }
4847 TextVerticalAlign::Baseline => {
4848 text.y + text.ascender
4852 }
4853 };
4854
4855 if text.strikethrough {
4857 let strikethrough_y = baseline_y - text.ascender * 0.35;
4859 let mut strike_rect = GpuPrimitive::rect(
4860 text.x,
4861 strikethrough_y - line_thickness / 2.0,
4862 decoration_width,
4863 line_thickness,
4864 )
4865 .with_color(dec_color[0], dec_color[1], dec_color[2], dec_color[3]);
4866
4867 if let Some(clip) = text.clip_bounds {
4869 strike_rect = strike_rect.with_clip_rect(clip[0], clip[1], clip[2], clip[3]);
4870 }
4871 layer_primitives.push(strike_rect);
4872 }
4873
4874 if text.underline {
4876 let underline_y = baseline_y + text.ascender * 0.05;
4878 let mut underline_rect = GpuPrimitive::rect(
4879 text.x,
4880 underline_y - line_thickness / 2.0,
4881 decoration_width,
4882 line_thickness,
4883 )
4884 .with_color(dec_color[0], dec_color[1], dec_color[2], dec_color[3]);
4885
4886 if let Some(clip) = text.clip_bounds {
4888 underline_rect = underline_rect.with_clip_rect(clip[0], clip[1], clip[2], clip[3]);
4889 }
4890 layer_primitives.push(underline_rect);
4891 }
4892 }
4893
4894 primitives_by_layer
4895}
4896
4897fn generate_text_debug_primitives(texts: &[TextElement]) -> Vec<GpuPrimitive> {
4905 let mut primitives = Vec::new();
4906
4907 for text in texts {
4908 let debug_width = if text.wrap && text.measured_width > text.width {
4912 text.width
4914 } else {
4915 text.measured_width.min(text.width)
4917 };
4918
4919 let bbox = GpuPrimitive::rect(text.x, text.y, debug_width, text.height)
4921 .with_color(0.0, 0.0, 0.0, 0.0) .with_border(1.0, 0.0, 1.0, 1.0, 0.7); primitives.push(bbox);
4924
4925 let baseline_y = text.y + text.ascender;
4928 let baseline = GpuPrimitive::rect(text.x, baseline_y - 0.5, debug_width, 1.0)
4929 .with_color(1.0, 0.0, 1.0, 0.6); primitives.push(baseline);
4931
4932 let ascender_line = GpuPrimitive::rect(text.x, text.y - 0.5, debug_width, 1.0)
4935 .with_color(0.0, 1.0, 0.0, 0.4); primitives.push(ascender_line);
4937
4938 let descender_y = text.y + text.height;
4940 let descender_line = GpuPrimitive::rect(text.x, descender_y - 0.5, debug_width, 1.0)
4941 .with_color(1.0, 1.0, 0.0, 0.4); primitives.push(descender_line);
4943 }
4944
4945 primitives
4946}
4947
4948fn collect_debug_bounds(tree: &RenderTree, scale: f32) -> Vec<DebugBoundsElement> {
4950 let mut bounds = Vec::new();
4951
4952 if let Some(root) = tree.root() {
4953 collect_debug_bounds_recursive(tree, root, (0.0, 0.0), 0, scale, &mut bounds);
4954 }
4955
4956 bounds
4957}
4958
4959fn collect_debug_bounds_recursive(
4961 tree: &RenderTree,
4962 node: LayoutNodeId,
4963 parent_offset: (f32, f32),
4964 depth: u32,
4965 scale: f32,
4966 bounds: &mut Vec<DebugBoundsElement>,
4967) {
4968 use blinc_layout::renderer::ElementType;
4969
4970 let Some(node_bounds) = tree.layout().get_bounds(node, parent_offset) else {
4971 return;
4972 };
4973
4974 let element_type = tree
4976 .get_render_node(node)
4977 .map(|n| match &n.element_type {
4978 ElementType::Div => "Div".to_string(),
4979 ElementType::Text(_) => "Text".to_string(),
4980 ElementType::StyledText(_) => "StyledText".to_string(),
4981 ElementType::Image(_) => "Image".to_string(),
4982 ElementType::Svg(_) => "Svg".to_string(),
4983 ElementType::Canvas(_) => "Canvas".to_string(),
4984 })
4985 .unwrap_or_else(|| "Unknown".to_string());
4986
4987 bounds.push(DebugBoundsElement {
4989 x: node_bounds.x * scale,
4990 y: node_bounds.y * scale,
4991 width: node_bounds.width * scale,
4992 height: node_bounds.height * scale,
4993 element_type,
4994 depth,
4995 });
4996
4997 let scroll_offset = tree.get_scroll_offset(node);
4999
5000 let new_offset = (
5002 node_bounds.x + scroll_offset.0,
5003 node_bounds.y + scroll_offset.1,
5004 );
5005
5006 for child in tree.layout().children(node) {
5008 collect_debug_bounds_recursive(tree, child, new_offset, depth + 1, scale, bounds);
5009 }
5010}
5011
5012fn generate_layout_debug_primitives(bounds: &[DebugBoundsElement]) -> Vec<GpuPrimitive> {
5018 let mut primitives = Vec::new();
5019
5020 let colors: [(f32, f32, f32); 6] = [
5022 (1.0, 0.3, 0.3), (0.3, 1.0, 0.3), (0.3, 0.3, 1.0), (1.0, 1.0, 0.3), (0.3, 1.0, 1.0), (1.0, 0.3, 1.0), ];
5029
5030 for elem in bounds {
5031 if elem.width < 1.0 || elem.height < 1.0 {
5033 continue;
5034 }
5035
5036 let (r, g, b) = colors[(elem.depth as usize) % colors.len()];
5037 let alpha = 0.5; let rect = GpuPrimitive::rect(elem.x, elem.y, elem.width, elem.height)
5041 .with_color(0.0, 0.0, 0.0, 0.0) .with_border(1.0, r, g, b, alpha); primitives.push(rect);
5045 }
5046
5047 primitives
5048}
5049
5050fn scale_and_translate_path(
5052 path: &blinc_core::Path,
5053 x: f32,
5054 y: f32,
5055 scale: f32,
5056) -> blinc_core::Path {
5057 use blinc_core::{PathCommand, Point, Vec2};
5058
5059 if scale == 1.0 && x == 0.0 && y == 0.0 {
5060 return path.clone();
5061 }
5062
5063 let transform_point = |p: Point| -> Point { Point::new(p.x * scale + x, p.y * scale + y) };
5064
5065 let new_commands: Vec<PathCommand> = path
5066 .commands()
5067 .iter()
5068 .map(|cmd| match cmd {
5069 PathCommand::MoveTo(p) => PathCommand::MoveTo(transform_point(*p)),
5070 PathCommand::LineTo(p) => PathCommand::LineTo(transform_point(*p)),
5071 PathCommand::QuadTo { control, end } => PathCommand::QuadTo {
5072 control: transform_point(*control),
5073 end: transform_point(*end),
5074 },
5075 PathCommand::CubicTo {
5076 control1,
5077 control2,
5078 end,
5079 } => PathCommand::CubicTo {
5080 control1: transform_point(*control1),
5081 control2: transform_point(*control2),
5082 end: transform_point(*end),
5083 },
5084 PathCommand::ArcTo {
5085 radii,
5086 rotation,
5087 large_arc,
5088 sweep,
5089 end,
5090 } => PathCommand::ArcTo {
5091 radii: Vec2::new(radii.x * scale, radii.y * scale),
5092 rotation: *rotation,
5093 large_arc: *large_arc,
5094 sweep: *sweep,
5095 end: transform_point(*end),
5096 },
5097 PathCommand::Close => PathCommand::Close,
5098 })
5099 .collect();
5100
5101 blinc_core::Path::from_commands(new_commands)
5102}