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

939 lines
29 KiB
Rust

use crate::error::{Error, Result};
use crate::math::{self as math, intersect_rays, Bounds, Matrix, Point, Ray, Vector, VectorExt};
use crate::shapes::{
AlignContent, AlignItems, AlignSelf, Frame, GridCell, GridData, GridTrack, GridTrackType,
JustifyContent, JustifyItems, JustifySelf, Layout, LayoutData, LayoutItem, Modifier, Shape,
Type,
};
use crate::state::ShapesPoolRef;
use crate::uuid::Uuid;
use std::collections::{HashMap, HashSet, VecDeque};
use super::common::GetBounds;
const MIN_SIZE: f32 = 0.01;
const MAX_SIZE: f32 = f32::INFINITY;
#[derive(Debug)]
pub struct CellData<'a> {
pub shape: Option<&'a Shape>,
pub anchor: Point,
pub width: f32,
pub height: f32,
pub align_self: Option<AlignSelf>,
pub justify_self: Option<JustifySelf>,
pub row: usize,
pub column: usize,
}
#[derive(Debug)]
pub struct TrackData {
pub track_type: GridTrackType,
pub value: f32,
pub size: f32,
pub max_size: f32,
pub anchor_start: Point,
pub anchor_end: Point,
}
// FIXME: We might be able to simplify these arguments
#[allow(clippy::too_many_arguments)]
pub fn calculate_tracks(
is_column: bool,
shape: &Shape,
layout_data: &LayoutData,
grid_data: &GridData,
layout_bounds: &Bounds,
cells: &Vec<GridCell>,
shapes: ShapesPoolRef,
bounds: &HashMap<Uuid, Bounds>,
) -> Vec<TrackData> {
let layout_size = if is_column {
layout_bounds.width() - layout_data.padding_left - layout_data.padding_right
} else {
layout_bounds.height() - layout_data.padding_top - layout_data.padding_bottom
};
let auto_layout = if is_column {
shape.is_layout_horizontal_auto()
} else {
shape.is_layout_vertical_auto()
};
let grid_tracks = if is_column {
&grid_data.columns
} else {
&grid_data.rows
};
let mut tracks = init_tracks(grid_tracks, layout_size);
set_auto_base_size(is_column, &mut tracks, cells, shapes, bounds);
set_auto_multi_span(is_column, &mut tracks, cells, shapes, bounds);
set_flex_multi_span(is_column, layout_data, &mut tracks, cells, shapes, bounds);
set_fr_value(
is_column,
layout_data,
&mut tracks,
layout_size,
auto_layout,
);
stretch_tracks(is_column, shape, layout_data, &mut tracks, layout_size);
assign_anchors(is_column, layout_data, layout_bounds, &mut tracks);
tracks
}
fn init_tracks(track: &[GridTrack], size: f32) -> Vec<TrackData> {
track
.iter()
.map(|t| {
let (size, max_size) = match t.track_type {
GridTrackType::Fixed => (t.value, t.value),
GridTrackType::Percent => (size * t.value / 100.0, size * t.value / 100.0),
_ => (MIN_SIZE, MAX_SIZE),
};
TrackData {
track_type: t.track_type,
value: t.value,
size,
max_size,
anchor_start: Point::default(),
anchor_end: Point::default(),
}
})
.collect()
}
fn min_size(column: bool, shape: &Shape, bounds: &HashMap<Uuid, Bounds>) -> f32 {
if column && shape.is_layout_horizontal_fill() {
shape.layout_item.and_then(|i| i.min_w).unwrap_or(MIN_SIZE)
} else if !column && shape.is_layout_vertical_fill() {
shape.layout_item.and_then(|i| i.min_h).unwrap_or(MIN_SIZE)
} else if column {
let bounds = bounds.find(shape);
bounds.width()
} else {
let bounds = bounds.find(shape);
bounds.height()
}
}
// Go through cells to adjust auto sizes for span=1. Base is the max of its children
fn set_auto_base_size(
column: bool,
tracks: &mut [TrackData],
cells: &Vec<GridCell>,
shapes: ShapesPoolRef,
bounds: &HashMap<Uuid, Bounds>,
) {
for cell in cells {
let (prop, prop_span) = if column {
(cell.column, cell.column_span)
} else {
(cell.row, cell.row_span)
};
if prop_span != 1 || (prop as usize) > tracks.len() {
continue;
}
let track = &mut tracks[(prop - 1) as usize];
// We change the size for auto+flex tracks
if track.track_type != GridTrackType::Auto && track.track_type != GridTrackType::Flex {
continue;
}
let Some(shape) = cell.shape.and_then(|id| shapes.get(&id)) else {
continue;
};
let min_size = min_size(column, shape, bounds);
track.size = f32::max(track.size, min_size);
}
}
fn track_index(is_column: bool, c: &GridCell) -> (usize, usize) {
if is_column {
(
(c.column - 1) as usize,
(c.column + c.column_span - 1) as usize,
)
} else {
((c.row - 1) as usize, (c.row + c.row_span - 1) as usize)
}
}
fn has_flex(is_column: bool, cell: &GridCell, tracks: &mut [TrackData]) -> bool {
let (start, end) = track_index(is_column, cell);
(start..end).any(|i| tracks[i].track_type == GridTrackType::Flex)
}
// Adjust multi-spaned cells with no flex columns
fn set_auto_multi_span(
column: bool,
tracks: &mut [TrackData],
cells: &[GridCell],
shapes: ShapesPoolRef,
bounds: &HashMap<Uuid, Bounds>,
) {
// Remove groups with flex (will be set in flex_multi_span)
let mut selected_cells: Vec<&GridCell> = cells
.iter()
.filter(|c| {
if column {
c.column_span > 1
} else {
c.row_span > 1
}
})
.filter(|c| !has_flex(column, c, tracks))
.collect();
// Sort descendant order of prop-span
selected_cells.sort_by(|a, b| {
if column {
b.column_span.cmp(&a.column_span)
} else {
b.row_span.cmp(&a.row_span)
}
});
for cell in selected_cells {
let Some(child) = cell.shape.and_then(|id| shapes.get(&id)) else {
continue;
};
// Retrieve the value we need to distribute (fixed cell size minus gaps)
let mut dist = min_size(column, child, bounds);
let mut num_auto = 0;
let (start, end) = track_index(column, cell);
// Distribute the size between the tracks that already have a set value
for track in tracks[start..end].iter() {
dist -= track.size;
if track.track_type == GridTrackType::Auto {
num_auto += 1;
}
}
// If we still have more space we distribute equally between all auto tracks
while dist > MIN_SIZE && num_auto > 0 {
let rest = dist / num_auto as f32;
// Distribute the space between auto tracks
for track in tracks[start..end].iter_mut() {
if track.track_type == GridTrackType::Auto {
// dist = dist - track[i].size;
let new_size = if track.size + rest < track.max_size {
track.size + rest
} else {
num_auto -= 1;
track.max_size
};
let aloc = new_size - track.size;
dist -= aloc;
track.size += aloc;
}
}
}
}
}
// Adjust multi-spaned cells with flex columns
fn set_flex_multi_span(
column: bool,
layout_data: &LayoutData,
tracks: &mut [TrackData],
cells: &[GridCell],
shapes: ShapesPoolRef,
bounds: &HashMap<Uuid, Bounds>,
) {
// Remove groups without flex
let mut selected_cells: Vec<&GridCell> = cells
.iter()
.filter(|c| {
if column {
c.column_span > 1
} else {
c.row_span > 1
}
})
.filter(|c| has_flex(column, c, tracks))
.collect();
// Sort descendant order of prop-span
selected_cells.sort_by(|a, b| {
if column {
b.column_span.cmp(&a.column_span)
} else {
b.row_span.cmp(&a.row_span)
}
});
let gap_value = if column {
layout_data.column_gap
} else {
layout_data.row_gap
};
// Retrieve the value that we need to distribute and the number of frs
for cell in selected_cells {
let Some(child) = cell.shape.and_then(|id| shapes.get(&id)) else {
continue;
};
// Retrieve the value we need to distribute (fixed cell size minus gaps)
let (start, end) = track_index(column, cell);
let gaps = (end - start - 1) as f32 * gap_value;
let mut dist = min_size(column, child, bounds) - gaps;
let mut num_flex = 0.0;
// Distribute the size between the tracks that already have a set value
for track in tracks[start..end].iter() {
match track.track_type {
GridTrackType::Flex => {
num_flex += track.value;
}
_ => {
dist -= track.size;
}
}
}
if dist <= MIN_SIZE {
// No space available to distribute
continue;
}
let alloc = dist / num_flex;
// Distribute the space between flex tracks in proportion to the division
for track in tracks[start..end].iter_mut() {
if track.track_type == GridTrackType::Flex {
let new_size = alloc.clamp(track.size, track.max_size);
let aloc = new_size - track.size;
dist -= aloc;
track.size = new_size;
}
}
}
}
// Calculate the `fr` unit and adjust the size
fn set_fr_value(
column: bool,
layout_data: &LayoutData,
tracks: &mut [TrackData],
layout_size: f32,
auto_layout: bool,
) {
let tot_gap: f32 = if column {
layout_data.column_gap * (tracks.len() as f32 - 1.0)
} else {
layout_data.row_gap * (tracks.len() as f32 - 1.0)
};
let mut tot_size_nofr = tot_gap;
let mut tot_frs = 0.0;
let mut cur_fr_value = 0.0;
for t in tracks.iter() {
if t.track_type == GridTrackType::Flex {
tot_frs += t.value;
cur_fr_value = f32::max(t.size / t.value, cur_fr_value);
} else {
tot_size_nofr += t.size;
}
}
let fr_space = f32::max(0.0, layout_size - tot_size_nofr);
let mut fr_value = if auto_layout {
// If auto_layout we assign the max fr
cur_fr_value
} else {
fr_space / tot_frs
};
if !auto_layout {
loop {
// While there is still tracks that can take more size
let mut pending = fr_space;
let mut free_frs = 0;
for t in tracks.iter() {
if t.track_type == GridTrackType::Flex {
// Use the current fr size
let current = t.value * fr_value;
if t.size > current {
// If it's not enough psace to allocate, this is full. Cannot grow anymore.
pending -= t.size;
} else {
// Otherwise, this track can still grow
free_frs += 1;
pending -= current;
}
}
}
// We finish when we cannot reduce the tracks more or we've allocated all
// the space
if free_frs == 0 || pending >= -0.01 {
break;
}
fr_value += pending / free_frs as f32;
}
}
// Assign the fr_value to the flex tracks
tracks
.iter_mut()
.filter(|t| t.track_type == GridTrackType::Flex)
.for_each(|t| t.size = (fr_value * t.value).clamp(t.size, t.max_size));
}
fn stretch_tracks(
column: bool,
shape: &Shape,
layout_data: &LayoutData,
tracks: &mut [TrackData],
layout_size: f32,
) {
if (tracks.is_empty()
|| column
&& (layout_data.justify_content != JustifyContent::Stretch
|| shape.is_layout_horizontal_auto()))
|| (!column
&& (layout_data.align_content != AlignContent::Stretch
|| shape.is_layout_vertical_auto()))
{
return;
}
let tot_gap: f32 = if column {
layout_data.column_gap * (tracks.len() - 1) as f32
} else {
layout_data.row_gap * (tracks.len() - 1) as f32
};
// Total size already used
let tot_size: f32 = tracks.iter().map(|t| t.size).sum::<f32>() + tot_gap;
let auto_tracks = tracks
.iter_mut()
.filter(|t| t.track_type == GridTrackType::Auto)
.count() as f32;
let free_space = layout_size - tot_size;
if free_space <= 0.0 {
return;
}
let add_size = free_space / auto_tracks;
// Assign the space to the FRS
tracks
.iter_mut()
.filter(|t| t.track_type == GridTrackType::Auto)
.for_each(|t| t.size = f32::min(t.max_size, t.size + add_size));
}
fn justify_to_align(justify: JustifyContent) -> AlignContent {
match justify {
JustifyContent::Start => AlignContent::Start,
JustifyContent::End => AlignContent::End,
JustifyContent::Center => AlignContent::Center,
JustifyContent::SpaceBetween => AlignContent::SpaceBetween,
JustifyContent::SpaceAround => AlignContent::SpaceAround,
JustifyContent::SpaceEvenly => AlignContent::SpaceEvenly,
JustifyContent::Stretch => AlignContent::Stretch,
}
}
fn assign_anchors(
column: bool,
layout_data: &LayoutData,
layout_bounds: &Bounds,
tracks: &mut Vec<TrackData>,
) {
let tot_track_length = tracks.iter().map(|t| t.size).sum::<f32>();
let mut cursor = layout_bounds.nw;
let (v, gap, size, padding_start, padding_end, align) = if column {
(
layout_bounds.hv(1.0),
layout_data.column_gap,
layout_bounds.width(),
layout_data.padding_left,
layout_data.padding_right,
justify_to_align(layout_data.justify_content),
)
} else {
(
layout_bounds.vv(1.0),
layout_data.row_gap,
layout_bounds.height(),
layout_data.padding_top,
layout_data.padding_bottom,
layout_data.align_content,
)
};
let tot_gap = if tracks.is_empty() {
0.0
} else {
gap * (tracks.len() - 1) as f32
};
let tot_size = tot_track_length + tot_gap;
let padding = padding_start + padding_end;
let pad_size = size - padding;
let (real_margin, real_gap) = match align {
AlignContent::End => (size - padding_end - tot_size, gap),
AlignContent::Center => ((size - tot_size) / 2.0, gap),
AlignContent::SpaceAround => {
let effective_gap = (pad_size - tot_track_length) / tracks.len() as f32;
(padding_start + effective_gap / 2.0, effective_gap)
}
AlignContent::SpaceBetween => (
padding_start,
f32::max(
gap,
(pad_size - tot_track_length) / (tracks.len() - 1) as f32,
),
),
_ => (padding_start + 0.0, gap),
};
cursor += v * real_margin;
for track in tracks {
track.anchor_start = cursor;
track.anchor_end = cursor + (v * track.size);
cursor = track.anchor_end + (v * real_gap);
}
}
fn cell_bounds(
layout_bounds: &Bounds,
column_start: Point,
column_end: Point,
row_start: Point,
row_end: Point,
) -> Option<Bounds> {
let hv = layout_bounds.hv(1.0);
let vv = layout_bounds.vv(1.0);
let nw = intersect_rays(&Ray::new(column_start, vv), &Ray::new(row_start, hv))?;
let ne = intersect_rays(&Ray::new(column_end, vv), &Ray::new(row_start, hv))?;
let sw = intersect_rays(&Ray::new(column_start, vv), &Ray::new(row_end, hv))?;
let se = intersect_rays(&Ray::new(column_end, vv), &Ray::new(row_end, hv))?;
Some(Bounds::new(nw, ne, se, sw))
}
pub fn create_cell_data<'a>(
layout_bounds: &Bounds,
children: &HashSet<Uuid>,
shapes: ShapesPoolRef<'a>,
cells: &Vec<GridCell>,
column_tracks: &[TrackData],
row_tracks: &[TrackData],
allow_empty: bool,
) -> Vec<CellData<'a>> {
let mut result = Vec::<CellData<'a>>::new();
for cell in cells {
let shape: Option<&Shape> = if let Some(shape_id) = cell.shape {
if !children.contains(&shape_id) {
None
} else {
shapes.get(&shape_id)
}
} else {
None
};
if !allow_empty && shape.is_none() {
continue;
}
let column_start = (cell.column - 1) as usize;
let column_end = (cell.column + cell.column_span - 2) as usize;
let row_start = (cell.row - 1) as usize;
let row_end = (cell.row + cell.row_span - 2) as usize;
if column_start >= column_tracks.len()
|| column_end >= column_tracks.len()
|| row_start >= row_tracks.len()
|| row_end >= row_tracks.len()
{
continue;
}
let Some(cell_bounds) = cell_bounds(
layout_bounds,
column_tracks[column_start].anchor_start,
column_tracks[column_end].anchor_end,
row_tracks[row_start].anchor_start,
row_tracks[row_end].anchor_end,
) else {
continue;
};
result.push(CellData {
shape,
anchor: cell_bounds.nw,
width: cell_bounds.width(),
height: cell_bounds.height(),
align_self: cell.align_self,
justify_self: cell.justify_self,
row: row_start,
column: column_start,
});
}
result
}
pub fn grid_cell_data<'a>(
shape: &Shape,
shapes: ShapesPoolRef<'a>,
allow_empty: bool,
) -> Vec<CellData<'a>> {
let Type::Frame(Frame {
layout: Some(Layout::GridLayout(layout_data, grid_data)),
..
}) = &shape.shape_type
else {
return vec![];
};
let bounds = &mut HashMap::<Uuid, Bounds>::new();
let layout_bounds = shape.bounds();
let children: HashSet<Uuid> = shape.children_ids_iter(false).copied().collect();
let column_tracks = calculate_tracks(
true,
shape,
layout_data,
grid_data,
&layout_bounds,
&grid_data.cells,
shapes,
bounds,
);
let row_tracks = calculate_tracks(
false,
shape,
layout_data,
grid_data,
&layout_bounds,
&grid_data.cells,
shapes,
bounds,
);
create_cell_data(
&layout_bounds,
&children,
shapes,
&grid_data.cells,
&column_tracks,
&row_tracks,
allow_empty,
)
}
// Returns `(h_min, v_min, h_size, v_size)` — the child's bounding box expressed in the
// layout frame's own coordinate system (projected onto its `hv`/`vv` unit vectors).
//
// Using the frame axes rather than screen x/y is necessary when the parent grid frame
// is itself rotated: in that case `max_x - min_x` is the screen-AABB width, which
// differs from the width measured along the frame's horizontal axis.
fn child_frame_aabb(child_bounds: &Bounds, hv: Vector, vv: Vector) -> (f32, f32, f32, f32) {
let corners = child_bounds.points();
let mut h_min = f32::INFINITY;
let mut h_max = f32::NEG_INFINITY;
let mut v_min = f32::INFINITY;
let mut v_max = f32::NEG_INFINITY;
for p in &corners {
let h = hv.x * p.x + hv.y * p.y;
let v = vv.x * p.x + vv.y * p.y;
if h < h_min {
h_min = h;
}
if h > h_max {
h_max = h;
}
if v < v_min {
v_min = v;
}
if v > v_max {
v_max = v;
}
}
(h_min, v_min, h_max - h_min, v_max - v_min)
}
fn child_position(
child: &Shape,
layout_bounds: &Bounds,
layout_data: &LayoutData,
child_bounds: &Bounds,
layout_item: Option<LayoutItem>,
cell: &CellData,
) -> Point {
let hv = layout_bounds.hv(1.0);
let vv = layout_bounds.vv(1.0);
let margin_left = layout_item.map(|i| i.margin_left).unwrap_or(0.0);
let margin_top = layout_item.map(|i| i.margin_top).unwrap_or(0.0);
let margin_right = layout_item.map(|i| i.margin_right).unwrap_or(0.0);
let margin_bottom = layout_item.map(|i| i.margin_bottom).unwrap_or(0.0);
// Project corners onto the frame's own axes so that both a rotated child *and* a
// rotated parent frame are handled correctly. For an axis-aligned frame this
// reduces to max_x-min_x / max_y-min_y, so non-rotated layouts are unaffected.
let (_, _, child_width, child_height) = child_frame_aabb(child_bounds, hv, vv);
let vpos = match (cell.align_self, layout_data.align_items) {
(Some(AlignSelf::Start), _) => margin_top,
(Some(AlignSelf::Center), _) => (cell.height - child_height) / 2.0,
(Some(AlignSelf::End), _) => margin_bottom + cell.height - child_height,
(_, AlignItems::Center) => (cell.height - child_height) / 2.0,
(_, AlignItems::End) => margin_bottom + cell.height - child_height,
_ => margin_top,
};
let vpos = if child.is_layout_vertical_fill() {
margin_top
} else {
vpos
};
let hpos = match (cell.justify_self, layout_data.justify_items) {
(Some(JustifySelf::Start), _) => margin_left,
(Some(JustifySelf::Center), _) => (cell.width - child_width) / 2.0,
(Some(JustifySelf::End), _) => margin_right + cell.width - child_width,
(_, JustifyItems::Center) => (cell.width - child_width) / 2.0,
(_, JustifyItems::End) => margin_right + cell.width - child_width,
_ => margin_left,
};
let hpos = if child.is_layout_horizontal_fill() {
margin_left
} else {
hpos
};
cell.anchor + vv * vpos + hv * hpos
}
pub fn reflow_grid_layout(
shape: &Shape,
layout_data: &LayoutData,
grid_data: &GridData,
shapes: ShapesPoolRef,
bounds: &mut HashMap<Uuid, Bounds>,
) -> Result<VecDeque<Modifier>> {
let mut result = VecDeque::new();
let layout_bounds = bounds.find(shape);
let children: HashSet<Uuid> = shape.children_ids_iter(true).copied().collect();
let column_tracks = calculate_tracks(
true,
shape,
layout_data,
grid_data,
&layout_bounds,
&grid_data.cells,
shapes,
bounds,
);
let row_tracks = calculate_tracks(
false,
shape,
layout_data,
grid_data,
&layout_bounds,
&grid_data.cells,
shapes,
bounds,
);
let cells = create_cell_data(
&layout_bounds,
&children,
shapes,
&grid_data.cells,
&column_tracks,
&row_tracks,
false,
);
for cell in cells.iter() {
let Some(child) = cell.shape else { continue };
let child_bounds = bounds.find(child);
// Compute frame-axis projections once; used for both sizing and positioning.
let hv = layout_bounds.hv(1.0);
let vv = layout_bounds.vv(1.0);
let (h_min, v_min, child_frame_w, child_frame_h) = child_frame_aabb(&child_bounds, hv, vv);
// resize_matrix scales the child in the parent's local frame coordinate system
// by (new_width / child_bounds.width()) in the h-axis and
// (new_height / child_bounds.height()) in the v-axis. For a rotated child the
// frame-projected extent differs from the intrinsic bounds dimensions, so we
// back-calculate the intrinsic target that will produce the desired
// frame-projected extent.
let mut new_width = child_bounds.width();
if child.is_layout_horizontal_fill() {
let margin_left = child.layout_item.map(|i| i.margin_left).unwrap_or(0.0);
let margin_right = child.layout_item.map(|i| i.margin_right).unwrap_or(0.0);
let target_frame_w = cell.width - margin_left - margin_right;
new_width = if child_frame_w > MIN_SIZE {
target_frame_w * child_bounds.width() / child_frame_w
} else {
target_frame_w
};
let min_width = child.layout_item.and_then(|i| i.min_w).unwrap_or(MIN_SIZE);
let max_width = child.layout_item.and_then(|i| i.max_w).unwrap_or(MAX_SIZE);
new_width = new_width.clamp(min_width, max_width);
}
let mut new_height = child_bounds.height();
if child.is_layout_vertical_fill() {
let margin_top = child.layout_item.map(|i| i.margin_top).unwrap_or(0.0);
let margin_bottom = child.layout_item.map(|i| i.margin_bottom).unwrap_or(0.0);
let target_frame_h = cell.height - margin_top - margin_bottom;
new_height = if child_frame_h > MIN_SIZE {
target_frame_h * child_bounds.height() / child_frame_h
} else {
target_frame_h
};
let min_height = child.layout_item.and_then(|i| i.min_h).unwrap_or(MIN_SIZE);
let max_height = child.layout_item.and_then(|i| i.max_h).unwrap_or(MAX_SIZE);
new_height = new_height.clamp(min_height, max_height);
}
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 a fill it needs to be reflown
force_reflow = true;
transform.post_concat(&math::resize_matrix(
&layout_bounds,
&child_bounds,
new_width,
new_height,
));
}
let position = child_position(
child,
&layout_bounds,
layout_data,
&child_bounds,
child.layout_item,
cell,
);
// Compute the child's reference point in the frame's coordinate system.
// For a rotated parent frame, (min_x, min_y) is wrong because it is the
// screen-AABB corner, not the frame-axis-aligned corner.
// child_ref = h_min * hv + v_min * vv gives the world-space point whose
// projections onto hv/vv are the child's minima along those axes —
// the "top-left in frame coordinates".
//
// For fill axes, resize_matrix scales local-x/y by (new_w / child_bounds.width())
// anchored at nw. This shifts h_min/v_min: the post-resize minimum is
// h_min_new = nw_h + (h_min - nw_h) * scale_w
// We must translate FROM this post-resize minimum, not the pre-resize one.
let nw_h = hv.x * child_bounds.nw.x + hv.y * child_bounds.nw.y;
let nw_v = vv.x * child_bounds.nw.x + vv.y * child_bounds.nw.y;
let h_anchor = if child.is_layout_horizontal_fill() && child_bounds.width() > MIN_SIZE {
let scale_w = new_width / child_bounds.width();
nw_h + (h_min - nw_h) * scale_w
} else {
h_min
};
let v_anchor = if child.is_layout_vertical_fill() && child_bounds.height() > MIN_SIZE {
let scale_h = new_height / child_bounds.height();
nw_v + (v_min - nw_v) * scale_h
} else {
v_min
};
let child_ref = Point::new(
h_anchor * hv.x + v_anchor * vv.x,
h_anchor * hv.y + v_anchor * vv.y,
);
let delta_v = Vector::new_points(&child_ref, &position);
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));
}
}
}
if shape.is_layout_horizontal_auto() || shape.is_layout_vertical_auto() {
let width = layout_bounds.width();
let height = layout_bounds.height();
let mut scale_width = 1.0;
let mut scale_height = 1.0;
if shape.is_layout_horizontal_auto() {
let auto_width = column_tracks.iter().map(|t| t.size).sum::<f32>()
+ layout_data.padding_left
+ layout_data.padding_right
+ column_tracks.len().saturating_sub(1) as f32 * layout_data.column_gap;
scale_width = auto_width / width;
}
if shape.is_layout_vertical_auto() {
let auto_height = row_tracks.iter().map(|t| t.size).sum::<f32>()
+ layout_data.padding_top
+ layout_data.padding_bottom
+ row_tracks.len().saturating_sub(1) as f32 * layout_data.row_gap;
scale_height = auto_height / height;
}
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);
result.push_back(Modifier::parent(shape.id, scale));
bounds.insert(shape.id, layout_bounds_after);
}
Ok(result)
}