🐛 Rebuild text layout when image fills finish loading (#12564)

Text image shaders are baked into Skia paragraphs at layout time.
touch_shapes_with_image only scanned shape.fills/strokes, so text
spans were never invalidated after store_image. Detect text content
image fills and force a layout rebuild before touching tiles.

Closes #12563
This commit is contained in:
Alejandro Alonso 2026-10-23 09:55:23 +02:00 committed by GitHub
parent ce5276065e
commit 6508449fa0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1,30 +1,67 @@
use crate::error::{Error, Result};
use crate::get_resources;
use crate::mem;
use crate::shapes::Fill;
use crate::shapes::{Fill, Shape, TextContent, Type};
use crate::state::State;
use crate::uuid::Uuid;
use crate::with_state;
use crate::{shapes::ImageFill, utils::uuid_from_u32_quartet};
use macros::wasm_error;
fn text_content_uses_image(content: &TextContent, image_id: Uuid) -> bool {
content.paragraphs().iter().any(|paragraph| {
paragraph.children().iter().any(|span| {
span.fills
.iter()
.any(|f| matches!(f, Fill::Image(i) if i.id() == image_id))
})
})
}
fn shape_uses_image(shape: &Shape, image_id: Uuid) -> bool {
if shape
.fills()
.any(|f| matches!(f, Fill::Image(i) if i.id() == image_id))
|| shape
.strokes
.iter()
.any(|s| matches!(&s.fill, Fill::Image(i) if i.id() == image_id))
{
return true;
}
match &shape.shape_type {
Type::Text(content) => text_content_uses_image(content, image_id),
_ => false,
}
}
fn rebuild_text_layout_for_image(shape: &mut Shape) {
let Type::Text(text_content) = &mut shape.shape_type else {
return;
};
// Image shaders are baked into Skia paragraphs at layout time. When the
// media arrives after the first layout, force a rebuild so text fills
// pick up `get_image` (rect fills look up the image every paint).
text_content.force_next_layout_update();
text_content.update_layout(shape.selrect);
shape.apply_deferred_batch_paint();
shape.invalidate_extrect();
}
fn touch_shapes_with_image(state: &mut State, image_id: Uuid) {
let ids: Vec<Uuid> = state
let ids: Vec<(Uuid, bool)> = state
.shapes
.iter()
.filter(|shape| {
shape
.fills()
.any(|f| matches!(f, Fill::Image(i) if i.id() == image_id))
|| shape
.strokes
.iter()
.any(|s| matches!(&s.fill, Fill::Image(i) if i.id() == image_id))
})
.map(|shape| shape.id)
.filter(|shape| shape_uses_image(shape, image_id))
.map(|shape| (shape.id, matches!(shape.shape_type, Type::Text(_))))
.collect();
for id in ids {
for (id, is_text) in ids {
if is_text {
if let Some(shape) = state.shapes.get_mut(&id) {
rebuild_text_layout_for_image(shape);
}
}
state.touch_shape(id);
}
}