diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index 8c9fea1bb0..9f28887df2 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -29,6 +29,8 @@ fn propagate_children( ) -> Result> { 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) { for child_id in shape.children_ids_iter(true) { result.push_back(Modifier::transform_propagate(*child_id, transform)); @@ -180,6 +182,7 @@ fn propagate_transform( modifiers: &mut HashMap, reflown: &mut HashSet, reflowed_shapes: &mut HashSet, + pending_reflows: &mut HashSet, ) -> Result<()> { let Some(shape) = state.shapes.get(&entry.id) else { return Ok(()); @@ -237,13 +240,29 @@ fn propagate_transform( transform.post_concat(&resize_transform); } GrowType::AutoWidth => { + let width_before = text_content.width(); + let height_before = text_content.size.height; let (new_width, new_height) = if height_changed { let mut clone = text_content.clone(); clone.update_layout(resized_selrect); (clone.width(), clone.size.height) } 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( &shape_bounds_after, &shape_bounds_after, @@ -284,7 +303,7 @@ fn propagate_transform( let is_propagate = entry.source == TransformEntrySource::Propagate; // 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)); } @@ -292,13 +311,17 @@ fn propagate_transform( // 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 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)); } } Ok(()) } +#[allow(clippy::too_many_arguments)] fn propagate_reflow( id: &Uuid, state: &State, @@ -307,6 +330,7 @@ fn propagate_reflow( layout_reflows: &mut HashSet, reflown: &mut HashSet, modifiers: &HashMap, + pending_reflows: &mut HashSet, ) { let Some(shape) = state.shapes.get(id) else { return; @@ -326,7 +350,7 @@ fn propagate_reflow( } Type::Group(Group { masked: 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); 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 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)); } } @@ -406,6 +430,14 @@ pub fn propagate_modifiers( let mut bounds = HashMap::::new(); let mut reflown = HashSet::::new(); let mut layout_reflows = HashSet::::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::::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::::new(); // We first propagate the transforms to the children and then after // 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 // one (the reflown set). while !entries.is_empty() { - let mut reflowed_shapes = HashSet::::new(); while let Some(modifier) = entries.pop_front() { match modifier { Modifier::Transform(entry, pixel) => propagate_transform( @@ -425,8 +456,10 @@ pub fn propagate_modifiers( &mut modifiers, &mut reflown, &mut reflowed_shapes, + &mut pending_reflows, )?, Modifier::Reflow(id, force_reflow) => { + pending_reflows.remove(&id); if force_reflow { reflown.remove(&id); } @@ -439,6 +472,7 @@ pub fn propagate_modifiers( &mut layout_reflows, &mut reflown, &modifiers, + &mut pending_reflows, ) } } @@ -465,7 +499,6 @@ pub fn propagate_modifiers( } reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds_temp)?; } - layout_reflows = HashSet::new(); } // #[allow(dead_code)] diff --git a/render-wasm/src/shapes/modifiers/flex_layout.rs b/render-wasm/src/shapes/modifiers/flex_layout.rs index d51d6cb36d..80fa3e6fcb 100644 --- a/render-wasm/src/shapes/modifiers/flex_layout.rs +++ b/render-wasm/src/shapes/modifiers/flex_layout.rs @@ -54,6 +54,7 @@ struct LayoutAxis { gap_across: f32, is_auto_main: bool, is_auto_across: bool, + is_wrap: bool, } impl LayoutAxis { @@ -78,6 +79,7 @@ impl LayoutAxis { gap_across: layout_data.row_gap, is_auto_main: num_child > 0 && shape.is_layout_horizontal_auto(), is_auto_across: num_child > 0 && shape.is_layout_vertical_auto(), + is_wrap: flex_data.is_wrap(), } } else { Self { @@ -93,6 +95,7 @@ impl LayoutAxis { gap_across: layout_data.column_gap, is_auto_main: num_child > 0 && shape.is_layout_vertical_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; - if layout_axis.is_auto_across { + if layout_axis.is_auto_across || !layout_axis.is_wrap { align_content = &AlignContent::Start; } @@ -427,7 +430,10 @@ fn calculate_track_positions( AlignContent::SpaceAround => { 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 => { @@ -473,7 +479,9 @@ fn calculate_track_data( let total_across_size = tracks.iter().map(|t| t.across_size).sum::(); - 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); } @@ -506,12 +514,12 @@ fn first_anchor( } JustifyContent::SpaceAround => { 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 => { let effective_gap = (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, }; @@ -538,8 +546,11 @@ fn next_anchor( + child_axis.margin_main_end + match layout_data.justify_content { JustifyContent::SpaceBetween => { - let effective_gap = (layout_axis.main_space() - total_shapes_size) - / (track.shapes.len() - 1) as f32; + let effective_gap = if track.shapes.len() > 1 { + (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) } JustifyContent::SpaceAround => { diff --git a/render-wasm/src/shapes/modifiers/grid_layout.rs b/render-wasm/src/shapes/modifiers/grid_layout.rs index 362d362851..7ddc877d91 100644 --- a/render-wasm/src/shapes/modifiers/grid_layout.rs +++ b/render-wasm/src/shapes/modifiers/grid_layout.rs @@ -193,7 +193,7 @@ fn set_auto_multi_span( // Sort descendant order of prop-span selected_cells.sort_by(|a, b| { if column { - b.column_span.cmp(&a.row_span) + b.column_span.cmp(&a.column_span) } else { b.row_span.cmp(&a.row_span) } @@ -268,7 +268,7 @@ fn set_flex_multi_span( // Sort descendant order of prop-span selected_cells.sort_by(|a, b| { if column { - b.column_span.cmp(&a.row_span) + b.column_span.cmp(&a.column_span) } else { 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::() + layout_data.padding_left + 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; } @@ -909,7 +909,7 @@ pub fn reflow_grid_layout( let auto_height = row_tracks.iter().map(|t| t.size).sum::() + layout_data.padding_top + 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; }