🐛 Fixing nested shadows

This commit is contained in:
Alejandro Alonso
2025-09-04 11:59:50 +02:00
parent 9c77296858
commit 7e52aadb98
10 changed files with 10505 additions and 211 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -179,3 +179,19 @@ test("Renders a file with blurs applied to any kind of shape", async ({
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with shadows applied to any kind of shape", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-shadows.json");
await workspace.goToWorkspace({
id: "9502081a-e1a4-80bc-8006-c2b968723199",
pageId: "9502081a-e1a4-80bc-8006-c2b96872319a",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});

View File

@@ -22,7 +22,7 @@ use options::RenderOptions;
pub use surfaces::{SurfaceId, Surfaces};
use crate::performance;
use crate::shapes::{Blur, BlurType, Corners, Fill, Shape, StructureEntry, Type};
use crate::shapes::{Blur, BlurType, Corners, Fill, Shadow, Shape, StructureEntry, Type};
use crate::state::ShapesPool;
use crate::textlayout::{
paragraph_builder_group_from_text, stroke_paragraph_builder_group_from_text,
@@ -61,16 +61,39 @@ impl NodeRenderState {
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,
/// and any additional modifiers.
///
/// # Parameters
///
/// * `element` - The shape element for which to calculate clip bounds
/// * `modifiers` - Optional transformation matrix to apply to the 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,
modifiers: Option<&Matrix>,
offset: Option<(f32, f32)>,
) -> Option<(Rect, Option<Corners>, Matrix)> {
if self.id.is_nil() || !element.clip() {
return self.clip_bounds;
}
let bounds = element.selrect();
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());
@@ -87,6 +110,45 @@ impl NodeRenderState {
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. It uses the shadow bounds but calculates the
/// transformation center based on the original shape, not the shadow bounds.
///
/// # Parameters
///
/// * `element` - The shape element for which to calculate shadow clip bounds
/// * `modifiers` - Optional transformation matrix to apply to the bounds
/// * `shadow` - The shadow configuration containing blur, offset, and other properties
pub fn get_shadow_clip_bounds(
&self,
element: &Shape,
modifiers: Option<&Matrix>,
shadow: &Shadow,
) -> Option<(Rect, Option<Corners>, Matrix)> {
if self.id.is_nil() {
return self.clip_bounds;
}
let bounds = element.get_frame_shadow_bounds(shadow);
let mut transform = element.transform;
transform.post_translate(element.center());
transform.pre_translate(-element.center());
if let Some(modifier) = modifiers {
transform.post_concat(modifier);
}
let corners = match &element.shape_type {
Type::Rect(data) => data.corners,
Type::Frame(data) => data.corners,
_ => None,
};
Some((bounds, corners, transform))
}
}
/// Represents the "focus mode" state used during rendering.
@@ -372,9 +434,6 @@ impl RenderState {
let paint = skia::Paint::default();
self.surfaces
.draw_into(SurfaceId::DropShadows, SurfaceId::Current, Some(&paint));
self.surfaces
.draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint));
@@ -396,10 +455,8 @@ impl RenderState {
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
}
let surface_ids = SurfaceId::Strokes as u32
| SurfaceId::Fills as u32
| SurfaceId::DropShadows as u32
| SurfaceId::InnerShadows as u32;
let surface_ids =
SurfaceId::Strokes as u32 | SurfaceId::Fills as u32 | SurfaceId::InnerShadows as u32;
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().clear(skia::Color::TRANSPARENT);
@@ -414,6 +471,7 @@ impl RenderState {
self.focus_mode.set_shapes(shapes);
}
#[allow(clippy::too_many_arguments)]
pub fn render_shape(
&mut self,
shapes: &ShapesPool,
@@ -422,6 +480,11 @@ impl RenderState {
shape: &Shape,
scale_content: Option<&f32>,
clip_bounds: Option<(Rect, Option<Corners>, Matrix)>,
fills_surface_id: SurfaceId,
strokes_surface_id: SurfaceId,
innershadows_surface_id: SurfaceId,
apply_to_current_surface: bool,
offset: Option<(f32, f32)>,
) {
let shape = if let Some(scale_content) = scale_content {
&shape.scale_content(*scale_content)
@@ -429,10 +492,8 @@ impl RenderState {
shape
};
let surface_ids = SurfaceId::Strokes as u32
| SurfaceId::Fills as u32
| SurfaceId::DropShadows as u32
| SurfaceId::InnerShadows as u32;
let surface_ids =
fills_surface_id as u32 | strokes_surface_id as u32 | innershadows_surface_id as u32;
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().save();
});
@@ -466,7 +527,7 @@ impl RenderState {
paint.set_color(skia::Color::from_argb(255, 255, 0, 0));
paint.set_stroke_width(4.);
self.surfaces
.canvas(SurfaceId::Fills)
.canvas(fills_surface_id)
.draw_rect(bounds, &paint);
}
@@ -505,22 +566,27 @@ impl RenderState {
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(shape_modifiers) = modifiers.get(&shape.id) {
self.surfaces
.canvas(SurfaceId::Fills)
.canvas(fills_surface_id)
.concat(shape_modifiers);
}
self.surfaces.canvas(SurfaceId::Fills).concat(&matrix);
self.surfaces.canvas(fills_surface_id).concat(&matrix);
if let Some(svg) = shape.svg.as_ref() {
svg.render(self.surfaces.canvas(SurfaceId::Fills))
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(SurfaceId::Fills));
dom.render(self.surfaces.canvas(fills_surface_id));
shape.to_mut().set_svg(dom);
}
Err(e) => {
@@ -531,16 +597,11 @@ impl RenderState {
}
Type::Text(text_content) => {
let surface_ids = SurfaceId::Strokes as u32
| SurfaceId::Fills as u32
| SurfaceId::DropShadows as u32
| SurfaceId::InnerShadows as u32;
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().concat(&matrix);
});
let text_content = text_content.new_bounds(shape.selrect());
let drop_shadows = shape.drop_shadow_paints();
let inner_shadows = shape.inner_shadow_paints();
let blur_filter = shape.image_filter(1.);
let blur_mask = shape.mask_filter(1.);
@@ -551,44 +612,9 @@ impl RenderState {
None,
);
// Render all drop shadows if there are no visible strokes
if !shape.has_visible_strokes() && !drop_shadows.is_empty() {
for drop_shadow in &drop_shadows {
let mut paragraphs_with_drop_shadows = paragraph_builder_group_from_text(
&text_content,
blur_filter.as_ref(),
blur_mask.as_ref(),
Some(drop_shadow),
);
shadows::render_text_drop_shadows(
self,
&shape,
&mut paragraphs_with_drop_shadows,
);
}
}
let count_inner_strokes = shape.count_visible_inner_strokes();
text::render(self, &shape, &mut paragraphs, None);
text::render(self, &shape, &mut paragraphs, Some(fills_surface_id));
for stroke in shape.visible_strokes().rev() {
for drop_shadow in &drop_shadows {
let mut stroke_paragraphs_with_drop_shadows =
stroke_paragraph_builder_group_from_text(
&text_content,
stroke,
&shape.selrect(),
blur_filter.as_ref(),
blur_mask.as_ref(),
Some(drop_shadow),
count_inner_strokes,
);
shadows::render_text_drop_shadows(
self,
&shape,
&mut stroke_paragraphs_with_drop_shadows,
);
}
let mut stroke_paragraphs = stroke_paragraph_builder_group_from_text(
&text_content,
stroke,
@@ -603,7 +629,7 @@ impl RenderState {
self,
&shape,
stroke,
None,
Some(strokes_surface_id),
None,
Some(&mut stroke_paragraphs),
antialias,
@@ -624,6 +650,7 @@ impl RenderState {
self,
&shape,
&mut stroke_paragraphs_with_inner_shadows,
innershadows_surface_id,
);
}
}
@@ -639,14 +666,11 @@ impl RenderState {
self,
&shape,
&mut paragraphs_with_inner_shadows,
innershadows_surface_id,
);
}
}
_ => {
let surface_ids = SurfaceId::Strokes as u32
| SurfaceId::Fills as u32
| SurfaceId::DropShadows as u32
| SurfaceId::InnerShadows as u32;
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().concat(&matrix);
});
@@ -676,34 +700,41 @@ impl RenderState {
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::render(self, shape, fill, antialias, fills_surface_id);
}
}
} else {
for fill in shape.fills().rev() {
fills::render(self, shape, fill, antialias);
fills::render(self, shape, fill, antialias, fills_surface_id);
}
}
for stroke in shape.visible_strokes().rev() {
shadows::render_stroke_drop_shadows(self, shape, stroke, antialias);
//In clipped content strokes are drawn over the contained elements in a subsequent step
if !shape.clip() {
strokes::render(self, shape, stroke, None, None, None, antialias);
}
shadows::render_stroke_inner_shadows(self, shape, stroke, antialias);
strokes::render(
self,
shape,
stroke,
Some(strokes_surface_id),
None,
None,
antialias,
);
shadows::render_stroke_inner_shadows(
self,
shape,
stroke,
antialias,
innershadows_surface_id,
);
}
shadows::render_fill_inner_shadows(self, shape, antialias);
shadows::render_fill_drop_shadows(self, shape, antialias);
shadows::render_fill_inner_shadows(self, shape, antialias, innershadows_surface_id);
// bools::debug_render_bool_paths(self, shape, shapes, modifiers, structure);
}
};
self.apply_drawing_to_render_canvas(Some(&shape));
let surface_ids = SurfaceId::Strokes as u32
| SurfaceId::Fills as u32
| SurfaceId::DropShadows as u32
| SurfaceId::InnerShadows as u32;
if apply_to_current_surface {
self.apply_drawing_to_render_canvas(Some(&shape));
}
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().restore();
});
@@ -782,10 +813,8 @@ impl RenderState {
performance::begin_measure!("start_render_loop");
self.reset_canvas();
let surface_ids = SurfaceId::Strokes as u32
| SurfaceId::Fills as u32
| SurfaceId::DropShadows as u32
| SurfaceId::InnerShadows as u32;
let surface_ids =
SurfaceId::Strokes as u32 | SurfaceId::Fills as u32 | SurfaceId::InnerShadows as u32;
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().scale((scale, scale));
});
@@ -868,13 +897,6 @@ impl RenderState {
}
}
match element.shape_type {
Type::Frame(_) | Type::Group(_) => {
self.nested_blurs.push(Some(element.blur));
}
_ => {}
}
let mut paint = skia::Paint::default();
paint.set_blend_mode(element.blend_mode().into());
paint.set_alpha_f(element.opacity());
@@ -970,6 +992,11 @@ impl RenderState {
&element_strokes,
scale_content,
None,
SurfaceId::Fills,
SurfaceId::Strokes,
SurfaceId::InnerShadows,
true,
None,
);
}
@@ -1016,6 +1043,61 @@ impl RenderState {
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,
shapes: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
shape: &Shape,
shadow: &Shadow,
scale_content: Option<&f32>,
clip_bounds: Option<(Rect, Option<Corners>, Matrix)>,
scale: f32,
translation: (f32, f32),
) {
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
transformed_shadow.to_mut().offset = (0., 0.);
transformed_shadow.to_mut().color = skia::Color::from_argb(255, 0, 0, 0);
transformed_shadow.to_mut().blur = transformed_shadow.blur * scale;
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(
shapes,
modifiers,
structure,
shape,
scale_content,
clip_bounds,
SurfaceId::DropShadows,
SurfaceId::DropShadows,
SurfaceId::DropShadows,
false,
Some((shadow.offset.0, shadow.offset.1)),
);
self.surfaces.canvas(SurfaceId::DropShadows).restore();
}
pub fn render_shape_tree_partial_uncached(
&mut self,
tree: &ShapesPool,
@@ -1093,6 +1175,88 @@ impl RenderState {
self.render_shape_enter(element, mask);
if !node_render_state.is_root() && self.focus_mode.is_active() {
let scale = self.get_scale();
let translation = self
.surfaces
.get_render_context_translation(self.render_area, scale);
// 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().rev().filter(|s| !s.hidden()) {
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(
tree,
modifiers,
structure,
element,
shadow,
scale_content.get(&element.id),
clip_bounds,
scale,
translation,
);
// 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();
let clip_bounds = node_render_state.get_shadow_clip_bounds(
element,
modifiers.get(&element.id),
shadow,
);
self.render_drop_black_shadow(
tree,
modifiers,
structure,
shadow_shape,
shadow,
scale_content.get(&element.id),
clip_bounds,
scale,
translation,
);
}
// 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(
tree,
modifiers,
@@ -1100,11 +1264,27 @@ impl RenderState {
element,
scale_content.get(&element.id),
clip_bounds,
SurfaceId::Fills,
SurfaceId::Strokes,
SurfaceId::InnerShadows,
true,
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(Some(element.blur));
}
_ => {}
}
// Set the node as visited_children before processing children
self.pending_nodes.push(NodeRenderState {
id: node_id,
@@ -1115,8 +1295,11 @@ impl RenderState {
});
if element.is_recursive() {
let children_clip_bounds =
node_render_state.get_children_clip_bounds(element, modifiers.get(&element.id));
let children_clip_bounds = node_render_state.get_children_clip_bounds(
element,
modifiers.get(&element.id),
None,
);
let mut children_ids =
element.modified_children_ids(structure.get(&element.id), false);

View File

@@ -10,6 +10,7 @@ fn draw_image_fill(
image_fill: &ImageFill,
paint: &Paint,
antialias: bool,
surface_id: SurfaceId,
) {
let image = render_state.images.get(&image_fill.id());
if image.is_none() {
@@ -17,7 +18,7 @@ fn draw_image_fill(
}
let size = image.unwrap().dimensions();
let canvas = render_state.surfaces.canvas(SurfaceId::Fills);
let canvas = render_state.surfaces.canvas(surface_id);
let container = &shape.selrect;
let path_transform = shape.to_path_transform();
@@ -90,7 +91,13 @@ fn draw_image_fill(
/**
* This SHOULD be the only public function in this module.
*/
pub fn render(render_state: &mut RenderState, shape: &Shape, fill: &Fill, antialias: bool) {
pub fn render(
render_state: &mut RenderState,
shape: &Shape,
fill: &Fill,
antialias: bool,
surface_id: SurfaceId,
) {
let mut paint = fill.to_paint(&shape.selrect, antialias);
if let Some(image_filter) = shape.image_filter(1.) {
paint.set_image_filter(image_filter);
@@ -98,22 +105,29 @@ pub fn render(render_state: &mut RenderState, shape: &Shape, fill: &Fill, antial
match (fill, &shape.shape_type) {
(Fill::Image(image_fill), _) => {
draw_image_fill(render_state, shape, image_fill, &paint, antialias);
draw_image_fill(
render_state,
shape,
image_fill,
&paint,
antialias,
surface_id,
);
}
(_, Type::Rect(_) | Type::Frame(_)) => {
render_state
.surfaces
.draw_rect_to(SurfaceId::Fills, shape, &paint);
.draw_rect_to(surface_id, shape, &paint);
}
(_, Type::Circle) => {
render_state
.surfaces
.draw_circle_to(SurfaceId::Fills, shape, &paint);
.draw_circle_to(surface_id, shape, &paint);
}
(_, Type::Path(_)) | (_, Type::Bool(_)) => {
render_state
.surfaces
.draw_path_to(SurfaceId::Fills, shape, &paint);
.draw_path_to(surface_id, shape, &paint);
}
(_, Type::Group(_)) => {
// Groups can have fills but they propagate them to their children

View File

@@ -6,28 +6,15 @@ use skia_safe::textlayout::ParagraphBuilder;
use skia_safe::{Paint, Path};
// Fill Shadows
pub fn render_fill_drop_shadows(render_state: &mut RenderState, shape: &Shape, antialias: bool) {
if shape.has_fills() {
for shadow in shape.drop_shadows().rev().filter(|s| !s.hidden()) {
render_fill_drop_shadow(render_state, shape, shadow, antialias);
}
}
}
fn render_fill_drop_shadow(
pub fn render_fill_inner_shadows(
render_state: &mut RenderState,
shape: &Shape,
shadow: &Shadow,
antialias: bool,
surface_id: SurfaceId,
) {
let paint = &shadow.get_drop_shadow_paint(antialias, shape.image_filter(1.).as_ref());
render_shadow_paint(render_state, shape, paint, SurfaceId::DropShadows);
}
pub fn render_fill_inner_shadows(render_state: &mut RenderState, shape: &Shape, antialias: bool) {
if shape.has_fills() {
for shadow in shape.inner_shadows().rev().filter(|s| !s.hidden()) {
render_fill_inner_shadow(render_state, shape, shadow, antialias);
render_fill_inner_shadow(render_state, shape, shadow, antialias, surface_id);
}
}
}
@@ -37,31 +24,10 @@ fn render_fill_inner_shadow(
shape: &Shape,
shadow: &Shadow,
antialias: bool,
surface_id: SurfaceId,
) {
let paint = &shadow.get_inner_shadow_paint(antialias, shape.image_filter(1.).as_ref());
render_shadow_paint(render_state, shape, paint, SurfaceId::InnerShadows);
}
pub fn render_stroke_drop_shadows(
render_state: &mut RenderState,
shape: &Shape,
stroke: &Stroke,
antialias: bool,
) {
if !shape.has_fills() {
for shadow in shape.drop_shadows().rev().filter(|s| !s.hidden()) {
let filter = shadow.get_drop_shadow_filter();
strokes::render(
render_state,
shape,
stroke,
None,
filter.as_ref(),
None,
antialias,
)
}
}
render_shadow_paint(render_state, shape, paint, surface_id);
}
pub fn render_stroke_inner_shadows(
@@ -69,6 +35,7 @@ pub fn render_stroke_inner_shadows(
shape: &Shape,
stroke: &Stroke,
antialias: bool,
surface_id: SurfaceId,
) {
if !shape.has_fills() {
for shadow in shape.inner_shadows().rev().filter(|s| !s.hidden()) {
@@ -77,7 +44,7 @@ pub fn render_stroke_inner_shadows(
render_state,
shape,
stroke,
None,
Some(surface_id),
filter.as_ref(),
None,
antialias,
@@ -86,19 +53,6 @@ pub fn render_stroke_inner_shadows(
}
}
pub fn render_text_drop_shadows(
render_state: &mut RenderState,
shape: &Shape,
paragraphs: &mut [Vec<ParagraphBuilder>],
) {
text::render(
render_state,
shape,
paragraphs,
Some(SurfaceId::DropShadows),
);
}
// Render text paths (unused)
#[allow(dead_code)]
pub fn render_text_path_stroke_drop_shadows(
@@ -126,13 +80,9 @@ pub fn render_text_inner_shadows(
render_state: &mut RenderState,
shape: &Shape,
paragraphs: &mut [Vec<ParagraphBuilder>],
surface_id: SurfaceId,
) {
text::render(
render_state,
shape,
paragraphs,
Some(SurfaceId::InnerShadows),
);
text::render(render_state, shape, paragraphs, Some(surface_id));
}
// Render text paths (unused)

View File

@@ -525,7 +525,7 @@ pub fn render(
let scale = render_state.get_scale();
let canvas = render_state
.surfaces
.canvas(surface_id.unwrap_or(SurfaceId::Strokes));
.canvas(surface_id.unwrap_or(surface_id.unwrap_or(SurfaceId::Strokes)));
let selrect = shape.selrect;
let path_transform = shape.to_path_transform();
let svg_attrs = &shape.svg_attrs;
@@ -569,7 +569,7 @@ pub fn render(
render_state,
shape,
paragraphs.expect("Text shapes should have paragraphs"),
Some(SurfaceId::Strokes),
surface_id,
);
}
shape_type @ (Type::Path(_) | Type::Bool(_)) => {

View File

@@ -175,16 +175,21 @@ impl Surfaces {
performance::begin_measure!("apply_mut::flags");
}
pub fn update_render_context(&mut self, render_area: skia::Rect, scale: f32) {
let translation = (
pub fn get_render_context_translation(
&mut self,
render_area: skia::Rect,
scale: f32,
) -> (f32, f32) {
(
-render_area.left() + self.margins.width as f32 / scale,
-render_area.top() + self.margins.height as f32 / scale,
);
)
}
pub fn update_render_context(&mut self, render_area: skia::Rect, scale: f32) {
let translation = self.get_render_context_translation(render_area, scale);
self.apply_mut(
SurfaceId::Fills as u32
| SurfaceId::Strokes as u32
| SurfaceId::DropShadows as u32
| SurfaceId::InnerShadows as u32,
SurfaceId::Fills as u32 | SurfaceId::Strokes as u32 | SurfaceId::InnerShadows as u32,
|s| {
s.canvas().restore();
s.canvas().save();
@@ -251,7 +256,6 @@ impl Surfaces {
pub fn reset(&mut self, color: skia::Color) {
self.canvas(SurfaceId::Fills).restore_to_count(1);
self.canvas(SurfaceId::DropShadows).restore_to_count(1);
self.canvas(SurfaceId::InnerShadows).restore_to_count(1);
self.canvas(SurfaceId::Strokes).restore_to_count(1);
self.canvas(SurfaceId::Current).restore_to_count(1);
@@ -259,7 +263,6 @@ impl Surfaces {
SurfaceId::Fills as u32
| SurfaceId::Strokes as u32
| SurfaceId::Current as u32
| SurfaceId::DropShadows as u32
| SurfaceId::InnerShadows as u32,
|s| {
s.canvas().clear(color).reset_matrix();

View File

@@ -719,6 +719,65 @@ impl Shape {
.get_or_init(|| self.calculate_extrect(shapes_pool, modifiers))
}
/// Calculates the bounding rectangle for a frame shape's shadow, taking into account
/// stroke widths and shadow properties.
///
/// This method computes the expanded bounds that would be needed to fully render
/// the shadow effect for a frame shape. It considers:
/// - The base frame bounds (selection rectangle)
/// - Maximum stroke width across all strokes, accounting for stroke rendering kind
/// - Shadow offset (x, y displacement)
/// - Shadow blur radius (expands bounds outward)
/// - Whether the shadow is hidden
///
/// # Arguments
/// * `shadow` - The shadow configuration containing offset, blur, and visibility
///
/// # Returns
/// A `math::Rect` representing the bounding rectangle that encompasses the shadow.
/// Returns an empty rectangle if the shadow is hidden.
pub fn get_frame_shadow_bounds(&self, shadow: &Shadow) -> math::Rect {
assert!(
self.is_frame(),
"This method can only be called on frame shapes"
);
let base_bounds = self.selrect();
let mut rect = skia::Rect::new_empty();
let mut max_stroke: Option<f32> = None;
for stroke in self.strokes.iter() {
let width = match stroke.render_kind(false) {
StrokeKind::Inner => -stroke.width / 2.,
StrokeKind::Center => 0.,
StrokeKind::Outer => stroke.width,
};
max_stroke = Some(max_stroke.unwrap_or(f32::MIN).max(width));
}
if !shadow.hidden() {
let (x, y) = shadow.offset;
let mut shadow_rect = base_bounds;
shadow_rect.left += x;
shadow_rect.right += x;
shadow_rect.top += y;
shadow_rect.bottom += y;
shadow_rect.left += shadow.blur;
shadow_rect.top += shadow.blur;
shadow_rect.right -= shadow.blur;
shadow_rect.bottom -= shadow.blur;
if let Some(max_stroke) = max_stroke {
shadow_rect.left -= max_stroke;
shadow_rect.right += max_stroke;
shadow_rect.top -= max_stroke;
shadow_rect.bottom += max_stroke;
}
rect.join(shadow_rect);
}
rect
}
pub fn calculate_extrect(
&self,
shapes_pool: &ShapesPool,
@@ -762,22 +821,24 @@ impl Shape {
}
for shadow in self.shadows.iter() {
let (x, y) = shadow.offset;
let mut shadow_rect = rect;
shadow_rect.left += x;
shadow_rect.right += x;
shadow_rect.top += y;
shadow_rect.bottom += y;
if !shadow.hidden() {
let (x, y) = shadow.offset;
let mut shadow_rect = rect;
shadow_rect.left += x;
shadow_rect.right += x;
shadow_rect.top += y;
shadow_rect.bottom += y;
shadow_rect.left -= shadow.blur;
shadow_rect.top -= shadow.blur;
shadow_rect.right += shadow.blur;
shadow_rect.bottom += shadow.blur;
shadow_rect.left -= shadow.blur;
shadow_rect.top -= shadow.blur;
shadow_rect.right += shadow.blur;
shadow_rect.bottom += shadow.blur;
rect.join(shadow_rect);
rect.join(shadow_rect);
}
}
if self.blur.blur_type != blurs::BlurType::None {
if self.blur.blur_type != blurs::BlurType::None && !self.blur.hidden {
rect.left -= self.blur.value;
rect.top -= self.blur.value;
rect.right += self.blur.value;
@@ -1101,10 +1162,6 @@ impl Shape {
!self.fills.is_empty()
}
pub fn has_visible_strokes(&self) -> bool {
self.visible_strokes().next().is_some()
}
#[allow(dead_code)]
pub fn has_visible_inner_strokes(&self) -> bool {
self.visible_strokes().any(|s| s.kind == StrokeKind::Inner)
@@ -1153,20 +1210,6 @@ impl Shape {
}
}
pub fn drop_shadow_paints(&self) -> Vec<skia_safe::Paint> {
let drop_shadows: Vec<&crate::shapes::shadows::Shadow> =
self.drop_shadows().filter(|s| !s.hidden()).collect();
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()
}
pub fn inner_shadow_paints(&self) -> Vec<skia_safe::Paint> {
let inner_shadows: Vec<&crate::shapes::shadows::Shadow> =
self.inner_shadows().filter(|s| !s.hidden()).collect();

View File

@@ -63,19 +63,6 @@ impl Shadow {
self.hidden
}
pub fn get_drop_shadow_paint(
&self,
antialias: bool,
blur_filter: Option<&ImageFilter>,
) -> Paint {
let mut paint = Paint::default();
let shadow_filter = self.get_drop_shadow_filter();
let filter = compose_filters(blur_filter, shadow_filter.as_ref());
paint.set_image_filter(filter);
paint.set_anti_alias(antialias);
paint
}
pub fn get_drop_shadow_filter(&self) -> Option<ImageFilter> {
let mut filter = image_filters::drop_shadow_only(
(self.offset.0, self.offset.1),