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, PendingMesh, 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 = 256;
34
35const SVG_CACHE_CAPACITY: usize = 128;
37
38fn intersect_clip_rects(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
40 let x1 = a[0].max(b[0]);
41 let y1 = a[1].max(b[1]);
42 let x2 = (a[0] + a[2]).min(b[0] + b[2]);
43 let y2 = (a[1] + a[3]).min(b[1] + b[3]);
44 [x1, y1, (x2 - x1).max(0.0), (y2 - y1).max(0.0)]
45}
46
47fn merge_scroll_clip(new_clip: [f32; 4], existing: Option<[f32; 4]>) -> Option<[f32; 4]> {
49 match existing {
50 Some(ex) => Some(intersect_clip_rects(new_clip, ex)),
51 None => Some(new_clip),
52 }
53}
54
55fn effective_single_clip(primary: Option<[f32; 4]>, scroll: Option<[f32; 4]>) -> Option<[f32; 4]> {
58 match (primary, scroll) {
59 (Some(c), Some(s)) => Some(intersect_clip_rects(c, s)),
60 (c, s) => c.or(s),
61 }
62}
63
64pub struct RenderContext {
68 renderer: GpuRenderer,
69 pub(crate) text_ctx: TextRenderingContext,
70 image_ctx: ImageRenderingContext,
71 device: Arc<wgpu::Device>,
72 queue: Arc<wgpu::Queue>,
73 sample_count: u32,
74 backdrop_texture: Option<CachedTexture>,
76 msaa_texture: Option<CachedTexture>,
78 image_cache: LruCache<String, GpuImage>,
80 image_load_times: std::collections::HashMap<String, web_time::Instant>,
82 svg_cache: LruCache<u64, SvgDocument>,
84 svg_atlas: SvgAtlas,
86 scratch_glyphs: Vec<GpuGlyph>,
88 scratch_texts: Vec<TextElement>,
89 scratch_svgs: Vec<SvgElement>,
90 scratch_images: Vec<ImageElement>,
91 cursor_pos: [f32; 2],
93 has_active_flows: bool,
95 frame_count: u64,
97}
98
99struct CachedTexture {
100 texture: wgpu::Texture,
101 view: wgpu::TextureView,
102 width: u32,
103 height: u32,
104}
105
106#[derive(Clone, Debug)]
110struct Transform3DLayerInfo {
111 node_id: LayoutNodeId,
113 layer_bounds: [f32; 4],
115 transform_3d: blinc_core::Transform3DParams,
117 opacity: f32,
119}
120
121#[derive(Clone)]
123struct TextElement {
124 content: String,
125 x: f32,
126 y: f32,
127 width: f32,
128 height: f32,
129 font_size: f32,
130 color: [f32; 4],
131 align: TextAlign,
132 weight: FontWeight,
133 italic: bool,
135 v_align: TextVerticalAlign,
137 clip_bounds: Option<[f32; 4]>,
139 motion_opacity: f32,
141 wrap: bool,
143 line_height: f32,
145 measured_width: f32,
147 font_family: FontFamily,
149 word_spacing: f32,
151 letter_spacing: f32,
153 z_index: u32,
155 ascender: f32,
157 strikethrough: bool,
159 underline: bool,
161 decoration_color: Option<[f32; 4]>,
163 decoration_thickness: Option<f32>,
165 css_affine: Option<[f32; 6]>,
168 text_shadow: Option<blinc_core::Shadow>,
170 transform_3d_layer: Option<Transform3DLayerInfo>,
172 is_foreground: bool,
174}
175
176#[derive(Clone)]
178struct ImageElement {
179 source: String,
180 x: f32,
181 y: f32,
182 width: f32,
183 height: f32,
184 object_fit: u8,
185 object_position: [f32; 2],
186 opacity: f32,
187 border_radius: f32,
188 tint: [f32; 4],
189 clip_bounds: Option<[f32; 4]>,
191 clip_radius: [f32; 4],
193 layer: RenderLayer,
195 loading_strategy: u8,
197 placeholder_type: u8,
199 placeholder_color: [f32; 4],
201 placeholder_image: Option<String>,
203 fade_duration_ms: u32,
205 z_index: u32,
207 border_width: f32,
209 border_color: blinc_core::Color,
211 css_affine: Option<[f32; 6]>,
213 shadow: Option<blinc_core::Shadow>,
215 filter_a: [f32; 4],
217 filter_b: [f32; 4],
219 scroll_clip: Option<[f32; 4]>,
223 mask_params: [f32; 4],
225 mask_info: [f32; 4],
227 transform_3d_layer: Option<Transform3DLayerInfo>,
229}
230
231#[derive(Clone)]
233struct SvgElement {
234 source: Arc<str>,
235 x: f32,
236 y: f32,
237 width: f32,
238 height: f32,
239 tint: Option<blinc_core::Color>,
241 fill: Option<blinc_core::Color>,
243 stroke: Option<blinc_core::Color>,
245 stroke_width: Option<f32>,
247 stroke_dasharray: Option<Vec<f32>>,
249 stroke_dashoffset: Option<f32>,
251 svg_path_data: Option<String>,
253 clip_bounds: Option<[f32; 4]>,
255 motion_opacity: f32,
257 css_affine: Option<[f32; 6]>,
260 tag_overrides: std::collections::HashMap<String, blinc_layout::element::SvgTagStyle>,
262 transform_3d_layer: Option<Transform3DLayerInfo>,
264}
265
266#[derive(Clone)]
268struct FlowElement {
269 flow_name: String,
271 flow_graph: Option<std::sync::Arc<blinc_core::FlowGraph>>,
273 x: f32,
275 y: f32,
276 width: f32,
277 height: f32,
278 z_index: u32,
280 corner_radius: f32,
282}
283
284#[derive(Clone)]
286struct DebugBoundsElement {
287 x: f32,
288 y: f32,
289 width: f32,
290 height: f32,
291 element_type: String,
293 depth: u32,
295}
296
297impl RenderContext {
298 pub(crate) fn new(
300 renderer: GpuRenderer,
301 text_ctx: TextRenderingContext,
302 device: Arc<wgpu::Device>,
303 queue: Arc<wgpu::Queue>,
304 sample_count: u32,
305 ) -> Self {
306 let image_ctx = ImageRenderingContext::new(device.clone(), queue.clone());
307 let svg_atlas = SvgAtlas::new(&device);
308 Self {
309 renderer,
310 text_ctx,
311 image_ctx,
312 device,
313 queue,
314 sample_count,
315 backdrop_texture: None,
316 msaa_texture: None,
317 image_cache: LruCache::new(NonZeroUsize::new(IMAGE_CACHE_CAPACITY).unwrap()),
318 image_load_times: std::collections::HashMap::new(),
319 svg_cache: LruCache::new(NonZeroUsize::new(SVG_CACHE_CAPACITY).unwrap()),
320 svg_atlas,
321 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],
326 has_active_flows: false,
327 frame_count: 0,
328 }
329 }
330
331 pub fn register_custom_pass(
339 &mut self,
340 pass: Box<dyn blinc_gpu::custom_pass::CustomRenderPass>,
341 ) {
342 self.renderer.register_custom_pass(pass);
343 }
344
345 pub fn set_cursor_position(&mut self, x: f32, y: f32) {
346 self.cursor_pos = [x, y];
347 }
348
349 pub fn has_active_flows(&self) -> bool {
352 self.has_active_flows
353 }
354
355 pub fn set_blend_target(&mut self, texture: &wgpu::Texture) {
358 self.renderer.set_blend_target(texture);
359 }
360
361 pub fn clear_blend_target(&mut self) {
363 self.renderer.clear_blend_target();
364 }
365
366 pub fn load_font_data_to_registry(&mut self, data: Vec<u8>) -> usize {
371 self.text_ctx.load_font_data_to_registry(data)
372 }
373
374 pub fn render_tree(
378 &mut self,
379 tree: &RenderTree,
380 width: u32,
381 height: u32,
382 target: &wgpu::TextureView,
383 ) -> Result<()> {
384 let scale_factor = tree.scale_factor();
386
387 let mut bg_ctx =
389 GpuPaintContext::with_text_context(width as f32, height as f32, &mut self.text_ctx);
390
391 tree.render_to_layer(&mut bg_ctx, RenderLayer::Background);
393 tree.render_to_layer(&mut bg_ctx, RenderLayer::Glass);
394
395 let mut bg_batch = bg_ctx.take_batch();
397
398 let mut fg_ctx =
400 GpuPaintContext::with_text_context(width as f32, height as f32, &mut self.text_ctx);
401 tree.render_to_layer(&mut fg_ctx, RenderLayer::Foreground);
402
403 let mut fg_batch = fg_ctx.take_batch();
405
406 let (texts, svgs, images, _flows) = self.collect_render_elements(tree);
408
409 self.preload_images(&images, width as f32, height as f32);
411
412 let mut all_glyphs = Vec::new();
414 let mut css_transformed_text_prims: Vec<GpuPrimitive> = Vec::new();
415 for text in &texts {
416 let alignment = match text.align {
418 TextAlign::Left => TextAlignment::Left,
419 TextAlign::Center => TextAlignment::Center,
420 TextAlign::Right => TextAlignment::Right,
421 };
422
423 let (anchor, y_pos, use_layout_height) = match text.v_align {
432 TextVerticalAlign::Center => {
433 (TextAnchor::Center, text.y + text.height / 2.0, false)
434 }
435 TextVerticalAlign::Top => (TextAnchor::Top, text.y, true),
436 TextVerticalAlign::Baseline => {
437 let baseline_y = text.y + text.ascender;
440 (TextAnchor::Baseline, baseline_y, false)
441 }
442 };
443
444 let wrap_width = if text.wrap {
447 if let Some(clip) = text.clip_bounds {
448 clip[2].min(text.width)
450 } else {
451 text.width
452 }
453 } else {
454 text.width
455 };
456
457 let font_name = text.font_family.name.as_deref();
459 let generic = to_gpu_generic_font(text.font_family.generic);
460 let font_weight = text.weight.weight();
461
462 let layout_height = if use_layout_height {
464 Some(text.height)
465 } else {
466 None
467 };
468
469 match self.text_ctx.prepare_text_with_style(
470 &text.content,
471 text.x,
472 y_pos,
473 text.font_size,
474 text.color,
475 anchor,
476 alignment,
477 Some(wrap_width),
478 text.wrap,
479 font_name,
480 generic,
481 font_weight,
482 text.italic,
483 layout_height,
484 text.letter_spacing,
485 ) {
486 Ok(mut glyphs) => {
487 tracing::trace!(
488 "Prepared {} glyphs for text '{}' (font={:?}, generic={:?})",
489 glyphs.len(),
490 text.content,
491 font_name,
492 generic
493 );
494 if let Some(clip) = text.clip_bounds {
496 for glyph in &mut glyphs {
497 glyph.clip_bounds = clip;
498 }
499 }
500
501 if let Some(affine) = text.css_affine {
502 let [a, b, c, d, tx, ty] = affine;
504 let tx_scaled = tx * scale_factor;
505 let ty_scaled = ty * scale_factor;
506 for glyph in &glyphs {
507 let gc_x = glyph.bounds[0] + glyph.bounds[2] / 2.0;
508 let gc_y = glyph.bounds[1] + glyph.bounds[3] / 2.0;
509 let new_gc_x = a * gc_x + c * gc_y + tx_scaled;
510 let new_gc_y = b * gc_x + d * gc_y + ty_scaled;
511 let mut prim = GpuPrimitive::from_glyph(glyph);
512 prim.bounds = [
513 new_gc_x - glyph.bounds[2] / 2.0,
514 new_gc_y - glyph.bounds[3] / 2.0,
515 glyph.bounds[2],
516 glyph.bounds[3],
517 ];
518 prim.local_affine = [a, b, c, d];
519 css_transformed_text_prims.push(prim);
520 }
521 } else {
522 all_glyphs.extend(glyphs);
523 }
524 }
525 Err(e) => {
526 tracing::warn!("Failed to prepare text '{}': {:?}", text.content, e);
527 }
528 }
529 }
530
531 tracing::trace!(
532 "Text rendering: {} texts collected, {} total glyphs prepared",
533 texts.len(),
534 all_glyphs.len()
535 );
536
537 self.renderer.resize(width, height);
541
542 if !css_transformed_text_prims.is_empty() {
545 if let (Some(atlas), Some(color_atlas)) =
546 (self.text_ctx.atlas_view(), self.text_ctx.color_atlas_view())
547 {
548 bg_batch.primitives.append(&mut css_transformed_text_prims);
549 self.renderer.set_glyph_atlas(atlas, color_atlas);
550 }
551 }
552
553 let has_glass = bg_batch.glass_count() > 0;
554
555 if has_glass {
557 self.ensure_glass_textures(width, height);
558 }
559 let use_msaa_overlay = self.sample_count > 1;
560
561 if has_glass {
565 let (bg_images, fg_images): (Vec<_>, Vec<_>) = images
568 .iter()
569 .partition(|img| img.layer == RenderLayer::Background);
570
571 let has_bg_images = !bg_images.is_empty();
573 if has_bg_images {
574 let backdrop_tex = self.backdrop_texture.take().unwrap();
576 self.renderer
577 .clear_target(&backdrop_tex.view, wgpu::Color::TRANSPARENT);
578 self.renderer.clear_target(target, wgpu::Color::BLACK);
579 self.render_images_ref(&backdrop_tex.view, &bg_images);
580 self.render_images_ref(target, &bg_images);
581 self.backdrop_texture = Some(backdrop_tex);
582 }
583
584 {
587 let backdrop = self.backdrop_texture.as_ref().unwrap();
588 self.renderer.render_glass_frame(
589 target,
590 &backdrop.view,
591 (backdrop.width, backdrop.height),
592 &bg_batch,
593 has_bg_images,
594 );
595 }
596
597 if use_msaa_overlay && bg_batch.has_paths() {
600 self.renderer
601 .render_paths_overlay_msaa(target, &bg_batch, self.sample_count);
602 }
603
604 if !has_bg_images {
606 self.render_images_ref(target, &bg_images);
607 }
608
609 self.render_images_ref(target, &fg_images);
611
612 if !bg_batch.dynamic_images.is_empty() {
614 self.renderer
615 .render_dynamic_images(target, &bg_batch.dynamic_images);
616 }
617 if !fg_batch.dynamic_images.is_empty() {
618 self.renderer
619 .render_dynamic_images(target, &fg_batch.dynamic_images);
620 }
621
622 let has_layer_effects = fg_batch.has_layer_effects();
625 if has_layer_effects {
626 fg_batch.convert_glyphs_to_primitives();
628 if !fg_batch.is_empty() {
629 self.preload_mask_images(&fg_batch);
631 self.renderer.render_overlay(target, &fg_batch);
632 }
633 if !svgs.is_empty() {
635 self.render_rasterized_svgs(target, &svgs, scale_factor);
636 }
637 } else if self.renderer.unified_text_rendering() {
638 let mut unified_primitives = fg_batch.get_unified_foreground_primitives();
644 for glyph in &all_glyphs {
645 unified_primitives.push(GpuPrimitive::from_glyph(glyph));
646 }
647 if !unified_primitives.is_empty() {
648 self.render_unified(target, &unified_primitives);
649 }
650
651 if use_msaa_overlay && fg_batch.has_paths() {
653 self.renderer
654 .render_paths_overlay_msaa(target, &fg_batch, self.sample_count);
655 }
656
657 if !svgs.is_empty() {
659 self.render_rasterized_svgs(target, &svgs, scale_factor);
660 }
661 } else {
662 if !fg_batch.is_empty() {
664 if use_msaa_overlay {
665 self.renderer
666 .render_overlay_msaa(target, &fg_batch, self.sample_count);
667 } else {
668 self.renderer.render_overlay(target, &fg_batch);
669 }
670 }
671
672 if !all_glyphs.is_empty() {
674 self.render_text(target, &all_glyphs);
675 }
676
677 if !svgs.is_empty() {
679 self.render_rasterized_svgs(target, &svgs, scale_factor);
680 }
681 }
682
683 let decorations_by_layer = generate_text_decoration_primitives_by_layer(&texts);
685 for primitives in decorations_by_layer.values() {
686 if !primitives.is_empty() {
687 self.render_unified(target, primitives);
688 }
689 }
690 } else {
691 self.renderer
698 .render_with_clear(target, &bg_batch, [0.0, 0.0, 0.0, 1.0]);
699
700 if use_msaa_overlay && bg_batch.has_paths() {
702 self.renderer
703 .render_paths_overlay_msaa(target, &bg_batch, self.sample_count);
704 }
705
706 self.render_images(target, &images, width as f32, height as f32, scale_factor);
708
709 let has_layer_effects = fg_batch.has_layer_effects();
713 if has_layer_effects {
714 fg_batch.convert_glyphs_to_primitives();
717
718 if !fg_batch.is_empty() {
720 self.renderer.render_overlay(target, &fg_batch);
721 }
722 if !svgs.is_empty() {
724 self.render_rasterized_svgs(target, &svgs, scale_factor);
725 }
726 } else if self.renderer.unified_text_rendering() {
727 let mut unified_primitives = fg_batch.get_unified_foreground_primitives();
740 for glyph in &all_glyphs {
741 unified_primitives.push(GpuPrimitive::from_glyph(glyph));
742 }
743 if !unified_primitives.is_empty() {
744 self.render_unified(target, &unified_primitives);
745 }
746
747 if use_msaa_overlay && fg_batch.has_paths() {
749 self.renderer
750 .render_paths_overlay_msaa(target, &fg_batch, self.sample_count);
751 }
752
753 if !svgs.is_empty() {
755 self.render_rasterized_svgs(target, &svgs, scale_factor);
756 }
757 } else {
758 if !fg_batch.is_empty() {
760 if use_msaa_overlay {
761 self.renderer
762 .render_overlay_msaa(target, &fg_batch, self.sample_count);
763 } else {
764 self.renderer.render_overlay(target, &fg_batch);
765 }
766 }
767
768 if !all_glyphs.is_empty() {
770 self.render_text(target, &all_glyphs);
771 }
772
773 if !svgs.is_empty() {
775 self.render_rasterized_svgs(target, &svgs, scale_factor);
776 }
777 }
778
779 let decorations_by_layer = generate_text_decoration_primitives_by_layer(&texts);
781 for primitives in decorations_by_layer.values() {
782 if !primitives.is_empty() {
783 self.render_unified(target, primitives);
784 }
785 }
786 }
787
788 self.return_scratch_elements(texts, svgs, images);
790
791 self.renderer.poll();
793
794 Ok(())
795 }
796
797 #[inline]
799 fn return_scratch_elements(
800 &mut self,
801 mut texts: Vec<TextElement>,
802 mut svgs: Vec<SvgElement>,
803 mut images: Vec<ImageElement>,
804 ) {
805 texts.clear();
807 svgs.clear();
808 images.clear();
809 self.scratch_texts = texts;
810 self.scratch_svgs = svgs;
811 self.scratch_images = images;
812 }
813
814 fn log_cache_stats(&mut self) {
817 self.frame_count += 1;
818 if self.frame_count % 300 != 1 {
819 return;
820 }
821 let (aw, ah) = self.text_ctx.atlas_dimensions();
822 let (caw, cah) = self.text_ctx.color_atlas_dimensions();
823 let atlas_glyphs = self.text_ctx.atlas().glyph_count();
824 let atlas_util = self.text_ctx.atlas().utilization();
825 let color_glyphs = self.text_ctx.color_atlas().glyph_count();
826 let color_util = self.text_ctx.color_atlas().utilization();
827 let glyph_cache = self.text_ctx.glyph_cache_len();
828 let glyph_cap = self.text_ctx.glyph_cache_capacity();
829 let color_cache = self.text_ctx.color_glyph_cache_len();
830 let color_cap = self.text_ctx.color_glyph_cache_capacity();
831 let img_cache = self.image_cache.len();
832 let svg_cache = self.svg_cache.len();
833 let svg_atlas_entries = self.svg_atlas.entry_count();
834 let svg_atlas_util = self.svg_atlas.utilization();
835 let (svg_aw, svg_ah) = (self.svg_atlas.width(), self.svg_atlas.height());
836
837 tracing::info!(
838 "Cache stats [frame {}]: \
839 atlas={}x{} ({} glyphs, {:.1}% used), \
840 color_atlas={}x{} ({} glyphs, {:.1}% used), \
841 glyph_lru={}/{}, color_glyph_lru={}/{}, \
842 image={}/{}, svg_doc={}/{}, svg_atlas={}x{} ({} entries, {:.1}% used)",
843 self.frame_count,
844 aw,
845 ah,
846 atlas_glyphs,
847 atlas_util * 100.0,
848 caw,
849 cah,
850 color_glyphs,
851 color_util * 100.0,
852 glyph_cache,
853 glyph_cap,
854 color_cache,
855 color_cap,
856 img_cache,
857 IMAGE_CACHE_CAPACITY,
858 svg_cache,
859 SVG_CACHE_CAPACITY,
860 svg_aw,
861 svg_ah,
862 svg_atlas_entries,
863 svg_atlas_util * 100.0,
864 );
865 }
866
867 fn ensure_glass_textures(&mut self, width: u32, height: u32) {
873 let format = self.renderer.texture_format();
875
876 let backdrop_width = (width / 2).max(1);
879 let backdrop_height = (height / 2).max(1);
880
881 let needs_backdrop = self
882 .backdrop_texture
883 .as_ref()
884 .map(|t| t.width != backdrop_width || t.height != backdrop_height)
885 .unwrap_or(true);
886
887 if needs_backdrop {
888 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
890 label: Some("Glass Backdrop"),
891 size: wgpu::Extent3d {
892 width: backdrop_width,
893 height: backdrop_height,
894 depth_or_array_layers: 1,
895 },
896 mip_level_count: 1,
897 sample_count: 1,
898 dimension: wgpu::TextureDimension::D2,
899 format,
900 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
901 | wgpu::TextureUsages::TEXTURE_BINDING,
902 view_formats: &[],
903 });
904 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
905 self.backdrop_texture = Some(CachedTexture {
906 texture,
907 view,
908 width: backdrop_width,
909 height: backdrop_height,
910 });
911 }
912 }
913
914 fn render_text(&mut self, target: &wgpu::TextureView, glyphs: &[GpuGlyph]) {
916 if let (Some(atlas_view), Some(color_atlas_view)) =
917 (self.text_ctx.atlas_view(), self.text_ctx.color_atlas_view())
918 {
919 self.renderer.render_text(
920 target,
921 glyphs,
922 atlas_view,
923 color_atlas_view,
924 self.text_ctx.sampler(),
925 );
926 }
927 }
928
929 fn render_unified(&mut self, target: &wgpu::TextureView, primitives: &[GpuPrimitive]) {
950 if primitives.is_empty() {
951 return;
952 }
953
954 if let (Some(atlas_view), Some(color_atlas_view)) =
955 (self.text_ctx.atlas_view(), self.text_ctx.color_atlas_view())
956 {
957 self.renderer.render_primitives_overlay_with_glyphs(
958 target,
959 primitives,
960 atlas_view,
961 color_atlas_view,
962 );
963 } else {
964 self.renderer.render_primitives_overlay(target, primitives);
969 }
970 }
971
972 fn render_text_decorations_for_layer(
974 &mut self,
975 target: &wgpu::TextureView,
976 decorations_by_layer: &std::collections::HashMap<u32, Vec<GpuPrimitive>>,
977 z_layer: u32,
978 ) {
979 if let Some(primitives) = decorations_by_layer.get(&z_layer) {
980 if !primitives.is_empty() {
981 self.renderer.render_primitives_overlay(target, primitives);
982 }
983 }
984 }
985
986 fn render_text_debug(&mut self, target: &wgpu::TextureView, texts: &[TextElement]) {
994 let debug_primitives = generate_text_debug_primitives(texts);
995 if !debug_primitives.is_empty() {
996 self.renderer
997 .render_primitives_overlay(target, &debug_primitives);
998 }
999 }
1000
1001 fn render_layout_debug(&mut self, target: &wgpu::TextureView, tree: &RenderTree, scale: f32) {
1007 let debug_bounds = collect_debug_bounds(tree, scale);
1008 let debug_primitives = generate_layout_debug_primitives(&debug_bounds);
1009 if !debug_primitives.is_empty() {
1010 self.renderer
1011 .render_primitives_overlay(target, &debug_primitives);
1012 }
1013 }
1014
1015 fn render_motion_debug(
1021 &mut self,
1022 target: &wgpu::TextureView,
1023 tree: &RenderTree,
1024 width: u32,
1025 _height: u32,
1026 ) {
1027 let stats = tree.debug_stats();
1028 let mut debug_primitives = Vec::new();
1029
1030 let panel_width = 200.0;
1032 let panel_height = 100.0;
1033 let panel_x = width as f32 - panel_width - 10.0;
1034 let panel_y = 10.0;
1035
1036 debug_primitives.push(
1038 GpuPrimitive::rect(panel_x, panel_y, panel_width, panel_height)
1039 .with_color(0.1, 0.1, 0.15, 0.85)
1040 .with_corner_radius(6.0),
1041 );
1042
1043 let has_active = stats.visual_animation_count > 0
1045 || stats.layout_animation_count > 0
1046 || stats.animated_bounds_count > 0;
1047
1048 let (r, g, b, a) = if has_active {
1049 (0.2, 0.9, 0.3, 1.0) } else {
1051 (0.4, 0.4, 0.5, 1.0) };
1053
1054 debug_primitives.push(
1055 GpuPrimitive::rect(panel_x + 10.0, panel_y + 12.0, 10.0, 10.0)
1056 .with_color(r, g, b, a)
1057 .with_corner_radius(5.0),
1058 );
1059
1060 let bar_x = panel_x + 12.0;
1062 let bar_width = panel_width - 24.0;
1063 let bar_height = 6.0;
1064
1065 let visual_ratio = (stats.visual_animation_count as f32).min(10.0) / 10.0;
1067 if visual_ratio > 0.0 {
1068 debug_primitives.push(
1069 GpuPrimitive::rect(bar_x, panel_y + 35.0, bar_width * visual_ratio, bar_height)
1070 .with_color(0.0, 0.8, 0.9, 0.9)
1071 .with_corner_radius(3.0),
1072 );
1073 }
1074
1075 let layout_ratio = (stats.layout_animation_count as f32).min(10.0) / 10.0;
1077 if layout_ratio > 0.0 {
1078 debug_primitives.push(
1079 GpuPrimitive::rect(bar_x, panel_y + 50.0, bar_width * layout_ratio, bar_height)
1080 .with_color(0.9, 0.2, 0.8, 0.9)
1081 .with_corner_radius(3.0),
1082 );
1083 }
1084
1085 let bounds_ratio = (stats.animated_bounds_count as f32).min(50.0) / 50.0;
1087 if bounds_ratio > 0.0 {
1088 debug_primitives.push(
1089 GpuPrimitive::rect(bar_x, panel_y + 65.0, bar_width * bounds_ratio, bar_height)
1090 .with_color(0.95, 0.85, 0.2, 0.9)
1091 .with_corner_radius(3.0),
1092 );
1093 }
1094
1095 let scroll_count = stats.scroll_physics_count.min(8);
1097 for i in 0..scroll_count {
1098 debug_primitives.push(
1099 GpuPrimitive::rect(bar_x + (i as f32 * 14.0), panel_y + 80.0, 8.0, 8.0)
1100 .with_color(1.0, 0.6, 0.2, 0.9)
1101 .with_corner_radius(4.0),
1102 );
1103 }
1104
1105 if !debug_primitives.is_empty() {
1106 self.renderer
1107 .render_primitives_overlay(target, &debug_primitives);
1108 }
1109 }
1110
1111 fn render_images_to_backdrop(&mut self, images: &[&ImageElement]) {
1113 let Some(ref backdrop) = self.backdrop_texture else {
1114 return;
1115 };
1116 let target = backdrop
1118 .texture
1119 .create_view(&wgpu::TextureViewDescriptor::default());
1120 self.render_images_ref(&target, images);
1121 }
1122
1123 fn preload_images(
1128 &mut self,
1129 images: &[ImageElement],
1130 viewport_width: f32,
1131 viewport_height: f32,
1132 ) {
1133 const VISIBILITY_BUFFER: f32 = 100.0;
1135
1136 for image in images {
1141 if image.placeholder_type == 2 {
1142 if let Some(ref placeholder_src) = image.placeholder_image {
1143 if self.image_cache.get(placeholder_src).is_none() {
1144 let source = blinc_image::ImageSource::from_uri(placeholder_src);
1145 if let Ok(data) = blinc_image::ImageData::load(source) {
1146 let gpu_image = self.image_ctx.create_image_labeled(
1147 data.pixels(),
1148 data.width(),
1149 data.height(),
1150 placeholder_src,
1151 );
1152 self.image_cache.put(placeholder_src.clone(), gpu_image);
1153 }
1154 }
1155 }
1156 }
1157 }
1158
1159 for image in images {
1160 if self.image_cache.get(&image.source).is_some() {
1167 continue;
1168 }
1169
1170 if image.loading_strategy == 1 {
1172 let is_visible = if let Some([clip_x, clip_y, clip_w, clip_h]) = image.clip_bounds {
1175 let clip_left = clip_x - VISIBILITY_BUFFER;
1177 let clip_top = clip_y - VISIBILITY_BUFFER;
1178 let clip_right = clip_x + clip_w + VISIBILITY_BUFFER;
1179 let clip_bottom = clip_y + clip_h + VISIBILITY_BUFFER;
1180
1181 let image_right = image.x + image.width;
1182 let image_bottom = image.y + image.height;
1183
1184 image.x < clip_right
1185 && image_right > clip_left
1186 && image.y < clip_bottom
1187 && image_bottom > clip_top
1188 } else {
1189 let viewport_left = -VISIBILITY_BUFFER;
1191 let viewport_top = -VISIBILITY_BUFFER;
1192 let viewport_right = viewport_width + VISIBILITY_BUFFER;
1193 let viewport_bottom = viewport_height + VISIBILITY_BUFFER;
1194
1195 let image_right = image.x + image.width;
1196 let image_bottom = image.y + image.height;
1197
1198 image.x < viewport_right
1199 && image_right > viewport_left
1200 && image.y < viewport_bottom
1201 && image_bottom > viewport_top
1202 };
1203
1204 if !is_visible {
1205 continue;
1207 }
1208 }
1209
1210 let source = blinc_image::ImageSource::from_uri(&image.source);
1212 let image_data = match blinc_image::ImageData::load(source) {
1213 Ok(data) => data,
1214 Err(e) => {
1215 tracing::trace!("Failed to load image '{}': {:?}", image.source, e);
1216 continue; }
1218 };
1219
1220 let gpu_image = self.image_ctx.create_image_labeled(
1222 image_data.pixels(),
1223 image_data.width(),
1224 image_data.height(),
1225 &image.source,
1226 );
1227
1228 self.image_cache.put(image.source.clone(), gpu_image);
1230 self.image_load_times
1232 .insert(image.source.clone(), web_time::Instant::now());
1233 }
1234 }
1235
1236 fn preload_mask_images(&mut self, batch: &PrimitiveBatch) {
1238 use blinc_core::LayerEffect;
1239 for entry in &batch.layer_commands {
1240 if let blinc_gpu::primitives::LayerCommand::Push { config } = &entry.command {
1241 for effect in &config.effects {
1242 if let LayerEffect::MaskImage { image_url, .. } = effect {
1243 if self.renderer.has_mask_image(image_url) {
1244 continue;
1245 }
1246 let source = blinc_image::ImageSource::from_uri(image_url);
1247 if let Ok(data) = blinc_image::ImageData::load(source) {
1248 self.renderer.load_mask_image_rgba(
1249 image_url,
1250 data.pixels(),
1251 data.width(),
1252 data.height(),
1253 );
1254 }
1255 }
1256 }
1257 }
1258 }
1259 }
1260
1261 fn mask_image_to_arrays(mask: Option<&blinc_core::MaskImage>) -> ([f32; 4], [f32; 4]) {
1266 match mask {
1267 Some(blinc_core::MaskImage::Gradient(gradient)) => match gradient {
1268 blinc_core::Gradient::Linear {
1269 start, end, stops, ..
1270 } => {
1271 let (sa, ea) = Self::extract_mask_alphas_from_stops(stops);
1272 ([start.x, start.y, end.x, end.y], [1.0, sa, ea, 0.0])
1273 }
1274 blinc_core::Gradient::Radial {
1275 center,
1276 radius,
1277 stops,
1278 ..
1279 } => {
1280 let (sa, ea) = Self::extract_mask_alphas_from_stops(stops);
1281 ([center.x, center.y, *radius, 0.0], [2.0, sa, ea, 0.0])
1282 }
1283 blinc_core::Gradient::Conic { center, stops, .. } => {
1284 let (sa, ea) = Self::extract_mask_alphas_from_stops(stops);
1285 ([center.x, center.y, 0.5, 0.0], [2.0, sa, ea, 0.0])
1286 }
1287 },
1288 _ => ([0.0; 4], [0.0; 4]),
1289 }
1290 }
1291
1292 fn extract_mask_alphas_from_stops(stops: &[blinc_core::GradientStop]) -> (f32, f32) {
1293 if stops.is_empty() {
1294 return (1.0, 0.0);
1295 }
1296 (stops[0].color.a, stops[stops.len() - 1].color.a)
1297 }
1298
1299 fn css_filter_to_arrays(
1300 filter: &blinc_layout::element_style::CssFilter,
1301 ) -> ([f32; 4], [f32; 4]) {
1302 (
1303 [
1304 filter.grayscale,
1305 filter.invert,
1306 filter.sepia,
1307 filter.hue_rotate.to_radians(),
1308 ],
1309 [filter.brightness, filter.contrast, filter.saturate, 0.0],
1310 )
1311 }
1312
1313 fn transform_clip_by_affine(
1317 clip: [f32; 4],
1318 clip_radius: [f32; 4],
1319 affine: [f32; 6],
1320 scale_factor: f32,
1321 ) -> ([f32; 4], [f32; 4]) {
1322 let [a, b, c, d, tx, ty] = affine;
1323 let tx_s = tx * scale_factor;
1324 let ty_s = ty * scale_factor;
1325 let ccx = clip[0] + clip[2] * 0.5;
1327 let ccy = clip[1] + clip[3] * 0.5;
1328 let new_cx = a * ccx + c * ccy + tx_s;
1329 let new_cy = b * ccx + d * ccy + ty_s;
1330 let s = (a * d - b * c).abs().sqrt().max(1e-6);
1332 let new_clip = [
1333 new_cx - clip[2] * s * 0.5,
1334 new_cy - clip[3] * s * 0.5,
1335 clip[2] * s,
1336 clip[3] * s,
1337 ];
1338 let new_radius = [
1339 clip_radius[0] * s,
1340 clip_radius[1] * s,
1341 clip_radius[2] * s,
1342 clip_radius[3] * s,
1343 ];
1344 (new_clip, new_radius)
1345 }
1346
1347 fn decompose_image_affine(
1352 x: f32,
1353 y: f32,
1354 w: f32,
1355 h: f32,
1356 affine: [f32; 6],
1357 scale_factor: f32,
1358 ) -> (f32, f32, f32, f32, f32, f32, f32, f32) {
1359 let [a, b, c, d, tx, ty] = affine;
1360 let tx_s = tx * scale_factor;
1362 let ty_s = ty * scale_factor;
1363 let cx = x + w * 0.5;
1365 let cy = y + h * 0.5;
1366 let new_cx = a * cx + c * cy + tx_s;
1367 let new_cy = b * cx + d * cy + ty_s;
1368 (new_cx - w * 0.5, new_cy - h * 0.5, w, h, a, b, c, d)
1370 }
1371
1372 fn render_images(
1374 &mut self,
1375 target: &wgpu::TextureView,
1376 images: &[ImageElement],
1377 viewport_width: f32,
1378 viewport_height: f32,
1379 scale_factor: f32,
1380 ) {
1381 use blinc_image::{calculate_fit_rects, src_rect_to_uv, ObjectFit, ObjectPosition};
1382
1383 for image in images {
1384 let gpu_image = self.image_cache.get(&image.source);
1386
1387 let fade_factor = if image.fade_duration_ms > 0 && gpu_image.is_some() {
1390 if let Some(loaded_at) = self.image_load_times.get(&image.source) {
1391 let elapsed_ms = loaded_at.elapsed().as_secs_f32() * 1000.0;
1392 (elapsed_ms / image.fade_duration_ms as f32).clamp(0.0, 1.0)
1393 } else {
1394 1.0
1395 }
1396 } else {
1397 1.0
1398 };
1399 if fade_factor < 1.0 {
1400 self.has_active_flows = true;
1402 }
1403
1404 if gpu_image.is_none() && image.placeholder_type != 0 {
1406 match image.placeholder_type {
1407 1 => {
1409 let color = blinc_core::Color::rgba(
1410 image.placeholder_color[0],
1411 image.placeholder_color[1],
1412 image.placeholder_color[2],
1413 image.placeholder_color[3],
1414 );
1415 let mut ctx = GpuPaintContext::new(viewport_width, viewport_height);
1416 let rect =
1417 blinc_core::Rect::new(image.x, image.y, image.width, image.height);
1418 ctx.fill_rounded_rect(
1419 rect,
1420 blinc_core::CornerRadius::uniform(image.border_radius),
1421 color,
1422 );
1423 let batch = ctx.take_batch();
1424 self.renderer.render_overlay(target, &batch);
1425 }
1426 2 => {
1428 if let Some(ref placeholder_src) = image.placeholder_image {
1429 if let Some(placeholder_gpu) = self.image_cache.get(placeholder_src) {
1430 let (src_rect, dst_rect) = calculate_fit_rects(
1431 placeholder_gpu.width(),
1432 placeholder_gpu.height(),
1433 image.width,
1434 image.height,
1435 ObjectFit::Cover,
1436 ObjectPosition::new(0.5, 0.5),
1437 );
1438 let src_uv = src_rect_to_uv(
1439 src_rect,
1440 placeholder_gpu.width(),
1441 placeholder_gpu.height(),
1442 );
1443 let instance = GpuImageInstance::new(
1444 image.x + dst_rect[0],
1445 image.y + dst_rect[1],
1446 dst_rect[2],
1447 dst_rect[3],
1448 )
1449 .with_src_uv(src_uv[0], src_uv[1], src_uv[2], src_uv[3])
1450 .with_border_radius(image.border_radius)
1451 .with_opacity(image.opacity);
1452 self.renderer.render_images(
1453 target,
1454 placeholder_gpu.view(),
1455 &[instance],
1456 );
1457 }
1458 }
1459 }
1460 3 => {
1462 let t =
1463 self.frame_count.saturating_mul(16).rem_euclid(2400) as f32 / 2400.0;
1464 let base_a = image.placeholder_color[3].max(0.4);
1465 let base = blinc_core::Color::rgba(
1466 image.placeholder_color[0],
1467 image.placeholder_color[1],
1468 image.placeholder_color[2],
1469 base_a,
1470 );
1471 let highlight_a = (base_a + 0.25).min(1.0);
1472 let highlight = blinc_core::Color::rgba(
1473 (image.placeholder_color[0] + 0.15).min(1.0),
1474 (image.placeholder_color[1] + 0.15).min(1.0),
1475 (image.placeholder_color[2] + 0.15).min(1.0),
1476 highlight_a,
1477 );
1478 let mut ctx = GpuPaintContext::new(viewport_width, viewport_height);
1479 let rect =
1480 blinc_core::Rect::new(image.x, image.y, image.width, image.height);
1481 ctx.fill_rounded_rect(
1483 rect,
1484 blinc_core::CornerRadius::uniform(image.border_radius),
1485 base,
1486 );
1487 let band_w = (image.width * 0.25).max(40.0);
1489 let band_x = image.x + (image.width + band_w) * t - band_w;
1490 let band_rect =
1491 blinc_core::Rect::new(band_x, image.y, band_w, image.height);
1492 ctx.fill_rounded_rect(
1493 band_rect,
1494 blinc_core::CornerRadius::uniform(image.border_radius),
1495 highlight,
1496 );
1497 let batch = ctx.take_batch();
1498 self.renderer.render_overlay(target, &batch);
1499 self.has_active_flows = true;
1501 }
1502 _ => {}
1503 }
1504 continue;
1505 }
1506
1507 let Some(gpu_image) = gpu_image else {
1508 continue; };
1510
1511 let object_fit = match image.object_fit {
1513 0 => ObjectFit::Cover,
1514 1 => ObjectFit::Contain,
1515 2 => ObjectFit::Fill,
1516 3 => ObjectFit::ScaleDown,
1517 4 => ObjectFit::None,
1518 _ => ObjectFit::Cover,
1519 };
1520
1521 let object_position =
1523 ObjectPosition::new(image.object_position[0], image.object_position[1]);
1524
1525 let (src_rect, dst_rect) = calculate_fit_rects(
1527 gpu_image.width(),
1528 gpu_image.height(),
1529 image.width,
1530 image.height,
1531 object_fit,
1532 object_position,
1533 );
1534
1535 let src_uv = src_rect_to_uv(src_rect, gpu_image.width(), gpu_image.height());
1537
1538 let base_x = image.x + dst_rect[0];
1540 let base_y = image.y + dst_rect[1];
1541 let base_w = dst_rect[2];
1542 let base_h = dst_rect[3];
1543
1544 let (draw_x, draw_y, draw_w, draw_h, ta, tb, tc, td) = if let Some(affine) =
1545 image.css_affine
1546 {
1547 Self::decompose_image_affine(base_x, base_y, base_w, base_h, affine, scale_factor)
1548 } else {
1549 (base_x, base_y, base_w, base_h, 1.0, 0.0, 0.0, 1.0)
1550 };
1551
1552 let effective_clip = image.clip_bounds.map(|clip| {
1554 if let Some(affine) = image.css_affine {
1555 Self::transform_clip_by_affine(clip, image.clip_radius, affine, scale_factor)
1556 } else {
1557 (clip, image.clip_radius)
1558 }
1559 });
1560
1561 if let Some(ref shadow) = image.shadow {
1563 let mut shadow_ctx = GpuPaintContext::new(viewport_width, viewport_height);
1564 if let Some(clip) = image.clip_bounds {
1566 shadow_ctx.push_clip(blinc_core::ClipShape::RoundedRect {
1567 rect: blinc_core::Rect::new(clip[0], clip[1], clip[2], clip[3]),
1568 corner_radius: blinc_core::CornerRadius {
1569 top_left: image.clip_radius[0],
1570 top_right: image.clip_radius[1],
1571 bottom_right: image.clip_radius[2],
1572 bottom_left: image.clip_radius[3],
1573 },
1574 });
1575 }
1576 let shadow_rect =
1577 blinc_core::Rect::new(image.x, image.y, image.width, image.height);
1578 let shadow_radius = blinc_core::CornerRadius::uniform(image.border_radius);
1579 shadow_ctx.draw_shadow(shadow_rect, shadow_radius, *shadow);
1580 let shadow_batch = shadow_ctx.take_batch();
1581 self.renderer.render_overlay(target, &shadow_batch);
1582 }
1583
1584 let mut instance = GpuImageInstance::new(draw_x, draw_y, draw_w, draw_h)
1586 .with_src_uv(src_uv[0], src_uv[1], src_uv[2], src_uv[3])
1587 .with_tint(image.tint[0], image.tint[1], image.tint[2], image.tint[3])
1588 .with_border_radius(image.border_radius)
1589 .with_opacity(image.opacity * fade_factor)
1590 .with_transform(ta, tb, tc, td)
1591 .with_filter(image.filter_a, image.filter_b);
1592
1593 if image.border_width > 0.0 {
1595 instance = instance.with_image_border(
1596 image.border_width,
1597 image.border_color.r,
1598 image.border_color.g,
1599 image.border_color.b,
1600 image.border_color.a,
1601 );
1602 }
1603
1604 if image.mask_info[0] > 0.5 {
1606 instance.mask_params = image.mask_params;
1607 instance.mask_info = image.mask_info;
1608 }
1609
1610 if let Some((clip, clip_r)) = effective_clip {
1612 instance = instance.with_clip_rounded_rect_corners(
1613 clip[0], clip[1], clip[2], clip[3], clip_r[0], clip_r[1], clip_r[2], clip_r[3],
1614 );
1615 }
1616 if let Some(sc) = image.scroll_clip {
1618 instance = instance.with_clip2_rect(sc[0], sc[1], sc[2], sc[3]);
1619 }
1620
1621 self.renderer
1623 .render_images(target, gpu_image.view(), &[instance]);
1624 }
1625 }
1626
1627 fn render_images_ref(&mut self, target: &wgpu::TextureView, images: &[&ImageElement]) {
1629 use blinc_image::{calculate_fit_rects, src_rect_to_uv, ObjectFit, ObjectPosition};
1630
1631 for image in images {
1632 let Some(gpu_image) = self.image_cache.get(&image.source) else {
1634 continue; };
1636
1637 let fade_factor = if image.fade_duration_ms > 0 {
1639 if let Some(loaded_at) = self.image_load_times.get(&image.source) {
1640 let elapsed_ms = loaded_at.elapsed().as_secs_f32() * 1000.0;
1641 (elapsed_ms / image.fade_duration_ms as f32).clamp(0.0, 1.0)
1642 } else {
1643 1.0
1644 }
1645 } else {
1646 1.0
1647 };
1648 if fade_factor < 1.0 {
1649 self.has_active_flows = true;
1650 }
1651
1652 let object_fit = match image.object_fit {
1654 0 => ObjectFit::Cover,
1655 1 => ObjectFit::Contain,
1656 2 => ObjectFit::Fill,
1657 3 => ObjectFit::ScaleDown,
1658 4 => ObjectFit::None,
1659 _ => ObjectFit::Cover,
1660 };
1661
1662 let object_position =
1664 ObjectPosition::new(image.object_position[0], image.object_position[1]);
1665
1666 let (src_rect, dst_rect) = calculate_fit_rects(
1668 gpu_image.width(),
1669 gpu_image.height(),
1670 image.width,
1671 image.height,
1672 object_fit,
1673 object_position,
1674 );
1675
1676 let src_uv = src_rect_to_uv(src_rect, gpu_image.width(), gpu_image.height());
1678
1679 let base_x = image.x + dst_rect[0];
1681 let base_y = image.y + dst_rect[1];
1682 let base_w = dst_rect[2];
1683 let base_h = dst_rect[3];
1684
1685 let (draw_x, draw_y, draw_w, draw_h, ta, tb, tc, td) =
1688 if let Some(affine) = image.css_affine {
1689 Self::decompose_image_affine(base_x, base_y, base_w, base_h, affine, 1.0)
1690 } else {
1691 (base_x, base_y, base_w, base_h, 1.0, 0.0, 0.0, 1.0)
1692 };
1693
1694 let effective_clip = image.clip_bounds.map(|clip| {
1696 if let Some(affine) = image.css_affine {
1697 Self::transform_clip_by_affine(clip, image.clip_radius, affine, 1.0)
1698 } else {
1699 (clip, image.clip_radius)
1700 }
1701 });
1702
1703 let mut instance = GpuImageInstance::new(draw_x, draw_y, draw_w, draw_h)
1705 .with_src_uv(src_uv[0], src_uv[1], src_uv[2], src_uv[3])
1706 .with_tint(image.tint[0], image.tint[1], image.tint[2], image.tint[3])
1707 .with_border_radius(image.border_radius)
1708 .with_opacity(image.opacity * fade_factor)
1709 .with_transform(ta, tb, tc, td)
1710 .with_filter(image.filter_a, image.filter_b);
1711
1712 if image.border_width > 0.0 {
1714 instance = instance.with_image_border(
1715 image.border_width,
1716 image.border_color.r,
1717 image.border_color.g,
1718 image.border_color.b,
1719 image.border_color.a,
1720 );
1721 }
1722
1723 if image.mask_info[0] > 0.5 {
1725 instance.mask_params = image.mask_params;
1726 instance.mask_info = image.mask_info;
1727 }
1728
1729 if let Some((clip, clip_r)) = effective_clip {
1731 instance = instance.with_clip_rounded_rect_corners(
1732 clip[0], clip[1], clip[2], clip[3], clip_r[0], clip_r[1], clip_r[2], clip_r[3],
1733 );
1734 }
1735 if let Some(sc) = image.scroll_clip {
1737 instance = instance.with_clip2_rect(sc[0], sc[1], sc[2], sc[3]);
1738 }
1739
1740 self.renderer
1742 .render_images(target, gpu_image.view(), &[instance]);
1743 }
1744 }
1745
1746 fn render_svg_element(&mut self, ctx: &mut GpuPaintContext, svg: &SvgElement) {
1748 if svg.motion_opacity <= 0.001 {
1750 return;
1751 }
1752
1753 if let Some([clip_x, clip_y, clip_w, clip_h]) = svg.clip_bounds {
1755 let svg_right = svg.x + svg.width;
1756 let svg_bottom = svg.y + svg.height;
1757 let clip_right = clip_x + clip_w;
1758 let clip_bottom = clip_y + clip_h;
1759
1760 if svg.x >= clip_right
1762 || svg_right <= clip_x
1763 || svg.y >= clip_bottom
1764 || svg_bottom <= clip_y
1765 {
1766 return;
1767 }
1768 }
1769
1770 let svg_hash = {
1772 let mut hasher = DefaultHasher::new();
1773 svg.source.hash(&mut hasher);
1774 hasher.finish()
1775 };
1776
1777 let doc = if let Some(cached) = self.svg_cache.get(&svg_hash) {
1779 cached.clone()
1780 } else {
1781 let Ok(parsed) = SvgDocument::from_str(&svg.source) else {
1782 return;
1783 };
1784 self.svg_cache.put(svg_hash, parsed.clone());
1785 parsed
1786 };
1787
1788 if let Some([clip_x, clip_y, clip_w, clip_h]) = svg.clip_bounds {
1790 ctx.push_clip(blinc_core::ClipShape::rect(Rect::new(
1791 clip_x, clip_y, clip_w, clip_h,
1792 )));
1793 }
1794
1795 if svg.motion_opacity < 1.0 {
1797 ctx.push_opacity(svg.motion_opacity);
1798 }
1799
1800 let has_css_overrides = svg.tint.is_some()
1802 || svg.fill.is_some()
1803 || svg.stroke.is_some()
1804 || svg.stroke_width.is_some();
1805 if has_css_overrides {
1806 self.render_svg_with_overrides(
1807 ctx,
1808 &doc,
1809 svg.x,
1810 svg.y,
1811 svg.width,
1812 svg.height,
1813 svg.tint,
1814 svg.fill,
1815 svg.stroke,
1816 svg.stroke_width,
1817 );
1818 } else {
1819 doc.render_fit(ctx, Rect::new(svg.x, svg.y, svg.width, svg.height));
1820 }
1821
1822 if svg.motion_opacity < 1.0 {
1824 ctx.pop_opacity();
1825 }
1826
1827 if svg.clip_bounds.is_some() {
1829 ctx.pop_clip();
1830 }
1831 }
1832
1833 #[allow(clippy::too_many_arguments)]
1835 fn render_svg_with_overrides(
1836 &self,
1837 ctx: &mut GpuPaintContext,
1838 doc: &SvgDocument,
1839 x: f32,
1840 y: f32,
1841 width: f32,
1842 height: f32,
1843 tint: Option<blinc_core::Color>,
1844 fill: Option<blinc_core::Color>,
1845 stroke: Option<blinc_core::Color>,
1846 stroke_width: Option<f32>,
1847 ) {
1848 use blinc_svg::SvgDrawCommand;
1849
1850 let scale_x = width / doc.width;
1852 let scale_y = height / doc.height;
1853 let scale = scale_x.min(scale_y);
1854
1855 let scaled_width = doc.width * scale;
1857 let scaled_height = doc.height * scale;
1858 let offset_x = x + (width - scaled_width) / 2.0;
1859 let offset_y = y + (height - scaled_height) / 2.0;
1860
1861 let commands = doc.commands();
1862
1863 for cmd in commands {
1864 match cmd {
1865 SvgDrawCommand::FillPath { path, brush } => {
1866 let scaled = scale_and_translate_path(&path, offset_x, offset_y, scale);
1867 let fill_brush = if let Some(f) = fill {
1869 Brush::Solid(f)
1870 } else if let Some(t) = tint {
1871 Brush::Solid(t)
1872 } else {
1873 brush.clone()
1874 };
1875 ctx.fill_path(&scaled, fill_brush);
1876 }
1877 SvgDrawCommand::StrokePath {
1878 path,
1879 stroke: orig_stroke,
1880 brush,
1881 } => {
1882 let scaled = scale_and_translate_path(&path, offset_x, offset_y, scale);
1883 let sw = stroke_width.unwrap_or(orig_stroke.width) * scale;
1885 let scaled_stroke = Stroke::new(sw)
1886 .with_cap(orig_stroke.cap)
1887 .with_join(orig_stroke.join);
1888 let stroke_brush = if let Some(s) = stroke {
1890 Brush::Solid(s)
1891 } else if let Some(t) = tint {
1892 Brush::Solid(t)
1893 } else {
1894 brush.clone()
1895 };
1896 ctx.stroke_path(&scaled, &scaled_stroke, stroke_brush);
1897 }
1898 }
1899 }
1900 }
1901
1902 fn render_rasterized_svgs(
1910 &mut self,
1911 target: &wgpu::TextureView,
1912 svgs: &[SvgElement],
1913 scale_factor: f32,
1914 ) {
1915 self.svg_atlas.begin_frame(&self.device);
1922
1923 let mut instances: Vec<GpuImageInstance> = Vec::with_capacity(svgs.len());
1925
1926 for svg in svgs {
1927 if svg.motion_opacity <= 0.001 {
1929 continue;
1930 }
1931
1932 if let Some([clip_x, clip_y, clip_w, clip_h]) = svg.clip_bounds {
1934 let svg_right = svg.x + svg.width;
1935 let svg_bottom = svg.y + svg.height;
1936 let clip_right = clip_x + clip_w;
1937 let clip_bottom = clip_y + clip_h;
1938
1939 if svg.x >= clip_right
1940 || svg_right <= clip_x
1941 || svg.y >= clip_bottom
1942 || svg_bottom <= clip_y
1943 {
1944 continue;
1945 }
1946 }
1947
1948 let raster_width = (svg.width.ceil() as u32).max(1);
1965 let raster_height = (svg.height.ceil() as u32).max(1);
1966
1967 let is_tintable = svg.tint.is_some()
1971 && svg.fill.is_none()
1972 && svg.stroke.is_none()
1973 && svg.stroke_width.is_none()
1974 && svg.stroke_dasharray.is_none()
1975 && svg.stroke_dashoffset.is_none()
1976 && svg.svg_path_data.is_none()
1977 && svg.tag_overrides.is_empty()
1978 && svg.source.contains("currentColor");
1979
1980 let cache_key = {
1983 let mut hasher = DefaultHasher::new();
1984 svg.source.hash(&mut hasher);
1985 raster_width.hash(&mut hasher);
1986 raster_height.hash(&mut hasher);
1987 if is_tintable {
1988 255u8.hash(&mut hasher);
1990 } else if let Some(tint) = &svg.tint {
1991 tint.r.to_bits().hash(&mut hasher);
1992 tint.g.to_bits().hash(&mut hasher);
1993 tint.b.to_bits().hash(&mut hasher);
1994 tint.a.to_bits().hash(&mut hasher);
1995 }
1996 if let Some(fill) = &svg.fill {
1997 1u8.hash(&mut hasher);
1998 fill.r.to_bits().hash(&mut hasher);
1999 fill.g.to_bits().hash(&mut hasher);
2000 fill.b.to_bits().hash(&mut hasher);
2001 fill.a.to_bits().hash(&mut hasher);
2002 }
2003 if let Some(stroke) = &svg.stroke {
2004 2u8.hash(&mut hasher);
2005 stroke.r.to_bits().hash(&mut hasher);
2006 stroke.g.to_bits().hash(&mut hasher);
2007 stroke.b.to_bits().hash(&mut hasher);
2008 stroke.a.to_bits().hash(&mut hasher);
2009 }
2010 if let Some(sw) = &svg.stroke_width {
2011 3u8.hash(&mut hasher);
2012 sw.to_bits().hash(&mut hasher);
2013 }
2014 if let Some(ref da) = svg.stroke_dasharray {
2015 4u8.hash(&mut hasher);
2016 for v in da {
2017 v.to_bits().hash(&mut hasher);
2018 }
2019 }
2020 if let Some(offset) = &svg.stroke_dashoffset {
2021 5u8.hash(&mut hasher);
2022 offset.to_bits().hash(&mut hasher);
2023 }
2024 if let Some(ref path_data) = svg.svg_path_data {
2025 6u8.hash(&mut hasher);
2026 path_data.hash(&mut hasher);
2027 }
2028 if !svg.tag_overrides.is_empty() {
2030 7u8.hash(&mut hasher);
2031 let mut keys: Vec<&String> = svg.tag_overrides.keys().collect();
2033 keys.sort();
2034 for key in keys {
2035 key.hash(&mut hasher);
2036 if let Some(ts) = svg.tag_overrides.get(key) {
2037 if let Some(f) = &ts.fill {
2038 for v in f {
2039 v.to_bits().hash(&mut hasher);
2040 }
2041 }
2042 if let Some(s) = &ts.stroke {
2043 for v in s {
2044 v.to_bits().hash(&mut hasher);
2045 }
2046 }
2047 if let Some(sw) = &ts.stroke_width {
2048 sw.to_bits().hash(&mut hasher);
2049 }
2050 if let Some(op) = &ts.opacity {
2051 op.to_bits().hash(&mut hasher);
2052 }
2053 }
2054 }
2055 }
2056 hasher.finish()
2057 };
2058
2059 if self.svg_atlas.get(cache_key).is_none() {
2061 let has_overrides = svg.tint.is_some()
2063 || svg.fill.is_some()
2064 || svg.stroke.is_some()
2065 || svg.stroke_width.is_some()
2066 || svg.stroke_dasharray.is_some()
2067 || svg.stroke_dashoffset.is_some()
2068 || svg.svg_path_data.is_some()
2069 || !svg.tag_overrides.is_empty();
2070
2071 fn color_val(c: blinc_core::Color) -> String {
2072 if c.a < 1.0 {
2073 format!(
2074 "rgba({},{},{},{})",
2075 (c.r * 255.0) as u8,
2076 (c.g * 255.0) as u8,
2077 (c.b * 255.0) as u8,
2078 c.a
2079 )
2080 } else {
2081 format!(
2082 "#{:02x}{:02x}{:02x}",
2083 (c.r * 255.0) as u8,
2084 (c.g * 255.0) as u8,
2085 (c.b * 255.0) as u8
2086 )
2087 }
2088 }
2089
2090 let effective_source = if has_overrides {
2091 let mut svg_attrs = String::new();
2093 if let Some(fill) = svg.fill {
2094 svg_attrs.push_str(&format!(r#" fill="{}""#, color_val(fill)));
2095 }
2096 if let Some(stroke) = svg.stroke {
2097 svg_attrs.push_str(&format!(r#" stroke="{}""#, color_val(stroke)));
2098 }
2099 if let Some(sw) = svg.stroke_width {
2100 svg_attrs.push_str(&format!(r#" stroke-width="{}""#, sw));
2101 }
2102 if let Some(ref da) = svg.stroke_dasharray {
2103 let da_str = da
2104 .iter()
2105 .map(|v| v.to_string())
2106 .collect::<Vec<_>>()
2107 .join(",");
2108 svg_attrs.push_str(&format!(r#" stroke-dasharray="{}""#, da_str));
2109 }
2110 if let Some(offset) = svg.stroke_dashoffset {
2111 svg_attrs.push_str(&format!(r#" stroke-dashoffset="{}""#, offset));
2112 }
2113
2114 fn strip_attr(s: &mut String, tag_start: usize, tag_end: usize, attr: &str) {
2116 let region = &s[tag_start..tag_end];
2117 let attr_eq = format!("{}=", attr);
2118 if let Some(attr_offset) = region.find(&attr_eq) {
2119 let abs_attr = tag_start + attr_offset;
2120 let after_eq = abs_attr + attr.len() + 1;
2121 if after_eq < s.len() {
2122 let quote = s.as_bytes()[after_eq];
2123 if quote == b'"' || quote == b'\'' {
2124 if let Some(end_quote) = s[after_eq + 1..].find(quote as char) {
2125 let remove_end = after_eq + 1 + end_quote + 1;
2126 let remove_start =
2127 if abs_attr > 0 && s.as_bytes()[abs_attr - 1] == b' ' {
2128 abs_attr - 1
2129 } else {
2130 abs_attr
2131 };
2132 s.replace_range(remove_start..remove_end, "");
2133 }
2134 }
2135 }
2136 }
2137 }
2138
2139 let mut modified = String::from(&*svg.source);
2140
2141 if let Some(svg_close) = modified.find('>') {
2143 if svg.stroke.is_some() {
2144 strip_attr(&mut modified, 0, svg_close, "stroke");
2145 }
2146 if svg.fill.is_some() {
2147 let svg_close = modified.find('>').unwrap_or(0);
2148 strip_attr(&mut modified, 0, svg_close, "fill");
2149 }
2150 if svg.stroke_width.is_some() {
2151 let svg_close = modified.find('>').unwrap_or(0);
2152 strip_attr(&mut modified, 0, svg_close, "stroke-width");
2153 }
2154 if svg.stroke_dasharray.is_some() {
2155 let svg_close = modified.find('>').unwrap_or(0);
2156 strip_attr(&mut modified, 0, svg_close, "stroke-dasharray");
2157 }
2158 if svg.stroke_dashoffset.is_some() {
2159 let svg_close = modified.find('>').unwrap_or(0);
2160 strip_attr(&mut modified, 0, svg_close, "stroke-dashoffset");
2161 }
2162 }
2163
2164 if !svg_attrs.is_empty() {
2166 if let Some(pos) = modified.find('>') {
2167 let insert_pos = if pos > 0 && modified.as_bytes()[pos - 1] == b'/' {
2168 pos - 1
2169 } else {
2170 pos
2171 };
2172 modified.insert_str(insert_pos, &svg_attrs);
2173 }
2174 }
2175
2176 let shape_tags = [
2178 "<path",
2179 "<circle",
2180 "<rect",
2181 "<polygon",
2182 "<line",
2183 "<ellipse",
2184 "<polyline",
2185 ];
2186 for tag in &shape_tags {
2187 let tag_name = tag.trim_start_matches('<');
2188 let tag_style = svg.tag_overrides.get(tag_name);
2189
2190 let effective_fill: Option<blinc_core::Color> = tag_style
2192 .and_then(|ts| ts.fill)
2193 .map(|c| blinc_core::Color::rgba(c[0], c[1], c[2], c[3]))
2194 .or(svg.fill);
2195 let effective_stroke: Option<blinc_core::Color> = tag_style
2196 .and_then(|ts| ts.stroke)
2197 .map(|c| blinc_core::Color::rgba(c[0], c[1], c[2], c[3]))
2198 .or(svg.stroke);
2199 let effective_stroke_width: Option<f32> = tag_style
2200 .and_then(|ts| ts.stroke_width)
2201 .or(svg.stroke_width);
2202 let effective_dasharray: Option<Vec<f32>> = tag_style
2203 .and_then(|ts| ts.stroke_dasharray.clone())
2204 .or_else(|| svg.stroke_dasharray.clone());
2205 let effective_dashoffset: Option<f32> = tag_style
2206 .and_then(|ts| ts.stroke_dashoffset)
2207 .or(svg.stroke_dashoffset);
2208 let effective_opacity: Option<f32> = tag_style.and_then(|ts| ts.opacity);
2209
2210 let mut search_from = 0;
2211 while let Some(tag_start) = modified[search_from..].find(tag) {
2212 let abs_tag = search_from + tag_start;
2213 let abs_start = abs_tag + tag.len();
2214 if let Some(close) = modified[abs_start..].find('>') {
2215 let abs_close = abs_start + close;
2216
2217 if effective_stroke.is_some() {
2218 strip_attr(&mut modified, abs_tag, abs_close, "stroke-width");
2219 let new_close = abs_start
2220 + modified[abs_start..].find('>').unwrap_or(close);
2221 strip_attr(&mut modified, abs_tag, new_close, "stroke");
2222 }
2223 if effective_fill.is_some() {
2224 let new_close = abs_start
2225 + modified[abs_start..].find('>').unwrap_or(close);
2226 strip_attr(&mut modified, abs_tag, new_close, "fill");
2227 }
2228 if effective_stroke_width.is_some() {
2229 let new_close = abs_start
2230 + modified[abs_start..].find('>').unwrap_or(close);
2231 strip_attr(&mut modified, abs_tag, new_close, "stroke-width");
2232 }
2233 if effective_dasharray.is_some() {
2234 let new_close = abs_start
2235 + modified[abs_start..].find('>').unwrap_or(close);
2236 strip_attr(
2237 &mut modified,
2238 abs_tag,
2239 new_close,
2240 "stroke-dasharray",
2241 );
2242 }
2243 if effective_dashoffset.is_some() {
2244 let new_close = abs_start
2245 + modified[abs_start..].find('>').unwrap_or(close);
2246 strip_attr(
2247 &mut modified,
2248 abs_tag,
2249 new_close,
2250 "stroke-dashoffset",
2251 );
2252 }
2253 if effective_opacity.is_some() {
2254 let new_close = abs_start
2255 + modified[abs_start..].find('>').unwrap_or(close);
2256 strip_attr(&mut modified, abs_tag, new_close, "opacity");
2257 }
2258 if svg.svg_path_data.is_some() && *tag == "<path" {
2259 let new_close = abs_start
2260 + modified[abs_start..].find('>').unwrap_or(close);
2261 strip_attr(&mut modified, abs_tag, new_close, "d");
2262 }
2263
2264 let abs_close =
2266 abs_start + modified[abs_start..].find('>').unwrap_or(0);
2267 let is_self_close =
2268 abs_close > 0 && modified.as_bytes()[abs_close - 1] == b'/';
2269 let insert_at = if is_self_close {
2270 abs_close - 1
2271 } else {
2272 abs_close
2273 };
2274 let mut elem_attrs = String::new();
2275 if let Some(fill) = effective_fill {
2276 elem_attrs.push_str(&format!(r#" fill="{}""#, color_val(fill)));
2277 }
2278 if let Some(stroke) = effective_stroke {
2279 elem_attrs
2280 .push_str(&format!(r#" stroke="{}""#, color_val(stroke)));
2281 }
2282 if let Some(sw) = effective_stroke_width {
2283 elem_attrs.push_str(&format!(r#" stroke-width="{}""#, sw));
2284 }
2285 if let Some(ref da) = effective_dasharray {
2286 let da_str = da
2287 .iter()
2288 .map(|v| v.to_string())
2289 .collect::<Vec<_>>()
2290 .join(",");
2291 elem_attrs
2292 .push_str(&format!(r#" stroke-dasharray="{}""#, da_str));
2293 }
2294 if let Some(offset) = effective_dashoffset {
2295 elem_attrs
2296 .push_str(&format!(r#" stroke-dashoffset="{}""#, offset));
2297 }
2298 if let Some(opacity) = effective_opacity {
2299 elem_attrs.push_str(&format!(r#" opacity="{}""#, opacity));
2300 }
2301 if let Some(ref path_data) = svg.svg_path_data {
2302 if *tag == "<path" {
2303 elem_attrs.push_str(&format!(r#" d="{}""#, path_data));
2304 }
2305 }
2306 modified.insert_str(insert_at, &elem_attrs);
2307 search_from = insert_at + elem_attrs.len() + 1;
2308 } else {
2309 break;
2310 }
2311 }
2312 }
2313
2314 std::borrow::Cow::Owned(modified)
2315 } else {
2316 std::borrow::Cow::Borrowed(&*svg.source)
2317 };
2318
2319 let has_current_color = effective_source.contains("currentColor");
2326 let needs_post_raster_tint =
2327 !is_tintable && svg.tint.is_some() && !has_current_color;
2328 let final_source = if is_tintable {
2329 std::borrow::Cow::Owned(effective_source.replace("currentColor", "#ffffff"))
2330 } else if let Some(tint) = svg.tint {
2331 if has_current_color {
2332 std::borrow::Cow::Owned(
2333 effective_source.replace("currentColor", &color_val(tint)),
2334 )
2335 } else {
2336 effective_source
2337 }
2338 } else {
2339 effective_source
2340 };
2341
2342 let rasterized =
2343 RasterizedSvg::from_str(&final_source, raster_width, raster_height);
2344
2345 let mut rasterized = match rasterized {
2346 Ok(r) => r,
2347 Err(e) => {
2348 tracing::warn!("Failed to rasterize SVG: {}", e);
2349 continue;
2350 }
2351 };
2352
2353 if needs_post_raster_tint {
2363 rasterized.apply_tint(svg.tint.unwrap());
2364 }
2365
2366 if self
2368 .svg_atlas
2369 .insert(
2370 cache_key,
2371 rasterized.width,
2372 rasterized.height,
2373 rasterized.data(),
2374 &self.device,
2375 )
2376 .is_none()
2377 {
2378 tracing::warn!(
2379 "SVG atlas full, could not allocate {}x{}",
2380 raster_width,
2381 raster_height
2382 );
2383 continue;
2384 }
2385 }
2386
2387 let Some(region) = self.svg_atlas.get(cache_key) else {
2389 continue;
2390 };
2391 let src_uv = region.uv_bounds(self.svg_atlas.width(), self.svg_atlas.height());
2392 self.svg_atlas.mark_used(cache_key);
2393
2394 let (draw_x, draw_y, draw_w, draw_h, ta, tb, tc, td) =
2397 if let Some([a, b, c, d, tx, ty]) = svg.css_affine {
2398 let tx_s = tx * scale_factor;
2400 let ty_s = ty * scale_factor;
2401
2402 let cx = svg.x + svg.width * 0.5;
2404 let cy = svg.y + svg.height * 0.5;
2405 let new_cx = a * cx + c * cy + tx_s;
2406 let new_cy = b * cx + d * cy + ty_s;
2407
2408 (
2410 new_cx - svg.width * 0.5,
2411 new_cy - svg.height * 0.5,
2412 svg.width,
2413 svg.height,
2414 a,
2415 b,
2416 c,
2417 d,
2418 )
2419 } else {
2420 (svg.x, svg.y, svg.width, svg.height, 1.0, 0.0, 0.0, 1.0)
2421 };
2422
2423 let mut instance = GpuImageInstance::new(draw_x, draw_y, draw_w, draw_h)
2425 .with_src_uv(src_uv[0], src_uv[1], src_uv[2], src_uv[3])
2426 .with_opacity(svg.motion_opacity)
2427 .with_transform(ta, tb, tc, td);
2428
2429 if is_tintable {
2432 if let Some(tint) = svg.tint {
2433 instance = instance.with_tint(tint.r, tint.g, tint.b, tint.a);
2434 }
2435 }
2436
2437 if let Some([clip_x, clip_y, clip_w, clip_h]) = svg.clip_bounds {
2439 instance = instance.with_clip_rect(clip_x, clip_y, clip_w, clip_h);
2440 }
2441
2442 instances.push(instance);
2443 }
2444
2445 if !instances.is_empty() {
2447 self.svg_atlas.upload(&self.queue);
2448 self.renderer
2449 .render_images(target, self.svg_atlas.view(), &instances);
2450 }
2451 }
2452
2453 fn collect_render_elements(
2455 &mut self,
2456 tree: &RenderTree,
2457 ) -> (
2458 Vec<TextElement>,
2459 Vec<SvgElement>,
2460 Vec<ImageElement>,
2461 Vec<FlowElement>,
2462 ) {
2463 self.collect_render_elements_with_state(tree, None)
2464 }
2465
2466 fn collect_render_elements_with_state(
2468 &mut self,
2469 tree: &RenderTree,
2470 render_state: Option<&blinc_layout::RenderState>,
2471 ) -> (
2472 Vec<TextElement>,
2473 Vec<SvgElement>,
2474 Vec<ImageElement>,
2475 Vec<FlowElement>,
2476 ) {
2477 let mut texts = std::mem::take(&mut self.scratch_texts);
2480 let mut svgs = std::mem::take(&mut self.scratch_svgs);
2481 let mut images = std::mem::take(&mut self.scratch_images);
2482 let mut flows = Vec::new();
2483 texts.clear();
2484 svgs.clear();
2485 images.clear();
2486
2487 let scale = tree.scale_factor();
2489
2490 if let Some(root) = tree.root() {
2491 let mut z_layer = 0u32;
2492 self.collect_elements_recursive(
2493 tree,
2494 root,
2495 (0.0, 0.0),
2496 false, false, None, None, 1.0, (0.0, 0.0), (1.0, 1.0), None, render_state,
2505 scale,
2506 &mut z_layer,
2507 &mut texts,
2508 &mut svgs,
2509 &mut images,
2510 &mut flows,
2511 None, 1.0, None, None, None, );
2517 }
2518
2519 texts.sort_by_key(|t| t.z_index);
2521
2522 (texts, svgs, images, flows)
2523 }
2524
2525 #[allow(clippy::too_many_arguments, clippy::only_used_in_recursion)]
2526 fn collect_elements_recursive(
2527 &self,
2528 tree: &RenderTree,
2529 node: LayoutNodeId,
2530 parent_offset: (f32, f32),
2531 inside_glass: bool,
2532 inside_foreground: bool,
2533 current_clip: Option<[f32; 4]>,
2534 current_clip_radius: Option<[f32; 4]>,
2535 inherited_motion_opacity: f32,
2536 inherited_motion_translate: (f32, f32),
2537 inherited_motion_scale: (f32, f32),
2538 inherited_motion_scale_center: Option<(f32, f32)>,
2541 render_state: Option<&blinc_layout::RenderState>,
2542 scale: f32,
2543 z_layer: &mut u32,
2544 texts: &mut Vec<TextElement>,
2545 svgs: &mut Vec<SvgElement>,
2546 images: &mut Vec<ImageElement>,
2547 flows: &mut Vec<FlowElement>,
2548 inherited_css_affine: Option<[f32; 6]>,
2551 inherited_css_opacity: f32,
2554 parent_node: Option<LayoutNodeId>,
2557 current_scroll_clip: Option<[f32; 4]>,
2561 inside_3d_layer: Option<Transform3DLayerInfo>,
2565 ) {
2566 use blinc_layout::Material;
2567
2568 let Some(bounds) = tree.get_render_bounds(node, parent_offset) else {
2571 return;
2572 };
2573
2574 let abs_x = bounds.x;
2575 let abs_y = bounds.y;
2576
2577 let motion_values = render_state.and_then(|rs| {
2579 if let Some(render_node) = tree.get_render_node(node) {
2581 if let Some(ref stable_key) = render_node.props.motion_stable_id {
2582 return rs.get_stable_motion_values(stable_key);
2583 }
2584 }
2585 rs.get_motion_values(node)
2586 });
2587
2588 let binding_scale = tree.get_motion_scale(node);
2593 let binding_opacity = tree.get_motion_opacity(node);
2594
2595 let node_motion_opacity = motion_values
2597 .and_then(|m| m.opacity)
2598 .unwrap_or_else(|| binding_opacity.unwrap_or(1.0));
2599
2600 let node_motion_translate = motion_values
2603 .map(|m| m.resolved_translate())
2604 .unwrap_or((0.0, 0.0));
2605
2606 let node_motion_scale = motion_values
2608 .map(|m| m.resolved_scale())
2609 .unwrap_or((1.0, 1.0));
2610
2611 let binding_scale_values = binding_scale.unwrap_or((1.0, 1.0));
2613
2614 let effective_motion_opacity = inherited_motion_opacity * node_motion_opacity;
2618 let effective_motion_translate = (
2619 inherited_motion_translate.0 + node_motion_translate.0,
2620 inherited_motion_translate.1 + node_motion_translate.1,
2621 );
2622 let effective_motion_scale = (
2624 inherited_motion_scale.0 * node_motion_scale.0 * binding_scale_values.0,
2625 inherited_motion_scale.1 * node_motion_scale.1 * binding_scale_values.1,
2626 );
2627
2628 let this_node_has_scale = (node_motion_scale.0 - 1.0).abs() > 0.001
2632 || (node_motion_scale.1 - 1.0).abs() > 0.001
2633 || (binding_scale_values.0 - 1.0).abs() > 0.001
2634 || (binding_scale_values.1 - 1.0).abs() > 0.001;
2635
2636 let effective_motion_scale_center = if this_node_has_scale {
2637 let center_x = abs_x + bounds.width / 2.0;
2639 let center_y = abs_y + bounds.height / 2.0;
2640 Some((center_x, center_y))
2641 } else {
2642 inherited_motion_scale_center
2644 };
2645
2646 if effective_motion_opacity <= 0.001 {
2648 return;
2649 }
2650
2651 if let Some(render_node) = tree.get_render_node(node) {
2653 if !render_node.props.visible {
2654 return;
2655 }
2656 }
2657
2658 let is_glass = tree
2660 .get_render_node(node)
2661 .map(|n| matches!(n.props.material, Some(Material::Glass(_))))
2662 .unwrap_or(false);
2663
2664 let children_inside_glass = inside_glass || is_glass;
2666
2667 let is_foreground_node = tree
2669 .get_render_node(node)
2670 .map(|n| n.props.layer == RenderLayer::Foreground)
2671 .unwrap_or(false);
2672 let children_inside_foreground = inside_foreground || is_foreground_node;
2673
2674 let clips_content = tree
2676 .get_render_node(node)
2677 .map(|n| n.props.clips_content)
2678 .unwrap_or(false);
2679
2680 let has_layout_animation = tree.is_layout_animating(node);
2683
2684 let is_stack_layer = tree
2686 .get_render_node(node)
2687 .map(|n| n.props.is_stack_layer)
2688 .unwrap_or(false);
2689 if is_stack_layer {
2690 *z_layer += 1;
2691 }
2692
2693 let saved_z_layer = *z_layer;
2695 let node_z_index = tree
2696 .get_render_node(node)
2697 .map(|n| n.props.z_index)
2698 .unwrap_or(0);
2699 if node_z_index > 0 {
2700 *z_layer = node_z_index as u32;
2701 }
2702
2703 let should_clip = clips_content || has_layout_animation;
2707 let (child_clip, child_clip_radius, child_scroll_clip) = if should_clip {
2708 let clip_bounds = if has_layout_animation {
2711 tree.get_render_bounds(node, parent_offset)
2713 .map(|b| [b.x, b.y, b.width, b.height])
2714 .unwrap_or([abs_x, abs_y, bounds.width, bounds.height])
2715 } else {
2716 [abs_x, abs_y, bounds.width, bounds.height]
2717 };
2718 let bw = tree
2722 .get_render_node(node)
2723 .map(|n| n.props.border_width)
2724 .unwrap_or(0.0);
2725 let this_clip = [
2726 clip_bounds[0] + bw,
2727 clip_bounds[1] + bw,
2728 (clip_bounds[2] - bw * 2.0).max(0.0),
2729 (clip_bounds[3] - bw * 2.0).max(0.0),
2730 ];
2731
2732 let this_clip_radius = tree.get_render_node(node).map(|n| {
2735 let r = &n.props.border_radius;
2736 [
2737 (r.top_left - bw).max(0.0),
2738 (r.top_right - bw).max(0.0),
2739 (r.bottom_right - bw).max(0.0),
2740 (r.bottom_left - bw).max(0.0),
2741 ]
2742 });
2743
2744 let this_has_radius = this_clip_radius
2745 .map(|r| r.iter().any(|&v| v > 0.5))
2746 .unwrap_or(false);
2747 let parent_has_radius = current_clip_radius
2748 .map(|r| r.iter().any(|&v| v > 0.5))
2749 .unwrap_or(false);
2750
2751 if let Some(parent_clip) = current_clip {
2752 if this_has_radius && !parent_has_radius {
2753 (
2758 Some(this_clip),
2759 this_clip_radius,
2760 merge_scroll_clip(parent_clip, current_scroll_clip),
2761 )
2762 } else if !this_has_radius && parent_has_radius {
2763 (
2767 current_clip,
2768 current_clip_radius,
2769 merge_scroll_clip(this_clip, current_scroll_clip),
2770 )
2771 } else {
2772 let x1 = parent_clip[0].max(this_clip[0]);
2774 let y1 = parent_clip[1].max(this_clip[1]);
2775 let parent_right = parent_clip[0] + parent_clip[2];
2776 let parent_bottom = parent_clip[1] + parent_clip[3];
2777 let this_right = this_clip[0] + this_clip[2];
2778 let this_bottom = this_clip[1] + this_clip[3];
2779 let x2 = parent_right.min(this_right);
2780 let y2 = parent_bottom.min(this_bottom);
2781 let w = (x2 - x1).max(0.0);
2782 let h = (y2 - y1).max(0.0);
2783 let clip = Some([x1, y1, w, h]);
2784
2785 let child_r = this_clip_radius.unwrap_or([0.0; 4]);
2786 let parent_r = current_clip_radius.unwrap_or([0.0; 4]);
2787 let radius = Some([
2788 child_r[0].max(parent_r[0]),
2789 child_r[1].max(parent_r[1]),
2790 child_r[2].max(parent_r[2]),
2791 child_r[3].max(parent_r[3]),
2792 ]);
2793
2794 (clip, radius, current_scroll_clip)
2795 }
2796 } else {
2797 if this_has_radius {
2799 (Some(this_clip), this_clip_radius, current_scroll_clip)
2801 } else {
2802 let new_scroll_clip = if let Some(existing) = current_scroll_clip {
2805 let x1 = existing[0].max(this_clip[0]);
2806 let y1 = existing[1].max(this_clip[1]);
2807 let x2 = (existing[0] + existing[2]).min(this_clip[0] + this_clip[2]);
2808 let y2 = (existing[1] + existing[3]).min(this_clip[1] + this_clip[3]);
2809 [x1, y1, (x2 - x1).max(0.0), (y2 - y1).max(0.0)]
2810 } else {
2811 this_clip
2812 };
2813 (None, None, Some(new_scroll_clip))
2814 }
2815 }
2816 } else {
2817 (current_clip, current_clip_radius, current_scroll_clip)
2818 };
2819
2820 let node_css_affine = if let Some(render_node) = tree.get_render_node(node) {
2828 let has_non_identity = if let Some(blinc_core::Transform::Affine2D(affine)) =
2829 &render_node.props.transform
2830 {
2831 let [a, b, c, d, tx, ty] = affine.elements;
2832 !((a - 1.0).abs() < 0.0001
2833 && b.abs() < 0.0001
2834 && c.abs() < 0.0001
2835 && (d - 1.0).abs() < 0.0001
2836 && tx.abs() < 0.0001
2837 && ty.abs() < 0.0001)
2838 } else {
2839 false
2840 };
2841
2842 if has_non_identity {
2843 let affine = match &render_node.props.transform {
2844 Some(blinc_core::Transform::Affine2D(a)) => a.elements,
2845 _ => unreachable!(),
2846 };
2847 let [a, b, c, d, tx, ty] = affine;
2848 let (cx, cy) = if let Some([ox_pct, oy_pct]) = render_node.props.transform_origin {
2850 (
2851 abs_x + bounds.width * ox_pct / 100.0,
2852 abs_y + bounds.height * oy_pct / 100.0,
2853 )
2854 } else {
2855 (abs_x + bounds.width / 2.0, abs_y + bounds.height / 2.0)
2856 };
2857 let this_affine = [
2860 a,
2861 b,
2862 c,
2863 d,
2864 cx * (1.0 - a) - cy * c + tx,
2865 cy * (1.0 - d) - cx * b + ty,
2866 ];
2867 match inherited_css_affine {
2868 Some(parent) => {
2869 let [pa, pb, pc, pd, ptx, pty] = parent;
2870 Some([
2871 a * pa + c * pb,
2872 b * pa + d * pb,
2873 a * pc + c * pd,
2874 b * pc + d * pd,
2875 a * ptx + c * pty + this_affine[4],
2876 b * ptx + d * pty + this_affine[5],
2877 ])
2878 }
2879 None => Some(this_affine),
2880 }
2881 } else {
2882 inherited_css_affine
2883 }
2884 } else {
2885 inherited_css_affine
2886 };
2887
2888 if let Some(render_node) = tree.get_render_node(node) {
2889 let effective_layer = if inside_glass && !is_glass {
2891 RenderLayer::Foreground
2892 } else if is_glass {
2893 RenderLayer::Glass
2894 } else {
2895 render_node.props.layer
2896 };
2897
2898 match &render_node.element_type {
2899 ElementType::Text(text_data) => {
2900 let base_x = abs_x * scale;
2904 let base_y = abs_y * scale;
2905 let base_width = bounds.width * scale;
2906 let base_height = bounds.height * scale;
2907
2908 let scaled_motion_tx = effective_motion_translate.0 * scale;
2910 let scaled_motion_ty = effective_motion_translate.1 * scale;
2911
2912 let (scaled_x, scaled_y, scaled_width, scaled_height) =
2919 if let Some((motion_center_x, motion_center_y)) =
2920 effective_motion_scale_center
2921 {
2922 let motion_center_x_scaled = motion_center_x * scale;
2924 let motion_center_y_scaled = motion_center_y * scale;
2925
2926 let rel_x = base_x - motion_center_x_scaled;
2928 let rel_y = base_y - motion_center_y_scaled;
2929
2930 let scaled_rel_x = rel_x * effective_motion_scale.0;
2932 let scaled_rel_y = rel_y * effective_motion_scale.1;
2933 let scaled_w = base_width * effective_motion_scale.0;
2934 let scaled_h = base_height * effective_motion_scale.1;
2935
2936 let final_x = motion_center_x_scaled + scaled_rel_x + scaled_motion_tx;
2938 let final_y = motion_center_y_scaled + scaled_rel_y + scaled_motion_ty;
2939
2940 (final_x, final_y, scaled_w, scaled_h)
2941 } else {
2942 let final_x = base_x + scaled_motion_tx;
2944 let final_y = base_y + scaled_motion_ty;
2945 (final_x, final_y, base_width, base_height)
2946 };
2947
2948 let base_font_size = render_node.props.font_size.unwrap_or(text_data.font_size);
2950 let scaled_font_size = base_font_size * effective_motion_scale.1 * scale;
2951 let scaled_measured_width =
2952 text_data.measured_width * effective_motion_scale.0 * scale;
2953
2954 let effective_clip = effective_single_clip(current_clip, current_scroll_clip);
2957 let scaled_clip = effective_clip
2958 .map(|[cx, cy, cw, ch]| [cx * scale, cy * scale, cw * scale, ch * scale]);
2959
2960 if effective_motion_translate.0.abs() > 0.1
2962 || effective_motion_translate.1.abs() > 0.1
2963 || (effective_motion_scale.0 - 1.0).abs() > 0.01
2964 || (effective_motion_scale.1 - 1.0).abs() > 0.01
2965 {
2966 tracing::trace!(
2967 "Text '{}': motion_translate=({:.1}, {:.1}), motion_scale=({:.2}, {:.2}), base=({:.1}, {:.1}), final=({:.1}, {:.1})",
2968 text_data.content,
2969 effective_motion_translate.0,
2970 effective_motion_translate.1,
2971 effective_motion_scale.0,
2972 effective_motion_scale.1,
2973 base_x,
2974 base_y,
2975 scaled_x,
2976 scaled_y,
2977 );
2978 }
2979 tracing::trace!(
2980 "Text '{}': abs=({:.1}, {:.1}), size=({:.1}x{:.1}), font={:.1}, align={:?}, v_align={:?}, z_layer={}",
2981 text_data.content,
2982 scaled_x,
2983 scaled_y,
2984 scaled_width,
2985 scaled_height,
2986 scaled_font_size,
2987 text_data.align,
2988 text_data.v_align,
2989 *z_layer
2990 );
2991
2992 let is_nowrap = !text_data.wrap
2996 || matches!(
2997 render_node.props.white_space,
2998 Some(blinc_layout::element_style::WhiteSpace::Nowrap)
2999 | Some(blinc_layout::element_style::WhiteSpace::Pre)
3000 );
3001 let content = if is_nowrap
3002 && matches!(
3003 render_node.props.text_overflow,
3004 Some(blinc_layout::element_style::TextOverflow::Ellipsis)
3005 )
3006 && scaled_measured_width > scaled_width
3007 && scaled_width > 0.0
3008 {
3009 let mut options = blinc_layout::text_measure::TextLayoutOptions::new();
3011 options.font_name = text_data.font_family.name.clone();
3012 options.generic_font = text_data.font_family.generic;
3013 options.font_weight =
3014 match render_node.props.font_weight.unwrap_or(text_data.weight) {
3015 FontWeight::Bold => 700,
3016 FontWeight::Normal => 400,
3017 FontWeight::Light => 300,
3018 _ => 400,
3019 };
3020 options.letter_spacing = render_node
3021 .props
3022 .letter_spacing
3023 .unwrap_or(text_data.letter_spacing);
3024
3025 let ellipsis = "\u{2026}";
3027 let ellipsis_w = blinc_layout::text_measure::measure_text_with_options(
3028 ellipsis,
3029 scaled_font_size / scale,
3030 &options,
3031 )
3032 .width
3033 * scale;
3034 let target_width = scaled_width - ellipsis_w;
3035
3036 if target_width > 0.0 {
3037 let chars: Vec<char> = text_data.content.chars().collect();
3039 let mut lo = 0usize;
3040 let mut hi = chars.len();
3041 while lo < hi {
3042 #[allow(clippy::manual_div_ceil)]
3043 let mid = (lo + hi + 1) / 2;
3044 let sub: String = chars[..mid].iter().collect();
3045 let w = blinc_layout::text_measure::measure_text_with_options(
3046 &sub,
3047 scaled_font_size / scale,
3048 &options,
3049 )
3050 .width
3051 * scale;
3052 if w <= target_width {
3053 lo = mid;
3054 } else {
3055 hi = mid - 1;
3056 }
3057 }
3058 let truncated: String = chars[..lo].iter().collect();
3059 format!("{}{}", truncated.trim_end(), ellipsis)
3060 } else {
3061 ellipsis.to_string()
3062 }
3063 } else {
3064 text_data.content.clone()
3065 };
3066
3067 texts.push(TextElement {
3068 content,
3069 x: scaled_x,
3070 y: scaled_y,
3071 width: scaled_width,
3072 height: scaled_height,
3073 font_size: scaled_font_size,
3074 color: render_node.props.text_color.unwrap_or(text_data.color),
3075 align: text_data.align,
3076 weight: render_node.props.font_weight.unwrap_or(text_data.weight),
3077 italic: text_data.italic,
3078 v_align: text_data.v_align,
3079 clip_bounds: scaled_clip,
3080 motion_opacity: effective_motion_opacity
3081 * render_node.props.opacity
3082 * inherited_css_opacity,
3083 wrap: !is_nowrap && text_data.wrap,
3084 line_height: text_data.line_height,
3085 measured_width: scaled_measured_width,
3086 font_family: text_data.font_family.clone(),
3087 word_spacing: text_data.word_spacing,
3088 letter_spacing: render_node
3089 .props
3090 .letter_spacing
3091 .unwrap_or(text_data.letter_spacing),
3092 z_index: *z_layer,
3093 ascender: text_data.ascender * effective_motion_scale.1 * scale,
3094 strikethrough: render_node.props.text_decoration.map_or(
3095 text_data.strikethrough,
3096 |td| {
3097 matches!(
3098 td,
3099 blinc_layout::element_style::TextDecoration::LineThrough
3100 )
3101 },
3102 ),
3103 underline: render_node.props.text_decoration.map_or(
3104 text_data.underline,
3105 |td| {
3106 matches!(td, blinc_layout::element_style::TextDecoration::Underline)
3107 },
3108 ),
3109 decoration_color: render_node.props.text_decoration_color,
3110 decoration_thickness: render_node.props.text_decoration_thickness,
3111 css_affine: node_css_affine,
3112 text_shadow: render_node.props.text_shadow,
3113 transform_3d_layer: inside_3d_layer.clone(),
3114 is_foreground: children_inside_foreground,
3115 });
3116 }
3117 ElementType::Svg(svg_data) => {
3118 let base_x = abs_x * scale;
3120 let base_y = abs_y * scale;
3121 let base_width = bounds.width * scale;
3122 let base_height = bounds.height * scale;
3123
3124 let scaled_motion_tx = effective_motion_translate.0 * scale;
3126 let scaled_motion_ty = effective_motion_translate.1 * scale;
3127
3128 let (scaled_x, scaled_y, scaled_width, scaled_height) =
3130 if let Some((motion_center_x, motion_center_y)) =
3131 effective_motion_scale_center
3132 {
3133 let motion_center_x_scaled = motion_center_x * scale;
3134 let motion_center_y_scaled = motion_center_y * scale;
3135
3136 let rel_x = base_x - motion_center_x_scaled;
3137 let rel_y = base_y - motion_center_y_scaled;
3138
3139 let scaled_rel_x = rel_x * effective_motion_scale.0;
3140 let scaled_rel_y = rel_y * effective_motion_scale.1;
3141 let scaled_w = base_width * effective_motion_scale.0;
3142 let scaled_h = base_height * effective_motion_scale.1;
3143
3144 let final_x = motion_center_x_scaled + scaled_rel_x + scaled_motion_tx;
3145 let final_y = motion_center_y_scaled + scaled_rel_y + scaled_motion_ty;
3146
3147 (final_x, final_y, scaled_w, scaled_h)
3148 } else {
3149 let final_x = base_x + scaled_motion_tx;
3150 let final_y = base_y + scaled_motion_ty;
3151 (final_x, final_y, base_width, base_height)
3152 };
3153
3154 let effective_clip = effective_single_clip(current_clip, current_scroll_clip);
3157 let scaled_clip = effective_clip
3158 .map(|[cx, cy, cw, ch]| [cx * scale, cy * scale, cw * scale, ch * scale]);
3159
3160 svgs.push(SvgElement {
3164 source: svg_data.source.clone(),
3165 x: scaled_x,
3166 y: scaled_y,
3167 width: scaled_width,
3168 height: scaled_height,
3169 tint: svg_data.tint.or_else(|| {
3170 render_node
3171 .props
3172 .text_color
3173 .map(|c| blinc_core::Color::rgba(c[0], c[1], c[2], c[3]))
3174 }),
3175 fill: render_node
3176 .props
3177 .fill
3178 .map(|c| blinc_core::Color::rgba(c[0], c[1], c[2], c[3]))
3179 .or(svg_data.fill),
3180 stroke: render_node
3181 .props
3182 .stroke
3183 .map(|c| blinc_core::Color::rgba(c[0], c[1], c[2], c[3]))
3184 .or(svg_data.stroke),
3185 stroke_width: render_node.props.stroke_width.or(svg_data.stroke_width),
3186 stroke_dasharray: render_node.props.stroke_dasharray.clone(),
3187 stroke_dashoffset: render_node.props.stroke_dashoffset,
3188 svg_path_data: render_node.props.svg_path_data.clone(),
3189 clip_bounds: scaled_clip,
3190 motion_opacity: effective_motion_opacity
3191 * render_node.props.opacity
3192 * inherited_css_opacity,
3193 css_affine: node_css_affine,
3194 tag_overrides: render_node.props.svg_tag_styles.clone(),
3195 transform_3d_layer: inside_3d_layer.clone(),
3196 });
3197 }
3198 ElementType::Image(image_data) => {
3199 let scaled_clip = current_clip
3201 .map(|[cx, cy, cw, ch]| [cx * scale, cy * scale, cw * scale, ch * scale]);
3202
3203 let scaled_clip_radius = current_clip_radius
3205 .map(|[tl, tr, br, bl]| [tl * scale, tr * scale, br * scale, bl * scale])
3206 .unwrap_or([0.0; 4]);
3207
3208 let scaled_scroll_clip = current_scroll_clip
3210 .map(|[cx, cy, cw, ch]| [cx * scale, cy * scale, cw * scale, ch * scale]);
3211
3212 let parent_props = parent_node
3216 .and_then(|pid| tree.get_render_node(pid))
3217 .map(|pn| &pn.props);
3218
3219 let own_css_opacity = render_node.props.opacity;
3221 let final_opacity = image_data.opacity
3222 * own_css_opacity
3223 * inherited_css_opacity
3224 * effective_motion_opacity;
3225
3226 let own_br = render_node.props.border_radius.top_left;
3229 let final_border_radius = if own_br > 0.0 {
3230 own_br * scale
3231 } else {
3232 image_data.border_radius * scale
3233 };
3234
3235 let border_width = render_node.props.border_width * scale;
3238 let border_color = render_node
3239 .props
3240 .border_color
3241 .unwrap_or(blinc_core::Color::TRANSPARENT);
3242
3243 let shadow = render_node.props.shadow;
3245
3246 let own_filter = &render_node.props.filter;
3248 let parent_filter = parent_props.and_then(|p| p.filter.as_ref());
3249 let effective_filter = own_filter.as_ref().or(parent_filter);
3250 let filter_a = effective_filter
3251 .map(|f| Self::css_filter_to_arrays(f).0)
3252 .unwrap_or([0.0, 0.0, 0.0, 0.0]);
3253 let filter_b = effective_filter
3254 .map(|f| Self::css_filter_to_arrays(f).1)
3255 .unwrap_or([1.0, 1.0, 1.0, 0.0]);
3256
3257 let final_object_fit = render_node
3259 .props
3260 .object_fit
3261 .unwrap_or(image_data.object_fit);
3262 let final_object_position = render_node
3263 .props
3264 .object_position
3265 .unwrap_or(image_data.object_position);
3266
3267 let final_loading_strategy = render_node
3269 .props
3270 .loading_strategy
3271 .unwrap_or(image_data.loading_strategy);
3272 let final_placeholder_type = render_node
3273 .props
3274 .placeholder_type
3275 .unwrap_or(image_data.placeholder_type);
3276 let final_placeholder_color = render_node
3277 .props
3278 .placeholder_color
3279 .unwrap_or(image_data.placeholder_color);
3280 let final_placeholder_image = render_node
3281 .props
3282 .placeholder_image
3283 .clone()
3284 .or_else(|| image_data.placeholder_image.clone());
3285 let final_fade_duration = render_node
3286 .props
3287 .fade_duration_ms
3288 .unwrap_or(image_data.fade_duration_ms);
3289
3290 let own_mask = render_node.props.mask_image.as_ref();
3292 let parent_mask = parent_props.and_then(|p| p.mask_image.as_ref());
3293 let effective_mask = own_mask.or(parent_mask);
3294 let (mask_params, mask_info) = Self::mask_image_to_arrays(effective_mask);
3295
3296 images.push(ImageElement {
3297 source: image_data.source.clone(),
3298 x: abs_x * scale,
3299 y: abs_y * scale,
3300 width: bounds.width * scale,
3301 height: bounds.height * scale,
3302 object_fit: final_object_fit,
3303 object_position: final_object_position,
3304 opacity: final_opacity,
3305 border_radius: final_border_radius,
3306 tint: image_data.tint,
3307 clip_bounds: scaled_clip,
3308 clip_radius: scaled_clip_radius,
3309 layer: effective_layer,
3310 loading_strategy: final_loading_strategy,
3311 placeholder_type: final_placeholder_type,
3312 placeholder_color: final_placeholder_color,
3313 placeholder_image: final_placeholder_image,
3314 fade_duration_ms: final_fade_duration,
3315 z_index: *z_layer,
3316 border_width,
3317 border_color,
3318 css_affine: node_css_affine,
3319 shadow,
3320 filter_a,
3321 filter_b,
3322 scroll_clip: scaled_scroll_clip,
3323 mask_params,
3324 mask_info,
3325 transform_3d_layer: inside_3d_layer.clone(),
3326 });
3327 }
3328 ElementType::Canvas(_) => {}
3330 ElementType::Div => {
3331 if let Some(blinc_core::Brush::Image(ref img_brush)) =
3333 render_node.props.background
3334 {
3335 let scaled_clip = current_clip.map(|[cx, cy, cw, ch]| {
3336 [cx * scale, cy * scale, cw * scale, ch * scale]
3337 });
3338 let scaled_clip_radius = current_clip_radius
3339 .map(|[tl, tr, br, bl]| {
3340 [tl * scale, tr * scale, br * scale, bl * scale]
3341 })
3342 .unwrap_or([0.0; 4]);
3343 let scaled_scroll_clip_bg = current_scroll_clip.map(|[cx, cy, cw, ch]| {
3344 [cx * scale, cy * scale, cw * scale, ch * scale]
3345 });
3346
3347 images.push(ImageElement {
3348 source: img_brush.source.clone(),
3349 x: abs_x * scale,
3350 y: abs_y * scale,
3351 width: bounds.width * scale,
3352 height: bounds.height * scale,
3353 object_fit: match img_brush.fit {
3354 blinc_core::ImageFit::Cover => 0,
3355 blinc_core::ImageFit::Contain => 1,
3356 blinc_core::ImageFit::Fill => 2,
3357 blinc_core::ImageFit::Tile => 0,
3358 },
3359 object_position: [img_brush.position.x, img_brush.position.y],
3360 opacity: img_brush.opacity
3361 * render_node.props.opacity
3362 * inherited_css_opacity
3363 * effective_motion_opacity,
3364 border_radius: render_node.props.border_radius.top_left * scale,
3365 tint: [
3366 img_brush.tint.r,
3367 img_brush.tint.g,
3368 img_brush.tint.b,
3369 img_brush.tint.a,
3370 ],
3371 clip_bounds: scaled_clip,
3372 clip_radius: scaled_clip_radius,
3373 layer: effective_layer,
3374 loading_strategy: 0, placeholder_type: 0, placeholder_color: [0.0; 4],
3377 placeholder_image: None,
3378 fade_duration_ms: 0,
3379 z_index: *z_layer,
3380 border_width: 0.0,
3381 border_color: blinc_core::Color::TRANSPARENT,
3382 css_affine: node_css_affine,
3383 shadow: render_node.props.shadow,
3384 filter_a: render_node
3385 .props
3386 .filter
3387 .as_ref()
3388 .map(|f| Self::css_filter_to_arrays(f).0)
3389 .unwrap_or([0.0, 0.0, 0.0, 0.0]),
3390 filter_b: render_node
3391 .props
3392 .filter
3393 .as_ref()
3394 .map(|f| Self::css_filter_to_arrays(f).1)
3395 .unwrap_or([1.0, 1.0, 1.0, 0.0]),
3396 scroll_clip: scaled_scroll_clip_bg,
3397 mask_params: {
3398 let (mp, _) = Self::mask_image_to_arrays(
3399 render_node.props.mask_image.as_ref(),
3400 );
3401 mp
3402 },
3403 mask_info: {
3404 let (_, mi) = Self::mask_image_to_arrays(
3405 render_node.props.mask_image.as_ref(),
3406 );
3407 mi
3408 },
3409 transform_3d_layer: inside_3d_layer.clone(),
3410 });
3411 }
3412 }
3413 ElementType::StyledText(styled_data) => {
3415 let base_x = abs_x * scale;
3417 let base_y = abs_y * scale;
3418 let base_width = bounds.width * scale;
3419 let base_height = bounds.height * scale;
3420
3421 let scaled_motion_tx = effective_motion_translate.0 * scale;
3423 let scaled_motion_ty = effective_motion_translate.1 * scale;
3424
3425 let (scaled_x, scaled_y, scaled_width, scaled_height) =
3427 if let Some((motion_center_x, motion_center_y)) =
3428 effective_motion_scale_center
3429 {
3430 let motion_center_x_scaled = motion_center_x * scale;
3431 let motion_center_y_scaled = motion_center_y * scale;
3432
3433 let rel_x = base_x - motion_center_x_scaled;
3434 let rel_y = base_y - motion_center_y_scaled;
3435
3436 let scaled_rel_x = rel_x * effective_motion_scale.0;
3437 let scaled_rel_y = rel_y * effective_motion_scale.1;
3438 let scaled_w = base_width * effective_motion_scale.0;
3439 let scaled_h = base_height * effective_motion_scale.1;
3440
3441 let final_x = motion_center_x_scaled + scaled_rel_x + scaled_motion_tx;
3442 let final_y = motion_center_y_scaled + scaled_rel_y + scaled_motion_ty;
3443
3444 (final_x, final_y, scaled_w, scaled_h)
3445 } else {
3446 let final_x = base_x + scaled_motion_tx;
3447 let final_y = base_y + scaled_motion_ty;
3448 (final_x, final_y, base_width, base_height)
3449 };
3450
3451 let base_styled_font_size =
3453 render_node.props.font_size.unwrap_or(styled_data.font_size);
3454 let scaled_font_size = base_styled_font_size * effective_motion_scale.1 * scale;
3455 let effective_clip = effective_single_clip(current_clip, current_scroll_clip);
3457 let scaled_clip = effective_clip
3458 .map(|[cx, cy, cw, ch]| [cx * scale, cy * scale, cw * scale, ch * scale]);
3459
3460 let content = &styled_data.content;
3463 let content_len = content.len();
3464
3465 let default_bold = styled_data.weight == FontWeight::Bold;
3467 let default_italic = styled_data.italic;
3468
3469 let mut boundaries: Vec<usize> = vec![0, content_len];
3471 for span in &styled_data.spans {
3472 if span.start < content_len {
3473 boundaries.push(span.start);
3474 }
3475 if span.end <= content_len {
3476 boundaries.push(span.end);
3477 }
3478 }
3479 boundaries.sort();
3480 boundaries.dedup();
3481
3482 #[allow(clippy::type_complexity)]
3484 let mut segments: Vec<(
3485 usize,
3486 usize,
3487 [f32; 4],
3488 bool,
3489 bool,
3490 bool,
3491 bool,
3492 )> = Vec::new();
3493
3494 for window in boundaries.windows(2) {
3495 let seg_start = window[0];
3496 let seg_end = window[1];
3497 if seg_start >= seg_end {
3498 continue;
3499 }
3500
3501 let mut color: Option<[f32; 4]> = None;
3503 let mut bold = default_bold;
3504 let mut italic = default_italic;
3505 let mut underline = false;
3506 let mut strikethrough = false;
3507
3508 for span in &styled_data.spans {
3509 if span.start <= seg_start && span.end >= seg_end {
3511 if span.bold {
3513 bold = true;
3514 }
3515 if span.italic {
3516 italic = true;
3517 }
3518 if span.underline {
3519 underline = true;
3520 }
3521 if span.strikethrough {
3522 strikethrough = true;
3523 }
3524 if span.color[3] > 0.0 {
3526 color = Some(span.color);
3527 }
3528 }
3529 }
3530
3531 let default_color = render_node
3533 .props
3534 .text_color
3535 .unwrap_or(styled_data.default_color);
3536 let final_color = color.unwrap_or(default_color);
3537 segments.push((
3538 seg_start,
3539 seg_end,
3540 final_color,
3541 bold,
3542 italic,
3543 underline,
3544 strikethrough,
3545 ));
3546 }
3547
3548 let scaled_ascender = styled_data.ascender * scale;
3550
3551 let mut x_offset = 0.0f32;
3553 for (start, end, color, bold, italic, underline, strikethrough) in segments {
3554 if start >= end || start >= content.len() {
3555 continue;
3556 }
3557 let segment_text = &content[start..end.min(content.len())];
3558 if segment_text.is_empty() {
3559 continue;
3560 }
3561
3562 let mut options = blinc_layout::text_measure::TextLayoutOptions::new();
3564 options.font_name = styled_data.font_family.name.clone();
3565 options.generic_font = styled_data.font_family.generic;
3566 options.font_weight = if bold { 700 } else { 400 };
3567 options.italic = italic;
3568 let metrics = blinc_layout::text_measure::measure_text_with_options(
3569 segment_text,
3570 styled_data.font_size,
3571 &options,
3572 );
3573 let segment_width = metrics.width * scale * effective_motion_scale.0;
3575
3576 texts.push(TextElement {
3577 content: segment_text.to_string(),
3578 x: scaled_x + x_offset,
3579 y: scaled_y,
3580 width: segment_width,
3581 height: scaled_height,
3582 font_size: scaled_font_size,
3583 color,
3584 align: TextAlign::Left, weight: if bold {
3586 FontWeight::Bold
3587 } else {
3588 FontWeight::Normal
3589 },
3590 italic,
3591 v_align: styled_data.v_align,
3592 clip_bounds: scaled_clip,
3593 motion_opacity: effective_motion_opacity
3594 * render_node.props.opacity
3595 * inherited_css_opacity,
3596 wrap: false, line_height: styled_data.line_height,
3598 measured_width: segment_width,
3599 font_family: styled_data.font_family.clone(),
3600 word_spacing: 0.0,
3601 letter_spacing: render_node.props.letter_spacing.unwrap_or(0.0),
3602 z_index: *z_layer,
3603 ascender: scaled_ascender * effective_motion_scale.1, strikethrough,
3605 underline,
3606 decoration_color: render_node.props.text_decoration_color,
3607 decoration_thickness: render_node.props.text_decoration_thickness,
3608 css_affine: node_css_affine,
3609 text_shadow: render_node.props.text_shadow,
3610 transform_3d_layer: inside_3d_layer.clone(),
3611 is_foreground: children_inside_foreground,
3612 });
3613
3614 x_offset += segment_width;
3615 }
3616 }
3617 }
3618
3619 if let Some(ref flow_name) = render_node.props.flow {
3622 flows.push(FlowElement {
3623 flow_name: flow_name.clone(),
3624 flow_graph: render_node.props.flow_graph.clone(),
3625 x: abs_x * scale,
3626 y: abs_y * scale,
3627 width: bounds.width * scale,
3628 height: bounds.height * scale,
3629 z_index: *z_layer,
3630 corner_radius: render_node.props.border_radius.top_left * scale,
3631 });
3632 }
3633 }
3634
3635 let scroll_offset = tree.get_scroll_offset(node);
3637 let static_motion_offset = tree
3638 .get_motion_transform(node)
3639 .map(|t| match t {
3640 blinc_core::Transform::Affine2D(a) => (a.elements[4], a.elements[5]),
3641 _ => (0.0, 0.0),
3642 })
3643 .unwrap_or((0.0, 0.0));
3644
3645 let new_offset = (
3646 abs_x + scroll_offset.0 + static_motion_offset.0,
3647 abs_y + scroll_offset.1 + static_motion_offset.1,
3648 );
3649
3650 let child_css_opacity = if let Some(rn) = tree.get_render_node(node) {
3653 inherited_css_opacity * rn.props.opacity
3654 } else {
3655 inherited_css_opacity
3656 };
3657
3658 let child_3d_layer = if let Some(rn) = tree.get_render_node(node) {
3661 let has_3d = rn.props.rotate_x.is_some()
3662 || rn.props.rotate_y.is_some()
3663 || rn.props.perspective.is_some();
3664 if has_3d {
3665 let rx = rn.props.rotate_x.unwrap_or(0.0).to_radians();
3666 let ry = rn.props.rotate_y.unwrap_or(0.0).to_radians();
3667 let d = rn.props.perspective.unwrap_or(800.0) * scale;
3668 Some(Transform3DLayerInfo {
3669 node_id: node,
3670 layer_bounds: [
3671 abs_x * scale,
3672 abs_y * scale,
3673 bounds.width * scale,
3674 bounds.height * scale,
3675 ],
3676 transform_3d: blinc_core::Transform3DParams {
3677 sin_rx: rx.sin(),
3678 cos_rx: rx.cos(),
3679 sin_ry: ry.sin(),
3680 cos_ry: ry.cos(),
3681 perspective_d: d,
3682 },
3683 opacity: rn.props.opacity,
3684 })
3685 } else {
3686 inside_3d_layer.clone()
3687 }
3688 } else {
3689 inside_3d_layer.clone()
3690 };
3691
3692 for child_id in tree.layout().children(node) {
3693 self.collect_elements_recursive(
3694 tree,
3695 child_id,
3696 new_offset,
3697 children_inside_glass,
3698 children_inside_foreground,
3699 child_clip,
3700 child_clip_radius,
3701 effective_motion_opacity,
3702 effective_motion_translate,
3703 effective_motion_scale,
3704 effective_motion_scale_center,
3705 render_state,
3706 scale,
3707 z_layer,
3708 texts,
3709 svgs,
3710 images,
3711 flows,
3712 node_css_affine,
3713 child_css_opacity,
3714 Some(node), child_scroll_clip,
3716 child_3d_layer.clone(),
3717 );
3718 }
3719
3720 if node_z_index > 0 {
3722 *z_layer = saved_z_layer;
3723 }
3724 }
3725
3726 pub fn device(&self) -> &Arc<wgpu::Device> {
3728 &self.device
3729 }
3730
3731 pub fn queue(&self) -> &Arc<wgpu::Queue> {
3733 &self.queue
3734 }
3735
3736 pub fn has_storage_buffers(&self) -> bool {
3739 self.renderer.has_storage_buffers()
3740 }
3741
3742 pub fn font_registry(&self) -> Arc<Mutex<FontRegistry>> {
3747 self.text_ctx.font_registry()
3748 }
3749
3750 pub fn texture_format(&self) -> wgpu::TextureFormat {
3752 self.renderer.texture_format()
3753 }
3754
3755 pub fn create_surface<W>(
3757 &self,
3758 window: Arc<W>,
3759 ) -> std::result::Result<wgpu::Surface<'static>, blinc_gpu::RendererError>
3760 where
3761 W: raw_window_handle::HasWindowHandle
3762 + raw_window_handle::HasDisplayHandle
3763 + Send
3764 + Sync
3765 + 'static,
3766 {
3767 self.renderer.create_surface(window)
3768 }
3769
3770 pub fn render_tree_with_state(
3779 &mut self,
3780 tree: &RenderTree,
3781 render_state: &blinc_layout::RenderState,
3782 width: u32,
3783 height: u32,
3784 target: &wgpu::TextureView,
3785 ) -> Result<()> {
3786 self.render_tree(tree, width, height, target)?;
3788
3789 self.render_overlays(render_state, width, height, target);
3791
3792 Ok(())
3793 }
3794
3795 pub fn render_tree_with_motion(
3804 &mut self,
3805 tree: &RenderTree,
3806 render_state: &blinc_layout::RenderState,
3807 width: u32,
3808 height: u32,
3809 target: &wgpu::TextureView,
3810 ) -> Result<()> {
3811 let scale_factor = tree.scale_factor();
3813
3814 let mut ctx =
3816 GpuPaintContext::with_text_context(width as f32, height as f32, &mut self.text_ctx);
3817
3818 tree.render_with_motion(&mut ctx, render_state);
3820
3821 let mut batch = ctx.take_batch();
3823
3824 let pending_meshes = ctx.take_pending_meshes();
3832
3833 let (all_texts, all_svgs, all_images, flow_elements) =
3835 self.collect_render_elements_with_state(tree, Some(render_state));
3836
3837 let mut texts = Vec::new();
3841 let mut fg_texts = Vec::new();
3842 let mut layer_3d_texts: std::collections::HashMap<
3843 LayoutNodeId,
3844 (Transform3DLayerInfo, Vec<TextElement>),
3845 > = std::collections::HashMap::new();
3846 for text in all_texts {
3847 if let Some(ref info) = text.transform_3d_layer {
3848 layer_3d_texts
3849 .entry(info.node_id)
3850 .or_insert_with(|| (info.clone(), Vec::new()))
3851 .1
3852 .push(text);
3853 } else if text.is_foreground {
3854 fg_texts.push(text);
3855 } else {
3856 texts.push(text);
3857 }
3858 }
3859
3860 let mut svgs = Vec::new();
3861 let mut layer_3d_svgs: std::collections::HashMap<LayoutNodeId, Vec<SvgElement>> =
3862 std::collections::HashMap::new();
3863 for svg in all_svgs {
3864 if let Some(ref info) = svg.transform_3d_layer {
3865 layer_3d_svgs.entry(info.node_id).or_default().push(svg);
3866 } else {
3867 svgs.push(svg);
3868 }
3869 }
3870
3871 let mut images = Vec::new();
3872 let mut layer_3d_images: std::collections::HashMap<LayoutNodeId, Vec<ImageElement>> =
3873 std::collections::HashMap::new();
3874 for image in all_images {
3875 if let Some(ref info) = image.transform_3d_layer {
3876 layer_3d_images.entry(info.node_id).or_default().push(image);
3877 } else {
3878 images.push(image);
3879 }
3880 }
3881
3882 let layer_3d_ids: Vec<LayoutNodeId> = layer_3d_texts.keys().cloned().collect();
3884
3885 self.preload_images(&images, width as f32, height as f32);
3887 for layer_imgs in layer_3d_images.values() {
3888 self.preload_images(layer_imgs, width as f32, height as f32);
3889 }
3890
3891 let mut glyphs_by_layer: std::collections::BTreeMap<u32, Vec<GpuGlyph>> =
3894 std::collections::BTreeMap::new();
3895 let mut css_transformed_text_prims: Vec<GpuPrimitive> = Vec::new();
3896 for text in &texts {
3897 if let Some([clip_x, clip_y, clip_w, clip_h]) = text.clip_bounds {
3900 let text_right = text.x + text.width;
3901 let text_bottom = text.y + text.height;
3902 let clip_right = clip_x + clip_w;
3903 let clip_bottom = clip_y + clip_h;
3904
3905 if text.x >= clip_right
3907 || text_right <= clip_x
3908 || text.y >= clip_bottom
3909 || text_bottom <= clip_y
3910 {
3911 continue;
3913 }
3914 }
3915
3916 let alignment = match text.align {
3917 TextAlign::Left => TextAlignment::Left,
3918 TextAlign::Center => TextAlignment::Center,
3919 TextAlign::Right => TextAlignment::Right,
3920 };
3921
3922 let color = if text.motion_opacity < 1.0 {
3924 [
3925 text.color[0],
3926 text.color[1],
3927 text.color[2],
3928 text.color[3] * text.motion_opacity,
3929 ]
3930 } else {
3931 text.color
3932 };
3933
3934 let effective_width = if let Some(clip) = text.clip_bounds {
3940 clip[2].min(text.width)
3942 } else {
3943 text.width
3944 };
3945
3946 let needs_wrap = text.wrap && effective_width < text.measured_width - 2.0;
3948
3949 let wrap_width = Some(text.width);
3952
3953 let font_name = text.font_family.name.as_deref();
3955 let generic = to_gpu_generic_font(text.font_family.generic);
3956 let font_weight = text.weight.weight();
3957
3958 let (anchor, y_pos, use_layout_height) = match text.v_align {
3960 TextVerticalAlign::Center => {
3961 (TextAnchor::Center, text.y + text.height / 2.0, false)
3962 }
3963 TextVerticalAlign::Top => (TextAnchor::Top, text.y, true),
3964 TextVerticalAlign::Baseline => {
3965 let baseline_y = text.y + text.ascender;
3966 (TextAnchor::Baseline, baseline_y, false)
3967 }
3968 };
3969 let layout_height = if use_layout_height {
3970 Some(text.height)
3971 } else {
3972 None
3973 };
3974
3975 if let Some(shadow) = &text.text_shadow {
3977 let shadow_color = [
3978 shadow.color.r,
3979 shadow.color.g,
3980 shadow.color.b,
3981 shadow.color.a * text.motion_opacity,
3982 ];
3983 let shadow_x = text.x + shadow.offset_x * scale_factor;
3984 let shadow_y = y_pos + shadow.offset_y * scale_factor;
3985 if let Ok(mut shadow_glyphs) = self.text_ctx.prepare_text_with_style(
3986 &text.content,
3987 shadow_x,
3988 shadow_y,
3989 text.font_size,
3990 shadow_color,
3991 anchor,
3992 alignment,
3993 wrap_width,
3994 needs_wrap,
3995 font_name,
3996 generic,
3997 font_weight,
3998 text.italic,
3999 layout_height,
4000 text.letter_spacing,
4001 ) {
4002 if let Some(clip) = text.clip_bounds {
4003 for glyph in &mut shadow_glyphs {
4004 glyph.clip_bounds = clip;
4005 }
4006 }
4007 if let Some(affine) = text.css_affine {
4008 let [a, b, c, d, tx, ty] = affine;
4009 let tx_scaled = tx * scale_factor;
4010 let ty_scaled = ty * scale_factor;
4011 for glyph in &shadow_glyphs {
4012 let gc_x = glyph.bounds[0] + glyph.bounds[2] / 2.0;
4013 let gc_y = glyph.bounds[1] + glyph.bounds[3] / 2.0;
4014 let new_gc_x = a * gc_x + c * gc_y + tx_scaled;
4015 let new_gc_y = b * gc_x + d * gc_y + ty_scaled;
4016 let mut prim = GpuPrimitive::from_glyph(glyph);
4017 prim.bounds = [
4018 new_gc_x - glyph.bounds[2] / 2.0,
4019 new_gc_y - glyph.bounds[3] / 2.0,
4020 glyph.bounds[2],
4021 glyph.bounds[3],
4022 ];
4023 prim.local_affine = [a, b, c, d];
4024 prim.set_z_layer(text.z_index);
4025 css_transformed_text_prims.push(prim);
4026 }
4027 } else {
4028 glyphs_by_layer
4029 .entry(text.z_index)
4030 .or_default()
4031 .extend(shadow_glyphs);
4032 }
4033 }
4034 }
4035
4036 match self.text_ctx.prepare_text_with_style(
4037 &text.content,
4038 text.x,
4039 y_pos,
4040 text.font_size,
4041 color,
4042 anchor,
4043 alignment,
4044 wrap_width,
4045 needs_wrap,
4046 font_name,
4047 generic,
4048 font_weight,
4049 text.italic,
4050 layout_height,
4051 text.letter_spacing,
4052 ) {
4053 Ok(mut glyphs) => {
4054 tracing::trace!(
4055 "render_tree_with_motion: prepared {} glyphs for '{}' (font={:?})",
4056 glyphs.len(),
4057 text.content,
4058 font_name
4059 );
4060 if let Some(clip) = text.clip_bounds {
4062 for glyph in &mut glyphs {
4063 glyph.clip_bounds = clip;
4064 }
4065 }
4066
4067 if let Some(affine) = text.css_affine {
4068 let [a, b, c, d, tx, ty] = affine;
4070 let tx_scaled = tx * scale_factor;
4071 let ty_scaled = ty * scale_factor;
4072 for glyph in &glyphs {
4073 let gc_x = glyph.bounds[0] + glyph.bounds[2] / 2.0;
4075 let gc_y = glyph.bounds[1] + glyph.bounds[3] / 2.0;
4076 let new_gc_x = a * gc_x + c * gc_y + tx_scaled;
4077 let new_gc_y = b * gc_x + d * gc_y + ty_scaled;
4078 let mut prim = GpuPrimitive::from_glyph(glyph);
4079 prim.bounds = [
4080 new_gc_x - glyph.bounds[2] / 2.0,
4081 new_gc_y - glyph.bounds[3] / 2.0,
4082 glyph.bounds[2],
4083 glyph.bounds[3],
4084 ];
4085 prim.local_affine = [a, b, c, d];
4086 prim.set_z_layer(text.z_index);
4087 css_transformed_text_prims.push(prim);
4088 }
4089 } else {
4090 glyphs_by_layer
4092 .entry(text.z_index)
4093 .or_default()
4094 .extend(glyphs);
4095 }
4096 }
4097 Err(e) => {
4098 tracing::warn!(
4099 "render_tree_with_motion: failed to prepare text '{}': {:?}",
4100 text.content,
4101 e
4102 );
4103 }
4104 }
4105 }
4106
4107 let mut fg_glyphs: Vec<GpuGlyph> = Vec::new();
4109 for text in &fg_texts {
4110 if let Some([clip_x, clip_y, clip_w, clip_h]) = text.clip_bounds {
4111 let text_right = text.x + text.width;
4112 let text_bottom = text.y + text.height;
4113 let clip_right = clip_x + clip_w;
4114 let clip_bottom = clip_y + clip_h;
4115 if text.x >= clip_right
4116 || text_right <= clip_x
4117 || text.y >= clip_bottom
4118 || text_bottom <= clip_y
4119 {
4120 continue;
4121 }
4122 }
4123
4124 let alignment = match text.align {
4125 TextAlign::Left => TextAlignment::Left,
4126 TextAlign::Center => TextAlignment::Center,
4127 TextAlign::Right => TextAlignment::Right,
4128 };
4129
4130 let color = if text.motion_opacity < 1.0 {
4131 [
4132 text.color[0],
4133 text.color[1],
4134 text.color[2],
4135 text.color[3] * text.motion_opacity,
4136 ]
4137 } else {
4138 text.color
4139 };
4140
4141 let effective_width = if let Some(clip) = text.clip_bounds {
4142 clip[2].min(text.width)
4143 } else {
4144 text.width
4145 };
4146 let needs_wrap = text.wrap && effective_width < text.measured_width - 2.0;
4147 let wrap_width = Some(text.width);
4148 let font_name = text.font_family.name.as_deref();
4149 let generic = to_gpu_generic_font(text.font_family.generic);
4150 let font_weight = text.weight.weight();
4151
4152 let (anchor, y_pos, use_layout_height) = match text.v_align {
4153 TextVerticalAlign::Center => {
4154 (TextAnchor::Center, text.y + text.height / 2.0, false)
4155 }
4156 TextVerticalAlign::Top => (TextAnchor::Top, text.y, true),
4157 TextVerticalAlign::Baseline => {
4158 let baseline_y = text.y + text.ascender;
4159 (TextAnchor::Baseline, baseline_y, false)
4160 }
4161 };
4162 let layout_height = if use_layout_height {
4163 Some(text.height)
4164 } else {
4165 None
4166 };
4167
4168 if let Ok(mut glyphs) = self.text_ctx.prepare_text_with_style(
4169 &text.content,
4170 text.x,
4171 y_pos,
4172 text.font_size,
4173 color,
4174 anchor,
4175 alignment,
4176 wrap_width,
4177 needs_wrap,
4178 font_name,
4179 generic,
4180 font_weight,
4181 text.italic,
4182 layout_height,
4183 text.letter_spacing,
4184 ) {
4185 if let Some(clip) = text.clip_bounds {
4186 for glyph in &mut glyphs {
4187 glyph.clip_bounds = clip;
4188 }
4189 }
4190 fg_glyphs.extend(glyphs);
4191 }
4192 }
4193
4194 let fg_decorations_by_layer = generate_text_decoration_primitives_by_layer(&fg_texts);
4200
4201 tracing::trace!(
4202 "render_tree_with_motion: {} texts, {} fg texts, {} z-layers with glyphs, {} css-transformed",
4203 texts.len(),
4204 fg_texts.len(),
4205 glyphs_by_layer.len(),
4206 css_transformed_text_prims.len()
4207 );
4208
4209 self.renderer.resize(width, height);
4213
4214 if !css_transformed_text_prims.is_empty() {
4217 if let (Some(atlas), Some(color_atlas)) =
4218 (self.text_ctx.atlas_view(), self.text_ctx.color_atlas_view())
4219 {
4220 batch.primitives.append(&mut css_transformed_text_prims);
4221 self.renderer.set_glyph_atlas(atlas, color_atlas);
4222 }
4223 }
4224
4225 let has_glass = batch.glass_count() > 0;
4226 let has_layer_effects_in_batch = batch.has_layer_effects();
4227
4228 if has_glass {
4230 self.ensure_glass_textures(width, height);
4231 }
4232 let use_msaa_overlay = self.sample_count > 1;
4233
4234 if has_glass {
4235 let (bg_images, fg_images): (Vec<_>, Vec<_>) = images
4237 .iter()
4238 .partition(|img| img.layer == RenderLayer::Background);
4239
4240 let has_bg_images = !bg_images.is_empty();
4242 if has_bg_images {
4243 let backdrop_tex = self.backdrop_texture.take().unwrap();
4244 self.renderer
4245 .clear_target(&backdrop_tex.view, wgpu::Color::TRANSPARENT);
4246 self.renderer.clear_target(target, wgpu::Color::BLACK);
4247 self.render_images_ref(&backdrop_tex.view, &bg_images);
4248 self.render_images_ref(target, &bg_images);
4249 self.backdrop_texture = Some(backdrop_tex);
4250 }
4251
4252 if has_layer_effects_in_batch {
4253 {
4259 let backdrop = self.backdrop_texture.as_ref().unwrap();
4260 self.renderer.render_to_backdrop(
4261 &backdrop.view,
4262 (backdrop.width, backdrop.height),
4263 &batch,
4264 has_bg_images,
4265 );
4266 }
4267
4268 self.renderer
4270 .render_with_clear(target, &batch, [0.0, 0.0, 0.0, 1.0]);
4271
4272 if !batch.dynamic_images.is_empty() {
4274 self.renderer
4275 .render_dynamic_images(target, &batch.dynamic_images);
4276 }
4277
4278 if has_bg_images {
4280 self.render_images_ref(target, &bg_images);
4281 }
4282
4283 if batch.glass_count() > 0 {
4285 let backdrop = self.backdrop_texture.as_ref().unwrap();
4286 self.renderer.render_glass(target, &backdrop.view, &batch);
4287 }
4288 } else {
4289 let backdrop = self.backdrop_texture.as_ref().unwrap();
4291 self.renderer.render_glass_frame(
4292 target,
4293 &backdrop.view,
4294 (backdrop.width, backdrop.height),
4295 &batch,
4296 has_bg_images,
4297 );
4298 }
4299
4300 if use_msaa_overlay && batch.has_paths() {
4303 self.renderer
4304 .render_paths_overlay_msaa(target, &batch, self.sample_count);
4305 }
4306
4307 if !has_bg_images {
4309 self.render_images_ref(target, &bg_images);
4310 }
4311 self.render_images_ref(target, &fg_images);
4312
4313 let max_z = batch.max_z_layer();
4315 let max_text_z = glyphs_by_layer.keys().cloned().max().unwrap_or(0);
4316 let decorations_by_layer = generate_text_decoration_primitives_by_layer(&texts);
4317 let max_decoration_z = decorations_by_layer.keys().cloned().max().unwrap_or(0);
4318 let max_glass_layer = max_z.max(max_text_z).max(max_decoration_z);
4319
4320 {
4322 let mut scratch = std::mem::take(&mut self.scratch_glyphs);
4323 scratch.clear();
4324 if let Some(glyphs) = glyphs_by_layer.get(&0) {
4325 scratch.extend_from_slice(glyphs);
4326 }
4327 if !scratch.is_empty() {
4328 self.render_text(target, &scratch);
4329 }
4330 self.scratch_glyphs = scratch;
4331 }
4332 self.render_text_decorations_for_layer(target, &decorations_by_layer, 0);
4333
4334 if max_glass_layer > 0 {
4335 let effect_indices = batch.effect_layer_indices();
4336 for z in 1..=max_glass_layer {
4337 let layer_primitives = if effect_indices.is_empty() {
4339 batch.primitives_for_layer(z)
4340 } else {
4341 batch.primitives_for_layer_excluding_effects(z, &effect_indices)
4342 };
4343 if !layer_primitives.is_empty() {
4344 self.renderer
4345 .render_primitives_overlay(target, &layer_primitives);
4346 }
4347
4348 {
4350 let mut scratch = std::mem::take(&mut self.scratch_glyphs);
4351 scratch.clear();
4352 if let Some(glyphs) = glyphs_by_layer.get(&z) {
4353 scratch.extend_from_slice(glyphs);
4354 }
4355 if !scratch.is_empty() {
4356 self.render_text(target, &scratch);
4357 }
4358 self.scratch_glyphs = scratch;
4359 }
4360 self.render_text_decorations_for_layer(target, &decorations_by_layer, z);
4361 }
4362 }
4363
4364 if !svgs.is_empty() {
4366 self.render_rasterized_svgs(target, &svgs, scale_factor);
4367 }
4368
4369 if !fg_glyphs.is_empty() {
4371 self.render_text(target, &fg_glyphs);
4372 }
4373 for &z in fg_decorations_by_layer.keys() {
4376 self.render_text_decorations_for_layer(target, &fg_decorations_by_layer, z);
4377 }
4378 } else {
4379 let decorations_by_layer = generate_text_decoration_primitives_by_layer(&texts);
4382
4383 let max_z = batch.max_z_layer();
4384 let max_text_z = glyphs_by_layer.keys().cloned().max().unwrap_or(0);
4385 let max_decoration_z = decorations_by_layer.keys().cloned().max().unwrap_or(0);
4386 let max_layer = max_z.max(max_text_z).max(max_decoration_z);
4387 let has_layer_effects = batch.has_layer_effects();
4388
4389 if max_layer > 0 && !has_layer_effects {
4390 let mut images_by_layer: std::collections::BTreeMap<u32, Vec<&ImageElement>> =
4393 std::collections::BTreeMap::new();
4394 for img in &images {
4395 images_by_layer.entry(img.z_index).or_default().push(img);
4396 }
4397 let max_image_z = images_by_layer.keys().cloned().max().unwrap_or(0);
4398 let max_layer = max_layer.max(max_image_z);
4399
4400 let z0_primitives = batch.primitives_for_layer(0);
4402 let mut z0_batch = PrimitiveBatch::new();
4404 z0_batch.primitives = z0_primitives;
4405 z0_batch.paths = batch.paths.clone();
4406 self.renderer
4407 .render_with_clear(target, &z0_batch, [0.0, 0.0, 0.0, 1.0]);
4408
4409 if !batch.dynamic_images.is_empty() {
4411 self.renderer
4412 .render_dynamic_images(target, &batch.dynamic_images);
4413 }
4414
4415 if use_msaa_overlay && z0_batch.has_paths() {
4417 self.renderer
4418 .render_paths_overlay_msaa(target, &z0_batch, self.sample_count);
4419 }
4420
4421 if let Some(z0_images) = images_by_layer.get(&0) {
4423 self.render_images_ref(target, z0_images);
4424 }
4425
4426 if let Some(glyphs) = glyphs_by_layer.get(&0) {
4428 if !glyphs.is_empty() {
4429 self.render_text(target, glyphs);
4430 }
4431 }
4432 self.render_text_decorations_for_layer(target, &decorations_by_layer, 0);
4433
4434 for z in 1..=max_layer {
4436 let layer_primitives = batch.primitives_for_layer(z);
4438 if !layer_primitives.is_empty() {
4439 self.renderer
4440 .render_primitives_overlay(target, &layer_primitives);
4441 }
4442
4443 if let Some(layer_images) = images_by_layer.get(&z) {
4445 self.render_images_ref(target, layer_images);
4446 }
4447
4448 if let Some(glyphs) = glyphs_by_layer.get(&z) {
4450 if !glyphs.is_empty() {
4451 self.render_text(target, glyphs);
4452 }
4453 }
4454 self.render_text_decorations_for_layer(target, &decorations_by_layer, z);
4455 }
4456
4457 if !svgs.is_empty() {
4459 self.render_rasterized_svgs(target, &svgs, scale_factor);
4460 }
4461
4462 if !batch.foreground_primitives.is_empty() {
4464 self.renderer
4465 .render_primitives_overlay(target, &batch.foreground_primitives);
4466 }
4467
4468 if !fg_glyphs.is_empty() {
4470 self.render_text(target, &fg_glyphs);
4471 }
4472 for &z in fg_decorations_by_layer.keys() {
4473 self.render_text_decorations_for_layer(target, &fg_decorations_by_layer, z);
4474 }
4475 } else {
4476 self.renderer
4478 .render_with_clear(target, &batch, [0.0, 0.0, 0.0, 1.0]);
4479
4480 if !batch.dynamic_images.is_empty() {
4482 self.renderer
4483 .render_dynamic_images(target, &batch.dynamic_images);
4484 }
4485
4486 if use_msaa_overlay && batch.has_paths() {
4488 self.renderer
4489 .render_paths_overlay_msaa(target, &batch, self.sample_count);
4490 }
4491
4492 self.render_images(target, &images, width as f32, height as f32, scale_factor);
4493
4494 if !batch.foreground_primitives.is_empty() {
4496 self.renderer
4497 .render_primitives_overlay(target, &batch.foreground_primitives);
4498 }
4499
4500 if !svgs.is_empty() {
4502 self.render_rasterized_svgs(target, &svgs, scale_factor);
4503 }
4504
4505 if let Some(glyphs) = glyphs_by_layer.get(&0) {
4508 if !glyphs.is_empty() {
4509 self.render_text(target, glyphs);
4510 }
4511 }
4512 self.render_text_decorations_for_layer(target, &decorations_by_layer, 0);
4513
4514 if max_layer > 0 {
4515 let effect_indices = batch.effect_layer_indices();
4516 for z in 1..=max_layer {
4517 let layer_primitives = if effect_indices.is_empty() {
4519 batch.primitives_for_layer(z)
4520 } else {
4521 batch.primitives_for_layer_excluding_effects(z, &effect_indices)
4522 };
4523 if !layer_primitives.is_empty() {
4524 self.renderer
4525 .render_primitives_overlay(target, &layer_primitives);
4526 }
4527
4528 if let Some(glyphs) = glyphs_by_layer.get(&z) {
4530 if !glyphs.is_empty() {
4531 self.render_text(target, glyphs);
4532 }
4533 }
4534 self.render_text_decorations_for_layer(target, &decorations_by_layer, z);
4535 }
4536 }
4537
4538 if !fg_glyphs.is_empty() {
4540 self.render_text(target, &fg_glyphs);
4541 }
4542 for &z in fg_decorations_by_layer.keys() {
4543 self.render_text_decorations_for_layer(target, &fg_decorations_by_layer, z);
4544 }
4545 }
4546 }
4547
4548 for layer_id in &layer_3d_ids {
4551 if let Some((info, layer_texts)) = layer_3d_texts.get(layer_id) {
4552 let layer_svgs_vec = layer_3d_svgs.get(layer_id);
4553 let layer_images_vec = layer_3d_images.get(layer_id);
4554 self.render_3d_layer_elements(
4555 target,
4556 info,
4557 layer_texts,
4558 layer_svgs_vec.map(|v| v.as_slice()).unwrap_or(&[]),
4559 layer_images_vec.map(|v| v.as_slice()).unwrap_or(&[]),
4560 scale_factor,
4561 );
4562 }
4563 }
4564
4565 self.has_active_flows = !flow_elements.is_empty();
4567 if !flow_elements.is_empty() {
4568 let stylesheet = tree.stylesheet();
4569
4570 static START_TIME: std::sync::OnceLock<web_time::Instant> = std::sync::OnceLock::new();
4572 let start = START_TIME.get_or_init(web_time::Instant::now);
4573 let elapsed_secs = start.elapsed().as_secs_f32();
4574
4575 for flow_el in &flow_elements {
4576 let graph = flow_el
4578 .flow_graph
4579 .as_deref()
4580 .or_else(|| stylesheet.and_then(|s| s.get_flow(&flow_el.flow_name)));
4581
4582 if let Some(graph) = graph {
4583 if let Err(e) = self.renderer.flow_pipeline_cache().compile(graph) {
4585 tracing::warn!("@flow '{}' compile error: {}", flow_el.flow_name, e);
4586 continue;
4587 }
4588
4589 let uniforms = blinc_gpu::FlowUniformData {
4590 viewport_size: [width as f32, height as f32],
4591 time: elapsed_secs,
4592 frame_index: 0.0, element_bounds: [flow_el.x, flow_el.y, flow_el.width, flow_el.height],
4594 pointer: [
4595 (self.cursor_pos[0] - flow_el.x) / flow_el.width.max(1.0),
4596 (self.cursor_pos[1] - flow_el.y) / flow_el.height.max(1.0),
4597 ],
4598 corner_radius: flow_el.corner_radius,
4599 _padding: 0.0,
4600 };
4601
4602 let viewport = [flow_el.x, flow_el.y, flow_el.width, flow_el.height];
4603 if !self.renderer.render_flow(
4604 target,
4605 &flow_el.flow_name,
4606 &uniforms,
4607 Some(viewport),
4608 ) {
4609 tracing::warn!("@flow '{}' render failed", flow_el.flow_name);
4610 }
4611 }
4612 }
4613 }
4614
4615 self.renderer.poll();
4617
4618 if !pending_meshes.is_empty() {
4632 dispatch_pending_meshes(&mut self.renderer, target, width, height, &pending_meshes);
4633 }
4634
4635 self.render_overlays(render_state, width, height, target);
4637
4638 let debug = DebugMode::from_env();
4640 if debug.text {
4641 self.render_text_debug(target, &texts);
4642 }
4643 if debug.layout {
4644 let scale = tree.scale_factor();
4645 self.render_layout_debug(target, tree, scale);
4646 }
4647 if debug.motion {
4648 self.render_motion_debug(target, tree, width, height);
4649 }
4650
4651 self.return_scratch_elements(texts, svgs, images);
4653
4654 self.log_cache_stats();
4656
4657 Ok(())
4658 }
4659
4660 fn render_3d_layer_elements(
4666 &mut self,
4667 target: &wgpu::TextureView,
4668 info: &Transform3DLayerInfo,
4669 texts: &[TextElement],
4670 svgs: &[SvgElement],
4671 images: &[ImageElement],
4672 scale_factor: f32,
4673 ) {
4674 let [lx, ly, lw, lh] = info.layer_bounds;
4675 if lw <= 0.0 || lh <= 0.0 {
4676 return;
4677 }
4678
4679 let tex_w = (lw.ceil() as u32).max(1);
4680 let tex_h = (lh.ceil() as u32).max(1);
4681
4682 let layer_tex = self.renderer.acquire_layer_texture((tex_w, tex_h), false);
4684 self.renderer
4685 .clear_target(&layer_tex.view, wgpu::Color::TRANSPARENT);
4686
4687 self.renderer.set_viewport_override((tex_w, tex_h));
4689
4690 if !texts.is_empty() {
4692 let mut layer_glyphs: Vec<GpuGlyph> = Vec::new();
4693 for text in texts {
4694 let alignment = match text.align {
4695 TextAlign::Left => TextAlignment::Left,
4696 TextAlign::Center => TextAlignment::Center,
4697 TextAlign::Right => TextAlignment::Right,
4698 };
4699
4700 let color = if text.motion_opacity < 1.0 {
4701 [
4702 text.color[0],
4703 text.color[1],
4704 text.color[2],
4705 text.color[3] * text.motion_opacity,
4706 ]
4707 } else {
4708 text.color
4709 };
4710
4711 let effective_width = if let Some(clip) = text.clip_bounds {
4712 clip[2].min(text.width)
4713 } else {
4714 text.width
4715 };
4716 let needs_wrap = text.wrap && effective_width < text.measured_width - 2.0;
4717 let wrap_width = Some(text.width);
4718 let font_name = text.font_family.name.as_deref();
4719 let generic = to_gpu_generic_font(text.font_family.generic);
4720 let font_weight = text.weight.weight();
4721
4722 let (anchor, y_pos, use_layout_height) = match text.v_align {
4723 TextVerticalAlign::Center => {
4724 (TextAnchor::Center, text.y + text.height / 2.0, false)
4725 }
4726 TextVerticalAlign::Top => (TextAnchor::Top, text.y, true),
4727 TextVerticalAlign::Baseline => {
4728 let baseline_y = text.y + text.ascender;
4729 (TextAnchor::Baseline, baseline_y, false)
4730 }
4731 };
4732 let layout_height = if use_layout_height {
4733 Some(text.height)
4734 } else {
4735 None
4736 };
4737
4738 if let Ok(mut glyphs) = self.text_ctx.prepare_text_with_style(
4739 &text.content,
4740 text.x - lx,
4741 y_pos - ly,
4742 text.font_size,
4743 color,
4744 anchor,
4745 alignment,
4746 wrap_width,
4747 needs_wrap,
4748 font_name,
4749 generic,
4750 font_weight,
4751 text.italic,
4752 layout_height,
4753 text.letter_spacing,
4754 ) {
4755 if let Some(clip) = text.clip_bounds {
4757 for glyph in &mut glyphs {
4758 glyph.clip_bounds = [clip[0] - lx, clip[1] - ly, clip[2], clip[3]];
4759 }
4760 }
4761 layer_glyphs.extend(glyphs);
4762 }
4763 }
4764
4765 if !layer_glyphs.is_empty() {
4766 self.render_text(&layer_tex.view, &layer_glyphs);
4767 }
4768 }
4769
4770 if !images.is_empty() {
4772 let mut offset_images = images.to_vec();
4773 for img in &mut offset_images {
4774 img.x -= lx;
4775 img.y -= ly;
4776 if let Some(ref mut clip) = img.clip_bounds {
4777 clip[0] -= lx;
4778 clip[1] -= ly;
4779 }
4780 if let Some(ref mut scroll) = img.scroll_clip {
4781 scroll[0] -= lx;
4782 scroll[1] -= ly;
4783 }
4784 }
4785 self.render_images(&layer_tex.view, &offset_images, lw, lh, scale_factor);
4786 }
4787
4788 if !svgs.is_empty() {
4790 let mut offset_svgs = svgs.to_vec();
4791 for svg in &mut offset_svgs {
4792 svg.x -= lx;
4793 svg.y -= ly;
4794 if let Some(ref mut clip) = svg.clip_bounds {
4795 clip[0] -= lx;
4796 clip[1] -= ly;
4797 }
4798 }
4799 self.render_rasterized_svgs(&layer_tex.view, &offset_svgs, scale_factor);
4800 }
4801
4802 self.renderer.restore_viewport();
4804
4805 self.renderer.blit_tight_texture_to_target(
4807 &layer_tex.view,
4808 (tex_w, tex_h),
4809 target,
4810 (lx, ly),
4811 (lw, lh),
4812 info.opacity,
4813 blinc_core::BlendMode::Normal,
4814 None,
4815 Some(info.transform_3d),
4816 );
4817
4818 self.renderer.release_layer_texture(layer_tex);
4819 }
4820
4821 pub fn render_overlay_tree_with_motion(
4826 &mut self,
4827 tree: &RenderTree,
4828 render_state: &blinc_layout::RenderState,
4829 width: u32,
4830 height: u32,
4831 target: &wgpu::TextureView,
4832 ) -> Result<()> {
4833 let scale_factor = tree.scale_factor();
4835
4836 let mut ctx =
4838 GpuPaintContext::with_text_context(width as f32, height as f32, &mut self.text_ctx);
4839
4840 tree.render_with_motion(&mut ctx, render_state);
4842
4843 let mut batch = ctx.take_batch();
4845
4846 let (texts, svgs, images, _flows) =
4848 self.collect_render_elements_with_state(tree, Some(render_state));
4849
4850 self.preload_images(&images, width as f32, height as f32);
4852
4853 let mut glyphs_by_layer: std::collections::BTreeMap<u32, Vec<GpuGlyph>> =
4855 std::collections::BTreeMap::new();
4856 let mut css_transformed_text_prims: Vec<GpuPrimitive> = Vec::new();
4857 for text in &texts {
4858 let alignment = match text.align {
4859 TextAlign::Left => TextAlignment::Left,
4860 TextAlign::Center => TextAlignment::Center,
4861 TextAlign::Right => TextAlignment::Right,
4862 };
4863
4864 let color = if text.motion_opacity < 1.0 {
4866 [
4867 text.color[0],
4868 text.color[1],
4869 text.color[2],
4870 text.color[3] * text.motion_opacity,
4871 ]
4872 } else {
4873 text.color
4874 };
4875
4876 let effective_width = if let Some(clip) = text.clip_bounds {
4878 clip[2].min(text.width)
4879 } else {
4880 text.width
4881 };
4882
4883 let needs_wrap = text.wrap && effective_width < text.measured_width - 2.0;
4884 let wrap_width = Some(text.width);
4885 let font_name = text.font_family.name.as_deref();
4886 let generic = to_gpu_generic_font(text.font_family.generic);
4887 let font_weight = text.weight.weight();
4888
4889 let (anchor, y_pos, use_layout_height) = match text.v_align {
4890 TextVerticalAlign::Center => {
4891 (TextAnchor::Center, text.y + text.height / 2.0, false)
4892 }
4893 TextVerticalAlign::Top => (TextAnchor::Top, text.y, true),
4894 TextVerticalAlign::Baseline => {
4895 let baseline_y = text.y + text.ascender;
4896 (TextAnchor::Baseline, baseline_y, false)
4897 }
4898 };
4899 let layout_height = if use_layout_height {
4900 Some(text.height)
4901 } else {
4902 None
4903 };
4904
4905 if let Ok(glyphs) = self.text_ctx.prepare_text_with_style(
4906 &text.content,
4907 text.x,
4908 y_pos,
4909 text.font_size,
4910 color,
4911 anchor,
4912 alignment,
4913 wrap_width,
4914 needs_wrap,
4915 font_name,
4916 generic,
4917 font_weight,
4918 text.italic,
4919 layout_height,
4920 text.letter_spacing,
4921 ) {
4922 let mut glyphs = glyphs;
4923 if let Some(clip) = text.clip_bounds {
4924 for glyph in &mut glyphs {
4925 glyph.clip_bounds = clip;
4926 }
4927 }
4928
4929 if let Some(affine) = text.css_affine {
4930 let [a, b, c, d, tx, ty] = affine;
4933 let tx_scaled = tx * scale_factor;
4934 let ty_scaled = ty * scale_factor;
4935 for glyph in &glyphs {
4936 let gc_x = glyph.bounds[0] + glyph.bounds[2] / 2.0;
4937 let gc_y = glyph.bounds[1] + glyph.bounds[3] / 2.0;
4938 let new_gc_x = a * gc_x + c * gc_y + tx_scaled;
4939 let new_gc_y = b * gc_x + d * gc_y + ty_scaled;
4940 let mut prim = GpuPrimitive::from_glyph(glyph);
4941 prim.bounds = [
4942 new_gc_x - glyph.bounds[2] / 2.0,
4943 new_gc_y - glyph.bounds[3] / 2.0,
4944 glyph.bounds[2],
4945 glyph.bounds[3],
4946 ];
4947 prim.local_affine = [a, b, c, d];
4948 prim.set_z_layer(text.z_index);
4949 css_transformed_text_prims.push(prim);
4950 }
4951 } else {
4952 glyphs_by_layer
4953 .entry(text.z_index)
4954 .or_default()
4955 .extend(glyphs);
4956 }
4957 }
4958 }
4959
4960 self.renderer.resize(width, height);
4964
4965 if !css_transformed_text_prims.is_empty() {
4968 if let (Some(atlas), Some(color_atlas)) =
4969 (self.text_ctx.atlas_view(), self.text_ctx.color_atlas_view())
4970 {
4971 batch.primitives.append(&mut css_transformed_text_prims);
4972 self.renderer.set_glyph_atlas(atlas, color_atlas);
4973 }
4974 }
4975
4976 let max_z = batch.max_z_layer();
4979 let max_text_z = glyphs_by_layer.keys().cloned().max().unwrap_or(0);
4980 let max_layer = max_z.max(max_text_z);
4981
4982 tracing::trace!(
4983 "render_overlay_tree: {} primitives, {} text layers, max_layer={}",
4984 batch.primitives.len(),
4985 glyphs_by_layer.len(),
4986 max_layer
4987 );
4988
4989 for z in 0..=max_layer {
4991 let layer_primitives = batch.primitives_for_layer(z);
4992 if !layer_primitives.is_empty() {
4993 tracing::trace!(
4994 "render_overlay_tree: rendering {} primitives at z={}",
4995 layer_primitives.len(),
4996 z
4997 );
4998 self.renderer
4999 .render_primitives_overlay(target, &layer_primitives);
5000 }
5001
5002 if let Some(glyphs) = glyphs_by_layer.get(&z) {
5003 if !glyphs.is_empty() {
5004 tracing::trace!(
5005 "render_overlay_tree: rendering {} glyphs at z={}",
5006 glyphs.len(),
5007 z
5008 );
5009 self.render_text(target, glyphs);
5010 }
5011 }
5012 }
5013
5014 self.render_images(target, &images, width as f32, height as f32, scale_factor);
5016
5017 if !batch.foreground_primitives.is_empty() {
5019 self.renderer
5020 .render_primitives_overlay(target, &batch.foreground_primitives);
5021 }
5022
5023 self.renderer.poll();
5025
5026 let debug = DebugMode::from_env();
5028 if debug.layout {
5029 let scale = tree.scale_factor();
5030 self.render_layout_debug(target, tree, scale);
5031 }
5032 if debug.motion {
5033 self.render_motion_debug(target, tree, width, height);
5034 }
5035
5036 self.return_scratch_elements(texts, svgs, images);
5038
5039 Ok(())
5040 }
5041
5042 fn render_overlays(
5044 &mut self,
5045 render_state: &blinc_layout::RenderState,
5046 width: u32,
5047 height: u32,
5048 target: &wgpu::TextureView,
5049 ) {
5050 let overlays = render_state.overlays();
5051 if overlays.is_empty() {
5052 return;
5053 }
5054
5055 let mut overlay_ctx = GpuPaintContext::new(width as f32, height as f32);
5057
5058 for overlay in overlays {
5059 match overlay {
5060 Overlay::Cursor {
5061 position,
5062 size,
5063 color,
5064 opacity,
5065 } => {
5066 if *opacity > 0.0 {
5067 let cursor_color =
5069 Color::rgba(color.r, color.g, color.b, color.a * opacity);
5070 overlay_ctx.execute_command(&DrawCommand::FillRect {
5071 rect: Rect::new(position.0, position.1, size.0, size.1),
5072 corner_radius: CornerRadius::default(),
5073 brush: Brush::Solid(cursor_color),
5074 });
5075 }
5076 }
5077 Overlay::Selection { rects: _, color: _ } => {
5078 }
5081 Overlay::FocusRing {
5082 position,
5083 size,
5084 radius,
5085 color,
5086 thickness,
5087 } => {
5088 overlay_ctx.execute_command(&DrawCommand::StrokeRect {
5089 rect: Rect::new(position.0, position.1, size.0, size.1),
5090 corner_radius: CornerRadius::uniform(*radius),
5091 stroke: Stroke::new(*thickness),
5092 brush: Brush::Solid(*color),
5093 });
5094 }
5095 }
5096 }
5097
5098 let overlay_batch = overlay_ctx.take_batch();
5100 if !overlay_batch.is_empty() {
5101 self.renderer.render_overlay(target, &overlay_batch);
5102 }
5103 }
5104}
5105
5106fn to_gpu_generic_font(generic: GenericFont) -> GpuGenericFont {
5108 match generic {
5109 GenericFont::System => GpuGenericFont::System,
5110 GenericFont::Monospace => GpuGenericFont::Monospace,
5111 GenericFont::Serif => GpuGenericFont::Serif,
5112 GenericFont::SansSerif => GpuGenericFont::SansSerif,
5113 }
5114}
5115
5116#[derive(Clone, Copy)]
5124pub struct DebugMode {
5125 pub text: bool,
5127 pub layout: bool,
5129 pub motion: bool,
5131}
5132
5133impl DebugMode {
5134 pub fn from_env() -> Self {
5136 let debug_value = std::env::var("BLINC_DEBUG")
5137 .map(|v| v.to_lowercase())
5138 .unwrap_or_default();
5139
5140 let all = debug_value == "all" || debug_value == "1" || debug_value == "true";
5141 let text = all || debug_value == "text";
5142 let layout = all || debug_value == "layout";
5143 let motion = all || debug_value == "motion";
5144
5145 Self {
5146 text,
5147 layout,
5148 motion,
5149 }
5150 }
5151
5152 pub fn any_enabled(&self) -> bool {
5154 self.text || self.layout || self.motion
5155 }
5156}
5157
5158fn generate_text_decoration_primitives_by_layer(
5166 texts: &[TextElement],
5167) -> std::collections::HashMap<u32, Vec<GpuPrimitive>> {
5168 let mut primitives_by_layer: std::collections::HashMap<u32, Vec<GpuPrimitive>> =
5169 std::collections::HashMap::new();
5170
5171 for text in texts {
5172 if !text.strikethrough && !text.underline {
5173 continue;
5174 }
5175
5176 let decoration_width = if text.wrap && text.measured_width > text.width {
5178 text.width
5179 } else {
5180 text.measured_width.min(text.width)
5181 };
5182
5183 if decoration_width <= 0.0 {
5185 continue;
5186 }
5187
5188 let line_thickness = text
5190 .decoration_thickness
5191 .unwrap_or_else(|| (text.font_size / 14.0).clamp(1.0, 3.0));
5192
5193 let dec_color = text.decoration_color.unwrap_or(text.color);
5195
5196 let layer_primitives = primitives_by_layer.entry(text.z_index).or_default();
5197
5198 let descender_approx = -text.ascender * 0.2;
5204 let glyph_extent = text.ascender - descender_approx;
5205
5206 let baseline_y = match text.v_align {
5207 TextVerticalAlign::Center => {
5208 let glyph_top = text.y + text.height / 2.0 - glyph_extent / 2.0;
5212 glyph_top + text.ascender
5213 }
5214 TextVerticalAlign::Top => {
5215 let glyph_top = text.y + (text.height - glyph_extent) / 2.0;
5219 glyph_top + text.ascender
5220 }
5221 TextVerticalAlign::Baseline => {
5222 text.y + text.ascender
5226 }
5227 };
5228
5229 if text.strikethrough {
5231 let strikethrough_y = baseline_y - text.ascender * 0.35;
5233 let mut strike_rect = GpuPrimitive::rect(
5234 text.x,
5235 strikethrough_y - line_thickness / 2.0,
5236 decoration_width,
5237 line_thickness,
5238 )
5239 .with_color(dec_color[0], dec_color[1], dec_color[2], dec_color[3]);
5240
5241 if let Some(clip) = text.clip_bounds {
5243 strike_rect = strike_rect.with_clip_rect(clip[0], clip[1], clip[2], clip[3]);
5244 }
5245 layer_primitives.push(strike_rect);
5246 }
5247
5248 if text.underline {
5250 let underline_y = baseline_y + text.ascender * 0.05;
5252 let mut underline_rect = GpuPrimitive::rect(
5253 text.x,
5254 underline_y - line_thickness / 2.0,
5255 decoration_width,
5256 line_thickness,
5257 )
5258 .with_color(dec_color[0], dec_color[1], dec_color[2], dec_color[3]);
5259
5260 if let Some(clip) = text.clip_bounds {
5262 underline_rect = underline_rect.with_clip_rect(clip[0], clip[1], clip[2], clip[3]);
5263 }
5264 layer_primitives.push(underline_rect);
5265 }
5266 }
5267
5268 primitives_by_layer
5269}
5270
5271fn generate_text_debug_primitives(texts: &[TextElement]) -> Vec<GpuPrimitive> {
5279 let mut primitives = Vec::new();
5280
5281 for text in texts {
5282 let debug_width = if text.wrap && text.measured_width > text.width {
5286 text.width
5288 } else {
5289 text.measured_width.min(text.width)
5291 };
5292
5293 let bbox = GpuPrimitive::rect(text.x, text.y, debug_width, text.height)
5295 .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);
5298
5299 let baseline_y = text.y + text.ascender;
5302 let baseline = GpuPrimitive::rect(text.x, baseline_y - 0.5, debug_width, 1.0)
5303 .with_color(1.0, 0.0, 1.0, 0.6); primitives.push(baseline);
5305
5306 let ascender_line = GpuPrimitive::rect(text.x, text.y - 0.5, debug_width, 1.0)
5309 .with_color(0.0, 1.0, 0.0, 0.4); primitives.push(ascender_line);
5311
5312 let descender_y = text.y + text.height;
5314 let descender_line = GpuPrimitive::rect(text.x, descender_y - 0.5, debug_width, 1.0)
5315 .with_color(1.0, 1.0, 0.0, 0.4); primitives.push(descender_line);
5317 }
5318
5319 primitives
5320}
5321
5322fn collect_debug_bounds(tree: &RenderTree, scale: f32) -> Vec<DebugBoundsElement> {
5324 let mut bounds = Vec::new();
5325
5326 if let Some(root) = tree.root() {
5327 collect_debug_bounds_recursive(tree, root, (0.0, 0.0), 0, scale, &mut bounds);
5328 }
5329
5330 bounds
5331}
5332
5333fn collect_debug_bounds_recursive(
5335 tree: &RenderTree,
5336 node: LayoutNodeId,
5337 parent_offset: (f32, f32),
5338 depth: u32,
5339 scale: f32,
5340 bounds: &mut Vec<DebugBoundsElement>,
5341) {
5342 use blinc_layout::renderer::ElementType;
5343
5344 let Some(node_bounds) = tree.layout().get_bounds(node, parent_offset) else {
5345 return;
5346 };
5347
5348 let element_type = tree
5350 .get_render_node(node)
5351 .map(|n| match &n.element_type {
5352 ElementType::Div => "Div".to_string(),
5353 ElementType::Text(_) => "Text".to_string(),
5354 ElementType::StyledText(_) => "StyledText".to_string(),
5355 ElementType::Image(_) => "Image".to_string(),
5356 ElementType::Svg(_) => "Svg".to_string(),
5357 ElementType::Canvas(_) => "Canvas".to_string(),
5358 })
5359 .unwrap_or_else(|| "Unknown".to_string());
5360
5361 bounds.push(DebugBoundsElement {
5363 x: node_bounds.x * scale,
5364 y: node_bounds.y * scale,
5365 width: node_bounds.width * scale,
5366 height: node_bounds.height * scale,
5367 element_type,
5368 depth,
5369 });
5370
5371 let scroll_offset = tree.get_scroll_offset(node);
5373
5374 let new_offset = (
5376 node_bounds.x + scroll_offset.0,
5377 node_bounds.y + scroll_offset.1,
5378 );
5379
5380 for child in tree.layout().children(node) {
5382 collect_debug_bounds_recursive(tree, child, new_offset, depth + 1, scale, bounds);
5383 }
5384}
5385
5386fn generate_layout_debug_primitives(bounds: &[DebugBoundsElement]) -> Vec<GpuPrimitive> {
5392 let mut primitives = Vec::new();
5393
5394 let colors: [(f32, f32, f32); 6] = [
5396 (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), ];
5403
5404 for elem in bounds {
5405 if elem.width < 1.0 || elem.height < 1.0 {
5407 continue;
5408 }
5409
5410 let (r, g, b) = colors[(elem.depth as usize) % colors.len()];
5411 let alpha = 0.5; let rect = GpuPrimitive::rect(elem.x, elem.y, elem.width, elem.height)
5415 .with_color(0.0, 0.0, 0.0, 0.0) .with_border(1.0, r, g, b, alpha); primitives.push(rect);
5419 }
5420
5421 primitives
5422}
5423
5424fn scale_and_translate_path(
5426 path: &blinc_core::Path,
5427 x: f32,
5428 y: f32,
5429 scale: f32,
5430) -> blinc_core::Path {
5431 use blinc_core::{PathCommand, Point, Vec2};
5432
5433 if scale == 1.0 && x == 0.0 && y == 0.0 {
5434 return path.clone();
5435 }
5436
5437 let transform_point = |p: Point| -> Point { Point::new(p.x * scale + x, p.y * scale + y) };
5438
5439 let new_commands: Vec<PathCommand> = path
5440 .commands()
5441 .iter()
5442 .map(|cmd| match cmd {
5443 PathCommand::MoveTo(p) => PathCommand::MoveTo(transform_point(*p)),
5444 PathCommand::LineTo(p) => PathCommand::LineTo(transform_point(*p)),
5445 PathCommand::QuadTo { control, end } => PathCommand::QuadTo {
5446 control: transform_point(*control),
5447 end: transform_point(*end),
5448 },
5449 PathCommand::CubicTo {
5450 control1,
5451 control2,
5452 end,
5453 } => PathCommand::CubicTo {
5454 control1: transform_point(*control1),
5455 control2: transform_point(*control2),
5456 end: transform_point(*end),
5457 },
5458 PathCommand::ArcTo {
5459 radii,
5460 rotation,
5461 large_arc,
5462 sweep,
5463 end,
5464 } => PathCommand::ArcTo {
5465 radii: Vec2::new(radii.x * scale, radii.y * scale),
5466 rotation: *rotation,
5467 large_arc: *large_arc,
5468 sweep: *sweep,
5469 end: transform_point(*end),
5470 },
5471 PathCommand::Close => PathCommand::Close,
5472 })
5473 .collect();
5474
5475 blinc_core::Path::from_commands(new_commands)
5476}
5477
5478fn dispatch_pending_meshes(
5499 renderer: &mut GpuRenderer,
5500 target: &wgpu::TextureView,
5501 width: u32,
5502 height: u32,
5503 meshes: &[PendingMesh],
5504) {
5505 if meshes.is_empty() {
5506 return;
5507 }
5508 let aspect = if height > 0 {
5509 width as f32 / height as f32
5510 } else {
5511 1.0
5512 };
5513
5514 for pending in meshes {
5515 if let Some(ref env) = pending.env_cubemap {
5520 renderer.upload_environment_cubemap(env);
5521 }
5522
5523 let vp_aspect = pending
5528 .viewport
5529 .map(|[_, _, w, h]| if h > 0.0 { w / h } else { 1.0 })
5530 .unwrap_or(aspect);
5531 let view_proj = camera_view_proj(&pending.camera, vp_aspect);
5532 let inv_view_proj = mat4_inverse_flat(&view_proj);
5533 let camera_pos = [
5534 pending.camera.position.x,
5535 pending.camera.position.y,
5536 pending.camera.position.z,
5537 ];
5538 let (light_dir, light_intensity) = first_directional_light(&pending.lights);
5539 let model = mat4_to_array(&pending.transform);
5540
5541 renderer.render_mesh_data(
5542 target,
5543 &pending.mesh,
5544 &model,
5545 &view_proj,
5546 camera_pos,
5547 light_dir,
5548 light_intensity,
5549 None,
5550 pending.viewport,
5551 );
5552 }
5553}
5554
5555fn camera_view_proj(camera: &blinc_core::Camera, frame_aspect: f32) -> [f32; 16] {
5565 let view = mat4_look_at(camera.position, camera.target, camera.up);
5566 let proj = match camera.projection {
5567 blinc_core::CameraProjection::Perspective {
5568 fov_y, near, far, ..
5569 } => mat4_perspective_rh(fov_y, frame_aspect, near, far),
5570 blinc_core::CameraProjection::Orthographic {
5571 left,
5572 right,
5573 bottom,
5574 top,
5575 near,
5576 far,
5577 } => mat4_orthographic_rh(left, right, bottom, top, near, far),
5578 };
5579 mat4_mul_flat(&proj, &view)
5580}
5581
5582fn first_directional_light(lights: &[blinc_core::Light]) -> ([f32; 3], f32) {
5587 for light in lights {
5588 if let blinc_core::Light::Directional {
5589 direction,
5590 intensity,
5591 ..
5592 } = light
5593 {
5594 let d = direction.normalize();
5595 return ([d.x, d.y, d.z], *intensity);
5596 }
5597 }
5598 ([0.0, -1.0, 0.3], 0.8)
5599}
5600
5601fn mat4_to_array(m: &blinc_core::Mat4) -> [f32; 16] {
5604 let mut out = [0.0f32; 16];
5605 for col in 0..4 {
5606 for row in 0..4 {
5607 out[col * 4 + row] = m.cols[col][row];
5608 }
5609 }
5610 out
5611}
5612
5613fn mat4_mul_flat(a: &[f32; 16], b: &[f32; 16]) -> [f32; 16] {
5617 let mut out = [0.0f32; 16];
5618 for col in 0..4 {
5619 for row in 0..4 {
5620 let mut s = 0.0;
5621 for k in 0..4 {
5622 s += a[k * 4 + row] * b[col * 4 + k];
5623 }
5624 out[col * 4 + row] = s;
5625 }
5626 }
5627 out
5628}
5629
5630fn mat4_look_at(
5633 eye: blinc_core::Vec3,
5634 target: blinc_core::Vec3,
5635 up: blinc_core::Vec3,
5636) -> [f32; 16] {
5637 let f = blinc_core::Vec3::new(target.x - eye.x, target.y - eye.y, target.z - eye.z).normalize();
5638 let r = f.cross(up).normalize();
5639 let u = r.cross(f);
5640 let tx = -(r.x * eye.x + r.y * eye.y + r.z * eye.z);
5641 let ty = -(u.x * eye.x + u.y * eye.y + u.z * eye.z);
5642 let tz = f.x * eye.x + f.y * eye.y + f.z * eye.z;
5643 [
5645 r.x, u.x, -f.x, 0.0, r.y, u.y, -f.y, 0.0, r.z, u.z, -f.z, 0.0, tx, ty, tz, 1.0,
5646 ]
5647}
5648
5649fn mat4_perspective_rh(fov_y: f32, aspect: f32, near: f32, far: f32) -> [f32; 16] {
5652 let f = 1.0 / (fov_y * 0.5).tan();
5653 let nf = 1.0 / (near - far);
5654 [
5655 f / aspect,
5656 0.0,
5657 0.0,
5658 0.0,
5659 0.0,
5660 f,
5661 0.0,
5662 0.0,
5663 0.0,
5664 0.0,
5665 far * nf,
5666 -1.0,
5667 0.0,
5668 0.0,
5669 far * near * nf,
5670 0.0,
5671 ]
5672}
5673
5674fn mat4_orthographic_rh(
5678 left: f32,
5679 right: f32,
5680 bottom: f32,
5681 top: f32,
5682 near: f32,
5683 far: f32,
5684) -> [f32; 16] {
5685 let rl = 1.0 / (right - left);
5686 let tb = 1.0 / (top - bottom);
5687 let fnn = 1.0 / (far - near);
5688 [
5689 2.0 * rl,
5690 0.0,
5691 0.0,
5692 0.0,
5693 0.0,
5694 2.0 * tb,
5695 0.0,
5696 0.0,
5697 0.0,
5698 0.0,
5699 -fnn,
5700 0.0,
5701 -(right + left) * rl,
5702 -(top + bottom) * tb,
5703 -near * fnn,
5704 1.0,
5705 ]
5706}
5707
5708fn mat4_inverse_flat(m: &[f32; 16]) -> [f32; 16] {
5710 let mut inv = [0.0f32; 16];
5711 inv[0] = m[5] * m[10] * m[15] - m[5] * m[11] * m[14] - m[9] * m[6] * m[15]
5712 + m[9] * m[7] * m[14]
5713 + m[13] * m[6] * m[11]
5714 - m[13] * m[7] * m[10];
5715 inv[4] = -m[4] * m[10] * m[15] + m[4] * m[11] * m[14] + m[8] * m[6] * m[15]
5716 - m[8] * m[7] * m[14]
5717 - m[12] * m[6] * m[11]
5718 + m[12] * m[7] * m[10];
5719 inv[8] = m[4] * m[9] * m[15] - m[4] * m[11] * m[13] - m[8] * m[5] * m[15]
5720 + m[8] * m[7] * m[13]
5721 + m[12] * m[5] * m[11]
5722 - m[12] * m[7] * m[9];
5723 inv[12] = -m[4] * m[9] * m[14] + m[4] * m[10] * m[13] + m[8] * m[5] * m[14]
5724 - m[8] * m[6] * m[13]
5725 - m[12] * m[5] * m[10]
5726 + m[12] * m[6] * m[9];
5727 inv[1] = -m[1] * m[10] * m[15] + m[1] * m[11] * m[14] + m[9] * m[2] * m[15]
5728 - m[9] * m[3] * m[14]
5729 - m[13] * m[2] * m[11]
5730 + m[13] * m[3] * m[10];
5731 inv[5] = m[0] * m[10] * m[15] - m[0] * m[11] * m[14] - m[8] * m[2] * m[15]
5732 + m[8] * m[3] * m[14]
5733 + m[12] * m[2] * m[11]
5734 - m[12] * m[3] * m[10];
5735 inv[9] = -m[0] * m[9] * m[15] + m[0] * m[11] * m[13] + m[8] * m[1] * m[15]
5736 - m[8] * m[3] * m[13]
5737 - m[12] * m[1] * m[11]
5738 + m[12] * m[3] * m[9];
5739 inv[13] = m[0] * m[9] * m[14] - m[0] * m[10] * m[13] - m[8] * m[1] * m[14]
5740 + m[8] * m[2] * m[13]
5741 + m[12] * m[1] * m[10]
5742 - m[12] * m[2] * m[9];
5743 inv[2] = m[1] * m[6] * m[15] - m[1] * m[7] * m[14] - m[5] * m[2] * m[15]
5744 + m[5] * m[3] * m[14]
5745 + m[13] * m[2] * m[7]
5746 - m[13] * m[3] * m[6];
5747 inv[6] = -m[0] * m[6] * m[15] + m[0] * m[7] * m[14] + m[4] * m[2] * m[15]
5748 - m[4] * m[3] * m[14]
5749 - m[12] * m[2] * m[7]
5750 + m[12] * m[3] * m[6];
5751 inv[10] = m[0] * m[5] * m[15] - m[0] * m[7] * m[13] - m[4] * m[1] * m[15]
5752 + m[4] * m[3] * m[13]
5753 + m[12] * m[1] * m[7]
5754 - m[12] * m[3] * m[5];
5755 inv[14] = -m[0] * m[5] * m[14] + m[0] * m[6] * m[13] + m[4] * m[1] * m[14]
5756 - m[4] * m[2] * m[13]
5757 - m[12] * m[1] * m[6]
5758 + m[12] * m[2] * m[5];
5759 inv[3] = -m[1] * m[6] * m[11] + m[1] * m[7] * m[10] + m[5] * m[2] * m[11]
5760 - m[5] * m[3] * m[10]
5761 - m[9] * m[2] * m[7]
5762 + m[9] * m[3] * m[6];
5763 inv[7] = m[0] * m[6] * m[11] - m[0] * m[7] * m[10] - m[4] * m[2] * m[11]
5764 + m[4] * m[3] * m[10]
5765 + m[8] * m[2] * m[7]
5766 - m[8] * m[3] * m[6];
5767 inv[11] = -m[0] * m[5] * m[11] + m[0] * m[7] * m[9] + m[4] * m[1] * m[11]
5768 - m[4] * m[3] * m[9]
5769 - m[8] * m[1] * m[7]
5770 + m[8] * m[3] * m[5];
5771 inv[15] = m[0] * m[5] * m[10] - m[0] * m[6] * m[9] - m[4] * m[1] * m[10]
5772 + m[4] * m[2] * m[9]
5773 + m[8] * m[1] * m[6]
5774 - m[8] * m[2] * m[5];
5775 let det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12];
5776 if det.abs() < 1e-12 {
5777 return [
5778 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
5779 ];
5780 }
5781 let id = 1.0 / det;
5782 for v in &mut inv {
5783 *v *= id;
5784 }
5785 inv
5786}