mirror of
https://github.com/penpot/penpot.git
synced 2026-08-29 08:08:46 +00:00
WIP
This commit is contained in:
parent
65cdc606f5
commit
885bb710cf
@ -23,7 +23,13 @@
|
|||||||
for stroke-free text (SrcOver, no blur/shadows). Multi-style text is fine: span styles
|
for stroke-free text (SrcOver, no blur/shadows). Multi-style text is fine: span styles
|
||||||
live in Paragraph `TextStyle`s. Text skips the `nested_fills` guard (fills are on spans).
|
live in Paragraph `TextStyle`s. Text skips the `nested_fills` guard (fills are on spans).
|
||||||
`draw_text` only `save_layer`s when stroke-group opacity is set; plain fill paint is direct.
|
`draw_text` only `save_layer`s when stroke-group opacity is set; plain fill paint is direct.
|
||||||
- Text with strokes / shadows / blur stays on the layered Fills/Strokes path.
|
- Plain text fill paint reuses `TextContent.layout` paragraphs when
|
||||||
|
`has_usable_paint_layout` (paragraphs present + version match; ignores AABB width), via
|
||||||
|
`text::try_paint_from_layout_cache`. Stroke/shadow builders must not use that cache.
|
||||||
|
- `TextContentLayout` paragraphs are `Rc`-shared on `Clone` so modifier clones
|
||||||
|
(rotate/pan) keep the paint cache; `needs_update` is paragraphs-empty only.
|
||||||
|
Rotation expands selrect AABB without invalidating glyph layout. Decorations are
|
||||||
|
skipped when no span requests underline/strike.
|
||||||
- Zoom settle: visible tiles present via `FrameType::ViewportReady` before interest-ring
|
- Zoom settle: visible tiles present via `FrameType::ViewportReady` before interest-ring
|
||||||
work; crop-cache rebuild is deferred to the later `Full` so the soft→sharp snap is
|
work; crop-cache rebuild is deferred to the later `Full` so the soft→sharp snap is
|
||||||
compose+present only.
|
compose+present only.
|
||||||
@ -34,4 +40,4 @@
|
|||||||
- Zoom changes rebuild the tile index while preserving cached tile textures. Avoid replacing that path with shallow rebuilds if blur/shadow cache preservation matters.
|
- Zoom changes rebuild the tile index while preserving cached tile textures. Avoid replacing that path with shallow rebuilds if blur/shadow cache preservation matters.
|
||||||
- Pending tile priority is intentionally reversed by pop order; check the queue construction before changing tile scheduling.
|
- Pending tile priority is intentionally reversed by pop order; check the queue construction before changing tile scheduling.
|
||||||
- Frames with a fill may use `render_frame_container_drop_shadow` (direct rrect +
|
- Frames with a fill may use `render_frame_container_drop_shadow` (direct rrect +
|
||||||
blur saveLayer on `DropShadows`) when `uses_direct_container_drop_shadow` is true.
|
blur saveLayer on `DropShadows`) when `uses_direct_container_drop_shadow` is true.
|
||||||
|
|||||||
@ -1483,29 +1483,37 @@ impl RenderState {
|
|||||||
if let Type::Text(stored_text_content) = &shape.shape_type {
|
if let Type::Text(stored_text_content) = &shape.shape_type {
|
||||||
self.tile_atlas_flushed = true;
|
self.tile_atlas_flushed = true;
|
||||||
|
|
||||||
let selrect = shape.selrect();
|
if !text::try_paint_from_layout_cache(
|
||||||
let stored_bounds = stored_text_content.bounds();
|
|
||||||
let bounds_match = (stored_bounds.width() - selrect.width()).abs() < 0.01
|
|
||||||
&& (stored_bounds.height() - selrect.height()).abs() < 0.01;
|
|
||||||
let rebound_text_content = if bounds_match {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(stored_text_content.new_bounds(selrect))
|
|
||||||
};
|
|
||||||
let text_content: &TextContent =
|
|
||||||
rebound_text_content.as_ref().unwrap_or(stored_text_content);
|
|
||||||
let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None);
|
|
||||||
text::render(
|
|
||||||
Some(self),
|
Some(self),
|
||||||
None,
|
None,
|
||||||
shape,
|
shape,
|
||||||
&mut paragraph_builders,
|
|
||||||
Some(target_surface),
|
Some(target_surface),
|
||||||
None,
|
)? {
|
||||||
None,
|
let selrect = shape.selrect();
|
||||||
None,
|
let stored_bounds = stored_text_content.bounds();
|
||||||
None,
|
let bounds_match = (stored_bounds.width() - selrect.width()).abs() < 0.01
|
||||||
)?;
|
&& (stored_bounds.height() - selrect.height()).abs() < 0.01;
|
||||||
|
let rebound_text_content = if bounds_match {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(stored_text_content.new_bounds(selrect))
|
||||||
|
};
|
||||||
|
let text_content: &TextContent =
|
||||||
|
rebound_text_content.as_ref().unwrap_or(stored_text_content);
|
||||||
|
let mut paragraph_builders =
|
||||||
|
text_content.paragraph_builder_group_from_text(None);
|
||||||
|
text::render(
|
||||||
|
Some(self),
|
||||||
|
None,
|
||||||
|
shape,
|
||||||
|
&mut paragraph_builders,
|
||||||
|
Some(target_surface),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fills::render(self, shape, &shape.fills, antialias, target_surface, None)?;
|
fills::render(self, shape, &shape.fills, antialias, target_surface, None)?;
|
||||||
|
|
||||||
@ -1643,141 +1651,46 @@ impl RenderState {
|
|||||||
s.canvas().concat(&matrix);
|
s.canvas().concat(&matrix);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Skip the paragraph-cloning `new_bounds` when shape size is unchanged.
|
// Plain fill (no strokes / parent shadows): reuse cached layout
|
||||||
let selrect = shape.selrect();
|
// paragraphs when valid. Skip builder rebuild + Skia layout.
|
||||||
let stored_bounds = stored_text_content.bounds();
|
let can_use_layout_cache = !shape.has_visible_strokes()
|
||||||
let bounds_match = (stored_bounds.width() - selrect.width()).abs() < 0.01
|
&& parent_shadows.is_none()
|
||||||
&& (stored_bounds.height() - selrect.height()).abs() < 0.01;
|
&& (skip_effects
|
||||||
let rebound_text_content = if bounds_match {
|
|| (shape.blur.is_none()
|
||||||
None
|
&& !shape
|
||||||
} else {
|
.drop_shadows_visible()
|
||||||
Some(stored_text_content.new_bounds(selrect))
|
.any(|s| s.is_perceptible_at_scale(self.get_scale()))
|
||||||
};
|
&& shape.inner_shadow_paints().is_empty()));
|
||||||
let text_content: &TextContent =
|
if !(can_use_layout_cache
|
||||||
rebound_text_content.as_ref().unwrap_or(stored_text_content);
|
&& text::try_paint_from_layout_cache(
|
||||||
let count_inner_strokes = shape.count_visible_inner_strokes();
|
|
||||||
// Erode the main text fill by 1px when there are inner strokes, to avoid a visible seam at the glyph edge.
|
|
||||||
let text_fill_inset = (count_inner_strokes > 0).then(|| 1.0 / self.get_scale());
|
|
||||||
let text_stroke_blur_outset =
|
|
||||||
Stroke::max_bounds_width(shape.visible_strokes(), false);
|
|
||||||
let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None);
|
|
||||||
let stroke_kinds: Vec<StrokeKind> =
|
|
||||||
shape.visible_strokes().rev().map(|s| s.kind).collect();
|
|
||||||
let (mut stroke_paragraphs_list, stroke_opacities): (Vec<_>, Vec<_>) = shape
|
|
||||||
.visible_strokes()
|
|
||||||
.rev()
|
|
||||||
.map(|stroke| {
|
|
||||||
text::stroke_paragraph_builder_group_from_text(
|
|
||||||
text_content,
|
|
||||||
stroke,
|
|
||||||
&shape.selrect(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.unzip();
|
|
||||||
if skip_effects {
|
|
||||||
// Fast path: render fills and strokes only (skip shadows/blur).
|
|
||||||
text::render(
|
|
||||||
Some(self),
|
Some(self),
|
||||||
None,
|
None,
|
||||||
&shape,
|
&shape,
|
||||||
&mut paragraph_builders,
|
|
||||||
Some(fills_surface_id),
|
Some(fills_surface_id),
|
||||||
None,
|
)?)
|
||||||
None,
|
{
|
||||||
text_fill_inset,
|
// Skip the paragraph-cloning `new_bounds` when shape size is unchanged.
|
||||||
None,
|
let selrect = shape.selrect();
|
||||||
)?;
|
let stored_bounds = stored_text_content.bounds();
|
||||||
|
let bounds_match = (stored_bounds.width() - selrect.width()).abs() < 0.01
|
||||||
for (i, (stroke_paragraphs, layer_opacity)) in stroke_paragraphs_list
|
&& (stored_bounds.height() - selrect.height()).abs() < 0.01;
|
||||||
.iter_mut()
|
let rebound_text_content = if bounds_match {
|
||||||
.zip(stroke_opacities.iter())
|
None
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
if stroke_kinds[i] == StrokeKind::Inner {
|
|
||||||
let mut fill_builders =
|
|
||||||
text_content.paragraph_builder_group_from_text(None);
|
|
||||||
text::render_inner_stroke(
|
|
||||||
Some(self),
|
|
||||||
None,
|
|
||||||
&shape,
|
|
||||||
stroke_paragraphs,
|
|
||||||
&mut fill_builders,
|
|
||||||
Some(strokes_surface_id),
|
|
||||||
None,
|
|
||||||
text_stroke_blur_outset,
|
|
||||||
*layer_opacity,
|
|
||||||
)?;
|
|
||||||
} else if stroke_kinds[i] == StrokeKind::Outer {
|
|
||||||
text::render_outer_stroke(
|
|
||||||
Some(self),
|
|
||||||
None,
|
|
||||||
&shape,
|
|
||||||
stroke_paragraphs,
|
|
||||||
Some(strokes_surface_id),
|
|
||||||
None,
|
|
||||||
text_stroke_blur_outset,
|
|
||||||
*layer_opacity,
|
|
||||||
)?;
|
|
||||||
} else {
|
|
||||||
text::render_with_bounds_outset(
|
|
||||||
Some(self),
|
|
||||||
None,
|
|
||||||
&shape,
|
|
||||||
stroke_paragraphs,
|
|
||||||
Some(strokes_surface_id),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
text_stroke_blur_outset,
|
|
||||||
None,
|
|
||||||
*layer_opacity,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if shape.has_visible_strokes() && text_content.has_non_ascii() {
|
|
||||||
let mut emoji_builders = text_content.paragraph_builder_group_opaque();
|
|
||||||
let mut deco_builders =
|
|
||||||
text_content.paragraph_builder_group_from_text(None);
|
|
||||||
text::render_emoji_overlay(
|
|
||||||
self,
|
|
||||||
&shape,
|
|
||||||
&mut emoji_builders,
|
|
||||||
&mut deco_builders,
|
|
||||||
strokes_surface_id,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let shape_scale = self.get_scale();
|
|
||||||
let mut drop_shadows = if skip_drop_shadows {
|
|
||||||
Vec::new()
|
|
||||||
} else {
|
} else {
|
||||||
shape
|
Some(stored_text_content.new_bounds(selrect))
|
||||||
.drop_shadows_visible()
|
|
||||||
.filter(|s| s.is_perceptible_at_scale(shape_scale))
|
|
||||||
.map(|shadow| {
|
|
||||||
let mut paint = skia_safe::Paint::default();
|
|
||||||
paint.set_image_filter(shadow.get_drop_shadow_filter());
|
|
||||||
paint
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
};
|
};
|
||||||
|
let text_content: &TextContent =
|
||||||
if !skip_drop_shadows {
|
rebound_text_content.as_ref().unwrap_or(stored_text_content);
|
||||||
if let Some(inherited_shadows) = self.get_inherited_drop_shadows() {
|
let count_inner_strokes = shape.count_visible_inner_strokes();
|
||||||
drop_shadows.extend(inherited_shadows);
|
// Erode the main text fill by 1px when there are inner strokes, to avoid a visible seam at the glyph edge.
|
||||||
}
|
let text_fill_inset = (count_inner_strokes > 0).then(|| 1.0 / self.get_scale());
|
||||||
}
|
let text_stroke_blur_outset =
|
||||||
|
Stroke::max_bounds_width(shape.visible_strokes(), false);
|
||||||
let inner_shadows = shape.inner_shadow_paints();
|
let mut paragraph_builders =
|
||||||
let blur_filter = shape.image_filter(1.);
|
text_content.paragraph_builder_group_from_text(None);
|
||||||
let mut paragraphs_with_shadows =
|
let stroke_kinds: Vec<StrokeKind> =
|
||||||
text_content.paragraph_builder_group_from_text(Some(true));
|
shape.visible_strokes().rev().map(|s| s.kind).collect();
|
||||||
let (mut stroke_paragraphs_with_shadows_list, _shadow_opacities): (
|
let (mut stroke_paragraphs_list, stroke_opacities): (Vec<_>, Vec<_>) = shape
|
||||||
Vec<_>,
|
|
||||||
Vec<_>,
|
|
||||||
) = shape
|
|
||||||
.visible_strokes()
|
.visible_strokes()
|
||||||
.rev()
|
.rev()
|
||||||
.map(|stroke| {
|
.map(|stroke| {
|
||||||
@ -1785,60 +1698,12 @@ impl RenderState {
|
|||||||
text_content,
|
text_content,
|
||||||
stroke,
|
stroke,
|
||||||
&shape.selrect(),
|
&shape.selrect(),
|
||||||
Some(true),
|
None,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.unzip();
|
.unzip();
|
||||||
|
if skip_effects {
|
||||||
if let Some(parent_shadows) = parent_shadows {
|
// Fast path: render fills and strokes only (skip shadows/blur).
|
||||||
if !skip_drop_shadows {
|
|
||||||
if !shape.has_visible_strokes() {
|
|
||||||
for shadow in parent_shadows {
|
|
||||||
text::render(
|
|
||||||
Some(self),
|
|
||||||
None,
|
|
||||||
&shape,
|
|
||||||
&mut paragraphs_with_shadows,
|
|
||||||
text_drop_shadows_surface_id.into(),
|
|
||||||
Some(&shadow),
|
|
||||||
blur_filter.as_ref(),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
shadows::render_text_shadows(
|
|
||||||
self,
|
|
||||||
&shape,
|
|
||||||
&mut paragraphs_with_shadows,
|
|
||||||
&mut stroke_paragraphs_with_shadows_list,
|
|
||||||
text_drop_shadows_surface_id.into(),
|
|
||||||
&parent_shadows,
|
|
||||||
&blur_filter,
|
|
||||||
&stroke_kinds,
|
|
||||||
text_content,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 1. Text drop shadows
|
|
||||||
if !shape.has_visible_strokes() {
|
|
||||||
for shadow in &drop_shadows {
|
|
||||||
text::render(
|
|
||||||
Some(self),
|
|
||||||
None,
|
|
||||||
&shape,
|
|
||||||
&mut paragraphs_with_shadows,
|
|
||||||
text_drop_shadows_surface_id.into(),
|
|
||||||
Some(shadow),
|
|
||||||
blur_filter.as_ref(),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Text fills
|
|
||||||
text::render(
|
text::render(
|
||||||
Some(self),
|
Some(self),
|
||||||
None,
|
None,
|
||||||
@ -1846,25 +1711,11 @@ impl RenderState {
|
|||||||
&mut paragraph_builders,
|
&mut paragraph_builders,
|
||||||
Some(fills_surface_id),
|
Some(fills_surface_id),
|
||||||
None,
|
None,
|
||||||
blur_filter.as_ref(),
|
None,
|
||||||
text_fill_inset,
|
text_fill_inset,
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// 3. Stroke drop shadows
|
|
||||||
shadows::render_text_shadows(
|
|
||||||
self,
|
|
||||||
&shape,
|
|
||||||
&mut paragraphs_with_shadows,
|
|
||||||
&mut stroke_paragraphs_with_shadows_list,
|
|
||||||
text_drop_shadows_surface_id.into(),
|
|
||||||
&drop_shadows,
|
|
||||||
&blur_filter,
|
|
||||||
&stroke_kinds,
|
|
||||||
text_content,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// 4. Stroke fills
|
|
||||||
for (i, (stroke_paragraphs, layer_opacity)) in stroke_paragraphs_list
|
for (i, (stroke_paragraphs, layer_opacity)) in stroke_paragraphs_list
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.zip(stroke_opacities.iter())
|
.zip(stroke_opacities.iter())
|
||||||
@ -1880,7 +1731,7 @@ impl RenderState {
|
|||||||
stroke_paragraphs,
|
stroke_paragraphs,
|
||||||
&mut fill_builders,
|
&mut fill_builders,
|
||||||
Some(strokes_surface_id),
|
Some(strokes_surface_id),
|
||||||
blur_filter.as_ref(),
|
None,
|
||||||
text_stroke_blur_outset,
|
text_stroke_blur_outset,
|
||||||
*layer_opacity,
|
*layer_opacity,
|
||||||
)?;
|
)?;
|
||||||
@ -1891,7 +1742,7 @@ impl RenderState {
|
|||||||
&shape,
|
&shape,
|
||||||
stroke_paragraphs,
|
stroke_paragraphs,
|
||||||
Some(strokes_surface_id),
|
Some(strokes_surface_id),
|
||||||
blur_filter.as_ref(),
|
None,
|
||||||
text_stroke_blur_outset,
|
text_stroke_blur_outset,
|
||||||
*layer_opacity,
|
*layer_opacity,
|
||||||
)?;
|
)?;
|
||||||
@ -1903,7 +1754,7 @@ impl RenderState {
|
|||||||
stroke_paragraphs,
|
stroke_paragraphs,
|
||||||
Some(strokes_surface_id),
|
Some(strokes_surface_id),
|
||||||
None,
|
None,
|
||||||
blur_filter.as_ref(),
|
None,
|
||||||
text_stroke_blur_outset,
|
text_stroke_blur_outset,
|
||||||
None,
|
None,
|
||||||
*layer_opacity,
|
*layer_opacity,
|
||||||
@ -1921,41 +1772,219 @@ impl RenderState {
|
|||||||
&mut emoji_builders,
|
&mut emoji_builders,
|
||||||
&mut deco_builders,
|
&mut deco_builders,
|
||||||
strokes_surface_id,
|
strokes_surface_id,
|
||||||
blur_filter.as_ref(),
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
let shape_scale = self.get_scale();
|
||||||
|
let mut drop_shadows = if skip_drop_shadows {
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
shape
|
||||||
|
.drop_shadows_visible()
|
||||||
|
.filter(|s| s.is_perceptible_at_scale(shape_scale))
|
||||||
|
.map(|shadow| {
|
||||||
|
let mut paint = skia_safe::Paint::default();
|
||||||
|
paint.set_image_filter(shadow.get_drop_shadow_filter());
|
||||||
|
paint
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
// 5. Stroke inner shadows
|
if !skip_drop_shadows {
|
||||||
shadows::render_text_shadows(
|
if let Some(inherited_shadows) = self.get_inherited_drop_shadows() {
|
||||||
self,
|
drop_shadows.extend(inherited_shadows);
|
||||||
&shape,
|
}
|
||||||
&mut paragraphs_with_shadows,
|
}
|
||||||
&mut stroke_paragraphs_with_shadows_list,
|
|
||||||
Some(innershadows_surface_id),
|
|
||||||
&inner_shadows,
|
|
||||||
&blur_filter,
|
|
||||||
&stroke_kinds,
|
|
||||||
text_content,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// 6. Fill Inner shadows
|
let inner_shadows = shape.inner_shadow_paints();
|
||||||
if !shape.has_visible_strokes() {
|
let blur_filter = shape.image_filter(1.);
|
||||||
for shadow in &inner_shadows {
|
let mut paragraphs_with_shadows =
|
||||||
text::render(
|
text_content.paragraph_builder_group_from_text(Some(true));
|
||||||
Some(self),
|
let (mut stroke_paragraphs_with_shadows_list, _shadow_opacities): (
|
||||||
None,
|
Vec<_>,
|
||||||
|
Vec<_>,
|
||||||
|
) = shape
|
||||||
|
.visible_strokes()
|
||||||
|
.rev()
|
||||||
|
.map(|stroke| {
|
||||||
|
text::stroke_paragraph_builder_group_from_text(
|
||||||
|
text_content,
|
||||||
|
stroke,
|
||||||
|
&shape.selrect(),
|
||||||
|
Some(true),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unzip();
|
||||||
|
|
||||||
|
if let Some(parent_shadows) = parent_shadows {
|
||||||
|
if !skip_drop_shadows {
|
||||||
|
if !shape.has_visible_strokes() {
|
||||||
|
for shadow in parent_shadows {
|
||||||
|
text::render(
|
||||||
|
Some(self),
|
||||||
|
None,
|
||||||
|
&shape,
|
||||||
|
&mut paragraphs_with_shadows,
|
||||||
|
text_drop_shadows_surface_id.into(),
|
||||||
|
Some(&shadow),
|
||||||
|
blur_filter.as_ref(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shadows::render_text_shadows(
|
||||||
|
self,
|
||||||
|
&shape,
|
||||||
|
&mut paragraphs_with_shadows,
|
||||||
|
&mut stroke_paragraphs_with_shadows_list,
|
||||||
|
text_drop_shadows_surface_id.into(),
|
||||||
|
&parent_shadows,
|
||||||
|
&blur_filter,
|
||||||
|
&stroke_kinds,
|
||||||
|
text_content,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 1. Text drop shadows
|
||||||
|
if !shape.has_visible_strokes() {
|
||||||
|
for shadow in &drop_shadows {
|
||||||
|
text::render(
|
||||||
|
Some(self),
|
||||||
|
None,
|
||||||
|
&shape,
|
||||||
|
&mut paragraphs_with_shadows,
|
||||||
|
text_drop_shadows_surface_id.into(),
|
||||||
|
Some(shadow),
|
||||||
|
blur_filter.as_ref(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Text fills
|
||||||
|
text::render(
|
||||||
|
Some(self),
|
||||||
|
None,
|
||||||
|
&shape,
|
||||||
|
&mut paragraph_builders,
|
||||||
|
Some(fills_surface_id),
|
||||||
|
None,
|
||||||
|
blur_filter.as_ref(),
|
||||||
|
text_fill_inset,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// 3. Stroke drop shadows
|
||||||
|
shadows::render_text_shadows(
|
||||||
|
self,
|
||||||
|
&shape,
|
||||||
|
&mut paragraphs_with_shadows,
|
||||||
|
&mut stroke_paragraphs_with_shadows_list,
|
||||||
|
text_drop_shadows_surface_id.into(),
|
||||||
|
&drop_shadows,
|
||||||
|
&blur_filter,
|
||||||
|
&stroke_kinds,
|
||||||
|
text_content,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// 4. Stroke fills
|
||||||
|
for (i, (stroke_paragraphs, layer_opacity)) in stroke_paragraphs_list
|
||||||
|
.iter_mut()
|
||||||
|
.zip(stroke_opacities.iter())
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
if stroke_kinds[i] == StrokeKind::Inner {
|
||||||
|
let mut fill_builders =
|
||||||
|
text_content.paragraph_builder_group_from_text(None);
|
||||||
|
text::render_inner_stroke(
|
||||||
|
Some(self),
|
||||||
|
None,
|
||||||
|
&shape,
|
||||||
|
stroke_paragraphs,
|
||||||
|
&mut fill_builders,
|
||||||
|
Some(strokes_surface_id),
|
||||||
|
blur_filter.as_ref(),
|
||||||
|
text_stroke_blur_outset,
|
||||||
|
*layer_opacity,
|
||||||
|
)?;
|
||||||
|
} else if stroke_kinds[i] == StrokeKind::Outer {
|
||||||
|
text::render_outer_stroke(
|
||||||
|
Some(self),
|
||||||
|
None,
|
||||||
|
&shape,
|
||||||
|
stroke_paragraphs,
|
||||||
|
Some(strokes_surface_id),
|
||||||
|
blur_filter.as_ref(),
|
||||||
|
text_stroke_blur_outset,
|
||||||
|
*layer_opacity,
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
text::render_with_bounds_outset(
|
||||||
|
Some(self),
|
||||||
|
None,
|
||||||
|
&shape,
|
||||||
|
stroke_paragraphs,
|
||||||
|
Some(strokes_surface_id),
|
||||||
|
None,
|
||||||
|
blur_filter.as_ref(),
|
||||||
|
text_stroke_blur_outset,
|
||||||
|
None,
|
||||||
|
*layer_opacity,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if shape.has_visible_strokes() && text_content.has_non_ascii() {
|
||||||
|
let mut emoji_builders =
|
||||||
|
text_content.paragraph_builder_group_opaque();
|
||||||
|
let mut deco_builders =
|
||||||
|
text_content.paragraph_builder_group_from_text(None);
|
||||||
|
text::render_emoji_overlay(
|
||||||
|
self,
|
||||||
&shape,
|
&shape,
|
||||||
&mut paragraphs_with_shadows,
|
&mut emoji_builders,
|
||||||
Some(innershadows_surface_id),
|
&mut deco_builders,
|
||||||
Some(shadow),
|
strokes_surface_id,
|
||||||
blur_filter.as_ref(),
|
blur_filter.as_ref(),
|
||||||
None,
|
);
|
||||||
None,
|
}
|
||||||
)?;
|
|
||||||
|
// 5. Stroke inner shadows
|
||||||
|
shadows::render_text_shadows(
|
||||||
|
self,
|
||||||
|
&shape,
|
||||||
|
&mut paragraphs_with_shadows,
|
||||||
|
&mut stroke_paragraphs_with_shadows_list,
|
||||||
|
Some(innershadows_surface_id),
|
||||||
|
&inner_shadows,
|
||||||
|
&blur_filter,
|
||||||
|
&stroke_kinds,
|
||||||
|
text_content,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// 6. Fill Inner shadows
|
||||||
|
if !shape.has_visible_strokes() {
|
||||||
|
for shadow in &inner_shadows {
|
||||||
|
text::render(
|
||||||
|
Some(self),
|
||||||
|
None,
|
||||||
|
&shape,
|
||||||
|
&mut paragraphs_with_shadows,
|
||||||
|
Some(innershadows_surface_id),
|
||||||
|
Some(shadow),
|
||||||
|
blur_filter.as_ref(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} // end layout-cache miss fallback
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
self.surfaces.apply_mut(surface_ids, |s| {
|
self.surfaces.apply_mut(surface_ids, |s| {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use crate::{
|
|||||||
math::Rect,
|
math::Rect,
|
||||||
shapes::{
|
shapes::{
|
||||||
add_text_with_tabs, calculate_text_layout_data, set_paint_fill, ParagraphBuilderGroup,
|
add_text_with_tabs, calculate_text_layout_data, set_paint_fill, ParagraphBuilderGroup,
|
||||||
ParagraphLayout, Stroke, StrokeKind, TextContent,
|
ParagraphLayout, Stroke, StrokeKind, TextContent, VerticalAlign,
|
||||||
},
|
},
|
||||||
utils::{get_fallback_fonts, get_font_collection},
|
utils::{get_fallback_fonts, get_font_collection},
|
||||||
};
|
};
|
||||||
@ -318,6 +318,135 @@ pub fn render_overlay_emoji(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Paint fill glyphs from `TextContent.layout` when the cache is valid.
|
||||||
|
///
|
||||||
|
/// Avoids rebuilding ParagraphBuilders and re-running Skia layout on every
|
||||||
|
/// paint. Only safe for the plain fill pass (no stroke/shadow-specific builders).
|
||||||
|
/// Returns `true` when painting was done from cache.
|
||||||
|
pub fn try_paint_from_layout_cache(
|
||||||
|
render_state: Option<&mut RenderState>,
|
||||||
|
canvas: Option<&Canvas>,
|
||||||
|
shape: &Shape,
|
||||||
|
surface_id: Option<SurfaceId>,
|
||||||
|
) -> Result<bool> {
|
||||||
|
let text_content = shape.get_text_content();
|
||||||
|
if !text_content.has_usable_paint_layout(shape.selrect()) {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(render_state) = render_state {
|
||||||
|
let target_surface = surface_id.unwrap_or(SurfaceId::Fills);
|
||||||
|
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
|
||||||
|
paint_from_cached_layout(canvas, shape, text_content);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(canvas) = canvas {
|
||||||
|
paint_from_cached_layout(canvas, shape, text_content);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
let total_text_height: f32 = paragraphs
|
||||||
|
.iter()
|
||||||
|
.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 mut y_accum = base_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);
|
||||||
|
}
|
||||||
|
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)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn render_text_on_canvas(
|
fn render_text_on_canvas(
|
||||||
canvas: &Canvas,
|
canvas: &Canvas,
|
||||||
|
|||||||
@ -23,6 +23,7 @@ use skia_safe::{
|
|||||||
|
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use super::FontFamily;
|
use super::FontFamily;
|
||||||
use crate::math::Point;
|
use crate::math::Point;
|
||||||
@ -196,7 +197,9 @@ struct CachedExtrect {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TextContentLayout {
|
pub struct TextContentLayout {
|
||||||
pub paragraph_builders: Vec<ParagraphBuilderGroup>,
|
pub paragraph_builders: Vec<ParagraphBuilderGroup>,
|
||||||
pub paragraphs: Vec<Vec<skia::textlayout::Paragraph>>,
|
/// Shared across shape clones (e.g. modifier transforms) so rotation/pan
|
||||||
|
/// can paint without rebuilding Skia layout. Cleared builders on clone are OK.
|
||||||
|
pub paragraphs: Rc<Vec<Vec<skia::textlayout::Paragraph>>>,
|
||||||
cached_extrect: Cell<Option<CachedExtrect>>,
|
cached_extrect: Cell<Option<CachedExtrect>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,8 +213,8 @@ impl Clone for TextContentLayout {
|
|||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
paragraph_builders: vec![],
|
paragraph_builders: vec![],
|
||||||
paragraphs: vec![],
|
paragraphs: Rc::clone(&self.paragraphs),
|
||||||
cached_extrect: Cell::new(None),
|
cached_extrect: Cell::new(self.cached_extrect.get()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,7 +229,7 @@ impl TextContentLayout {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
paragraph_builders: vec![],
|
paragraph_builders: vec![],
|
||||||
paragraphs: vec![],
|
paragraphs: Rc::new(Vec::new()),
|
||||||
cached_extrect: Cell::new(None),
|
cached_extrect: Cell::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -237,12 +240,18 @@ impl TextContentLayout {
|
|||||||
paragraphs: Vec<Vec<skia::textlayout::Paragraph>>,
|
paragraphs: Vec<Vec<skia::textlayout::Paragraph>>,
|
||||||
) {
|
) {
|
||||||
self.paragraph_builders = paragraph_builders;
|
self.paragraph_builders = paragraph_builders;
|
||||||
self.paragraphs = paragraphs;
|
self.paragraphs = Rc::new(paragraphs);
|
||||||
|
self.cached_extrect.set(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.paragraph_builders.clear();
|
||||||
|
self.paragraphs = Rc::new(Vec::new());
|
||||||
self.cached_extrect.set(None);
|
self.cached_extrect.set(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn needs_update(&self) -> bool {
|
pub fn needs_update(&self) -> bool {
|
||||||
self.paragraph_builders.is_empty() || self.paragraphs.is_empty()
|
self.paragraphs.is_empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -471,7 +480,7 @@ impl TextContent {
|
|||||||
let mut has_lines = false;
|
let mut has_lines = false;
|
||||||
let mut y_accum = base_y + vertical_offset;
|
let mut y_accum = base_y + vertical_offset;
|
||||||
|
|
||||||
for group in paragraphs {
|
for group in paragraphs.iter() {
|
||||||
if let Some(paragraph) = group.first() {
|
if let Some(paragraph) = group.first() {
|
||||||
let line_metrics = paragraph.get_line_metrics();
|
let line_metrics = paragraph.get_line_metrics();
|
||||||
for line in &line_metrics {
|
for line in &line_metrics {
|
||||||
@ -871,6 +880,28 @@ impl TextContent {
|
|||||||
self.layout.needs_update()
|
self.layout.needs_update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True when cached Skia paragraphs can be painted as-is (no rebuild/layout).
|
||||||
|
///
|
||||||
|
/// Does **not** require `layout_width == selrect.width()`: modifier rotation
|
||||||
|
/// expands the AABB selrect without changing glyph layout. Width matching
|
||||||
|
/// stays in [`Self::update_layout`] for real resize/reflow.
|
||||||
|
/// Layout paragraphs are `Rc`-shared across modifier clones.
|
||||||
|
pub fn has_usable_paint_layout(&self, _selrect: Rect) -> bool {
|
||||||
|
!self.layout.needs_update() && self.layout_version == self.content_version
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True when any span requests underline/overline/line-through (custom draw path).
|
||||||
|
pub fn has_text_decorations(&self) -> bool {
|
||||||
|
self.paragraphs().iter().any(|paragraph| {
|
||||||
|
paragraph.children().iter().any(|span| {
|
||||||
|
matches!(
|
||||||
|
span.text_decoration,
|
||||||
|
Some(d) if d != skia::textlayout::TextDecoration::NO_DECORATION
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_layout_from_result(
|
pub fn set_layout_from_result(
|
||||||
&mut self,
|
&mut self,
|
||||||
result: TextContentLayoutResult,
|
result: TextContentLayoutResult,
|
||||||
|
|||||||
@ -796,8 +796,7 @@ impl TextEditorState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
text_content.layout.paragraphs.clear();
|
text_content.layout.clear();
|
||||||
text_content.layout.paragraph_builders.clear();
|
|
||||||
|
|
||||||
self.reset_blink();
|
self.reset_blink();
|
||||||
self.push_event(TextEditorEvent::ContentChanged);
|
self.push_event(TextEditorEvent::ContentChanged);
|
||||||
@ -822,8 +821,7 @@ impl TextEditorState {
|
|||||||
self.selection.set_caret(clamped);
|
self.selection.set_caret(clamped);
|
||||||
}
|
}
|
||||||
|
|
||||||
text_content.layout.paragraphs.clear();
|
text_content.layout.clear();
|
||||||
text_content.layout.paragraph_builders.clear();
|
|
||||||
|
|
||||||
self.reset_blink();
|
self.reset_blink();
|
||||||
self.push_event(TextEditorEvent::ContentChanged);
|
self.push_event(TextEditorEvent::ContentChanged);
|
||||||
@ -844,8 +842,7 @@ impl TextEditorState {
|
|||||||
self.selection.set_caret(new_cursor);
|
self.selection.set_caret(new_cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
text_content.layout.paragraphs.clear();
|
text_content.layout.clear();
|
||||||
text_content.layout.paragraph_builders.clear();
|
|
||||||
|
|
||||||
self.reset_blink();
|
self.reset_blink();
|
||||||
self.push_event(TextEditorEvent::ContentChanged);
|
self.push_event(TextEditorEvent::ContentChanged);
|
||||||
|
|||||||
@ -396,8 +396,7 @@ pub extern "C" fn text_editor_composition_end() -> Result<()> {
|
|||||||
get_text_editor_state().selection.set_caret(new_cursor);
|
get_text_editor_state().selection.set_caret(new_cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
text_content.layout.paragraphs.clear();
|
text_content.layout.clear();
|
||||||
text_content.layout.paragraph_builders.clear();
|
|
||||||
|
|
||||||
get_text_editor_state().reset_blink();
|
get_text_editor_state().reset_blink();
|
||||||
get_text_editor_state().push_event(crate::state::TextEditorEvent::ContentChanged);
|
get_text_editor_state().push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||||
@ -448,8 +447,7 @@ pub extern "C" fn text_editor_composition_update() -> Result<()> {
|
|||||||
let cursor = get_text_editor_state().selection.focus;
|
let cursor = get_text_editor_state().selection.focus;
|
||||||
text_helpers::insert_text_with_newlines(text_content, &cursor, &text);
|
text_helpers::insert_text_with_newlines(text_content, &cursor, &text);
|
||||||
|
|
||||||
text_content.layout.paragraphs.clear();
|
text_content.layout.clear();
|
||||||
text_content.layout.paragraph_builders.clear();
|
|
||||||
|
|
||||||
get_text_editor_state().reset_blink();
|
get_text_editor_state().reset_blink();
|
||||||
get_text_editor_state().push_event(crate::state::TextEditorEvent::ContentChanged);
|
get_text_editor_state().push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||||
@ -517,8 +515,7 @@ pub extern "C" fn text_editor_insert_text() -> Result<()> {
|
|||||||
get_text_editor_state().selection.set_caret(new_cursor);
|
get_text_editor_state().selection.set_caret(new_cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
text_content.layout.paragraphs.clear();
|
text_content.layout.clear();
|
||||||
text_content.layout.paragraph_builders.clear();
|
|
||||||
|
|
||||||
get_text_editor_state().reset_blink();
|
get_text_editor_state().reset_blink();
|
||||||
get_text_editor_state().push_event(TextEditorEvent::ContentChanged);
|
get_text_editor_state().push_event(TextEditorEvent::ContentChanged);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user