diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 0da2be9575..6a64ef2026 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -2392,7 +2392,52 @@ impl RenderState { } #[inline] - pub fn render_shape_enter(&mut self, element: &Shape, mask: bool, target_surface: SurfaceId) { + fn clip_target_surface_to_stack( + &mut self, + clips: &ClipStack, + target_surface: SurfaceId, + scale: f32, + antialias: bool, + ) { + let translation = self + .surfaces + .get_render_context_translation(self.render_area, scale); + + for (bounds, corners, transform) in clips.iter() { + let mut total_matrix = Matrix::new_identity(); + if target_surface == SurfaceId::Export { + let Some((export_rect, export_scale)) = self.export_context else { + continue; + }; + total_matrix.pre_scale((export_scale, export_scale), None); + total_matrix.pre_translate((-export_rect.x(), -export_rect.y())); + } else { + total_matrix.pre_scale((scale, scale), None); + total_matrix.pre_translate((translation.0, translation.1)); + } + total_matrix.pre_concat(transform); + + let canvas = self.surfaces.canvas(target_surface); + canvas.concat(&total_matrix); + if let Some(corners) = corners { + let rrect = RRect::new_rect_radii(*bounds, corners); + canvas.clip_rrect(rrect, skia::ClipOp::Intersect, antialias); + } else { + canvas.clip_rect(*bounds, skia::ClipOp::Intersect, antialias); + } + self.surfaces + .canvas(target_surface) + .concat(&total_matrix.invert().unwrap_or_default()); + } + } + + pub fn render_shape_enter( + &mut self, + element: &Shape, + mask: bool, + clip_bounds: Option<&ClipStack>, + target_surface: SurfaceId, + ) { // 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 @@ -2404,7 +2449,33 @@ impl RenderState { self.nested_shadows.push(shadows.to_vec()); if group.masked { - let paint = skia::Paint::default(); + // A masked group's blur is applied as a single layer blur over + // the whole masked result. + let mask_group_blur = element.masked_group_layer_blur().is_some(); + if mask_group_blur { + self.surfaces.canvas(target_surface).save(); + if let Some(clips) = clip_bounds { + let scale = self.get_scale(); + let antialias = !self.options.is_fast_mode() + && element + .should_use_antialias(scale, self.options.antialias_threshold); + self.clip_target_surface_to_stack(clips, target_surface, scale, antialias); + } + } + + let mut paint = skia::Paint::default(); + if !self.options.is_fast_mode() { + if let Some(blur) = element.masked_group_layer_blur() { + let scale = self.get_scale(); + let sigma = radius_to_sigma(blur.value * scale); + if let Some(filter) = + skia::image_filters::blur((sigma, sigma), None, None, None) + { + paint.set_image_filter(filter); + } + } + } + let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); self.surfaces.canvas(target_surface).save_layer(&layer_rec); } @@ -2558,6 +2629,10 @@ impl RenderState { self.surfaces.canvas(target_surface).restore(); } + if visited_mask && element.masked_group_layer_blur().is_some() { + self.surfaces.canvas(target_surface).restore(); + } + self.focus_mode.exit(&element.id); Ok(()) } @@ -2864,7 +2939,6 @@ impl RenderState { extrect: &mut Option, clip_bounds: Option, scale: f32, - translation: (f32, f32), node_render_state: &NodeRenderState, target_surface: SurfaceId, ) -> Result<()> { @@ -2970,60 +3044,7 @@ impl RenderState { let antialias = !self.options.is_fast_mode() && element.should_use_antialias(scale, self.options.antialias_threshold); self.surfaces.canvas(target_surface).save(); - for (bounds, corners, transform) in clips.iter() { - if target_surface == SurfaceId::Export { - let Some((export_rect, export_scale)) = self.export_context else { - continue; - }; - - let mut total_matrix = Matrix::new_identity(); - - total_matrix.pre_scale((export_scale, export_scale), None); - total_matrix.pre_translate((-export_rect.x(), -export_rect.y())); - - total_matrix.pre_concat(transform); - - let canvas = self.surfaces.canvas(target_surface); - canvas.concat(&total_matrix); - - let bounds = *bounds; - if let Some(corners) = corners { - let rrect = RRect::new_rect_radii(bounds, corners); - canvas.clip_rrect(rrect, skia::ClipOp::Intersect, antialias); - } else { - canvas.clip_rect(bounds, skia::ClipOp::Intersect, antialias); - } - self.surfaces - .canvas(target_surface) - .concat(&total_matrix.invert().unwrap_or_default()); - } else { - let mut total_matrix = Matrix::new_identity(); - total_matrix.pre_scale((scale, scale), None); - total_matrix.pre_translate((translation.0, translation.1)); - total_matrix.pre_concat(transform); - - self.surfaces.canvas(target_surface).concat(&total_matrix); - - if let Some(corners) = corners { - let rrect = RRect::new_rect_radii(*bounds, corners); - self.surfaces.canvas(target_surface).clip_rrect( - rrect, - skia::ClipOp::Intersect, - antialias, - ); - } else { - self.surfaces.canvas(target_surface).clip_rect( - *bounds, - skia::ClipOp::Intersect, - antialias, - ); - } - - self.surfaces - .canvas(target_surface) - .concat(&total_matrix.invert().unwrap_or_default()); - } - } + self.clip_target_surface_to_stack(clips, target_surface, scale, antialias); self.surfaces .draw_into(SurfaceId::DropShadows, target_surface, None); self.surfaces.canvas(target_surface).restore(); @@ -3249,17 +3270,12 @@ impl RenderState { && element.drop_shadows_visible().next().is_some(); if shadow_before_layer { - let translation = self - .surfaces - .get_render_context_translation(self.render_area, scale); - self.render_element_drop_shadows_and_composite( element, tree, &mut extrect, clip_bounds.clone(), scale, - translation, &node_render_state, target_surface, )?; @@ -3271,14 +3287,10 @@ impl RenderState { self.render_background_blur(element, target_surface); } - self.render_shape_enter(element, mask, target_surface); + self.render_shape_enter(element, mask, clip_bounds.as_ref(), target_surface); } if !node_render_state.is_root() && self.focus_mode.is_active() { - let translation = self - .surfaces - .get_render_context_translation(self.render_area, scale); - // Skip expensive drop shadow rendering in fast mode (during pan/zoom). let skip_shadows = self.options.is_fast_mode(); @@ -3296,7 +3308,6 @@ impl RenderState { &mut extrect, clip_bounds.clone(), scale, - translation, &node_render_state, target_surface, )?; @@ -3350,6 +3361,9 @@ impl RenderState { Type::Frame(_) if Self::frame_clip_layer_blur(element).is_some() => { self.nested_blurs.push(None); } + Type::Group(_) if element.masked_group_layer_blur().is_some() => { + self.nested_blurs.push(None); + } Type::Frame(_) | Type::Group(_) => { self.nested_blurs.push(element.blur); } diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 1eb22a3b93..a1e79a3637 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -1711,6 +1711,16 @@ impl Shape { } } + pub fn masked_group_layer_blur(&self) -> Option { + use crate::shapes::BlurType; + match self.shape_type { + Type::Group(Group { masked: true }) => self.blur.filter(|blur| { + !blur.hidden && blur.blur_type == BlurType::LayerBlur && blur.value > 0.0 + }), + _ => None, + } + } + /// Checks if this shape has visual effects that might extend its bounds beyond selrect /// Shapes with these effects require expensive extrect calculation for accurate visibility checks pub fn has_effects_that_extend_bounds(&self) -> bool {