mod debug; mod fills; pub mod filters; mod fonts; mod gpu_state; pub mod grid_layout; mod images; mod options; mod shadows; mod strokes; mod surfaces; mod text; mod ui; use skia_safe::{self as skia, Matrix, RRect, Rect}; use std::borrow::Cow; use std::collections::HashSet; use gpu_state::GpuState; use options::RenderOptions; pub use surfaces::{SurfaceId, Surfaces}; use crate::performance; use crate::shapes::{ all_with_ancestors, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, Stroke, Type, }; use crate::state::{ShapesPoolMutRef, ShapesPoolRef}; use crate::tiles::{self, PendingTiles, TileRect}; use crate::uuid::Uuid; use crate::view::Viewbox; use crate::wapi; pub use fonts::*; pub use images::*; // This is the extra are used for tile rendering. const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 1; const MAX_BLOCKING_TIME_MS: i32 = 32; const NODE_BATCH_THRESHOLD: i32 = 10; pub struct NodeRenderState { pub id: Uuid, // We use this bool to keep that we've traversed all the children inside this node. visited_children: bool, // This is used to clip the content of frames. clip_bounds: Option<(Rect, Option, Matrix)>, // This is a flag to indicate that we've already drawn the mask of a masked group. visited_mask: bool, // This bool indicates that we're drawing the mask shape. mask: bool, } impl NodeRenderState { pub fn is_root(&self) -> bool { self.id.is_nil() } /// Calculates the clip bounds for child elements of a given shape. /// /// This function determines the clipping region that should be applied to child elements /// when rendering. It takes into account the element's selection rectangle, transform. /// /// # Parameters /// /// * `element` - The shape element for which to calculate clip bounds /// * `offset` - Optional offset (x, y) to adjust the bounds position. When provided, /// the bounds are translated by the negative of this offset, effectively moving /// the clipping region to compensate for coordinate system transformations. /// This is useful for nested coordinate systems or when elements are grouped /// and need relative positioning adjustments. pub fn get_children_clip_bounds( &self, element: &Shape, offset: Option<(f32, f32)>, ) -> Option<(Rect, Option, Matrix)> { if self.id.is_nil() || !element.clip() { return self.clip_bounds; } let mut bounds = element.selrect(); if let Some(offset) = offset { let x = bounds.x() - offset.0; let y = bounds.y() - offset.1; let width = bounds.width(); let height = bounds.height(); bounds.set_xywh(x, y, width, height); } let mut transform = element.transform; transform.post_translate(bounds.center()); transform.pre_translate(-bounds.center()); let corners = match &element.shape_type { Type::Rect(data) => data.corners, Type::Frame(data) => data.corners, _ => None, }; Some((bounds, corners, transform)) } /// Calculates the clip bounds for shadow rendering of a given shape. /// /// This function determines the clipping region that should be applied when rendering a /// shadow for a shape element. For frames, it uses the shadow bounds to clip nested /// shadows. For groups, it returns the existing clip bounds since groups should not /// constrain nested shadows based on their selection rectangle bounds. /// /// # Parameters /// /// * `element` - The shape element for which to calculate shadow clip bounds /// * `shadow` - The shadow configuration containing blur, offset, and other properties pub fn get_nested_shadow_clip_bounds( &self, element: &Shape, shadow: &Shadow, ) -> Option<(Rect, Option, Matrix)> { if self.id.is_nil() { return self.clip_bounds; } // Assert that the shape is either a Frame or Group assert!( matches!(element.shape_type, Type::Frame(_) | Type::Group(_)), "Shape must be a Frame or Group for nested shadow clip bounds calculation" ); match &element.shape_type { Type::Frame(_) => { let bounds = element.get_selrect_shadow_bounds(shadow); let mut transform = element.transform; transform.post_translate(element.center()); transform.pre_translate(-element.center()); let corners = match &element.shape_type { Type::Frame(data) => data.corners, _ => None, }; Some((bounds, corners, transform)) } _ => self.clip_bounds, } } } /// Represents the "focus mode" state used during rendering. /// /// Focus mode allows selectively highlighting or isolating specific shapes (UUIDs) /// during the render pass. It maintains a list of shapes to focus and tracks /// whether the current rendering context is inside a focused element. /// /// # Focus Propagation /// If a shape is in focus, all its nested content /// is also considered to be in focus for the duration of the render traversal. Focus /// state propagates *downward* through the tree while rendering. /// /// # Usage /// - `set_shapes(...)` to activate focus mode for specific elements and their anidated content. /// - `clear()` to disable focus mode. /// - `reset()` should be called at the beginning of the render loop. /// - `enter(...)` / `exit(...)` should be called when entering and leaving shape /// render contexts. /// - `is_active()` returns whether the current shape is being rendered in focus. pub struct FocusMode { shapes: Vec, active: bool, } impl FocusMode { pub fn new() -> Self { FocusMode { shapes: Vec::new(), active: false, } } pub fn clear(&mut self) { self.shapes.clear(); self.active = false; } pub fn set_shapes(&mut self, shapes: Vec) { self.shapes = shapes; } /// Returns `true` if the given shape ID should be focused. /// If the `shapes` list is empty, focus applies to all shapes. pub fn should_focus(&self, id: &Uuid) -> bool { self.shapes.is_empty() || self.shapes.contains(id) } pub fn enter(&mut self, id: &Uuid) { if !self.active && self.should_focus(id) { self.active = true; } } pub fn exit(&mut self, id: &Uuid) { if self.active && self.should_focus(id) { self.active = false; } } pub fn is_active(&self) -> bool { self.active } pub fn reset(&mut self) { self.active = false; } } pub(crate) struct RenderState { gpu_state: GpuState, pub options: RenderOptions, pub surfaces: Surfaces, pub fonts: FontStore, pub viewbox: Viewbox, pub cached_viewbox: Viewbox, pub cached_target_snapshot: Option, pub images: ImageStore, pub background_color: skia::Color, // Identifier of the current requestAnimationFrame call, if any. pub render_request_id: Option, // Indicates whether the rendering process has pending frames. pub render_in_progress: bool, // Stack of nodes pending to be rendered. pending_nodes: Vec, pub current_tile: Option, pub sampling_options: skia::SamplingOptions, pub render_area: Rect, pub tile_viewbox: tiles::TileViewbox, pub tiles: tiles::TileHashMap, pub pending_tiles: PendingTiles, // nested_fills maintains a stack of group fills that apply to nested shapes // without their own fill definitions. This is necessary because in SVG, a group's `fill` // can affect its child elements if they don't specify one themselves. If the planned // migration to remove group-level fills is completed, this code should be removed. // Frames contained in groups must reset this nested_fills stack pushing a new empty vector. pub nested_fills: Vec>, pub nested_blurs: Vec>, // FIXME: why is this an option? pub nested_shadows: Vec>, pub show_grid: Option, pub focus_mode: FocusMode, pub touched_ids: HashSet, } pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize { // First we retrieve the extended area of the viewport that we could render. let TileRect(isx, isy, iex, iey) = tiles::get_tiles_for_viewbox_with_interest( viewbox, VIEWPORT_INTEREST_AREA_THRESHOLD, scale, ); let dx = if isx.signum() != iex.signum() { 1 } else { 0 }; let dy = if isy.signum() != iey.signum() { 1 } else { 0 }; let tile_size = tiles::TILE_SIZE; ( ((iex - isx).abs() + dx) * tile_size as i32, ((iey - isy).abs() + dy) * tile_size as i32, ) .into() } impl RenderState { pub fn new(width: i32, height: i32) -> RenderState { // This needs to be done once per WebGL context. let mut gpu_state = GpuState::new(); let sampling_options = skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest); let fonts = FontStore::new(); let surfaces = Surfaces::new( &mut gpu_state, (width, height), sampling_options, tiles::get_tile_dimensions(), ); // This is used multiple times everywhere so instead of creating new instances every // time we reuse this one. let viewbox = Viewbox::new(width as f32, height as f32); let tiles = tiles::TileHashMap::new(); RenderState { gpu_state: gpu_state.clone(), options: RenderOptions::default(), surfaces, fonts, viewbox, cached_viewbox: Viewbox::new(0., 0.), cached_target_snapshot: None, images: ImageStore::new(gpu_state.context.clone()), background_color: skia::Color::TRANSPARENT, render_request_id: None, render_in_progress: false, pending_nodes: vec![], current_tile: None, sampling_options, render_area: Rect::new_empty(), tiles, tile_viewbox: tiles::TileViewbox::new_with_interest( viewbox, VIEWPORT_INTEREST_AREA_THRESHOLD, 1.0, ), pending_tiles: PendingTiles::new_empty(), nested_fills: vec![], nested_blurs: vec![], nested_shadows: vec![], show_grid: None, focus_mode: FocusMode::new(), touched_ids: HashSet::default(), } } pub fn fonts(&self) -> &FontStore { &self.fonts } pub fn fonts_mut(&mut self) -> &mut FontStore { &mut self.fonts } pub fn add_image( &mut self, id: Uuid, is_thumbnail: bool, image_data: &[u8], ) -> Result<(), String> { self.images.add(id, is_thumbnail, image_data) } /// Adds an image from an existing WebGL texture, avoiding re-decoding pub fn add_image_from_gl_texture( &mut self, id: Uuid, is_thumbnail: bool, texture_id: u32, width: i32, height: i32, ) -> Result<(), String> { self.images .add_image_from_gl_texture(id, is_thumbnail, texture_id, width, height) } pub fn has_image(&self, id: &Uuid, is_thumbnail: bool) -> bool { self.images.contains(id, is_thumbnail) } pub fn set_debug_flags(&mut self, debug: u32) { self.options.flags = debug; } pub fn set_dpr(&mut self, dpr: f32) { if Some(dpr) != self.options.dpr { self.options.dpr = Some(dpr); self.resize( self.viewbox.width.floor() as i32, self.viewbox.height.floor() as i32, ); self.fonts.set_scale_debug_font(dpr); } } pub fn set_background_color(&mut self, color: skia::Color) { self.background_color = color; } pub fn resize(&mut self, width: i32, height: i32) { let dpr_width = (width as f32 * self.options.dpr()).floor() as i32; let dpr_height = (height as f32 * self.options.dpr()).floor() as i32; self.surfaces .resize(&mut self.gpu_state, dpr_width, dpr_height); self.viewbox.set_wh(width as f32, height as f32); self.tile_viewbox.update(self.viewbox, self.get_scale()); } pub fn flush_and_submit(&mut self) { self.surfaces .flush_and_submit(&mut self.gpu_state, SurfaceId::Target); } pub fn reset_canvas(&mut self) { self.surfaces.reset(self.background_color); } #[allow(dead_code)] pub fn get_canvas_at(&mut self, surface_id: SurfaceId) -> &skia::Canvas { self.surfaces.canvas(surface_id) } #[allow(dead_code)] pub fn restore_canvas(&mut self, surface_id: SurfaceId) { self.surfaces.canvas(surface_id).restore(); } pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) { let tile_rect = self.get_current_aligned_tile_bounds(); self.surfaces.cache_current_tile_texture( &self.tile_viewbox, &self.current_tile.unwrap(), &tile_rect, ); self.surfaces.draw_cached_tile_surface( self.current_tile.unwrap(), rect, self.background_color, ); } pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>) { performance::begin_measure!("apply_drawing_to_render_canvas"); let paint = skia::Paint::default(); self.surfaces .draw_into(SurfaceId::TextDropShadows, SurfaceId::Current, Some(&paint)); self.surfaces .draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint)); let mut render_overlay_below_strokes = false; if let Some(shape) = shape { render_overlay_below_strokes = shape.has_fills(); } if render_overlay_below_strokes { self.surfaces .draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint)); } self.surfaces .draw_into(SurfaceId::Strokes, SurfaceId::Current, Some(&paint)); if !render_overlay_below_strokes { self.surfaces .draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint)); } let surface_ids = SurfaceId::Strokes as u32 | SurfaceId::Fills as u32 | SurfaceId::InnerShadows as u32 | SurfaceId::TextDropShadows as u32; self.surfaces.apply_mut(surface_ids, |s| { s.canvas().clear(skia::Color::TRANSPARENT); }); } pub fn clear_focus_mode(&mut self) { self.focus_mode.clear(); } pub fn set_focus_mode(&mut self, shapes: Vec) { self.focus_mode.set_shapes(shapes); } fn get_inherited_drop_shadows(&self) -> Option> { let drop_shadows: Vec<&Shadow> = self .nested_shadows .iter() .flat_map(|shadows| shadows.iter()) .filter(|shadow| !shadow.hidden() && shadow.style() == crate::shapes::ShadowStyle::Drop) .collect(); if drop_shadows.is_empty() { return None; } Some( drop_shadows .into_iter() .map(|shadow| { let mut paint = skia_safe::Paint::default(); let filter = shadow.get_drop_shadow_filter(); paint.set_image_filter(filter); paint }) .collect(), ) } #[allow(clippy::too_many_arguments)] pub fn render_shape( &mut self, shape: &Shape, clip_bounds: Option<(Rect, Option, Matrix)>, fills_surface_id: SurfaceId, strokes_surface_id: SurfaceId, innershadows_surface_id: SurfaceId, text_drop_shadows_surface_id: SurfaceId, apply_to_current_surface: bool, offset: Option<(f32, f32)>, parent_shadows: Option>, ) { let surface_ids = fills_surface_id as u32 | strokes_surface_id as u32 | innershadows_surface_id as u32 | text_drop_shadows_surface_id as u32; self.surfaces.apply_mut(surface_ids, |s| { s.canvas().save(); }); let antialias = shape.should_use_antialias(self.get_scale()); // set clipping if let Some((bounds, corners, transform)) = clip_bounds { self.surfaces.apply_mut(surface_ids, |s| { s.canvas().concat(&transform); }); if let Some(corners) = corners { let rrect = RRect::new_rect_radii(bounds, &corners); self.surfaces.apply_mut(surface_ids, |s| { s.canvas() .clip_rrect(rrect, skia::ClipOp::Intersect, antialias); }); } else { self.surfaces.apply_mut(surface_ids, |s| { s.canvas() .clip_rect(bounds, skia::ClipOp::Intersect, antialias); }); } // This renders a red line around clipped // shapes (frames). if self.options.is_debug_visible() { let mut paint = skia::Paint::default(); paint.set_style(skia::PaintStyle::Stroke); paint.set_color(skia::Color::from_argb(255, 255, 0, 0)); paint.set_stroke_width(4.); self.surfaces .canvas(fills_surface_id) .draw_rect(bounds, &paint); } self.surfaces.apply_mut(surface_ids, |s| { s.canvas() .concat(&transform.invert().unwrap_or(Matrix::default())); }); } // We don't want to change the value in the global state let mut shape: Cow = Cow::Borrowed(shape); let mut nested_blur_value = 0.; for nested_blur in self.nested_blurs.iter().flatten() { if !nested_blur.hidden && nested_blur.blur_type == BlurType::LayerBlur { nested_blur_value += nested_blur.value.powf(2.); } } if let Some(blur) = shape.blur { if !blur.hidden { nested_blur_value += blur.value.powf(2.); } } if nested_blur_value > 0. { let blur = Blur::new(BlurType::LayerBlur, false, nested_blur_value.sqrt()); shape.to_mut().set_blur(Some(blur)); } let center = shape.center(); let mut matrix = shape.transform; matrix.post_translate(center); matrix.pre_translate(-center); // Apply the additional transformation matrix if exists if let Some(offset) = offset { matrix.pre_translate(offset); } match &shape.shape_type { Type::SVGRaw(sr) => { if let Some(svg_transform) = shape.svg_transform() { matrix.pre_concat(&svg_transform); } self.surfaces.canvas(fills_surface_id).concat(&matrix); if let Some(svg) = shape.svg.as_ref() { svg.render(self.surfaces.canvas(fills_surface_id)) } else { let font_manager = skia::FontMgr::from(self.fonts().font_provider().clone()); let dom_result = skia::svg::Dom::from_str(&sr.content, font_manager); match dom_result { Ok(dom) => { dom.render(self.surfaces.canvas(fills_surface_id)); shape.to_mut().set_svg(dom); } Err(e) => { eprintln!("Error parsing SVG. Error: {}", e); } } } } Type::Text(text_content) => { self.surfaces.apply_mut(surface_ids, |s| { s.canvas().concat(&matrix); }); let text_content = text_content.new_bounds(shape.selrect()); let mut drop_shadows = shape.drop_shadow_paints(); if let Some(inherited_shadows) = self.get_inherited_drop_shadows() { drop_shadows.extend(inherited_shadows); } let inner_shadows = shape.inner_shadow_paints(); let blur_filter = shape.image_filter(1.); let count_inner_strokes = shape.count_visible_inner_strokes(); let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None); let mut paragraphs_with_shadows = text_content.paragraph_builder_group_from_text(Some(true)); let mut stroke_paragraphs_list = shape .visible_strokes() .rev() .map(|stroke| { text::stroke_paragraph_builder_group_from_text( &text_content, stroke, &shape.selrect(), count_inner_strokes, None, ) }) .collect::>(); let mut stroke_paragraphs_with_shadows_list = shape .visible_strokes() .rev() .map(|stroke| { text::stroke_paragraph_builder_group_from_text( &text_content, stroke, &shape.selrect(), count_inner_strokes, Some(true), ) }) .collect::>(); if let Some(parent_shadows) = parent_shadows { if !shape.has_visible_strokes() { for shadow in parent_shadows { text::render( Some(self), None, &shape, &mut paragraphs_with_shadows, text_drop_shadows_surface_id.into(), Some(&shadow), blur_filter.as_ref(), ); } } else { shadows::render_text_shadows( self, &shape, &mut paragraphs_with_shadows, &mut stroke_paragraphs_with_shadows_list, text_drop_shadows_surface_id.into(), &parent_shadows, &blur_filter, ); } } else { // 1. Text drop shadows if !shape.has_visible_strokes() { for shadow in &drop_shadows { text::render( Some(self), None, &shape, &mut paragraphs_with_shadows, text_drop_shadows_surface_id.into(), Some(shadow), blur_filter.as_ref(), ); } } // 2. Text fills text::render( Some(self), None, &shape, &mut paragraph_builders, Some(fills_surface_id), None, blur_filter.as_ref(), ); // 3. Stroke drop shadows shadows::render_text_shadows( self, &shape, &mut paragraphs_with_shadows, &mut stroke_paragraphs_with_shadows_list, text_drop_shadows_surface_id.into(), &drop_shadows, &blur_filter, ); // 4. Stroke fills for stroke_paragraphs in stroke_paragraphs_list.iter_mut() { text::render( Some(self), None, &shape, stroke_paragraphs, Some(strokes_surface_id), None, blur_filter.as_ref(), ); } // 5. Stroke inner shadows shadows::render_text_shadows( self, &shape, &mut paragraphs_with_shadows, &mut stroke_paragraphs_with_shadows_list, Some(innershadows_surface_id), &inner_shadows, &blur_filter, ); // 6. Fill Inner shadows if !shape.has_visible_strokes() { for shadow in &inner_shadows { text::render( Some(self), None, &shape, &mut paragraphs_with_shadows, Some(innershadows_surface_id), Some(shadow), blur_filter.as_ref(), ); } } } } _ => { self.surfaces.apply_mut(surface_ids, |s| { s.canvas().concat(&matrix); }); let shape = &shape; if shape.fills.is_empty() && !matches!(shape.shape_type, Type::Group(_)) && !matches!(shape.shape_type, Type::Frame(_)) && !shape .svg_attrs .as_ref() .is_some_and(|attrs| attrs.fill_none) { if let Some(fills_to_render) = self.nested_fills.last() { let fills_to_render = fills_to_render.clone(); for fill in fills_to_render.iter() { fills::render(self, shape, fill, antialias, fills_surface_id); } } } else { for fill in shape.fills().rev() { fills::render(self, shape, fill, antialias, fills_surface_id); } } for stroke in shape.visible_strokes().rev() { strokes::render( self, shape, stroke, Some(strokes_surface_id), None, antialias, ); shadows::render_stroke_inner_shadows( self, shape, stroke, antialias, innershadows_surface_id, ); } shadows::render_fill_inner_shadows(self, shape, antialias, innershadows_surface_id); // bools::debug_render_bool_paths(self, shape, shapes, modifiers, structure); } }; if self.options.is_debug_visible() { let shape_selrect_bounds = self.get_shape_selrect_bounds(&shape); debug::render_debug_shape(self, Some(shape_selrect_bounds), None); } if apply_to_current_surface { self.apply_drawing_to_render_canvas(Some(&shape)); } self.surfaces.apply_mut(surface_ids, |s| { s.canvas().restore(); }); } pub fn update_render_context(&mut self, tile: tiles::Tile) { self.current_tile = Some(tile); self.render_area = tiles::get_tile_rect(tile, self.get_scale()); self.surfaces .update_render_context(self.render_area, self.get_scale()); } pub fn cancel_animation_frame(&mut self) { if self.render_in_progress { if let Some(frame_id) = self.render_request_id { wapi::cancel_animation_frame!(frame_id); } } } pub fn render_from_cache(&mut self, shapes: ShapesPoolRef) { let scale = self.get_cached_scale(); if let Some(snapshot) = &self.cached_target_snapshot { let canvas = self.surfaces.canvas(SurfaceId::Target); canvas.save(); // Scale and translate the target according to the cached data let navigate_zoom = self.viewbox.zoom / self.cached_viewbox.zoom; canvas.scale((navigate_zoom, navigate_zoom)); let TileRect(start_tile_x, start_tile_y, _, _) = tiles::get_tiles_for_viewbox_with_interest( self.cached_viewbox, VIEWPORT_INTEREST_AREA_THRESHOLD, scale, ); let offset_x = self.viewbox.area.left * self.cached_viewbox.zoom * self.options.dpr(); let offset_y = self.viewbox.area.top * self.cached_viewbox.zoom * self.options.dpr(); canvas.translate(( (start_tile_x as f32 * tiles::TILE_SIZE) - offset_x, (start_tile_y as f32 * tiles::TILE_SIZE) - offset_y, )); canvas.clear(self.background_color); canvas.draw_image(snapshot, (0, 0), Some(&skia::Paint::default())); canvas.restore(); if self.options.is_debug_visible() { debug::render(self); } ui::render(self, shapes); debug::render_wasm_label(self); self.flush_and_submit(); } } pub fn start_render_loop(&mut self, tree: ShapesPoolRef, timestamp: i32) -> Result<(), String> { let scale = self.get_scale(); self.tile_viewbox.update(self.viewbox, scale); self.focus_mode.reset(); performance::begin_measure!("render"); performance::begin_measure!("start_render_loop"); self.reset_canvas(); let surface_ids = SurfaceId::Strokes as u32 | SurfaceId::Fills as u32 | SurfaceId::InnerShadows as u32 | SurfaceId::TextDropShadows as u32; self.surfaces.apply_mut(surface_ids, |s| { s.canvas().scale((scale, scale)); }); let viewbox_cache_size = get_cache_size(self.viewbox, scale); let cached_viewbox_cache_size = get_cache_size(self.cached_viewbox, scale); if viewbox_cache_size != cached_viewbox_cache_size { self.surfaces.resize_cache( &mut self.gpu_state, viewbox_cache_size, VIEWPORT_INTEREST_AREA_THRESHOLD, ); } // FIXME - review debug // debug::render_debug_tiles_for_viewbox(self); performance::begin_measure!("tile_cache"); self.pending_tiles .update(&self.tile_viewbox, &self.surfaces); performance::end_measure!("tile_cache"); self.pending_nodes.clear(); if self.pending_nodes.capacity() < tree.len() { self.pending_nodes .reserve(tree.len() - self.pending_nodes.capacity()); } // Clear nested state stacks to avoid residual fills/blurs from previous renders // being incorrectly applied to new frames self.nested_fills.clear(); self.nested_blurs.clear(); self.nested_shadows.clear(); // reorder by distance to the center. self.current_tile = None; self.render_in_progress = true; self.apply_drawing_to_render_canvas(None); self.process_animation_frame(tree, timestamp)?; performance::end_measure!("start_render_loop"); Ok(()) } pub fn process_animation_frame( &mut self, tree: ShapesPoolRef, timestamp: i32, ) -> Result<(), String> { performance::begin_measure!("process_animation_frame"); if self.render_in_progress { if tree.len() != 0 { self.render_shape_tree_partial(tree, timestamp)?; } else { println!("Empty tree"); } self.flush_and_submit(); if self.render_in_progress { self.cancel_animation_frame(); self.render_request_id = Some(wapi::request_animation_frame!()); } else { performance::end_measure!("render"); } } performance::end_measure!("process_animation_frame"); Ok(()) } #[inline] pub fn should_stop_rendering(&self, iteration: i32, timestamp: i32) -> bool { iteration % NODE_BATCH_THRESHOLD == 0 && performance::get_time() - timestamp > MAX_BLOCKING_TIME_MS } #[inline] pub fn render_shape_enter(&mut self, element: &Shape, mask: bool) { // Masked groups needs two rendering passes, the first one rendering // the content and the second one rendering the mask so we need to do // an extra save_layer to keep all the masked group separate from // other already drawn elements. if let Type::Group(group) = element.shape_type { let fills = &element.fills; let shadows = &element.shadows; self.nested_fills.push(fills.to_vec()); self.nested_shadows.push(shadows.to_vec()); if group.masked { let paint = skia::Paint::default(); let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); self.surfaces .canvas(SurfaceId::Current) .save_layer(&layer_rec); } } if let Type::Frame(_) = element.shape_type { self.nested_fills.push(Vec::new()); } let mut paint = skia::Paint::default(); paint.set_blend_mode(element.blend_mode().into()); paint.set_alpha_f(element.opacity()); // When we're rendering the mask shape we need to set a special blend mode // called 'destination-in' that keeps the drawn content within the mask. // @see https://skia.org/docs/user/api/skblendmode_overview/ if mask { let mut mask_paint = skia::Paint::default(); mask_paint.set_blend_mode(skia::BlendMode::DstIn); let mask_rec = skia::canvas::SaveLayerRec::default().paint(&mask_paint); self.surfaces .canvas(SurfaceId::Current) .save_layer(&mask_rec); } let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); self.surfaces .canvas(SurfaceId::Current) .save_layer(&layer_rec); self.focus_mode.enter(&element.id); } #[inline] pub fn render_shape_exit(&mut self, element: &Shape, visited_mask: bool) { if visited_mask { // Because masked groups needs two rendering passes (first drawing // the content and then drawing the mask), we need to do an // extra restore. if let Type::Group(group) = element.shape_type { if group.masked { self.surfaces.canvas(SurfaceId::Current).restore(); } } } else { // !visited_mask if let Type::Group(group) = element.shape_type { // When we're dealing with masked groups we need to // do a separate extra step to draw the mask (the last // element of a masked group) and blend (using // the blend mode 'destination-in') the content // of the group and the mask. if group.masked { self.pending_nodes.push(NodeRenderState { id: element.id, visited_children: true, clip_bounds: None, visited_mask: true, mask: false, }); if let Some(&mask_id) = element.mask_id() { self.pending_nodes.push(NodeRenderState { id: mask_id, visited_children: false, clip_bounds: None, visited_mask: false, mask: true, }); } } } } match element.shape_type { Type::Frame(_) | Type::Group(_) => { self.nested_fills.pop(); self.nested_blurs.pop(); self.nested_shadows.pop(); } _ => {} } //In clipped content strokes are drawn over the contained elements if element.clip() { let mut element_strokes: Cow = Cow::Borrowed(element); element_strokes.to_mut().clear_fills(); element_strokes.to_mut().clear_shadows(); element_strokes.to_mut().clip_content = false; self.render_shape( &element_strokes, None, SurfaceId::Fills, SurfaceId::Strokes, SurfaceId::InnerShadows, SurfaceId::TextDropShadows, true, None, None, ); } self.surfaces.canvas(SurfaceId::Current).restore(); self.focus_mode.exit(&element.id); } pub fn get_current_tile_bounds(&mut self) -> Rect { let tiles::Tile(tile_x, tile_y) = self.current_tile.unwrap(); let scale = self.get_scale(); let offset_x = self.viewbox.area.left * scale; let offset_y = self.viewbox.area.top * scale; Rect::from_xywh( (tile_x as f32 * tiles::TILE_SIZE) - offset_x, (tile_y as f32 * tiles::TILE_SIZE) - offset_y, tiles::TILE_SIZE, tiles::TILE_SIZE, ) } pub fn get_rect_bounds(&mut self, rect: skia::Rect) -> Rect { let scale = self.get_scale(); let offset_x = self.viewbox.area.left * scale; let offset_y = self.viewbox.area.top * scale; Rect::from_xywh( (rect.left * scale) - offset_x, (rect.top * scale) - offset_y, rect.width() * scale, rect.height() * scale, ) } pub fn get_shape_selrect_bounds(&mut self, shape: &Shape) -> Rect { let rect = shape.selrect(); self.get_rect_bounds(rect) } pub fn get_shape_extrect_bounds(&mut self, shape: &Shape, tree: ShapesPoolRef) -> Rect { let scale = self.get_scale(); let rect = self.get_cached_extrect(shape, tree, scale); self.get_rect_bounds(rect) } pub fn get_aligned_tile_bounds(&mut self, tile: tiles::Tile) -> Rect { let scale = self.get_scale(); let start_tile_x = (self.viewbox.area.left * scale / tiles::TILE_SIZE).floor() * tiles::TILE_SIZE; let start_tile_y = (self.viewbox.area.top * scale / tiles::TILE_SIZE).floor() * tiles::TILE_SIZE; Rect::from_xywh( (tile.0 as f32 * tiles::TILE_SIZE) - start_tile_x, (tile.1 as f32 * tiles::TILE_SIZE) - start_tile_y, tiles::TILE_SIZE, tiles::TILE_SIZE, ) } // Returns the bounds of the current tile relative to the viewbox, // aligned to the nearest tile grid origin. // // Unlike `get_current_tile_bounds`, which calculates bounds using the exact // scaled offset of the viewbox, this method snaps the origin to the nearest // lower multiple of `TILE_SIZE`. This ensures the tile bounds are aligned // with the global tile grid, which is useful for rendering tiles in a /// consistent and predictable layout. pub fn get_current_aligned_tile_bounds(&mut self) -> Rect { self.get_aligned_tile_bounds(self.current_tile.unwrap()) } /// Renders a drop shadow effect for the given shape. /// /// Creates a black shadow by converting the original shadow color to black, /// scaling the blur radius, and rendering the shape with the shadow offset applied. #[allow(clippy::too_many_arguments)] fn render_drop_black_shadow( &mut self, shape: &Shape, shadow: &Shadow, clip_bounds: Option<(Rect, Option, Matrix)>, scale: f32, translation: (f32, f32), ) { let mut transformed_shadow: Cow = Cow::Borrowed(shadow); transformed_shadow.to_mut().offset = (0.0, 0.0); transformed_shadow.to_mut().color = skia::Color::BLACK; // Scale blur to maintain consistent appearance across zoom levels // When canvas is scaled down (zoom out), blur should be scaled down too transformed_shadow.to_mut().blur = shadow.blur * scale; transformed_shadow.to_mut().spread = shadow.spread * scale; let mut plain_shape = Cow::Borrowed(shape); // The opacity of fills and strokes shouldn't affect the shadow, // so we paint everything black with the same opacity plain_shape.to_mut().clear_fills(); if shape.has_fills() { plain_shape .to_mut() .add_fill(Fill::Solid(SolidColor(skia::Color::BLACK))); } plain_shape.to_mut().clear_strokes(); for stroke in shape.strokes.iter() { plain_shape.to_mut().add_stroke(Stroke { fill: Fill::Solid(SolidColor(skia::Color::BLACK)), width: stroke.width, style: stroke.style, cap_end: stroke.cap_end, cap_start: stroke.cap_start, kind: stroke.kind, }); } let mut shadow_paint = skia::Paint::default(); shadow_paint.set_image_filter(transformed_shadow.get_drop_shadow_filter()); shadow_paint.set_blend_mode(skia::BlendMode::SrcOver); let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint); self.surfaces .canvas(SurfaceId::DropShadows) .save_layer(&layer_rec); self.surfaces .canvas(SurfaceId::DropShadows) .scale((scale, scale)); self.surfaces .canvas(SurfaceId::DropShadows) .translate(translation); self.render_shape( &plain_shape, clip_bounds, SurfaceId::DropShadows, SurfaceId::DropShadows, SurfaceId::DropShadows, SurfaceId::DropShadows, false, Some((shadow.offset.0, shadow.offset.1)), None, ); self.surfaces.canvas(SurfaceId::DropShadows).restore(); } pub fn render_shape_tree_partial_uncached( &mut self, tree: ShapesPoolRef, timestamp: i32, ) -> Result<(bool, bool), String> { let mut iteration = 0; let mut is_empty = true; while let Some(node_render_state) = self.pending_nodes.pop() { let NodeRenderState { id: node_id, visited_children, clip_bounds, visited_mask, mask, } = node_render_state; is_empty = false; let element = tree.get(&node_id).ok_or(format!( "Error: Element with root_id {} not found in the tree.", node_render_state.id ))?; // If the shape is not in the tile set, then we add them. if self.tiles.get_tiles_of(node_id).is_none() { self.add_shape_tiles(element, tree); } if visited_children { self.render_shape_exit(element, visited_mask); continue; } if !node_render_state.is_root() { let transformed_element: Cow = Cow::Borrowed(element); let scale = self.get_scale(); let extrect = transformed_element.extrect(tree, scale); let is_visible = extrect.intersects(self.render_area) && !transformed_element.hidden && !transformed_element.visually_insignificant(scale, tree); if self.options.is_debug_visible() { let shape_extrect_bounds = self.get_shape_extrect_bounds(&transformed_element, tree); debug::render_debug_shape(self, None, Some(shape_extrect_bounds)); } if !is_visible { continue; } } self.render_shape_enter(element, mask); if !node_render_state.is_root() && self.focus_mode.is_active() { let scale: f32 = self.get_scale(); let translation = self .surfaces .get_render_context_translation(self.render_area, scale); // For text shapes, render drop shadow using text rendering logic if !matches!(element.shape_type, Type::Text(_)) { // Shadow rendering technique: Two-pass approach for proper opacity handling // // The shadow rendering uses a two-pass technique to ensure that overlapping // shadow areas maintain correct opacity without unwanted darkening: // // 1. First pass: Render shadow shape in pure black (alpha channel preserved) // - This creates the shadow silhouette with proper alpha gradients // - The black color acts as a mask for the final shadow color // // 2. Second pass: Apply actual shadow color using SrcIn blend mode // - SrcIn preserves the alpha channel from the black shadow // - Only the color channels are replaced, maintaining transparency // - This prevents overlapping shadows from accumulating opacity // // This approach is essential for complex shapes with transparency where // multiple shadow areas might overlap, ensuring visual consistency. for shadow in element.drop_shadows_visible() { let paint = skia::Paint::default(); let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); self.surfaces .canvas(SurfaceId::DropShadows) .save_layer(&layer_rec); // First pass: Render shadow in black to establish alpha mask self.render_drop_black_shadow( element, shadow, clip_bounds, scale, translation, ); if !matches!(element.shape_type, Type::Bool(_)) { // Nested shapes shadowing - apply black shadow to child shapes too for shadow_shape_id in element.children.iter() { let shadow_shape = tree.get(shadow_shape_id).unwrap(); if shadow_shape.hidden { continue; } let clip_bounds = node_render_state .get_nested_shadow_clip_bounds(element, shadow); if !matches!(shadow_shape.shape_type, Type::Text(_)) { self.render_drop_black_shadow( shadow_shape, shadow, clip_bounds, scale, translation, ); } else { let paint = skia::Paint::default(); let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); self.surfaces .canvas(SurfaceId::DropShadows) .save_layer(&layer_rec); self.surfaces .canvas(SurfaceId::DropShadows) .scale((scale, scale)); self.surfaces .canvas(SurfaceId::DropShadows) .translate(translation); let mut transformed_shadow: Cow = Cow::Borrowed(shadow); transformed_shadow.to_mut().color = skia::Color::BLACK; transformed_shadow.to_mut().blur = transformed_shadow.blur * scale; transformed_shadow.to_mut().spread = transformed_shadow.spread * scale; let mut new_shadow_paint = skia::Paint::default(); new_shadow_paint.set_image_filter( transformed_shadow.get_drop_shadow_filter(), ); new_shadow_paint.set_blend_mode(skia::BlendMode::SrcOver); self.render_shape( shadow_shape, clip_bounds, SurfaceId::DropShadows, SurfaceId::DropShadows, SurfaceId::DropShadows, SurfaceId::DropShadows, true, None, Some(vec![new_shadow_paint.clone()]), ); self.surfaces.canvas(SurfaceId::DropShadows).restore(); } } } // Second pass: Apply actual shadow color using SrcIn blend mode // This preserves the alpha channel from the black shadow while // replacing only the color channels, preventing opacity accumulation let mut paint = skia::Paint::default(); paint.set_color(shadow.color); paint.set_blend_mode(skia::BlendMode::SrcIn); self.surfaces .canvas(SurfaceId::DropShadows) .draw_paint(&paint); self.surfaces.canvas(SurfaceId::DropShadows).restore(); } } self.surfaces .draw_into(SurfaceId::DropShadows, SurfaceId::Current, None); self.surfaces .canvas(SurfaceId::DropShadows) .clear(skia::Color::TRANSPARENT); self.render_shape( element, clip_bounds, SurfaceId::Fills, SurfaceId::Strokes, SurfaceId::InnerShadows, SurfaceId::TextDropShadows, true, None, None, ); self.surfaces .canvas(SurfaceId::DropShadows) .clear(skia::Color::TRANSPARENT); } else if visited_children { self.apply_drawing_to_render_canvas(Some(element)); } match element.shape_type { Type::Frame(_) | Type::Group(_) => { self.nested_blurs.push(element.blur); } _ => {} } // Set the node as visited_children before processing children self.pending_nodes.push(NodeRenderState { id: node_id, visited_children: true, clip_bounds, visited_mask: false, mask, }); if element.is_recursive() { let children_clip_bounds = node_render_state.get_children_clip_bounds(element, None); let mut children_ids: Vec<_> = element.children_ids_iter(false).collect(); // Z-index ordering on Layouts if element.has_layout() { children_ids.sort_by(|id1, id2| { let z1 = tree.get(id1).map_or_else(|| 0, |s| s.z_index()); let z2 = tree.get(id2).map_or_else(|| 0, |s| s.z_index()); z1.cmp(&z2) }); } for child_id in children_ids.iter() { self.pending_nodes.push(NodeRenderState { id: **child_id, visited_children: false, clip_bounds: children_clip_bounds, visited_mask: false, mask: false, }); } } // We try to avoid doing too many calls to get_time if self.should_stop_rendering(iteration, timestamp) { return Ok((is_empty, true)); } iteration += 1; } Ok((is_empty, false)) } pub fn render_shape_tree_partial( &mut self, tree: ShapesPoolRef, timestamp: i32, ) -> Result<(), String> { let mut should_stop = false; while !should_stop { if let Some(current_tile) = self.current_tile { if self.surfaces.has_cached_tile_surface(current_tile) { performance::begin_measure!("render_shape_tree::cached"); let tile_rect = self.get_current_tile_bounds(); self.surfaces.draw_cached_tile_surface( current_tile, tile_rect, self.background_color, ); performance::end_measure!("render_shape_tree::cached"); if self.options.is_debug_visible() { debug::render_workspace_current_tile( self, "Cached".to_string(), current_tile, tile_rect, ); } } else { performance::begin_measure!("render_shape_tree::uncached"); let (is_empty, early_return) = self.render_shape_tree_partial_uncached(tree, timestamp)?; if early_return { return Ok(()); } performance::end_measure!("render_shape_tree::uncached"); let tile_rect = self.get_current_tile_bounds(); if !is_empty { self.apply_render_to_final_canvas(tile_rect); if self.options.is_debug_visible() { debug::render_workspace_current_tile( self, "".to_string(), current_tile, tile_rect, ); } } else { self.surfaces.apply_mut(SurfaceId::Target as u32, |s| { let mut paint = skia::Paint::default(); paint.set_color(self.background_color); s.canvas().draw_rect(tile_rect, &paint); }); } } } self.surfaces .canvas(SurfaceId::Current) .clear(self.background_color); let Some(root) = tree.get(&Uuid::nil()) else { return Err(String::from("Root shape not found")); }; let root_ids = root.children_ids(false); // If we finish processing every node rendering is complete // let's check if there are more pending nodes if let Some(next_tile) = self.pending_tiles.pop() { self.update_render_context(next_tile); if !self.surfaces.has_cached_tile_surface(next_tile) { if let Some(ids) = self.tiles.get_shapes_at(next_tile) { // We only need first level shapes let mut valid_ids: Vec = ids .iter() .filter(|id| root_ids.contains(id)) .copied() .collect(); // These shapes for the tile should be ordered as they are in the parent node valid_ids.sort_by_key(|id| { root_ids.iter().position(|x| x == id).unwrap_or(usize::MAX) }); self.pending_nodes.extend(valid_ids.into_iter().map(|id| { NodeRenderState { id, visited_children: false, clip_bounds: None, visited_mask: false, mask: false, } })); } } } else { should_stop = true; } } self.render_in_progress = false; self.surfaces.gc(); // Cache target surface in a texture self.cached_viewbox = self.viewbox; self.cached_target_snapshot = Some(self.surfaces.snapshot(SurfaceId::Cache)); if self.options.is_debug_visible() { debug::render(self); } ui::render(self, tree); debug::render_wasm_label(self); Ok(()) } /* * Given a shape returns the TileRect with the range of tiles that the shape is in */ pub fn get_tiles_for_shape(&mut self, shape: &Shape, tree: ShapesPoolRef) -> TileRect { let scale = self.get_scale(); let extrect = self.get_cached_extrect(shape, tree, scale); let tile_size = tiles::get_tile_size(scale); tiles::get_tiles_for_rect(extrect, tile_size) } /* * Given a shape, check the indexes and update it's location in the tile set * returns the tiles that have changed in the process. */ pub fn update_shape_tiles(&mut self, shape: &Shape, tree: ShapesPoolRef) -> Vec { let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape, tree); let old_tiles = self .tiles .get_tiles_of(shape.id) .map_or(Vec::new(), |tiles| tiles.iter().copied().collect()); let new_tiles = (rsx..=rex).flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile(x, y))); let mut result = HashSet::::new(); // First, remove the shape from all tiles where it was previously located for tile in old_tiles { self.tiles.remove_shape_at(tile, shape.id); result.insert(tile); } // Then, add the shape to the new tiles for tile in new_tiles { self.tiles.add_shape_at(tile, shape.id); result.insert(tile); } result.iter().copied().collect() } /* * Add the tiles forthe shape to the index. * returns the tiles that have been updated */ pub fn add_shape_tiles(&mut self, shape: &Shape, tree: ShapesPoolRef) -> Vec { let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape, tree); let tiles: Vec<_> = (rsx..=rex) .flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile(x, y))) .collect(); for tile in tiles.iter() { self.tiles.add_shape_at(*tile, shape.id); } tiles } pub fn remove_cached_tile(&mut self, tile: tiles::Tile) { let rect = self.get_aligned_tile_bounds(tile); self.surfaces .remove_cached_tile_surface(tile, rect, self.background_color); } pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) { performance::begin_measure!("rebuild_tiles_shallow"); let mut all_tiles = HashSet::::new(); let mut nodes = vec![Uuid::nil()]; while let Some(shape_id) = nodes.pop() { if let Some(shape) = tree.get(&shape_id) { if shape_id != Uuid::nil() { all_tiles.extend(self.update_shape_tiles(shape, tree)); } else { // We only need to rebuild tiles from the first level. for child_id in shape.children_ids_iter(false) { nodes.push(*child_id); } } } } // Update the changed tiles self.surfaces.remove_cached_tiles(self.background_color); for tile in all_tiles { self.remove_cached_tile(tile); } performance::end_measure!("rebuild_tiles_shallow"); } pub fn rebuild_tiles(&mut self, tree: ShapesPoolRef) { performance::begin_measure!("rebuild_tiles"); self.tiles.invalidate(); let mut all_tiles = HashSet::::new(); let mut nodes = vec![Uuid::nil()]; while let Some(shape_id) = nodes.pop() { if let Some(shape) = tree.get(&shape_id) { if shape_id != Uuid::nil() { // We have invalidated the tiles so we only need to add the shape all_tiles.extend(self.add_shape_tiles(shape, tree)); } for child_id in shape.children_ids_iter(false) { nodes.push(*child_id); } } } // Update the changed tiles self.surfaces.remove_cached_tiles(self.background_color); for tile in all_tiles { self.remove_cached_tile(tile); } performance::end_measure!("rebuild_tiles"); } /* * Rebuild the tiles for the shapes that have been modified from the * last time this was executed. */ pub fn rebuild_touched_tiles(&mut self, tree: ShapesPoolRef) { performance::begin_measure!("rebuild_touched_tiles"); let mut all_tiles = HashSet::::new(); let ids = self.touched_ids.clone(); for shape_id in ids.iter() { if let Some(shape) = tree.get(shape_id) { if shape_id != &Uuid::nil() { all_tiles.extend(self.update_shape_tiles(shape, tree)); } } } // Update the changed tiles for tile in all_tiles { self.remove_cached_tile(tile); } self.clean_touched(); performance::end_measure!("rebuild_touched_tiles"); } /// Invalidates extended rectangles and updates tiles for a set of shapes /// /// This function takes a set of shape IDs and for each one: /// 1. Invalidates the extrect cache /// 2. Updates the tiles to ensure proper rendering /// /// This is useful when you have a pre-computed set of shape IDs that need to be refreshed, /// regardless of their relationship to other shapes (e.g., ancestors, descendants, or any other collection). pub fn update_tiles_shapes(&mut self, shape_ids: &[Uuid], tree: ShapesPoolMutRef<'_>) { performance::begin_measure!("invalidate_and_update_tiles"); let mut all_tiles = HashSet::::new(); for shape_id in shape_ids { if let Some(shape) = tree.get(shape_id) { all_tiles.extend(self.update_shape_tiles(shape, tree)); } } for tile in all_tiles { self.remove_cached_tile(tile); } performance::end_measure!("invalidate_and_update_tiles"); } /// Rebuilds tiles for shapes with modifiers and processes their ancestors /// /// This function applies transformation modifiers to shapes and updates their tiles. /// Additionally, it processes all ancestors of modified shapes to ensure their /// extended rectangles are properly recalculated and their tiles are updated. /// This is crucial for frames and groups that contain transformed children. pub fn rebuild_modifier_tiles(&mut self, tree: ShapesPoolMutRef<'_>, ids: Vec) { let ancestors = all_with_ancestors(&ids, tree, false); self.update_tiles_shapes(&ancestors, tree); } pub fn get_scale(&self) -> f32 { self.viewbox.zoom() * self.options.dpr() } pub fn get_cached_scale(&self) -> f32 { self.cached_viewbox.zoom() * self.options.dpr() } pub fn mark_touched(&mut self, uuid: Uuid) { self.touched_ids.insert(uuid); } pub fn clean_touched(&mut self) { self.touched_ids.clear(); } pub fn get_cached_extrect(&mut self, shape: &Shape, tree: ShapesPoolRef, scale: f32) -> Rect { shape.extrect(tree, scale) } }