diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 12092c8f30..449ec4eef0 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -10,7 +10,6 @@ mod shadows; mod strokes; mod surfaces; pub mod text; - mod ui; use skia_safe::{self as skia, Matrix, RRect, Rect}; @@ -53,6 +52,25 @@ pub struct NodeRenderState { mask: bool, } +/// Get simplified children of a container, flattening nested flattened containers +fn get_simplified_children<'a>(tree: ShapesPoolRef<'a>, shape: &'a Shape) -> Vec { + let mut result = Vec::new(); + + for child_id in shape.children_ids_iter(false) { + if let Some(child) = tree.get(child_id) { + if child.can_flatten() { + // Child is flattened: recursively get its simplified children + result.extend(get_simplified_children(tree, child)); + } else { + // Child is not flattened: add it directly + result.push(*child_id); + } + } + } + + result +} + impl NodeRenderState { pub fn is_root(&self) -> bool { self.id.is_nil() @@ -1038,6 +1056,7 @@ impl RenderState { // reorder by distance to the center. self.current_tile = None; self.render_in_progress = true; + self.apply_drawing_to_render_canvas(None); if sync_render { @@ -1478,31 +1497,42 @@ impl RenderState { } if visited_children { - self.render_shape_exit(element, visited_mask); + // Skip render_shape_exit for flattened containers + if !element.can_flatten() { + self.render_shape_exit(element, visited_mask); + } continue; } if !node_render_state.is_root() { let transformed_element: Cow = Cow::Borrowed(element); - // Aggressive early exit: check hidden and selrect first (fastest checks) + // Aggressive early exit: check hidden first (fastest check) if transformed_element.hidden { continue; } - let selrect = transformed_element.selrect(); - if !selrect.intersects(self.render_area) { - continue; - } + // For frames and groups, we must use extrect because they can have nested content + // that extends beyond their selrect. Using selrect for early exit would incorrectly + // skip frames/groups that have nested content in the current tile. + let is_container = matches!( + transformed_element.shape_type, + crate::shapes::Type::Frame(_) | crate::shapes::Type::Group(_) + ); - // For simple shapes without effects, selrect check is sufficient - // Only calculate expensive extrect for shapes with effects that might extend bounds let scale = self.get_scale(); let has_effects = transformed_element.has_effects_that_extend_bounds(); - let is_visible = if !has_effects { + let is_visible = if is_container { + // Containers (frames/groups) must always use extrect to include nested content + let extrect = transformed_element.extrect(tree, scale); + extrect.intersects(self.render_area) + && !transformed_element.visually_insignificant(scale, tree) + } else if !has_effects { // Simple shape: selrect check is sufficient, skip expensive extrect - !transformed_element.visually_insignificant(scale, tree) + let selrect = transformed_element.selrect(); + selrect.intersects(self.render_area) + && !transformed_element.visually_insignificant(scale, tree) } else { // Shape with effects: need extrect for accurate bounds let extrect = transformed_element.extrect(tree, scale); @@ -1521,7 +1551,12 @@ impl RenderState { } } - self.render_shape_enter(element, mask); + // Skip render_shape_enter/exit for flattened containers + // If a container was flattened, it doesn't affect children visually, so we skip + // the expensive enter/exit operations and process children directly + if !element.can_flatten() { + self.render_shape_enter(element, mask); + } if !node_render_state.is_root() && self.focus_mode.is_active() { let scale: f32 = self.get_scale(); @@ -1725,14 +1760,18 @@ impl RenderState { self.apply_drawing_to_render_canvas(Some(element)); } - match element.shape_type { - Type::Frame(_) if Self::frame_clip_layer_blur(element).is_some() => { - self.nested_blurs.push(None); + // Skip nested state updates for flattened containers + // Flattened containers don't affect children, so we don't need to track their state + if !element.can_flatten() { + match element.shape_type { + Type::Frame(_) if Self::frame_clip_layer_blur(element).is_some() => { + self.nested_blurs.push(None); + } + Type::Frame(_) | Type::Group(_) => { + self.nested_blurs.push(element.blur); + } + _ => {} } - Type::Frame(_) | Type::Group(_) => { - self.nested_blurs.push(element.blur); - } - _ => {} } // Set the node as visited_children before processing children @@ -1747,24 +1786,35 @@ impl RenderState { 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(); + + let children_ids: Vec<_> = if element.can_flatten() { + // Container was flattened: get simplified children (which skip this level) + get_simplified_children(tree, element) + } else { + // Container not flattened: use original children + element.children_ids_iter(false).copied().collect() + }; // Z-index ordering on Layouts - if element.has_layout() { + let children_ids = if element.has_layout() { + let mut ids = children_ids; if element.is_flex() && !element.is_flex_reverse() { - children_ids.reverse(); + ids.reverse(); } - children_ids.sort_by(|id1, id2| { + ids.sort_by(|id1, id2| { let z1 = tree.get(id1).map(|s| s.z_index()).unwrap_or(0); let z2 = tree.get(id2).map(|s| s.z_index()).unwrap_or(0); z2.cmp(&z1) }); - } + ids + } else { + children_ids + }; for child_id in children_ids.iter() { self.pending_nodes.push(NodeRenderState { - id: **child_id, + id: *child_id, visited_children: false, clip_bounds: children_clip_bounds.clone(), visited_mask: false, diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 3ddb297fb1..e90478c36a 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -920,27 +920,15 @@ impl Shape { } Type::Group(_) | Type::Frame(_) if !self.clip_content => { - // Use selrect as a fast approximation first, then calculate - // extrect only if needed. This avoids expensive recursive extrect calculations - // for children that don't significantly expand the bounds. + // For frames and groups, we must always calculate extrect for all children + // to ensure accurate bounds that include nested content across all tiles. + // Using selrect for children can cause frames to be incorrectly omitted from + // tiles where they have nested content. for child_id in self.children_ids_iter(false) { if let Some(child_shape) = shapes_pool.get(child_id) { - // Fast path: check if child has effects that might expand bounds - // If no effects, selrect is likely sufficient - let has_effects = !child_shape.shadows.is_empty() - || child_shape.blur.is_some() - || !child_shape.strokes.is_empty() - || matches!(child_shape.shape_type, Type::Group(_) | Type::Frame(_)); - - if has_effects { - // Calculate full extrect for shapes with effects - let child_extrect = child_shape.calculate_extrect(shapes_pool, scale); - rect.join(child_extrect); - } else { - // No effects, selrect is sufficient (much faster) - let child_selrect = child_shape.selrect(); - rect.join(child_selrect); - } + // Always calculate full extrect for children to ensure accurate bounds + let child_extrect = child_shape.calculate_extrect(shapes_pool, scale); + rect.join(child_extrect); } } } @@ -1436,6 +1424,61 @@ impl Shape { !self.fills.is_empty() } + /// Determines if this frame or group can be flattened (doesn't affect children visually) + /// A container can be flattened if it has no visual effects that affect its children + /// and doesn't render its own content (no fills/strokes) + pub fn can_flatten(&self) -> bool { + // Only frames and groups can be flattened + if !matches!(self.shape_type, Type::Frame(_) | Type::Group(_)) { + return false; + } + + // Cannot flatten if it has visual effects that affect children: + + if self.clip_content { + return false; + } + + if !self.transform.is_identity() { + return false; + } + + if self.opacity != 1.0 { + return false; + } + + if self.blend_mode() != BlendMode::default() { + return false; + } + + if self.blur.is_some() { + return false; + } + + if !self.shadows.is_empty() { + return false; + } + + if let Type::Group(group) = &self.shape_type { + if group.masked { + return false; + } + } + + if self.hidden { + return false; + } + + // If the container itself has fills/strokes, it renders something visible + // We cannot flatten containers that render their own background/border + // because they need to be rendered even if they don't affect children + if self.has_fills() || self.has_visible_strokes() { + return false; + } + + true + } + /// Checks if this shape needs a layer for rendering due to visual effects /// (opacity < 1.0, non-default blend mode, or frame clip layer blur) pub fn needs_layer(&self) -> bool {