diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-a-file-with-shadows-applied-to-any-kind-of-shape-1.png b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-a-file-with-shadows-applied-to-any-kind-of-shape-1.png index b0e6c72e8d..f207d013c2 100644 Binary files a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-a-file-with-shadows-applied-to-any-kind-of-shape-1.png and b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-a-file-with-shadows-applied-to-any-kind-of-shape-1.png differ diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-shapes-with-exif-rotated-images-fills-and-strokes-1.png b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-shapes-with-exif-rotated-images-fills-and-strokes-1.png index d1d5459015..7b92260bd5 100644 Binary files a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-shapes-with-exif-rotated-images-fills-and-strokes-1.png and b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-shapes-with-exif-rotated-images-fills-and-strokes-1.png differ diff --git a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-different-text-shadows-combinations-1.png b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-different-text-shadows-combinations-1.png index ff8b34555d..44a9e86bbe 100644 Binary files a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-different-text-shadows-combinations-1.png and b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-different-text-shadows-combinations-1.png differ diff --git a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-multiple-text-shadows-in-order-1.png b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-multiple-text-shadows-in-order-1.png index 103e1f87f7..c55afad8f0 100644 Binary files a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-multiple-text-shadows-in-order-1.png and b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-multiple-text-shadows-in-order-1.png differ diff --git a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-styled-texts-1.png b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-styled-texts-1.png index d4f7241ea5..e5e0313b22 100644 Binary files a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-styled-texts-1.png and b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-styled-texts-1.png differ diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 4c2980ebce..129c3eab81 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -737,50 +737,20 @@ impl Shape { rect } - pub fn calculate_extrect( - &self, - shapes_pool: &ShapesPool, - modifiers: &HashMap, - ) -> math::Rect { - let shape = self.transformed(modifiers.get(&self.id)); - let mut max_stroke: f32 = 0.; - let is_open = if let Type::Path(p) = &shape.shape_type { - p.is_open() - } else { - false - }; + fn apply_stroke_bounds(&self, rect: math::Rect, stroke_width: f32) -> math::Rect { + let mut expanded_rect = rect; + expanded_rect.left -= stroke_width; + expanded_rect.right += stroke_width; + expanded_rect.top -= stroke_width; + expanded_rect.bottom += stroke_width; - for stroke in shape.strokes.iter() { - let width = match stroke.render_kind(is_open) { - StrokeKind::Inner => 0., - StrokeKind::Center => stroke.width / 2., - StrokeKind::Outer => stroke.width, - }; - max_stroke = max_stroke.max(width); - } + let mut result = rect; + result.join(expanded_rect); + result + } - let mut rect = if let Some(path) = shape.get_skia_path() { - path.compute_tight_bounds() - .with_outset((max_stroke, max_stroke)) - } else { - let mut bounds_rect = shape.bounds().to_rect(); - let mut stroke_rect = bounds_rect; - stroke_rect.left -= max_stroke; - stroke_rect.right += max_stroke; - stroke_rect.top -= max_stroke; - stroke_rect.bottom += max_stroke; - - bounds_rect.join(stroke_rect); - bounds_rect - }; - - if let Type::Text(text_content) = &shape.shape_type { - let (width, height) = text_content.visual_bounds(); - rect.right = rect.left + width; - rect.bottom = rect.top + height; - } - - for shadow in shape.shadows.iter() { + fn apply_shadow_bounds(&self, mut rect: math::Rect) -> math::Rect { + for shadow in self.shadows_visible() { if !shadow.hidden() { let (x, y) = shadow.offset; let mut shadow_rect = rect; @@ -797,8 +767,12 @@ impl Shape { rect.join(shadow_rect); } } + rect + } - if let Some(blur) = shape.blur { + fn apply_blur_bounds(&self, mut rect: math::Rect) -> math::Rect { + let blur = self.blur.as_ref(); + if let Some(blur) = blur { if !blur.hidden { rect.left -= blur.value; rect.top -= blur.value; @@ -806,17 +780,23 @@ impl Shape { rect.bottom += blur.value; } } + rect + } - // For groups and frames without clipping, extend the bounding rectangle to include all nested shapes - // This ensures that these containers properly encompass their content - let include_children = match &shape.shape_type { + fn apply_children_bounds( + &self, + mut rect: math::Rect, + shapes_pool: &ShapesPool, + modifiers: &HashMap, + ) -> math::Rect { + let include_children = match self.shape_type { Type::Group(_) => true, - Type::Frame(_) => !shape.clip_content, + Type::Frame(_) => !self.clip_content, _ => false, }; if include_children { - for child_id in shape.children_ids(false) { + for child_id in self.children_ids(false) { if let Some(child_shape) = shapes_pool.get(&child_id) { // Create a copy of the child shape to apply any transformations let mut transformed_element: Cow = Cow::Borrowed(child_shape); @@ -834,6 +814,38 @@ impl Shape { rect } + pub fn calculate_extrect( + &self, + shapes_pool: &ShapesPool, + modifiers: &HashMap, + ) -> math::Rect { + let shape = self.transformed(modifiers.get(&self.id)); + let max_stroke = Stroke::max_bounds_width(shape.strokes.iter(), shape.is_open()); + + let mut rect = match &shape.shape_type { + Type::Path(_) | Type::Bool(_) => { + if let Some(path) = shape.get_skia_path() { + return path + .compute_tight_bounds() + .with_outset((max_stroke, max_stroke)); + } + shape.bounds().to_rect() + } + Type::Text(text_content) => { + let text_bounds = text_content.get_bounds(&shape); + text_bounds.to_rect() + } + _ => shape.bounds().to_rect(), + }; + + rect = self.apply_stroke_bounds(rect, max_stroke); + rect = self.apply_shadow_bounds(rect); + rect = self.apply_blur_bounds(rect); + rect = self.apply_children_bounds(rect, shapes_pool, modifiers); + + rect + } + pub fn left_top(&self) -> Point { Point::new(self.selrect.left, self.selrect.top) } @@ -996,6 +1008,10 @@ impl Shape { ) } + pub fn is_open(&self) -> bool { + matches!(&self.shape_type, Type::Path(p) if p.is_open()) + } + pub fn add_shadow(&mut self, shadow: Shadow) { self.invalidate_extrect(); self.shadows.push(shadow); @@ -1036,6 +1052,10 @@ impl Shape { .filter(|shadow| shadow.style() == ShadowStyle::Inner && !shadow.hidden()) } + pub fn shadows_visible(&self) -> impl DoubleEndedIterator { + self.shadows.iter().rev().filter(|shadow| !shadow.hidden()) + } + pub fn to_path_transform(&self) -> Option { match self.shape_type { Type::Path(_) | Type::Bool(_) => { diff --git a/render-wasm/src/shapes/strokes.rs b/render-wasm/src/shapes/strokes.rs index 108b03bcd3..fa33cb9c1d 100644 --- a/render-wasm/src/shapes/strokes.rs +++ b/render-wasm/src/shapes/strokes.rs @@ -50,6 +50,20 @@ impl Stroke { } } + pub fn bounds_width(&self, is_open: bool) -> f32 { + match self.render_kind(is_open) { + StrokeKind::Inner => 0., + StrokeKind::Center => self.width / 2., + StrokeKind::Outer => self.width, + } + } + + pub fn max_bounds_width<'a>(strokes: impl Iterator, is_open: bool) -> f32 { + strokes + .map(|stroke| stroke.bounds_width(is_open)) + .fold(0.0, f32::max) + } + pub fn new_center_stroke( width: f32, style: StrokeStyle, diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index e31bac1195..c98f99cec4 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -1,5 +1,5 @@ use crate::{ - math::{Matrix, Rect}, + math::{Bounds, Matrix, Rect}, render::{default_font, DEFAULT_EMOJI_FONT}, }; @@ -16,7 +16,7 @@ use std::collections::HashSet; use super::FontFamily; use crate::math::Point; -use crate::shapes::{self, merge_fills}; +use crate::shapes::{self, merge_fills, Shape}; use crate::utils::{get_fallback_fonts, get_font_collection}; use crate::Uuid; @@ -194,11 +194,7 @@ impl TextContent { } pub fn width(&self) -> f32 { - if self.grow_type() == GrowType::AutoWidth { - self.size.width - } else { - self.bounds.width() - } + self.size.width } pub fn grow_type(&self) -> GrowType { @@ -209,8 +205,34 @@ impl TextContent { self.grow_type = grow_type; } - pub fn visual_bounds(&self) -> (f32, f32) { - (self.size.width, self.size.height) + pub fn get_bounds(&self, shape: &Shape) -> Bounds { + let (x, y, transform, center) = ( + shape.selrect.x(), + shape.selrect.y(), + &shape.transform, + &shape.center(), + ); + let (width, height) = (self.size.width, self.size.height); + let text_rect = Rect::from_xywh(x, y, width, height); + + let mut bounds = Bounds::new( + Point::new(text_rect.x(), text_rect.y()), + Point::new(text_rect.x() + text_rect.width(), text_rect.y()), + Point::new( + text_rect.x() + text_rect.width(), + text_rect.y() + text_rect.height(), + ), + Point::new(text_rect.x(), text_rect.y() + text_rect.height()), + ); + + if !transform.is_identity() { + let mut matrix = *transform; + matrix.post_translate(*center); + matrix.pre_translate(-*center); + bounds.transform_mut(&matrix); + } + + bounds } pub fn transform(&mut self, transform: &Matrix) { @@ -315,14 +337,13 @@ impl TextContent { let width = self.width(); let mut paragraph_builders = self.paragraph_builder_group_from_text(None); let paragraphs = - self.build_paragraphs_from_paragraph_builders(&mut paragraph_builders, f32::INFINITY); + self.build_paragraphs_from_paragraph_builders(&mut paragraph_builders, width); let height = paragraphs .iter() .flatten() .fold(0.0, |auto_height, paragraph| { auto_height + paragraph.height() }); - let size = TextContentSize::new_with_size(width.ceil(), height.ceil()); TextContentLayoutResult(paragraph_builders, paragraphs, size) } @@ -349,6 +370,7 @@ impl TextContent { pub fn update_layout(&mut self, selrect: Rect) -> TextContentSize { self.size.set_size(selrect.width(), selrect.height()); + match self.grow_type() { GrowType::AutoHeight => { let result = self.text_layout_auto_height(); diff --git a/render-wasm/src/wasm/text.rs b/render-wasm/src/wasm/text.rs index aaa4a309d7..41f293fa8f 100644 --- a/render-wasm/src/wasm/text.rs +++ b/render-wasm/src/wasm/text.rs @@ -348,6 +348,7 @@ pub extern "C" fn update_shape_text_layout_for(a: u32, b: u32, c: u32, d: u32) { with_state_mut!(state, { let shape_id = uuid_from_u32_quartet(a, b, c, d); if let Some(shape) = state.shapes.get_mut(&shape_id) { + shape.invalidate_extrect(); if let Type::Text(text_content) = &mut shape.shape_type { text_content.update_layout(shape.selrect); } @@ -359,6 +360,7 @@ pub extern "C" fn update_shape_text_layout_for(a: u32, b: u32, c: u32, d: u32) { pub extern "C" fn update_shape_text_layout_for_all() { with_state_mut!(state, { for shape in state.shapes.iter_mut() { + shape.invalidate_extrect(); if let Type::Text(text_content) = &mut shape.shape_type { text_content.update_layout(shape.selrect); }