Merge pull request #6274 from penpot/alotor-perf-grid-layout-modifiers-3

 Modifiers grid multi-span
This commit is contained in:
Alejandro Alonso 2025-04-15 13:39:45 +02:00 committed by GitHub
commit 99e64ad387
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 373 additions and 203 deletions

View file

@ -357,7 +357,8 @@
to-idx (+ (dec (get cell prop)) (get cell prop-span))
indexed-tracks (subvec (d/enumerate track-list) from-idx to-idx)
to-allocate (size-to-allocate type parent (get children-map shape-id) cell bounds objects)
to-allocate
(size-to-allocate type parent (get children-map shape-id) cell bounds objects)
;; Remove the size and the tracks that are not allocated
[to-allocate total-frs indexed-tracks]

View file

@ -255,8 +255,13 @@ pub fn propagate_modifiers(state: &State, modifiers: Vec<TransformEntry>) -> Vec
}
if let Some(Layout::GridLayout(layout_data, grid_data)) = &frame_data.layout {
let mut children =
grid_layout::reflow_grid_layout(shape, layout_data, grid_data, shapes, &bounds);
let mut children = grid_layout::reflow_grid_layout(
shape,
layout_data,
grid_data,
shapes,
&mut bounds,
);
entries.append(&mut children);
}
reflown.insert(*id);

View file

@ -412,94 +412,6 @@ fn calculate_track_positions(
track.anchor = next_anchor;
next_anchor = next_anchor + layout_axis.across_v * real_gap;
}
/*
match align_content {
AlignContent::End => {
let total_across_size_gap: f32 =
total_across_size + (tracks.len() - 1) as f32 * layout_axis.gap_across;
let delta =
layout_axis.across_size - total_across_size_gap - layout_axis.padding_across_end;
let mut next_anchor = layout_bounds.nw + layout_axis.across_v * delta;
for track in tracks.iter_mut() {
track.anchor = next_anchor;
next_anchor = next_anchor
+ layout_axis.across_v * (track.across_size + layout_axis.gap_across);
}
}
AlignContent::Center => {
let total_across_size_gap: f32 =
total_across_size + (tracks.len() - 1) as f32 * layout_axis.gap_across;
let center_margin = (layout_axis.across_size - total_across_size_gap) / 2.0;
let mut next_anchor = layout_bounds.nw + layout_axis.across_v * center_margin;
for track in tracks.iter_mut() {
track.anchor = next_anchor;
next_anchor = next_anchor
+ layout_axis.across_v * (track.across_size + layout_axis.gap_across);
}
}
AlignContent::SpaceBetween => {
let mut next_anchor =
layout_bounds.nw + layout_axis.across_v * layout_axis.padding_across_start;
let effective_gap = f32::max(
layout_axis.gap_across,
(layout_axis.across_space() - total_across_size) / (tracks.len() - 1) as f32,
);
for track in tracks.iter_mut() {
track.anchor = next_anchor;
next_anchor =
next_anchor + layout_axis.across_v * (track.across_size + effective_gap);
}
}
AlignContent::SpaceAround => {
let effective_gap =
(layout_axis.across_space() - total_across_size) / tracks.len() as f32;
let mut next_anchor = layout_bounds.nw
+ layout_axis.across_v * (layout_axis.padding_across_start + effective_gap / 2.0);
for track in tracks.iter_mut() {
track.anchor = next_anchor;
next_anchor =
next_anchor + layout_axis.across_v * (track.across_size + effective_gap);
}
}
AlignContent::SpaceEvenly => {
let effective_gap =
(layout_axis.across_space() - total_across_size) / (tracks.len() + 1) as f32;
let mut next_anchor = layout_bounds.nw
+ layout_axis.across_v * (layout_axis.padding_across_start + effective_gap);
for track in tracks.iter_mut() {
track.anchor = next_anchor;
next_anchor =
next_anchor + layout_axis.across_v * (track.across_size + effective_gap);
}
}
_ => {
let mut next_anchor =
layout_bounds.nw + layout_axis.across_v * layout_axis.padding_across_start;
for track in tracks.iter_mut() {
track.anchor = next_anchor;
next_anchor = next_anchor
+ layout_axis.across_v * (track.across_size + layout_axis.gap_across);
}
}
}*/
}
fn calculate_track_data(
@ -708,8 +620,8 @@ pub fn reflow_flex_layout(
let auto_across_size = if layout_axis.is_auto_across {
tracks.iter().map(|track| track.across_size).sum::<f32>()
+ (tracks.len() - 1) as f32 * layout_axis.gap_across
+ layout_axis.padding_main_start
+ layout_axis.padding_main_end
+ layout_axis.padding_across_start
+ layout_axis.padding_across_end
} else {
0.0
};
@ -723,8 +635,8 @@ pub fn reflow_flex_layout(
})
.reduce(f32::max)
.unwrap_or(0.01)
+ layout_axis.padding_across_start
+ layout_axis.padding_across_end
+ layout_axis.padding_main_start
+ layout_axis.padding_main_end
} else {
0.0
};

