🐛 Fix layout render-wasm issues

This commit is contained in:
alonso.torres 2026-05-29 11:53:24 +02:00 committed by Aitor Moreno
parent d3148e1a10
commit dddb4cf0b6
3 changed files with 62 additions and 18 deletions

View File

@ -29,6 +29,8 @@ fn propagate_children(
) -> Result<VecDeque<Modifier>> { ) -> Result<VecDeque<Modifier>> {
let mut result = VecDeque::new(); let mut result = VecDeque::new();
// We use the identity transform as a mark that a reflow is needed.
// It's needed to be propagated to its children.
if identitish(&transform) { if identitish(&transform) {
for child_id in shape.children_ids_iter(true) { for child_id in shape.children_ids_iter(true) {
result.push_back(Modifier::transform_propagate(*child_id, transform)); result.push_back(Modifier::transform_propagate(*child_id, transform));
@ -180,6 +182,7 @@ fn propagate_transform(
modifiers: &mut HashMap<Uuid, Matrix>, modifiers: &mut HashMap<Uuid, Matrix>,
reflown: &mut HashSet<Uuid>, reflown: &mut HashSet<Uuid>,
reflowed_shapes: &mut HashSet<Uuid>, reflowed_shapes: &mut HashSet<Uuid>,
pending_reflows: &mut HashSet<Uuid>,
) -> Result<()> { ) -> Result<()> {
let Some(shape) = state.shapes.get(&entry.id) else { let Some(shape) = state.shapes.get(&entry.id) else {
return Ok(()); return Ok(());
@ -237,13 +240,29 @@ fn propagate_transform(
transform.post_concat(&resize_transform); transform.post_concat(&resize_transform);
} }
GrowType::AutoWidth => { GrowType::AutoWidth => {
let width_before = text_content.width();
let height_before = text_content.size.height;
let (new_width, new_height) = if height_changed { let (new_width, new_height) = if height_changed {
let mut clone = text_content.clone(); let mut clone = text_content.clone();
clone.update_layout(resized_selrect); clone.update_layout(resized_selrect);
(clone.width(), clone.size.height) (clone.width(), clone.size.height)
} else { } else {
(text_content.width(), text_content.size.height) (width_before, height_before)
}; };
if (!is_close_to(width_before, new_width)
|| !is_close_to(height_before, new_height))
&& reflowed_shapes.insert(shape.id)
{
entries.push_back(Modifier::reflow(shape.id, false));
if let Some(parent_id) = shape.parent_id {
for pid in
shapes::all_with_ancestors(&[parent_id], shapes, false).iter()
{
reflown.remove(pid);
}
}
}
let resize_transform = math::resize_matrix( let resize_transform = math::resize_matrix(
&shape_bounds_after, &shape_bounds_after,
&shape_bounds_after, &shape_bounds_after,
@ -284,7 +303,7 @@ fn propagate_transform(
let is_propagate = entry.source == TransformEntrySource::Propagate; let is_propagate = entry.source == TransformEntrySource::Propagate;
// If this is a layout and we're only moving don't need to reflow // If this is a layout and we're only moving don't need to reflow
if shape.has_layout() && is_resize { if shape.has_layout() && is_resize && pending_reflows.insert(shape.id) {
entries.push_back(Modifier::reflow(shape.id, false)); entries.push_back(Modifier::reflow(shape.id, false));
} }
@ -292,13 +311,17 @@ fn propagate_transform(
// When the parent is either a group or a layout we only mark for reflow // When the parent is either a group or a layout we only mark for reflow
// if the current transformation is not a move propagation. // if the current transformation is not a move propagation.
// If it's a move propagation we don't need to reflow, the parent is already changed. // If it's a move propagation we don't need to reflow, the parent is already changed.
if (parent.has_layout() || parent.is_group_like()) && (is_resize || !is_propagate) { if (parent.has_layout() || parent.is_group_like())
&& (is_resize || !is_propagate)
&& pending_reflows.insert(parent.id)
{
entries.push_back(Modifier::reflow(parent.id, false)); entries.push_back(Modifier::reflow(parent.id, false));
} }
} }
Ok(()) Ok(())
} }
#[allow(clippy::too_many_arguments)]
fn propagate_reflow( fn propagate_reflow(
id: &Uuid, id: &Uuid,
state: &State, state: &State,
@ -307,6 +330,7 @@ fn propagate_reflow(
layout_reflows: &mut HashSet<Uuid>, layout_reflows: &mut HashSet<Uuid>,
reflown: &mut HashSet<Uuid>, reflown: &mut HashSet<Uuid>,
modifiers: &HashMap<Uuid, Matrix>, modifiers: &HashMap<Uuid, Matrix>,
pending_reflows: &mut HashSet<Uuid>,
) { ) {
let Some(shape) = state.shapes.get(id) else { let Some(shape) = state.shapes.get(id) else {
return; return;
@ -326,7 +350,7 @@ fn propagate_reflow(
} }
Type::Group(Group { masked: true }) => { Type::Group(Group { masked: true }) => {
let children_ids = shape.children_ids(true); let children_ids = shape.children_ids(true);
if let Some(child) = shapes.get(&children_ids[0]) { if let Some(child) = children_ids.first().and_then(|id| shapes.get(id)) {
let child_bounds = bounds.find(child); let child_bounds = bounds.find(child);
bounds.insert(shape.id, child_bounds); bounds.insert(shape.id, child_bounds);
} }
@ -348,7 +372,7 @@ fn propagate_reflow(
} }
if let Some(parent) = shape.parent_id.and_then(|id| shapes.get(&id)) { if let Some(parent) = shape.parent_id.and_then(|id| shapes.get(&id)) {
if parent.has_layout() || parent.is_group_like() { if (parent.has_layout() || parent.is_group_like()) && pending_reflows.insert(parent.id) {
entries.push_back(Modifier::reflow(parent.id, false)); entries.push_back(Modifier::reflow(parent.id, false));
} }
} }
@ -406,6 +430,14 @@ pub fn propagate_modifiers(
let mut bounds = HashMap::<Uuid, Bounds>::new(); let mut bounds = HashMap::<Uuid, Bounds>::new();
let mut reflown = HashSet::<Uuid>::new(); let mut reflown = HashSet::<Uuid>::new();
let mut layout_reflows = HashSet::<Uuid>::new(); let mut layout_reflows = HashSet::<Uuid>::new();
// Tracks text shapes that have already triggered a reflow across all outer
// iterations, preventing oscillation when a parent layout re-emits a
// transform for the same text shape in a later pass.
let mut reflowed_shapes = HashSet::<Uuid>::new();
// Tracks reflow ids already queued to avoid flooding entries with
// duplicate Reflow entries when many children of the same parent
// are transformed in the same pass.
let mut pending_reflows = HashSet::<Uuid>::new();
// We first propagate the transforms to the children and then after // We first propagate the transforms to the children and then after
// recalculate the layouts. The layout can create further transforms that // recalculate the layouts. The layout can create further transforms that
@ -413,7 +445,6 @@ pub fn propagate_modifiers(
// In order for loop to eventualy finish, we limit the flex reflow to just // In order for loop to eventualy finish, we limit the flex reflow to just
// one (the reflown set). // one (the reflown set).
while !entries.is_empty() { while !entries.is_empty() {
let mut reflowed_shapes = HashSet::<Uuid>::new();
while let Some(modifier) = entries.pop_front() { while let Some(modifier) = entries.pop_front() {
match modifier { match modifier {
Modifier::Transform(entry, pixel) => propagate_transform( Modifier::Transform(entry, pixel) => propagate_transform(
@ -425,8 +456,10 @@ pub fn propagate_modifiers(
&mut modifiers, &mut modifiers,
&mut reflown, &mut reflown,
&mut reflowed_shapes, &mut reflowed_shapes,
&mut pending_reflows,
)?, )?,
Modifier::Reflow(id, force_reflow) => { Modifier::Reflow(id, force_reflow) => {
pending_reflows.remove(&id);
if force_reflow { if force_reflow {
reflown.remove(&id); reflown.remove(&id);
} }
@ -439,6 +472,7 @@ pub fn propagate_modifiers(
&mut layout_reflows, &mut layout_reflows,
&mut reflown, &mut reflown,
&modifiers, &modifiers,
&mut pending_reflows,
) )
} }
} }
@ -465,7 +499,6 @@ pub fn propagate_modifiers(
} }
reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds_temp)?; reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds_temp)?;
} }
layout_reflows = HashSet::new();
} }
// #[allow(dead_code)] // #[allow(dead_code)]

View File

@ -54,6 +54,7 @@ struct LayoutAxis {
gap_across: f32, gap_across: f32,
is_auto_main: bool, is_auto_main: bool,
is_auto_across: bool, is_auto_across: bool,
is_wrap: bool,
} }
impl LayoutAxis { impl LayoutAxis {
@ -78,6 +79,7 @@ impl LayoutAxis {
gap_across: layout_data.row_gap, gap_across: layout_data.row_gap,
is_auto_main: num_child > 0 && shape.is_layout_horizontal_auto(), is_auto_main: num_child > 0 && shape.is_layout_horizontal_auto(),
is_auto_across: num_child > 0 && shape.is_layout_vertical_auto(), is_auto_across: num_child > 0 && shape.is_layout_vertical_auto(),
is_wrap: flex_data.is_wrap(),
} }
} else { } else {
Self { Self {
@ -93,6 +95,7 @@ impl LayoutAxis {
gap_across: layout_data.column_gap, gap_across: layout_data.column_gap,
is_auto_main: num_child > 0 && shape.is_layout_vertical_auto(), is_auto_main: num_child > 0 && shape.is_layout_vertical_auto(),
is_auto_across: num_child > 0 && shape.is_layout_horizontal_auto(), is_auto_across: num_child > 0 && shape.is_layout_horizontal_auto(),
is_wrap: flex_data.is_wrap(),
} }
} }
} }
@ -399,7 +402,7 @@ fn calculate_track_positions(
) { ) {
let mut align_content = &layout_data.align_content; let mut align_content = &layout_data.align_content;
if layout_axis.is_auto_across { if layout_axis.is_auto_across || !layout_axis.is_wrap {
align_content = &AlignContent::Start; align_content = &AlignContent::Start;
} }
@ -427,7 +430,10 @@ fn calculate_track_positions(
AlignContent::SpaceAround => { AlignContent::SpaceAround => {
let effective_gap = (layout_axis.across_space() - total_across_size) / tlen as f32; let effective_gap = (layout_axis.across_space() - total_across_size) / tlen as f32;
(effective_gap / 2.0, effective_gap) (
layout_axis.padding_across_start + effective_gap / 2.0,
effective_gap,
)
} }
AlignContent::SpaceEvenly => { AlignContent::SpaceEvenly => {
@ -473,7 +479,9 @@ fn calculate_track_data(
let total_across_size = tracks.iter().map(|t| t.across_size).sum::<f32>(); let total_across_size = tracks.iter().map(|t| t.across_size).sum::<f32>();
if !layout_axis.is_auto_across && layout_data.align_content == AlignContent::Stretch { let stretch_tracks = !layout_axis.is_wrap || layout_data.align_content == AlignContent::Stretch;
if !layout_axis.is_auto_across && stretch_tracks {
stretch_tracks_sizes(&layout_axis, &mut tracks, total_across_size); stretch_tracks_sizes(&layout_axis, &mut tracks, total_across_size);
} }
@ -506,12 +514,12 @@ fn first_anchor(
} }
JustifyContent::SpaceAround => { JustifyContent::SpaceAround => {
let effective_gap = (layout_axis.main_space() - total_shapes_size) / slen as f32; let effective_gap = (layout_axis.main_space() - total_shapes_size) / slen as f32;
layout_axis.padding_main_end + f32::max(layout_axis.gap_main, effective_gap / 2.0) layout_axis.padding_main_start + f32::max(layout_axis.gap_main, effective_gap / 2.0)
} }
JustifyContent::SpaceEvenly => { JustifyContent::SpaceEvenly => {
let effective_gap = let effective_gap =
(layout_axis.main_space() - total_shapes_size) / (track.shapes.len() + 1) as f32; (layout_axis.main_space() - total_shapes_size) / (track.shapes.len() + 1) as f32;
layout_axis.padding_main_end + f32::max(layout_axis.gap_main, effective_gap) layout_axis.padding_main_start + f32::max(layout_axis.gap_main, effective_gap)
} }
_ => layout_axis.padding_main_start, _ => layout_axis.padding_main_start,
}; };
@ -538,8 +546,11 @@ fn next_anchor(
+ child_axis.margin_main_end + child_axis.margin_main_end
+ match layout_data.justify_content { + match layout_data.justify_content {
JustifyContent::SpaceBetween => { JustifyContent::SpaceBetween => {
let effective_gap = (layout_axis.main_space() - total_shapes_size) let effective_gap = if track.shapes.len() > 1 {
/ (track.shapes.len() - 1) as f32; (layout_axis.main_space() - total_shapes_size) / (track.shapes.len() - 1) as f32
} else {
0.0
};
child_axis.main_size + f32::max(layout_axis.gap_main, effective_gap) child_axis.main_size + f32::max(layout_axis.gap_main, effective_gap)
} }
JustifyContent::SpaceAround => { JustifyContent::SpaceAround => {

View File

@ -193,7 +193,7 @@ fn set_auto_multi_span(
// Sort descendant order of prop-span // Sort descendant order of prop-span
selected_cells.sort_by(|a, b| { selected_cells.sort_by(|a, b| {
if column { if column {
b.column_span.cmp(&a.row_span) b.column_span.cmp(&a.column_span)
} else { } else {
b.row_span.cmp(&a.row_span) b.row_span.cmp(&a.row_span)
} }
@ -268,7 +268,7 @@ fn set_flex_multi_span(
// Sort descendant order of prop-span // Sort descendant order of prop-span
selected_cells.sort_by(|a, b| { selected_cells.sort_by(|a, b| {
if column { if column {
b.column_span.cmp(&a.row_span) b.column_span.cmp(&a.column_span)
} else { } else {
b.row_span.cmp(&a.row_span) b.row_span.cmp(&a.row_span)
} }
@ -901,7 +901,7 @@ pub fn reflow_grid_layout(
let auto_width = column_tracks.iter().map(|t| t.size).sum::<f32>() let auto_width = column_tracks.iter().map(|t| t.size).sum::<f32>()
+ layout_data.padding_left + layout_data.padding_left
+ layout_data.padding_right + layout_data.padding_right
+ (column_tracks.len() - 1) as f32 * layout_data.column_gap; + column_tracks.len().saturating_sub(1) as f32 * layout_data.column_gap;
scale_width = auto_width / width; scale_width = auto_width / width;
} }
@ -909,7 +909,7 @@ pub fn reflow_grid_layout(
let auto_height = row_tracks.iter().map(|t| t.size).sum::<f32>() let auto_height = row_tracks.iter().map(|t| t.size).sum::<f32>()
+ layout_data.padding_top + layout_data.padding_top
+ layout_data.padding_bottom + layout_data.padding_bottom
+ (row_tracks.len() - 1) as f32 * layout_data.row_gap; + row_tracks.len().saturating_sub(1) as f32 * layout_data.row_gap;
scale_height = auto_height / height; scale_height = auto_height / height;
} }