mirror of
https://github.com/penpot/penpot.git
synced 2026-09-12 23:19:28 +00:00
🐛 Keep text image fills aligned during drag (#11610)
Cached Skia paragraphs bake absolute image/gradient shaders at layout time. On move, clones reuse those paragraphs while painting at the new selrect, so glyphs move and the fill stays put. Record the paint origin when layout is built and translate the canvas when painting from cache so shaders track the text. Also sync bounds before update_layout so fills bake against the current container.
This commit is contained in:
parent
7c27ed812a
commit
aa78ca0be8
@ -358,8 +358,19 @@ 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();
|
||||
// Absolute image/gradient shaders were baked at `layout_paint_origin`. Paint
|
||||
// glyphs in that space and translate the canvas so both track selrect moves
|
||||
// without rebuilding Skia paragraphs.
|
||||
let anchor = text_content.cached_layout_paint_anchor(&selrect);
|
||||
let offset = text_content.cached_layout_paint_offset(&selrect);
|
||||
let needs_translate = offset.x.abs() > f32::EPSILON || offset.y.abs() > f32::EPSILON;
|
||||
if needs_translate {
|
||||
canvas.save();
|
||||
canvas.translate((offset.x, offset.y));
|
||||
}
|
||||
|
||||
let x = anchor.x;
|
||||
let base_y = anchor.y;
|
||||
let paragraphs = &text_content.layout.paragraphs;
|
||||
let draw_decorations = text_content.has_text_decorations();
|
||||
|
||||
@ -389,6 +400,10 @@ fn paint_from_cached_layout(canvas: &Canvas, shape: &Shape, text_content: &TextC
|
||||
}
|
||||
y_accum += paragraph.height();
|
||||
}
|
||||
|
||||
if needs_translate {
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
||||
@ -403,8 +403,8 @@ impl Shape {
|
||||
self.invalidate_extrect();
|
||||
self.selrect.set_ltrb(left, top, right, bottom);
|
||||
if let Type::Text(ref mut text) = self.shape_type {
|
||||
// `update_layout` syncs bounds via set_xywh before baking fill paints.
|
||||
text.update_layout(self.selrect);
|
||||
text.set_xywh(left, top, self.selrect.width(), self.selrect.height());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -420,6 +420,10 @@ pub struct TextContent {
|
||||
content_version: u64,
|
||||
layout_version: u64,
|
||||
layout_width: Option<f32>,
|
||||
/// Canvas origin used when absolute fill shaders (image/gradient) were baked
|
||||
/// into cached Skia paragraphs. Kept across move clones so paint can
|
||||
/// translate glyphs + shaders together. See `cached_layout_paint_offset`.
|
||||
layout_paint_origin: Option<Point>,
|
||||
}
|
||||
|
||||
impl PartialEq for TextContent {
|
||||
@ -443,6 +447,7 @@ impl TextContent {
|
||||
content_version: 0,
|
||||
layout_version: 0,
|
||||
layout_width: None,
|
||||
layout_paint_origin: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -458,6 +463,7 @@ impl TextContent {
|
||||
content_version: 0,
|
||||
layout_version: 0,
|
||||
layout_width: None,
|
||||
layout_paint_origin: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -465,6 +471,23 @@ impl TextContent {
|
||||
self.bounds
|
||||
}
|
||||
|
||||
/// Anchor used when painting from the layout cache. Absolute image/gradient
|
||||
/// shaders were built in this coordinate space; paint glyphs here and apply
|
||||
/// [`cached_layout_paint_offset`] on the canvas so both move together.
|
||||
pub fn cached_layout_paint_anchor(&self, selrect: &Rect) -> Point {
|
||||
self.layout_paint_origin
|
||||
.unwrap_or_else(|| Point::new(selrect.x(), selrect.y()))
|
||||
}
|
||||
|
||||
/// Canvas translation from the baked paint origin to the current selrect.
|
||||
/// Zero when there is no recorded origin (fall back to painting at selrect).
|
||||
pub fn cached_layout_paint_offset(&self, selrect: &Rect) -> Point {
|
||||
match self.layout_paint_origin {
|
||||
Some(origin) => Point::new(selrect.x() - origin.x, selrect.y() - origin.y),
|
||||
None => Point::new(0.0, 0.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Text content for paint when [`Rect`] size may differ from stored bounds
|
||||
/// (e.g. modifier transform). Reuses `self` when width/height match; otherwise
|
||||
/// clones paragraphs into a rebound copy with an empty layout cache.
|
||||
@ -1130,10 +1153,14 @@ impl TextContent {
|
||||
self.layout.set(result.0, result.1);
|
||||
self.size
|
||||
.copy_finite_size(result.2, default_width, default_height);
|
||||
// Paragraph paints (incl. absolute image/gradient shaders) were built
|
||||
// against `self.bounds()` in `paragraph_builder_group_from_text`.
|
||||
self.layout_paint_origin = Some(Point::new(self.bounds.x(), self.bounds.y()));
|
||||
}
|
||||
|
||||
pub fn force_next_layout_update(&mut self) {
|
||||
self.layout_width = None;
|
||||
self.layout_paint_origin = None;
|
||||
self.layout.cached_extrect.set(None);
|
||||
// Bump the content version so update_layout can't early-return: auto-width
|
||||
// shapes always match their container and clearing the cache above doesn't
|
||||
@ -1142,6 +1169,10 @@ impl TextContent {
|
||||
}
|
||||
|
||||
pub fn update_layout(&mut self, selrect: Rect) -> TextContentSize {
|
||||
// Keep bounds in sync before building paints so absolute fill shaders
|
||||
// match the container we are laying out for.
|
||||
self.set_xywh(selrect.x(), selrect.y(), selrect.width(), selrect.height());
|
||||
|
||||
// Auto-width ignores selrect width so get-text-dimensions can reuse the cached layout.
|
||||
let layout_matches_container = self.grow_type() == GrowType::AutoWidth
|
||||
|| self
|
||||
@ -1319,6 +1350,7 @@ impl Default for TextContent {
|
||||
content_version: 0,
|
||||
layout_version: 0,
|
||||
layout_width: None,
|
||||
layout_paint_origin: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2225,6 +2257,86 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_paint_origin_set_when_layout_result_applied() {
|
||||
let mut content = sample_text_content();
|
||||
content.set_xywh(40.0, 60.0, 200.0, 100.0);
|
||||
let empty =
|
||||
TextContentLayoutResult(vec![], vec![], TextContentSize::new_with_size(200.0, 100.0));
|
||||
content.set_layout_from_result(empty, 200.0, 100.0);
|
||||
let selrect = Rect::from_xywh(40.0, 60.0, 200.0, 100.0);
|
||||
assert_eq!(
|
||||
content.cached_layout_paint_anchor(&selrect),
|
||||
Point::new(40.0, 60.0)
|
||||
);
|
||||
assert_eq!(
|
||||
content.cached_layout_paint_offset(&selrect),
|
||||
Point::new(0.0, 0.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_layout_paint_offset_tracks_selrect_move() {
|
||||
let mut content = sample_text_content();
|
||||
content.layout_paint_origin = Some(Point::new(10.0, 20.0));
|
||||
// Simulate a move clone: bounds follow the new selrect, origin stays.
|
||||
content.set_xywh(110.0, 220.0, 200.0, 100.0);
|
||||
let selrect = Rect::from_xywh(110.0, 220.0, 200.0, 100.0);
|
||||
let offset = content.cached_layout_paint_offset(&selrect);
|
||||
assert_eq!(offset, Point::new(100.0, 200.0));
|
||||
assert_eq!(
|
||||
content.cached_layout_paint_anchor(&selrect),
|
||||
Point::new(10.0, 20.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_layout_paint_offset_zero_without_origin() {
|
||||
let content = sample_text_content();
|
||||
let selrect = Rect::from_xywh(50.0, 75.0, 200.0, 100.0);
|
||||
assert_eq!(
|
||||
content.cached_layout_paint_offset(&selrect),
|
||||
Point::new(0.0, 0.0)
|
||||
);
|
||||
assert_eq!(
|
||||
content.cached_layout_paint_anchor(&selrect),
|
||||
Point::new(50.0, 75.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_paint_origin_survives_bounds_transform_on_clone() {
|
||||
let mut content = sample_text_content();
|
||||
content.set_xywh(10.0, 20.0, 200.0, 100.0);
|
||||
content.layout_paint_origin = Some(Point::new(10.0, 20.0));
|
||||
content.layout.paragraphs = Rc::new(vec![vec![]]);
|
||||
content.layout_width = Some(200.0);
|
||||
content.layout_version = 1;
|
||||
content.content_version = 1;
|
||||
|
||||
let mut moved = content.clone();
|
||||
let mut move_matrix = Matrix::new_identity();
|
||||
move_matrix.set_translate_x(50.0);
|
||||
move_matrix.set_translate_y(30.0);
|
||||
moved.transform(&move_matrix);
|
||||
|
||||
assert_eq!(moved.bounds().x(), 60.0);
|
||||
assert_eq!(moved.bounds().y(), 50.0);
|
||||
assert!(Rc::ptr_eq(
|
||||
&content.layout.paragraphs,
|
||||
&moved.layout.paragraphs
|
||||
));
|
||||
let selrect = Rect::from_xywh(60.0, 50.0, 200.0, 100.0);
|
||||
assert_eq!(
|
||||
moved.cached_layout_paint_anchor(&selrect),
|
||||
Point::new(10.0, 20.0)
|
||||
);
|
||||
assert_eq!(
|
||||
moved.cached_layout_paint_offset(&selrect),
|
||||
Point::new(50.0, 30.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_clone_shares_skia_paragraphs() {
|
||||
let mut layout = TextContentLayout::new();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user