🐛 Fix paragraph layout width on autowidth

This commit is contained in:
Elena Torro
2025-08-22 10:00:23 +02:00
parent c0c2c9489c
commit 46b3e174ed
7 changed files with 99 additions and 107 deletions

View File

@@ -295,7 +295,8 @@
"uppercase" 1 "uppercase" 1
"lowercase" 2 "lowercase" 2
"capitalize" 3 "capitalize" 3
nil)) nil 0
0))
(defn translate-text-decoration (defn translate-text-decoration
[text-decoration] [text-decoration]
@@ -304,13 +305,15 @@
"underline" 1 "underline" 1
"line-through" 2 "line-through" 2
"overline" 3 "overline" 3
nil)) nil 0
0))
(defn translate-text-direction (defn translate-text-direction
[text-direction] [text-direction]
(case text-direction (case text-direction
"ltr" 0 "ltr" 0
"rtl" 1 "rtl" 1
nil 0
0)) 0))
(defn translate-font-style (defn translate-font-style

View File

@@ -511,7 +511,7 @@ impl RenderState {
}); });
let text_content = text_content.new_bounds(shape.selrect()); let text_content = text_content.new_bounds(shape.selrect());
let mut paragraphs = text_content.get_skia_paragraphs( let mut paragraphs = text_content.to_paragraphs(
shape.image_filter(1.).as_ref(), shape.image_filter(1.).as_ref(),
shape.mask_filter(1.).as_ref(), shape.mask_filter(1.).as_ref(),
); );
@@ -524,7 +524,7 @@ impl RenderState {
text::render(self, &shape, &mut paragraphs, None, None); text::render(self, &shape, &mut paragraphs, None, None);
for stroke in shape.visible_strokes().rev() { for stroke in shape.visible_strokes().rev() {
let mut stroke_paragraphs = text_content.get_skia_stroke_paragraphs( let mut stroke_paragraphs = text_content.to_stroke_paragraphs(
stroke, stroke,
&shape.selrect(), &shape.selrect(),
shape.image_filter(1.).as_ref(), shape.image_filter(1.).as_ref(),

View File

@@ -14,10 +14,17 @@ pub fn render(
let canvas = render_state let canvas = render_state
.surfaces .surfaces
.canvas(surface_id.unwrap_or(SurfaceId::Fills)); .canvas(surface_id.unwrap_or(SurfaceId::Fills));
let container_height = shape.selrect().height();
// Calculate total height for vertical alignment // Width
let total_content_height = calculate_all_paragraphs_height(paragraphs, shape.bounds().width()); let paragraph_width = if let crate::shapes::Type::Text(text_content) = &shape.shape_type {
text_content.get_width()
} else {
shape.width()
};
// Height
let container_height = shape.selrect().height();
let total_content_height = calculate_all_paragraphs_height(paragraphs, paragraph_width);
let mut global_offset_y = match shape.vertical_align() { let mut global_offset_y = match shape.vertical_align() {
VerticalAlign::Center => (container_height - total_content_height) / 2.0, VerticalAlign::Center => (container_height - total_content_height) / 2.0,
VerticalAlign::Bottom => container_height - total_content_height, VerticalAlign::Bottom => container_height - total_content_height,
@@ -48,8 +55,7 @@ pub fn render(
continue; continue;
} }
skia_paragraph.layout(shape.bounds().width()); skia_paragraph.layout(paragraph_width);
let paragraph_height = skia_paragraph.height(); let paragraph_height = skia_paragraph.height();
let xy = (shape.selrect().x(), shape.selrect().y() + group_offset_y); let xy = (shape.selrect().x(), shape.selrect().y() + group_offset_y);
skia_paragraph.paint(canvas, xy); skia_paragraph.paint(canvas, xy);
@@ -126,7 +132,7 @@ pub fn render(
// For stroke groups (multiple elements), increment global_offset_y once per group // For stroke groups (multiple elements), increment global_offset_y once per group
if group_len > 1 { if group_len > 1 {
let mut first_paragraph = group[0].build(); let mut first_paragraph = group[0].build();
first_paragraph.layout(shape.bounds().width()); first_paragraph.layout(paragraph_width);
global_offset_y += first_paragraph.height(); global_offset_y += first_paragraph.height();
} else { } else {
// For regular paragraphs, global_offset_y was already incremented inside the loop // For regular paragraphs, global_offset_y was already incremented inside the loop

View File

@@ -10,8 +10,8 @@ use crate::math::bools;
use crate::math::{self as math, identitish, Bounds, Matrix, Point}; use crate::math::{self as math, identitish, Bounds, Matrix, Point};
use crate::shapes::{ use crate::shapes::{
auto_height, set_paragraphs_width, ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, auto_height, ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, Shape,
Modifier, Shape, StructureEntry, TransformEntry, Type, StructureEntry, TransformEntry, Type,
}; };
use crate::state::ShapesPool; use crate::state::ShapesPool;
use crate::state::State; use crate::state::State;
@@ -197,10 +197,11 @@ fn propagate_transform(
let mut transform = entry.transform; let mut transform = entry.transform;
if let Type::Text(content) = &shape.shape_type { if let Type::Text(content) = &shape.shape_type {
if content.grow_type() == GrowType::AutoHeight { match content.grow_type() {
let mut paragraphs = content.get_skia_paragraphs(None, None); GrowType::AutoHeight => {
set_paragraphs_width(shape_bounds_after.width(), &mut paragraphs); let paragraph_width = shape_bounds_after.width();
let height = auto_height(&mut paragraphs, shape_bounds_after.width()); let mut paragraphs = content.to_paragraphs(None, None);
let height = auto_height(&mut paragraphs, paragraph_width);
let resize_transform = math::resize_matrix( let resize_transform = math::resize_matrix(
&shape_bounds_after, &shape_bounds_after,
&shape_bounds_after, &shape_bounds_after,
@@ -210,6 +211,21 @@ fn propagate_transform(
shape_bounds_after = shape_bounds_after.transform(&resize_transform); shape_bounds_after = shape_bounds_after.transform(&resize_transform);
transform.post_concat(&resize_transform); transform.post_concat(&resize_transform);
} }
GrowType::AutoWidth => {
let paragraph_width = content.get_width();
let mut paragraphs = content.to_paragraphs(None, None);
let height = auto_height(&mut paragraphs, paragraph_width);
let resize_transform = math::resize_matrix(
&shape_bounds_after,
&shape_bounds_after,
paragraph_width,
height,
);
shape_bounds_after = shape_bounds_after.transform(&resize_transform);
transform.post_concat(&resize_transform);
}
GrowType::Fixed => {}
}
} }
if pixel_precision { if pixel_precision {

View File

@@ -41,14 +41,28 @@ pub struct TextContent {
pub grow_type: GrowType, pub grow_type: GrowType,
} }
pub fn set_paragraphs_width(width: f32, paragraphs: &mut Vec<Vec<ParagraphBuilder>>) { pub fn build_paragraphs_with_width(
for group in paragraphs { paragraphs: &mut [Vec<ParagraphBuilder>],
for p in group { width: f32,
let mut paragraph = p.build(); ) -> Vec<Vec<skia_safe::textlayout::Paragraph>> {
paragraphs
.iter_mut()
.map(|builders| {
builders
.iter_mut()
.map(|builder| {
let mut paragraph = builder.build();
// For auto-width, always layout with infinite width first to get intrinsic width
paragraph.layout(f32::MAX); paragraph.layout(f32::MAX);
paragraph.layout(f32::max(width, paragraph.min_intrinsic_width().ceil())); let intrinsic_width = paragraph.max_intrinsic_width().ceil();
} // Use the larger of the requested width or intrinsic width to prevent line breaks
} let final_width = f32::max(width, intrinsic_width);
paragraph.layout(final_width);
paragraph
})
.collect()
})
.collect()
} }
impl TextContent { impl TextContent {
@@ -177,43 +191,14 @@ impl TextContent {
paragraph_group paragraph_group
} }
pub fn collect_paragraphs( pub fn get_width(&self) -> f32 {
&self,
mut paragraphs: Vec<Vec<ParagraphBuilder>>,
) -> Vec<Vec<ParagraphBuilder>> {
if self.grow_type() == GrowType::AutoWidth { if self.grow_type() == GrowType::AutoWidth {
set_paragraphs_width(f32::MAX, &mut paragraphs); let temp_paragraphs = self.to_paragraphs(None, None);
let max_width = auto_width(&mut paragraphs, self.width()).ceil(); let mut temp_paragraphs = temp_paragraphs;
set_paragraphs_width(max_width, &mut paragraphs); auto_width(&mut temp_paragraphs, f32::MAX).ceil()
} else { } else {
set_paragraphs_width(self.width(), &mut paragraphs); self.width()
} }
paragraphs
}
pub fn get_skia_paragraphs(
&self,
blur: Option<&ImageFilter>,
blur_mask: Option<&MaskFilter>,
) -> Vec<Vec<ParagraphBuilder>> {
self.collect_paragraphs(self.to_paragraphs(blur, blur_mask))
}
pub fn get_skia_stroke_paragraphs(
&self,
stroke: &Stroke,
bounds: &Rect,
blur: Option<&ImageFilter>,
blur_mask: Option<&MaskFilter>,
count_inner_strokes: usize,
) -> Vec<Vec<ParagraphBuilder>> {
self.collect_paragraphs(self.to_stroke_paragraphs(
stroke,
bounds,
blur,
blur_mask,
count_inner_strokes,
))
} }
pub fn grow_type(&self) -> GrowType { pub fn grow_type(&self) -> GrowType {
@@ -225,9 +210,10 @@ impl TextContent {
} }
pub fn visual_bounds(&self) -> (f32, f32) { pub fn visual_bounds(&self) -> (f32, f32) {
let paragraph_width = self.get_width();
let mut paragraphs = self.to_paragraphs(None, None); let mut paragraphs = self.to_paragraphs(None, None);
let height = auto_height(&mut paragraphs, self.width()); let paragraph_height = auto_height(&mut paragraphs, paragraph_width);
(self.width(), height) (paragraph_width, paragraph_height)
} }
pub fn transform(&mut self, transform: &Matrix) { pub fn transform(&mut self, transform: &Matrix) {
@@ -728,19 +714,7 @@ pub fn get_built_paragraphs(
paragraphs: &mut [Vec<ParagraphBuilder>], paragraphs: &mut [Vec<ParagraphBuilder>],
width: f32, width: f32,
) -> Vec<Vec<skia_safe::textlayout::Paragraph>> { ) -> Vec<Vec<skia_safe::textlayout::Paragraph>> {
paragraphs build_paragraphs_with_width(paragraphs, width)
.iter_mut()
.map(|builders| {
builders
.iter_mut()
.map(|builder_handle| {
let mut paragraph = builder_handle.build();
paragraph.layout(width);
paragraph
})
.collect()
})
.collect()
} }
pub fn auto_width(paragraphs: &mut [Vec<ParagraphBuilder>], width: f32) -> f32 { pub fn auto_width(paragraphs: &mut [Vec<ParagraphBuilder>], width: f32) -> f32 {
@@ -754,15 +728,6 @@ pub fn auto_width(paragraphs: &mut [Vec<ParagraphBuilder>], width: f32) -> f32 {
}) })
} }
pub fn max_width(paragraphs: &mut [Vec<ParagraphBuilder>], width: f32) -> f32 {
let built_paragraphs = get_built_paragraphs(paragraphs, width);
built_paragraphs
.iter()
.flatten()
.fold(0.0, |max_width, p| f32::max(p.max_width(), max_width))
}
pub fn auto_height(paragraphs: &mut [Vec<ParagraphBuilder>], width: f32) -> f32 { pub fn auto_height(paragraphs: &mut [Vec<ParagraphBuilder>], width: f32) -> f32 {
paragraphs.iter_mut().fold(0.0, |auto_height, p| { paragraphs.iter_mut().fold(0.0, |auto_height, p| {
p.iter_mut().fold(auto_height, |auto_height, paragraph| { p.iter_mut().fold(auto_height, |auto_height, paragraph| {

View File

@@ -1,7 +1,6 @@
use crate::shapes::text::TextContent; use crate::shapes::text::TextContent;
use skia_safe::{ use skia_safe::{
self as skia, textlayout::Paragraph as SkiaParagraph, textlayout::ParagraphBuilder, self as skia, textlayout::Paragraph as SkiaParagraph, FontMetrics, Point, Rect, TextBlob,
FontMetrics, Point, Rect, TextBlob,
}; };
use std::ops::Deref; use std::ops::Deref;
@@ -17,16 +16,11 @@ impl TextPaths {
Self(content) Self(content)
} }
pub fn get_skia_paragraphs(&self) -> Vec<Vec<ParagraphBuilder>> {
let paragraphs = self.to_paragraphs(None, None);
self.collect_paragraphs(paragraphs)
}
pub fn get_paths(&self, antialias: bool) -> Vec<(skia::Path, skia::Paint)> { pub fn get_paths(&self, antialias: bool) -> Vec<(skia::Path, skia::Paint)> {
let mut paths = Vec::new(); let mut paths = Vec::new();
let mut offset_y = self.bounds.y(); let mut offset_y = self.bounds.y();
let mut paragraphs = self.get_skia_paragraphs(); let mut paragraphs = self.to_paragraphs(None, None);
for paragraphs in paragraphs.iter_mut() { for paragraphs in paragraphs.iter_mut() {
for paragraph_builder in paragraphs.iter_mut() { for paragraph_builder in paragraphs.iter_mut() {

View File

@@ -1,5 +1,5 @@
use crate::mem; use crate::mem;
use crate::shapes::{auto_height, auto_width, max_width, GrowType, RawTextData, Type}; use crate::shapes::{auto_height, build_paragraphs_with_width, GrowType, RawTextData, Type};
use crate::STATE; use crate::STATE;
use crate::{with_current_shape, with_current_shape_mut}; use crate::{with_current_shape, with_current_shape_mut};
@@ -43,19 +43,27 @@ pub extern "C" fn get_text_dimensions() -> *mut u8 {
height = shape.selrect.height(); height = shape.selrect.height();
if let Type::Text(content) = &shape.shape_type { if let Type::Text(content) = &shape.shape_type {
let mut paragraphs = content.get_skia_paragraphs( // 1. Reset Paragraphs
shape.image_filter(1.).as_ref(), let paragraph_width = content.get_width();
shape.mask_filter(1.).as_ref(), let mut paragraphs = content.to_paragraphs(None, None);
); let built_paragraphs = build_paragraphs_with_width(&mut paragraphs, paragraph_width);
m_width = max_width(&mut paragraphs, width);
// 2. Max Width Calculation
m_width = built_paragraphs
.iter()
.flatten()
.fold(0.0, |max_width, p| f32::max(p.max_width(), max_width));
// 3. Width and Height Calculation
match content.grow_type() { match content.grow_type() {
GrowType::AutoHeight => { GrowType::AutoHeight => {
height = auto_height(&mut paragraphs, width).ceil(); let mut paragraph_height = content.to_paragraphs(None, None);
height = auto_height(&mut paragraph_height, paragraph_width).ceil();
} }
GrowType::AutoWidth => { GrowType::AutoWidth => {
width = auto_width(&mut paragraphs, width).ceil(); width = paragraph_width;
height = auto_height(&mut paragraphs, width).ceil(); let mut paragraph_height = content.to_paragraphs(None, None);
height = auto_height(&mut paragraph_height, paragraph_width).ceil();
} }
GrowType::Fixed => {} GrowType::Fixed => {}
} }