mirror of
https://github.com/penpot/penpot.git
synced 2026-11-05 17:40:39 +00:00
* 🐛 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
939 lines
29 KiB
Rust
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)
|
|
}
|