♻️ Unify text methods

This commit is contained in:
Elena Torro 2026-09-02 16:27:54 +02:00
parent fb22c1547c
commit 6a9bf0fda5
2 changed files with 89 additions and 162 deletions

View File

@ -3,8 +3,9 @@ use crate::{
error::Result,
math::Rect,
shapes::{
add_text_with_tabs, calculate_text_layout_data, set_paint_fill, ParagraphBuilderGroup,
ParagraphLayout, Stroke, StrokeKind, TextContent, VerticalAlign,
add_text_with_tabs, calculate_text_layout_data, paragraph_decoration_segments,
set_paint_fill, vertical_align_offset, ParagraphBuilderGroup, ParagraphLayout, Stroke,
StrokeKind, TextContent,
},
utils::{get_fallback_fonts, get_font_collection},
};
@ -358,7 +359,6 @@ pub fn try_paint_from_layout_cache(
fn paint_from_cached_layout(canvas: &Canvas, shape: &Shape, text_content: &TextContent) {
let selrect = shape.selrect();
let x = selrect.x();
let base_y = selrect.y();
let paragraphs = &text_content.layout.paragraphs;
let draw_decorations = text_content.has_text_decorations();
@ -367,92 +367,31 @@ fn paint_from_cached_layout(canvas: &Canvas, shape: &Shape, text_content: &TextC
.filter_map(|group| group.first())
.map(|p| p.height())
.sum();
let vertical_offset = match shape.vertical_align() {
VerticalAlign::Center => (selrect.height() - total_text_height) / 2.0,
VerticalAlign::Bottom => selrect.height() - total_text_height,
_ => 0.0,
};
let vertical_offset =
vertical_align_offset(selrect.height(), total_text_height, shape.vertical_align());
let mut y_accum = base_y + vertical_offset;
let mut y_accum = selrect.y() + vertical_offset;
for group in paragraphs.iter() {
let Some(paragraph) = group.first() else {
continue;
};
paragraph.paint(canvas, (x, y_accum));
if draw_decorations {
paint_decorations_for_paragraph(canvas, paragraph, x, y_accum);
for deco in paragraph_decoration_segments(paragraph, x, y_accum) {
draw_text_decorations(
canvas,
&deco.text_style,
Some(deco.y),
deco.thickness,
deco.left,
deco.width,
);
}
}
y_accum += paragraph.height();
}
}
fn paint_decorations_for_paragraph(
canvas: &Canvas,
paragraph: &skia::textlayout::Paragraph,
x: f32,
y_accum: f32,
) {
let line_metrics = paragraph.get_line_metrics();
for line in &line_metrics {
let style_metrics: Vec<_> = line
.get_style_metrics(line.start_index..line.end_index)
.into_iter()
.collect();
let line_baseline = y_accum + line.baseline as f32;
let (max_underline_thickness, underline_y, max_strike_thickness, strike_y) =
calculate_decoration_metrics(&style_metrics, line_baseline);
for (i, (style_start, style_metric)) in style_metrics.iter().enumerate() {
let text_style = &style_metric.text_style;
let style_end = style_metrics
.get(i + 1)
.map(|(next_i, _)| *next_i)
.unwrap_or(line.end_index);
let seg_start = (*style_start).max(line.start_index);
let seg_end = style_end.min(line.end_index);
if seg_start >= seg_end {
continue;
}
let rects = paragraph.get_rects_for_range(
seg_start..seg_end,
skia::textlayout::RectHeightStyle::Tight,
skia::textlayout::RectWidthStyle::Tight,
);
let (segment_width, actual_x_offset) = if !rects.is_empty() {
let total_width: f32 = rects.iter().map(|r| r.rect.width()).sum();
let skia_x_offset = rects
.first()
.map(|r| r.rect.left - line.left as f32)
.unwrap_or(0.0);
(total_width, skia_x_offset)
} else {
(0.0, 0.0)
};
let text_left = x + line.left as f32 + actual_x_offset;
let text_width = segment_width;
if text_style.decoration().ty == TextDecoration::UNDERLINE {
draw_text_decorations(
canvas,
text_style,
Some(underline_y.unwrap_or(line_baseline)),
max_underline_thickness,
text_left,
text_width,
);
}
if text_style.decoration().ty == TextDecoration::LINE_THROUGH {
draw_text_decorations(
canvas,
text_style,
Some(strike_y.unwrap_or(line_baseline)),
max_strike_thickness,
text_left,
text_width,
);
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn render_text_on_canvas(
canvas: &Canvas,

View File

@ -41,7 +41,7 @@ pub fn modifier_changes_text_layout(base: &Shape, modifier: &Matrix) -> bool {
let Type::Text(text_content) = &base.shape_type else {
return false;
};
let before = oriented_container_bounds(base);
let before = base.bounds();
let after = before.transform(modifier);
match text_content.grow_type() {
GrowType::AutoWidth => !crate::math::is_close_to(before.height(), after.height()),
@ -51,27 +51,6 @@ pub fn modifier_changes_text_layout(base: &Shape, modifier: &Matrix) -> bool {
}
}
fn oriented_container_bounds(shape: &Shape) -> Bounds {
let selrect = shape.selrect();
let mut bounds = Bounds::new(
Point::new(selrect.x(), selrect.y()),
Point::new(selrect.x() + selrect.width(), selrect.y()),
Point::new(
selrect.x() + selrect.width(),
selrect.y() + selrect.height(),
),
Point::new(selrect.x(), selrect.y() + selrect.height()),
);
if !shape.transform.is_identity() {
let mut matrix = shape.transform;
let center = shape.center();
matrix.post_translate(center);
matrix.pre_translate(-center);
bounds.transform_mut(&matrix);
}
bounds
}
#[repr(u8)]
#[derive(Debug, PartialEq, Clone, Copy, ToJs)]
pub enum GrowType {
@ -304,7 +283,77 @@ pub struct TextDecorationSegment {
pub width: f32,
}
fn vertical_align_offset(container_h: f32, content_h: f32, valign: VerticalAlign) -> f32 {
/// Underline / line-through geometry for one laid-out paragraph placed at
/// `x`, `y_accum`. Shared by [`calculate_text_layout_data`] and the
/// layout-cache paint path.
pub fn paragraph_decoration_segments(
paragraph: &skia::textlayout::Paragraph,
x: f32,
y_accum: f32,
) -> Vec<TextDecorationSegment> {
let mut decorations = Vec::new();
for line in &paragraph.get_line_metrics() {
let style_metrics: Vec<_> = line
.get_style_metrics(line.start_index..line.end_index)
.into_iter()
.collect();
let line_baseline = y_accum + line.baseline as f32;
let (max_underline_thickness, underline_y, max_strike_thickness, strike_y) =
calculate_decoration_metrics(&style_metrics, line_baseline);
for (i, (style_start, style_metric)) in style_metrics.iter().enumerate() {
let text_style = &style_metric.text_style;
let style_end = style_metrics
.get(i + 1)
.map(|(next_i, _)| *next_i)
.unwrap_or(line.end_index);
let seg_start = (*style_start).max(line.start_index);
let seg_end = style_end.min(line.end_index);
if seg_start >= seg_end {
continue;
}
let rects = paragraph.get_rects_for_range(
seg_start..seg_end,
skia::textlayout::RectHeightStyle::Tight,
skia::textlayout::RectWidthStyle::Tight,
);
let (segment_width, actual_x_offset) = if !rects.is_empty() {
let total_width: f32 = rects.iter().map(|r| r.rect.width()).sum();
let skia_x_offset = rects
.first()
.map(|r| r.rect.left - line.left as f32)
.unwrap_or(0.0);
(total_width, skia_x_offset)
} else {
(0.0, 0.0)
};
let text_left = x + line.left as f32 + actual_x_offset;
let text_width = segment_width;
if text_style.decoration().ty == skia::textlayout::TextDecoration::UNDERLINE {
decorations.push(TextDecorationSegment {
kind: skia::textlayout::TextDecoration::UNDERLINE,
text_style: (*text_style).clone(),
y: underline_y.unwrap_or(line_baseline),
thickness: max_underline_thickness,
left: text_left,
width: text_width,
});
}
if text_style.decoration().ty == skia::textlayout::TextDecoration::LINE_THROUGH {
decorations.push(TextDecorationSegment {
kind: skia::textlayout::TextDecoration::LINE_THROUGH,
text_style: (*text_style).clone(),
y: strike_y.unwrap_or(line_baseline),
thickness: max_strike_thickness,
left: text_left,
width: text_width,
});
}
}
}
decorations
}
pub fn vertical_align_offset(container_h: f32, content_h: f32, valign: VerticalAlign) -> f32 {
match valign {
VerticalAlign::Center => (container_h - content_h) / 2.0,
VerticalAlign::Bottom => container_h - content_h,
@ -1680,68 +1729,7 @@ pub fn calculate_text_layout_data(
for (i, group_paragraphs) in built_groups.into_iter().enumerate() {
// For each paragraph in the group (e.g., fill, stroke, etc.)
for skia_paragraph in group_paragraphs.into_iter() {
// Calculate text decorations for this paragraph
let mut decorations = Vec::new();
let line_metrics = skia_paragraph.get_line_metrics();
for line in &line_metrics {
let style_metrics: Vec<_> = line
.get_style_metrics(line.start_index..line.end_index)
.into_iter()
.collect();
let line_baseline = y_accum + line.baseline as f32;
let (max_underline_thickness, underline_y, max_strike_thickness, strike_y) =
calculate_decoration_metrics(&style_metrics, line_baseline);
for (i, (style_start, style_metric)) in style_metrics.iter().enumerate() {
let text_style = &style_metric.text_style;
let style_end = style_metrics
.get(i + 1)
.map(|(next_i, _)| *next_i)
.unwrap_or(line.end_index);
let seg_start = (*style_start).max(line.start_index);
let seg_end = style_end.min(line.end_index);
if seg_start >= seg_end {
continue;
}
let rects = skia_paragraph.get_rects_for_range(
seg_start..seg_end,
skia::textlayout::RectHeightStyle::Tight,
skia::textlayout::RectWidthStyle::Tight,
);
let (segment_width, actual_x_offset) = if !rects.is_empty() {
let total_width: f32 = rects.iter().map(|r| r.rect.width()).sum();
let skia_x_offset = rects
.first()
.map(|r| r.rect.left - line.left as f32)
.unwrap_or(0.0);
(total_width, skia_x_offset)
} else {
(0.0, 0.0)
};
let text_left = x + line.left as f32 + actual_x_offset;
let text_width = segment_width;
use skia::textlayout::TextDecoration;
if text_style.decoration().ty == TextDecoration::UNDERLINE {
decorations.push(TextDecorationSegment {
kind: TextDecoration::UNDERLINE,
text_style: (*text_style).clone(),
y: underline_y.unwrap_or(line_baseline),
thickness: max_underline_thickness,
left: text_left,
width: text_width,
});
}
if text_style.decoration().ty == TextDecoration::LINE_THROUGH {
decorations.push(TextDecorationSegment {
kind: TextDecoration::LINE_THROUGH,
text_style: (*text_style).clone(),
y: strike_y.unwrap_or(line_baseline),
thickness: max_strike_thickness,
left: text_left,
width: text_width,
});
}
}
}
let decorations = paragraph_decoration_segments(&skia_paragraph, x, y_accum);
paragraph_layouts.push(ParagraphLayout {
paragraph: skia_paragraph,
x,