Alejandro Alonso f21bd45893
🐛 Fix paths and layout performance and rendering on boolean exclusions (#10778)
* 🐛 Skip identity transforms in layout reflow propagation

Layout reflow emitted identity transforms for unchanged children, which
fanned out through the whole subtree on every drag frame and froze large
files in the WASM renderer.

* 🐛 Fix exclude boolean rendering in render WASM
2026-07-22 15:47:43 +02:00

797 lines
27 KiB
Rust

use crate::error::{Error, Result};
use crate::math::{self as math, Bounds, Matrix, Point, Vector, VectorExt};
use crate::shapes::{
AlignContent, AlignItems, AlignSelf, ConstraintH, ConstraintV, FlexData, JustifyContent,
LayoutData, LayoutItem, Modifier, Shape,
};
use crate::state::ShapesPoolRef;
use crate::uuid::Uuid;
use std::collections::{HashMap, VecDeque};
use super::common::GetBounds;
const MIN_SIZE: f32 = 0.01;
const MAX_SIZE: f32 = f32::INFINITY;
const TRACK_TOLERANCE: f32 = 0.01;
#[derive(Debug)]
struct TrackData {
main_size: f32,
across_size: f32,
max_across_size: f32,
is_fill_across: bool,
shapes: Vec<ChildAxis>,
anchor: Point,
}
impl TrackData {
fn default() -> Self {
Self {
main_size: MIN_SIZE,
across_size: MIN_SIZE,
max_across_size: MAX_SIZE,
is_fill_across: false,
shapes: Vec::new(),
anchor: Point::default(),
}
}
}
#[derive(Debug)]
struct LayoutAxis {
main_size: f32,
across_size: f32,
main_v: Vector,
across_v: Vector,
padding_main_start: f32,
padding_main_end: f32,
padding_across_start: f32,
padding_across_end: f32,
gap_main: f32,
gap_across: f32,
is_auto_main: bool,
is_auto_across: bool,
is_wrap: bool,
}
impl LayoutAxis {
fn new(
shape: &Shape,
layout_bounds: &Bounds,
layout_data: &LayoutData,
flex_data: &FlexData,
) -> Self {
let num_child = shape.children_count();
if flex_data.is_row() {
Self {
main_size: layout_bounds.width(),
across_size: layout_bounds.height(),
main_v: layout_bounds.hv(1.0),
across_v: layout_bounds.vv(1.0),
padding_main_start: layout_data.padding_left,
padding_main_end: layout_data.padding_right,
padding_across_start: layout_data.padding_top,
padding_across_end: layout_data.padding_bottom,
gap_main: layout_data.column_gap,
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 {
main_size: layout_bounds.height(),
across_size: layout_bounds.width(),
main_v: layout_bounds.vv(1.0),
across_v: layout_bounds.hv(1.0),
padding_main_start: layout_data.padding_top,
padding_main_end: layout_data.padding_bottom,
padding_across_start: layout_data.padding_left,
padding_across_end: layout_data.padding_right,
gap_main: layout_data.row_gap,
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(),
}
}
}
fn main_space(&self) -> f32 {
self.main_size - self.padding_main_start - self.padding_main_end
}
fn across_space(&self) -> f32 {
self.across_size - self.padding_across_start - self.padding_across_end
}
}
#[derive(Debug, Copy, Clone)]
struct ChildAxis {
id: Uuid,
main_size: f32,
across_size: f32,
margin_main_start: f32,
margin_main_end: f32,
margin_across_start: f32,
margin_across_end: f32,
min_main_size: f32,
max_main_size: f32,
min_across_size: f32,
max_across_size: f32,
is_fill_main: bool,
is_fill_across: bool,
_z_index: i32, // unused
bounds: Bounds,
}
impl ChildAxis {
fn new(child: &Shape, child_bounds: &Bounds, flex_data: &FlexData) -> Self {
let id = child.id;
let layout_item = child.layout_item;
let mut result = if flex_data.is_row() {
Self {
id,
main_size: child_bounds.width(),
across_size: child_bounds.height(),
margin_main_start: layout_item.map(|i| i.margin_left).unwrap_or(0.0),
margin_main_end: layout_item.map(|i| i.margin_right).unwrap_or(0.0),
margin_across_start: layout_item.map(|i| i.margin_top).unwrap_or(0.0),
margin_across_end: layout_item.map(|i| i.margin_bottom).unwrap_or(0.0),
min_main_size: layout_item.and_then(|i| i.min_w).unwrap_or(MIN_SIZE),
max_main_size: layout_item.and_then(|i| i.max_w).unwrap_or(MAX_SIZE),
min_across_size: layout_item.and_then(|i| i.min_h).unwrap_or(MIN_SIZE),
max_across_size: layout_item.and_then(|i| i.max_h).unwrap_or(MAX_SIZE),
is_fill_main: child.is_layout_horizontal_fill(),
is_fill_across: child.is_layout_vertical_fill(),
_z_index: layout_item.and_then(|i| i.z_index).unwrap_or(0),
bounds: *child_bounds,
}
} else {
Self {
id,
across_size: child_bounds.width(),
main_size: child_bounds.height(),
margin_across_start: layout_item.map(|i| i.margin_left).unwrap_or(0.0),
margin_across_end: layout_item.map(|i| i.margin_right).unwrap_or(0.0),
margin_main_start: layout_item.map(|i| i.margin_top).unwrap_or(0.0),
margin_main_end: layout_item.map(|i| i.margin_bottom).unwrap_or(0.0),
min_across_size: layout_item.and_then(|i| i.min_w).unwrap_or(MIN_SIZE),
max_across_size: layout_item.and_then(|i| i.max_w).unwrap_or(MAX_SIZE),
min_main_size: layout_item.and_then(|i| i.min_h).unwrap_or(MIN_SIZE),
max_main_size: layout_item.and_then(|i| i.max_h).unwrap_or(MAX_SIZE),
is_fill_main: child.is_layout_vertical_fill(),
is_fill_across: child.is_layout_horizontal_fill(),
_z_index: layout_item.and_then(|i| i.z_index).unwrap_or(0),
bounds: *child_bounds,
}
};
if result.is_fill_main {
result.main_size = result.min_main_size;
}
if result.is_fill_across {
result.across_size = result.min_across_size;
}
result
}
}
fn initialize_tracks(
shape: &Shape,
layout_bounds: &Bounds,
layout_axis: &LayoutAxis,
flex_data: &FlexData,
shapes: ShapesPoolRef,
bounds: &HashMap<Uuid, Bounds>,
) -> Vec<TrackData> {
let mut tracks = Vec::<TrackData>::new();
let mut current_track = TrackData::default();
let mut first = true;
// When is_reverse() is true, we need forward order (children_ids_iter_forward).
// When is_reverse() is false, we need reversed order (children_ids_iter).
let children_iter: Box<dyn Iterator<Item = Uuid>> = if flex_data.is_reverse() {
Box::new(shape.children_ids_iter_forward(true).copied())
} else {
Box::new(shape.children_ids_iter(true).copied())
};
for child_id in children_iter {
let Some(child) = shapes.get(&child_id) else {
continue;
};
if child.is_absolute() || child.hidden() {
continue;
}
let default_bounds = bounds.find(child);
let child_bounds = layout_bounds
.box_bounds(&default_bounds)
.unwrap_or(default_bounds);
let child_axis = ChildAxis::new(child, &child_bounds, flex_data);
let child_main_size = child_axis.margin_main_start
+ child_axis.margin_main_end
+ if child_axis.is_fill_main {
child_axis.min_main_size
} else {
child_axis.main_size
};
let child_across_size = child_axis.margin_across_start
+ child_axis.margin_across_end
+ if child_axis.is_fill_across {
child_axis.min_across_size
} else {
child_axis.across_size
};
let child_max_across_size = if child_axis.is_fill_across {
child_axis.max_across_size
} else {
child_axis.across_size
};
let gap_main = if first { 0.0 } else { layout_axis.gap_main };
let next_main_size = if current_track.shapes.is_empty() {
child_main_size
} else {
current_track.main_size + child_main_size + gap_main
};
let main_space = layout_axis.main_space();
let exceeds_main_space = next_main_size > main_space + TRACK_TOLERANCE;
if !layout_axis.is_auto_main && flex_data.is_wrap() && exceeds_main_space {
tracks.push(current_track);
current_track = TrackData {
main_size: child_main_size,
across_size: child_across_size,
shapes: Vec::from([child_axis]),
is_fill_across: child_axis.is_fill_across,
anchor: Point::default(),
max_across_size: child_max_across_size,
};
} else {
// Update current track
current_track.main_size = next_main_size;
current_track.across_size = f32::max(child_across_size, current_track.across_size);
current_track.shapes.push(child_axis);
current_track.is_fill_across =
current_track.is_fill_across || child_axis.is_fill_across;
current_track.max_across_size =
f32::max(current_track.max_across_size, child_max_across_size);
}
first = false;
}
// Finalize current track
tracks.push(current_track);
tracks
}
// Resize main axis fill
fn distribute_fill_main_space(layout_axis: &LayoutAxis, tracks: &mut [TrackData]) {
for track in tracks.iter_mut() {
let mut left_space = if layout_axis.is_auto_main {
0.0
} else {
layout_axis.main_space() - track.main_size
};
let mut to_resize_children: Vec<&mut ChildAxis> = Vec::new();
for child in track.shapes.iter_mut() {
if child.is_fill_main && child.main_size < child.max_main_size {
to_resize_children.push(child);
}
}
while left_space > MIN_SIZE && !to_resize_children.is_empty() {
let current = left_space / to_resize_children.len() as f32;
for i in (0..to_resize_children.len()).rev() {
let child = &mut to_resize_children[i];
let delta =
f32::min(child.max_main_size, child.main_size + current) - child.main_size;
child.main_size += delta;
left_space -= delta;
track.main_size += delta;
if (child.main_size - child.max_main_size).abs() < MIN_SIZE {
to_resize_children.swap_remove(i);
}
}
}
}
}
fn distribute_fill_across_space(layout_axis: &LayoutAxis, tracks: &mut [TrackData]) {
let tlen = usize::max(tracks.len(), 1);
let total_across_size = tracks.iter().map(|t| t.across_size).sum::<f32>()
+ (tlen - 1) as f32 * layout_axis.gap_across;
let mut left_space = if layout_axis.is_auto_across {
0.0
} else {
layout_axis.across_space() - total_across_size
};
let mut to_resize_tracks: Vec<&mut TrackData> = Vec::new();
for track in tracks.iter_mut() {
if track.is_fill_across && track.across_size < track.max_across_size {
to_resize_tracks.push(track);
}
}
while left_space > MIN_SIZE && !to_resize_tracks.is_empty() {
let current = left_space / to_resize_tracks.len() as f32;
for i in (0..to_resize_tracks.len()).rev() {
let track = &mut to_resize_tracks[i];
let delta = if math::is_close_to(track.across_size, MIN_SIZE) {
f32::min(track.max_across_size, track.across_size + current)
} else {
f32::min(track.max_across_size, track.across_size + current) - track.across_size
};
if math::is_close_to(track.across_size, MIN_SIZE) {
track.across_size = delta;
} else {
track.across_size += delta;
}
left_space -= delta;
if (track.across_size - track.max_across_size).abs() < MIN_SIZE {
to_resize_tracks.swap_remove(i);
}
}
}
// After assigning the across size to the tracks we can assing the size to the shapes
for track in tracks.iter_mut() {
if !track.is_fill_across {
continue;
}
for child in track.shapes.iter_mut() {
if child.is_fill_across {
let mut size =
track.across_size - child.margin_across_start - child.margin_across_end;
size = size.clamp(child.min_across_size, child.max_across_size);
if !layout_axis.is_auto_across {
size = f32::min(size, layout_axis.across_space());
}
child.across_size = size;
}
}
}
}
fn stretch_tracks_sizes(
layout_axis: &LayoutAxis,
tracks: &mut [TrackData],
total_across_size: f32,
) {
let tlen = usize::max(tracks.len(), 1);
let total_across_size = total_across_size + (tlen - 1) as f32 * layout_axis.gap_across;
let left_space = layout_axis.across_space() - total_across_size;
let delta = left_space / tracks.len() as f32;
for track in tracks.iter_mut() {
track.across_size += delta;
}
}
fn calculate_track_positions(
layout_data: &LayoutData,
layout_axis: &LayoutAxis,
layout_bounds: &Bounds,
tracks: &mut [TrackData],
total_across_size: f32,
) {
let mut align_content = &layout_data.align_content;
if layout_axis.is_auto_across || !layout_axis.is_wrap {
align_content = &AlignContent::Start;
}
let tlen = usize::max(tracks.len(), 1);
let total_across_size_gap: f32 = total_across_size + (tlen - 1) as f32 * layout_axis.gap_across;
let (real_margin, real_gap) = match align_content {
AlignContent::End => (
layout_axis.across_size - total_across_size_gap - layout_axis.padding_across_end,
layout_axis.gap_across,
),
AlignContent::Center => (
(layout_axis.across_size - total_across_size_gap) / 2.0,
layout_axis.gap_across,
),
AlignContent::SpaceBetween => (
layout_axis.padding_across_start,
f32::max(
layout_axis.gap_across,
(layout_axis.across_space() - total_across_size) / (tlen - 1) as f32,
),
),
AlignContent::SpaceAround => {
let effective_gap = (layout_axis.across_space() - total_across_size) / tlen as f32;
(
layout_axis.padding_across_start + effective_gap / 2.0,
effective_gap,
)
}
AlignContent::SpaceEvenly => {
let effective_gap =
(layout_axis.across_space() - total_across_size) / (tracks.len() + 1) as f32;
(
layout_axis.padding_across_start + effective_gap,
effective_gap,
)
}
_ => (layout_axis.padding_across_start, layout_axis.gap_across),
};
let mut next_anchor = layout_bounds.nw + layout_axis.across_v * real_margin;
for track in tracks.iter_mut() {
track.anchor = next_anchor;
next_anchor += layout_axis.across_v * (track.across_size + real_gap);
}
}
fn calculate_track_data(
shape: &Shape,
layout_data: &LayoutData,
flex_data: &FlexData,
layout_bounds: &Bounds,
shapes: ShapesPoolRef,
bounds: &HashMap<Uuid, Bounds>,
) -> Vec<TrackData> {
let layout_axis = LayoutAxis::new(shape, layout_bounds, layout_data, flex_data);
let mut tracks = initialize_tracks(
shape,
layout_bounds,
&layout_axis,
flex_data,
shapes,
bounds,
);
distribute_fill_main_space(&layout_axis, &mut tracks);
distribute_fill_across_space(&layout_axis, &mut tracks);
let total_across_size = tracks.iter().map(|t| t.across_size).sum::<f32>();
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);
}
calculate_track_positions(
layout_data,
&layout_axis,
layout_bounds,
&mut tracks,
total_across_size,
);
tracks
}
fn first_anchor(
layout_data: &LayoutData,
layout_axis: &LayoutAxis,
track: &TrackData,
total_shapes_size: f32,
) -> Point {
if layout_axis.is_auto_main {
return track.anchor + layout_axis.main_v * layout_axis.padding_main_start;
}
let slen = usize::max(track.shapes.len(), 1);
let delta = match layout_data.justify_content {
JustifyContent::Center => (layout_axis.main_size - track.main_size) / 2.0,
JustifyContent::End => {
layout_axis.main_size - layout_axis.padding_main_end - track.main_size
}
JustifyContent::SpaceAround => {
let effective_gap = (layout_axis.main_space() - total_shapes_size) / slen as f32;
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_start + f32::max(layout_axis.gap_main, effective_gap)
}
_ => layout_axis.padding_main_start,
};
track.anchor + layout_axis.main_v * delta
}
fn next_anchor(
layout_data: &LayoutData,
layout_axis: &LayoutAxis,
child_axis: &ChildAxis,
track: &TrackData,
prev_anchor: Point,
total_shapes_size: f32,
) -> Point {
if layout_axis.is_auto_main {
let delta = child_axis.margin_main_start
+ child_axis.margin_main_end
+ child_axis.main_size
+ layout_axis.gap_main;
return prev_anchor + layout_axis.main_v * delta;
}
let delta = child_axis.margin_main_start
+ child_axis.margin_main_end
+ match layout_data.justify_content {
JustifyContent::SpaceBetween => {
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 => {
let effective_gap =
(layout_axis.main_space() - total_shapes_size) / (track.shapes.len()) as f32;
child_axis.main_size + f32::max(layout_axis.gap_main, effective_gap)
}
JustifyContent::SpaceEvenly => {
let effective_gap = (layout_axis.main_space() - total_shapes_size)
/ (track.shapes.len() + 1) as f32;
child_axis.main_size + f32::max(layout_axis.gap_main, effective_gap)
}
_ => child_axis.main_size + layout_axis.gap_main,
};
prev_anchor + layout_axis.main_v * delta
}
fn child_position(
child: &Shape,
shape_anchor: Point,
layout_data: &LayoutData,
layout_axis: &LayoutAxis,
child_axis: &ChildAxis,
track: &TrackData,
) -> Point {
let mid_point = (track.across_size - child_axis.across_size + child_axis.margin_across_start
- child_axis.margin_across_end)
/ 2.0;
let end_point = track.across_size - child_axis.across_size - child_axis.margin_across_end;
let across_position = match child.layout_item {
Some(LayoutItem {
align_self: Some(align_self),
..
}) => match align_self {
AlignSelf::Center => mid_point,
AlignSelf::End => end_point,
_ => match layout_data.align_items {
AlignItems::Center => mid_point,
AlignItems::End => end_point,
_ => child_axis.margin_across_start,
},
},
_ => match layout_data.align_items {
AlignItems::Center => mid_point,
AlignItems::End => end_point,
_ => child_axis.margin_across_start,
},
};
shape_anchor
+ layout_axis.main_v * child_axis.margin_main_start
+ layout_axis.across_v * across_position
}
pub fn reflow_flex_layout(
shape: &Shape,
layout_data: &LayoutData,
flex_data: &FlexData,
shapes: ShapesPoolRef,
bounds: &mut HashMap<Uuid, Bounds>,
) -> Result<VecDeque<Modifier>> {
let mut result = VecDeque::new();
let layout_bounds = &bounds.find(shape);
let layout_axis = LayoutAxis::new(shape, layout_bounds, layout_data, flex_data);
let tracks = calculate_track_data(shape, layout_data, flex_data, layout_bounds, shapes, bounds);
for track in tracks.iter() {
let total_shapes_size = track
.shapes
.iter()
.map(|s| s.main_size + s.margin_main_start + s.margin_main_end)
.sum::<f32>();
let mut shape_anchor = first_anchor(layout_data, &layout_axis, track, total_shapes_size);
for child_axis in track.shapes.iter() {
let child_id = child_axis.id;
let Some(child) = shapes.get(&child_id) else {
continue;
};
let position = child_position(
child,
shape_anchor,
layout_data,
&layout_axis,
child_axis,
track,
);
let child_bounds = &child_axis.bounds;
let delta_v = Vector::new_points(&child_bounds.nw, &position);
let (new_width, new_height) = if flex_data.is_row() {
(child_axis.main_size, child_axis.across_size)
} else {
(child_axis.across_size, child_axis.main_size)
};
let mut transform = Matrix::default();
let mut force_reflow = false;
if (new_width - child_bounds.width()).abs() > MIN_SIZE
|| (new_height - child_bounds.height()).abs() > MIN_SIZE
{
// When the child is fill we need to force a reflow
force_reflow = true;
transform.post_concat(&math::resize_matrix(
layout_bounds,
child_bounds,
new_width,
new_height,
));
}
if delta_v.x.abs() > MIN_SIZE || delta_v.y.abs() > MIN_SIZE {
transform.post_concat(&Matrix::translate(delta_v));
}
// Skip identity: propagating it fans out through the whole subtree.
if !math::identitish(&transform) {
result.push_back(Modifier::transform_propagate(child.id, transform));
if child.has_layout() {
result.push_back(Modifier::reflow(child.id, force_reflow));
}
}
shape_anchor = next_anchor(
layout_data,
&layout_axis,
child_axis,
track,
shape_anchor,
total_shapes_size,
);
}
}
if layout_axis.is_auto_across || layout_axis.is_auto_main {
let width = layout_bounds.width();
let height = layout_bounds.height();
let auto_across_size = if layout_axis.is_auto_across {
tracks.iter().map(|track| track.across_size).sum::<f32>()
+ (tracks.len() as f32 - 1.0) * layout_axis.gap_across
+ layout_axis.padding_across_start
+ layout_axis.padding_across_end
} else {
0.0
};
let auto_main_size = if layout_axis.is_auto_main {
tracks
.iter()
.map(|track| {
let nshapes = usize::max(track.shapes.len(), 1);
track
.shapes
.iter()
.map(|s| s.margin_main_start + s.margin_main_end + s.main_size)
.sum::<f32>()
+ (nshapes as f32 - 1.0) * layout_axis.gap_main
})
.reduce(f32::max)
.unwrap_or(MIN_SIZE)
+ layout_axis.padding_main_start
+ layout_axis.padding_main_end
} else {
0.0
};
let (scale_width, scale_height) = if flex_data.is_row() {
(
if layout_axis.is_auto_main {
auto_main_size / width
} else {
1.0
},
if layout_axis.is_auto_across {
auto_across_size / height
} else {
1.0
},
)
} else {
(
if layout_axis.is_auto_across {
auto_across_size / width
} else {
1.0
},
if layout_axis.is_auto_main {
auto_main_size / height
} else {
1.0
},
)
};
let parent_transform = layout_bounds.transform_matrix().unwrap_or_default();
let parent_transform_inv = &parent_transform.invert().ok_or(Error::CriticalError(
"Failed to invert parent transform".to_string(),
))?;
let origin = parent_transform_inv.map_point(layout_bounds.nw);
let mut scale = Matrix::scale((scale_width, scale_height));
scale.post_translate(origin);
scale.post_concat(&parent_transform);
scale.pre_translate(-origin);
scale.pre_concat(parent_transform_inv);
let layout_bounds_after = layout_bounds.transform(&scale);
// Propagate the parent auto-resize to absolute children using their constraints.
for child_id in shape.children_ids_iter(true) {
let Some(child) = shapes.get(child_id) else {
continue;
};
if !child.is_absolute() {
continue;
}
let child_bounds = bounds.find(child);
let constraint_h = child.constraint_h(ConstraintH::Left);
let constraint_v = child.constraint_v(ConstraintV::Top);
let child_transform = super::constraints::propagate_shape_constraints(
layout_bounds,
&layout_bounds_after,
&child_bounds,
constraint_h,
constraint_v,
scale,
child.ignore_constraints,
)?;
result.push_back(Modifier::transform_propagate(child.id, child_transform));
}
result.push_back(Modifier::parent(shape.id, scale));
bounds.insert(shape.id, layout_bounds_after);
}
Ok(result)
}