mirror of
https://github.com/penpot/penpot.git
synced 2026-06-16 12:22:22 +00:00
⚡ Cache paragraphs to improve nested frames with texts performance
This commit is contained in:
parent
070f3f8522
commit
c59bd644b0
@ -3,8 +3,8 @@ use crate::{
|
||||
error::Result,
|
||||
math::Rect,
|
||||
shapes::{
|
||||
calculate_text_layout_data, set_paint_fill, ParagraphBuilderGroup, ParagraphLayout, Stroke,
|
||||
StrokeKind, TextContent,
|
||||
calculate_text_layout_data, position_cached_paragraphs, set_paint_fill, GrowType,
|
||||
ParagraphBuilderGroup, ParagraphLayout, Stroke, StrokeKind, TextContent,
|
||||
},
|
||||
utils::{get_fallback_fonts, get_font_collection},
|
||||
};
|
||||
@ -300,6 +300,118 @@ pub fn render(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn render_fill_cached(
|
||||
render_state: &mut RenderState,
|
||||
shape: &Shape,
|
||||
text_content: &TextContent,
|
||||
surface_id: Option<SurfaceId>,
|
||||
blur: Option<&ImageFilter>,
|
||||
fill_inset: Option<f32>,
|
||||
) -> Result<()> {
|
||||
if text_content.layout.paragraphs.is_empty() || text_content.grow_type() == GrowType::AutoWidth
|
||||
{
|
||||
let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None);
|
||||
return render(
|
||||
Some(render_state),
|
||||
None,
|
||||
shape,
|
||||
&mut paragraph_builders,
|
||||
surface_id,
|
||||
None,
|
||||
blur,
|
||||
fill_inset,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
let target_surface = surface_id.unwrap_or(SurfaceId::Fills);
|
||||
let groups = &text_content.layout.paragraphs;
|
||||
|
||||
if let Some(blur_filter) = blur {
|
||||
let text_bounds = shape
|
||||
.get_text_content()
|
||||
.calculate_bounds(shape, false)
|
||||
.to_rect();
|
||||
let bounds = blur_filter.compute_fast_bounds(text_bounds);
|
||||
if bounds.is_finite() && bounds.width() > 0.0 && bounds.height() > 0.0 {
|
||||
let blur_filter_clone = blur_filter.clone();
|
||||
if filters::render_with_filter_surface(
|
||||
render_state,
|
||||
bounds,
|
||||
target_surface,
|
||||
|state, temp_surface| {
|
||||
let temp_canvas = state.surfaces.canvas(temp_surface);
|
||||
paint_fill_cached_on_canvas(
|
||||
temp_canvas,
|
||||
shape,
|
||||
groups,
|
||||
Some(&blur_filter_clone),
|
||||
fill_inset,
|
||||
);
|
||||
Ok(())
|
||||
},
|
||||
)? {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
|
||||
paint_fill_cached_on_canvas(canvas, shape, groups, blur, fill_inset);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn paint_fill_cached_on_canvas(
|
||||
canvas: &Canvas,
|
||||
shape: &Shape,
|
||||
groups: &[Vec<skia::textlayout::Paragraph>],
|
||||
blur: Option<&ImageFilter>,
|
||||
fill_inset: Option<f32>,
|
||||
) {
|
||||
if let Some(blur_filter) = blur {
|
||||
let mut blur_paint = Paint::default();
|
||||
blur_paint.set_image_filter(blur_filter.clone());
|
||||
canvas.save_layer(&SaveLayerRec::default().paint(&blur_paint));
|
||||
}
|
||||
|
||||
if let Some(eps) = fill_inset.filter(|&e| e > 0.0) {
|
||||
if let Some(erode) = skia_safe::image_filters::erode((eps, eps), None, None) {
|
||||
let mut layer_paint = Paint::default();
|
||||
layer_paint.set_image_filter(erode);
|
||||
canvas.save_layer(&SaveLayerRec::default().paint(&layer_paint));
|
||||
draw_cached_fill(canvas, shape, groups);
|
||||
canvas.restore();
|
||||
} else {
|
||||
draw_cached_fill(canvas, shape, groups);
|
||||
}
|
||||
} else {
|
||||
draw_cached_fill(canvas, shape, groups);
|
||||
}
|
||||
|
||||
if blur.is_some() {
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
fn draw_cached_fill(canvas: &Canvas, shape: &Shape, groups: &[Vec<skia::textlayout::Paragraph>]) {
|
||||
canvas.save_layer(&SaveLayerRec::default());
|
||||
for para in position_cached_paragraphs(shape, groups) {
|
||||
para.paragraph.paint(canvas, (para.x, para.y));
|
||||
for deco in ¶.decorations {
|
||||
draw_text_decorations(
|
||||
canvas,
|
||||
&deco.text_style,
|
||||
Some(deco.y),
|
||||
deco.thickness,
|
||||
deco.left,
|
||||
deco.width,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [`render`] but rasterizes color emoji as bitmap overlays for PDF/vector export.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_overlay_emoji(
|
||||
|
||||
@ -1395,6 +1395,13 @@ pub struct ParagraphLayout {
|
||||
pub decorations: Vec<TextDecorationSegment>,
|
||||
}
|
||||
|
||||
pub struct ParagraphLayoutRef<'a> {
|
||||
pub paragraph: &'a skia::textlayout::Paragraph,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub decorations: Vec<TextDecorationSegment>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TextLayoutData {
|
||||
pub position_data: Vec<PositionData>,
|
||||
@ -1408,6 +1415,110 @@ fn direction_to_int(direction: TextDirection) -> u32 {
|
||||
}
|
||||
}
|
||||
|
||||
fn paragraph_decorations(
|
||||
skia_paragraph: &skia::textlayout::Paragraph,
|
||||
x: f32,
|
||||
y_top: f32,
|
||||
) -> Vec<TextDecorationSegment> {
|
||||
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_top + 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,
|
||||
RectHeightStyle::Tight,
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
decorations
|
||||
}
|
||||
|
||||
pub fn position_cached_paragraphs<'a>(
|
||||
shape: &Shape,
|
||||
built_groups: &'a [Vec<skia::textlayout::Paragraph>],
|
||||
) -> Vec<ParagraphLayoutRef<'a>> {
|
||||
let selrect_height = shape.selrect().height();
|
||||
let x = shape.selrect.x();
|
||||
let base_y = shape.selrect.y();
|
||||
let paragraph_heights: Vec<f32> = built_groups
|
||||
.iter()
|
||||
.map(|group| group.first().map(|p| p.height()).unwrap_or(0.0))
|
||||
.collect();
|
||||
let total_text_height: f32 = paragraph_heights.iter().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 mut layouts: Vec<ParagraphLayoutRef<'a>> = Vec::new();
|
||||
let mut y_accum = base_y + vertical_offset;
|
||||
for (i, group) in built_groups.iter().enumerate() {
|
||||
for paragraph in group.iter() {
|
||||
let decorations = paragraph_decorations(paragraph, x, y_accum);
|
||||
layouts.push(ParagraphLayoutRef {
|
||||
paragraph,
|
||||
x,
|
||||
y: y_accum,
|
||||
decorations,
|
||||
});
|
||||
}
|
||||
y_accum += paragraph_heights[i];
|
||||
}
|
||||
layouts
|
||||
}
|
||||
|
||||
pub fn calculate_text_layout_data(
|
||||
shape: &Shape,
|
||||
text_content: &TextContent,
|
||||
@ -1463,67 +1574,7 @@ pub fn calculate_text_layout_data(
|
||||
// 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_decorations(&skia_paragraph, x, y_accum);
|
||||
paragraph_layouts.push(ParagraphLayout {
|
||||
paragraph: skia_paragraph,
|
||||
x,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user