View file

@ -1,5 +1,4 @@
#![allow(dead_code, unused_variables)]
use crate::math::{intersect_rays, Bounds, Matrix, Point, Ray, Vector, VectorExt};
use crate::math::{self as math, intersect_rays, Bounds, Matrix, Point, Ray, Vector, VectorExt};
use crate::shapes::{
AlignContent, AlignItems, AlignSelf, GridCell, GridData, GridTrack, GridTrackType,
JustifyContent, JustifyItems, JustifySelf, LayoutData, LayoutItem, Modifier, Shape,
@ -34,6 +33,7 @@ struct TrackData {
fn calculate_tracks(
is_column: bool,
shape: &Shape,
layout_data: &LayoutData,
grid_data: &GridData,
layout_bounds: &Bounds,
@ -55,14 +55,11 @@ fn calculate_tracks(
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, layout_data, &layout_bounds, &mut tracks);
set_flex_multi_span(is_column, layout_data, &layout_bounds, &mut tracks);
set_fr_value(is_column, layout_data, &mut tracks, layout_size, 0.0);
stretch_tracks(is_column, layout_data, &mut tracks, layout_size);
set_auto_multi_span(is_column, &mut tracks, cells, shapes, bounds);
set_flex_multi_span(is_column, &mut tracks, cells, shapes, bounds);
set_fr_value(is_column, shape, layout_data, &mut tracks, layout_size);
stretch_tracks(is_column, shape, layout_data, &mut tracks, layout_size);
assign_anchors(is_column, layout_data, &layout_bounds, &mut tracks);
return tracks;
}
@ -87,6 +84,20 @@ fn init_tracks(track: &Vec<GridTrack>, size: f32) -> Vec<TrackData> {
.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,
@ -108,6 +119,7 @@ fn set_auto_base_size(
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;
}
@ -116,62 +128,208 @@ fn set_auto_base_size(
continue;
};
let bounds = bounds.find(shape);
let shape_size = if column {
bounds.width()
} else {
bounds.height()
};
let min_size = 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 {
shape_size
};
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 Vec<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,
_layout_data: &LayoutData,
_layout_bounds: &Bounds,
_tracks: &mut Vec<TrackData>,
column: bool,
tracks: &mut Vec<TrackData>,
cells: &Vec<GridCell>,
shapes: &HashMap<Uuid, Shape>,
bounds: &HashMap<Uuid, Bounds>,
) {
// Sort descendant order of prop-span
// Remove groups with flex (will be set in flex_multi_span)
// Retrieve teh value we need to distribute (fixed cell size minus gaps)
// Distribute the size between the tracks that already have a set value
// Distribute the space between auto tracks
// If we still have more space we distribute equally between all tracks
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.row_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 i in start..end {
dist = dist - tracks[i].size;
if tracks[i].track_type == GridTrackType::Auto {
num_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 i in start..end {
if tracks[i].track_type == GridTrackType::Auto {
// dist = dist - track[i].size;
let new_size = if tracks[i].size + rest < tracks[i].max_size {
tracks[i].size + rest
} else {
num_auto = num_auto - 1;
tracks[i].max_size
};
let aloc = new_size - tracks[i].size;
dist = dist - aloc;
tracks[i].size = tracks[i].size + aloc;
}
}
}
}
}
// Adjust multi-spaned cells with flex columns
fn set_flex_multi_span(
_column: bool,
_layout_data: &LayoutData,
_layout_bounds: &Bounds,
_tracks: &mut Vec<TrackData>,
column: bool,
tracks: &mut Vec<TrackData>,
cells: &Vec<GridCell>,
shapes: &HashMap<Uuid, Shape>,
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
// Remove groups without flex (will be set in flex_auto_span)
// Retrieve the value that we need to distribute (fixed size of cell minus gaps)
// Distribute the size first between the tracks that have the fixed size
// When finished we distribute equally between the the rest of the tracks
selected_cells.sort_by(|a, b| {
if column {
b.column_span.cmp(&a.row_span)
} else {
b.row_span.cmp(&a.row_span)
}
});
// 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 mut dist = min_size(column, child, bounds);
let mut num_flex = 0.0;
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 i in start..end {
dist = dist - tracks[i].size;
match tracks[i].track_type {
GridTrackType::Flex => {
num_flex = num_flex + tracks[i].value;
num_auto = num_auto + 1;
}
GridTrackType::Auto => {
num_auto = num_auto + 1;
}
_ => {}
}
}
if dist <= MIN_SIZE {
// No space available to distribute
continue;
}
let rest = dist / num_flex as f32;
// Distribute the space between flex tracks in proportion to the division
for i in start..end {
if tracks[i].track_type == GridTrackType::Flex {
let new_size = f32::min(tracks[i].size + rest, tracks[i].max_size);
let aloc = new_size - tracks[i].size;
dist = dist - aloc;
tracks[i].size = tracks[i].size + aloc;
}
}
// Distribute the space between auto tracks if any
while dist > MIN_SIZE && num_auto > 0 {
let rest = dist / num_auto as f32;
for i in start..end {
if tracks[i].track_type == GridTrackType::Auto
|| tracks[i].track_type == GridTrackType::Flex
{
let new_size = if tracks[i].size + rest < tracks[i].max_size {
tracks[i].size + rest
} else {
num_auto = num_auto - 1;
tracks[i].max_size
};
let aloc = new_size - tracks[i].size;
dist = dist - aloc;
tracks[i].size = tracks[i].size + aloc;
}
}
}
}
}
// Calculate the `fr` unit and adjust the size
fn set_fr_value(
column: bool,
shape: &Shape,
layout_data: &LayoutData,
tracks: &mut Vec<TrackData>,
layout_size: f32,
min_fr_size: f32,
) {
let tot_gap: f32 = if column {
layout_data.column_gap * (tracks.len() - 1) as f32
@ -187,31 +345,48 @@ fn set_fr_value(
.sum::<f32>()
+ tot_gap;
// Get the total of frs to divide the space into
let tot_frs: f32 = tracks
.iter()
.filter(|t| t.track_type == GridTrackType::Flex)
.map(|t| t.value)
.sum();
let cur_fr_size = tracks
.iter()
.filter(|t| t.track_type == GridTrackType::Flex)
.map(|t| t.size / t.value)
.reduce(f32::max)
.unwrap_or(0.0);
// Divide the space between FRS
let fr = f32::max(min_fr_size, (layout_size - tot_size) / tot_frs);
let fr = if column && shape.is_layout_horizontal_auto()
|| !column && shape.is_layout_vertical_auto()
{
cur_fr_size
} else {
f32::max(cur_fr_size, (layout_size - tot_size) / tot_frs)
};
// Assign the space to the FRS
tracks
.iter_mut()
.filter(|t| t.track_type == GridTrackType::Flex)
.for_each(|t| t.size = f32::max(t.size, fr * t.value));
.for_each(|t| t.size = f32::min(fr * t.value, t.max_size));
}
fn stretch_tracks(
column: bool,
shape: &Shape,
layout_data: &LayoutData,
tracks: &mut Vec<TrackData>,
layout_size: f32,
) {
if (column && layout_data.justify_content != JustifyContent::Stretch)
|| (!column && layout_data.align_content != AlignContent::Stretch)
if (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;
}
@ -373,46 +548,8 @@ fn create_cell_data<'a>(
result
}
fn calculate_cell_data<'a>(
shape: &Shape,
layout_data: &LayoutData,
grid_data: &GridData,
shapes: &'a HashMap<Uuid, Shape>,
bounds: &HashMap<Uuid, Bounds>,
) -> Vec<CellData<'a>> {
let result: Vec<CellData<'a>> = vec![];
let layout_bounds = bounds.find(shape);
let column_tracks = calculate_tracks(
true,
layout_data,
grid_data,
&layout_bounds,
&grid_data.cells,
shapes,
bounds,
);
let row_tracks = calculate_tracks(
false,
layout_data,
grid_data,
&layout_bounds,
&grid_data.cells,
shapes,
bounds,
);
create_cell_data(
&layout_bounds,
shapes,
&grid_data.cells,
&column_tracks,
&row_tracks,
)
}
fn child_position(
child: &Shape,
layout_bounds: &Bounds,
layout_data: &LayoutData,
child_bounds: &Bounds,
@ -427,23 +564,37 @@ fn child_position(
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);
cell.anchor
+ vv * match (cell.align_self, layout_data.align_items) {
(Some(AlignSelf::Start), _) => margin_left,
(Some(AlignSelf::Center), _) => (cell.height - child_bounds.height()) / 2.0,
(Some(AlignSelf::End), _) => margin_right + cell.height - child_bounds.height(),
(_, AlignItems::Center) => (cell.height - child_bounds.height()) / 2.0,
(_, AlignItems::End) => margin_right + cell.height - child_bounds.height(),
_ => margin_left,
}
+ hv * match (cell.justify_self, layout_data.justify_items) {
(Some(JustifySelf::Start), _) => margin_top,
(Some(JustifySelf::Center), _) => (cell.width - child_bounds.width()) / 2.0,
(Some(JustifySelf::End), _) => margin_bottom + cell.width - child_bounds.width(),
(_, JustifyItems::Center) => (cell.width - child_bounds.width()) / 2.0,
(_, JustifyItems::End) => margin_bottom + cell.width - child_bounds.width(),
_ => margin_top,
}
let vpos = match (cell.align_self, layout_data.align_items) {
(Some(AlignSelf::Start), _) => margin_top,
(Some(AlignSelf::Center), _) => (cell.height - child_bounds.height()) / 2.0,
(Some(AlignSelf::End), _) => margin_bottom + cell.height - child_bounds.height(),
(_, AlignItems::Center) => (cell.height - child_bounds.height()) / 2.0,
(_, AlignItems::End) => margin_bottom + cell.height - child_bounds.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_bounds.width()) / 2.0,
(Some(JustifySelf::End), _) => margin_right + cell.width - child_bounds.width(),
(_, JustifyItems::Center) => (cell.width - child_bounds.width()) / 2.0,
(_, JustifyItems::End) => margin_right + cell.width - child_bounds.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<'a>(
@ -451,17 +602,78 @@ pub fn reflow_grid_layout<'a>(
layout_data: &LayoutData,
grid_data: &GridData,
shapes: &'a HashMap<Uuid, Shape>,
bounds: &HashMap<Uuid, Bounds>,
bounds: &mut HashMap<Uuid, Bounds>,
) -> VecDeque<Modifier> {
let mut result = VecDeque::new();
let cells = calculate_cell_data(shape, layout_data, grid_data, shapes, bounds);
let layout_bounds = bounds.find(shape);
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,
shapes,
&grid_data.cells,
&column_tracks,
&row_tracks,
);
for cell in cells.iter() {
let child = cell.shape;
let child_bounds = bounds.find(child);
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);
new_width = cell.width - margin_left;
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);
new_height = cell.height - margin_top;
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();
if (new_width - child_bounds.width()).abs() > MIN_SIZE
|| (new_height - child_bounds.height()).abs() > MIN_SIZE
{
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,
@ -469,7 +681,6 @@ pub fn reflow_grid_layout<'a>(
cell,
);
let mut transform = Matrix::default();
let delta_v = Vector::new_points(&child_bounds.nw, &position);
if delta_v.x.abs() > MIN_SIZE || delta_v.y.abs() > MIN_SIZE {
@ -479,5 +690,46 @@ pub fn reflow_grid_layout<'a>(
result.push_back(Modifier::transform(child.id, transform));
}
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() - 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() - 1) as f32 * layout_data.row_gap;
scale_height = auto_height / height;
}
let parent_transform = layout_bounds
.transform_matrix()
.unwrap_or(Matrix::default());
let parent_transform_inv = &parent_transform.invert().unwrap();
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);
}
result
}