Merge pull request #6125 from penpot/alotor-perf-flex-layout-2

 Improvements on flex layout positioning
This commit is contained in:
Aitor Moreno 2025-03-24 09:53:30 +01:00 committed by GitHub
commit 22efd6574d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 175 additions and 72 deletions

View file

@ -640,8 +640,8 @@
margin-bottom (or (dm/get-prop margins :m3) 0) margin-bottom (or (dm/get-prop margins :m3) 0)
margin-left (or (dm/get-prop margins :m4) 0) margin-left (or (dm/get-prop margins :m4) 0)
h-sizing (-> (dm/get-prop shape :layout-item-h-sizing) (or :auto) translate-layout-sizing) h-sizing (-> (dm/get-prop shape :layout-item-h-sizing) (or :fix) translate-layout-sizing)
v-sizing (-> (dm/get-prop shape :layout-item-v-sizing) (or :auto) translate-layout-sizing) v-sizing (-> (dm/get-prop shape :layout-item-v-sizing) (or :fix) translate-layout-sizing)
align-self (-> (dm/get-prop shape :layout-item-align-self) translate-align-self) align-self (-> (dm/get-prop shape :layout-item-align-self) translate-align-self)
max-h (dm/get-prop shape :layout-item-max-h) max-h (dm/get-prop shape :layout-item-max-h)

View file

@ -5,6 +5,8 @@ pub type Matrix = skia::Matrix;
pub type Vector = skia::Vector; pub type Vector = skia::Vector;
pub type Point = skia::Point; pub type Point = skia::Point;
const THRESHOLD: f32 = 0.001;
pub trait VectorExt { pub trait VectorExt {
fn new_points(a: &Point, b: &Point) -> Vector; fn new_points(a: &Point, b: &Point) -> Vector;
} }
@ -16,7 +18,20 @@ impl VectorExt for Vector {
} }
} }
#[derive(Debug, Clone, PartialEq)] pub fn is_close_to(current: f32, value: f32) -> bool {
(current - value).abs() <= THRESHOLD
}
pub fn identitish(m: Matrix) -> bool {
is_close_to(m.scale_x(), 1.0)
&& is_close_to(m.scale_y(), 1.0)
&& is_close_to(m.translate_x(), 0.0)
&& is_close_to(m.translate_y(), 0.0)
&& is_close_to(m.skew_x(), 0.0)
&& is_close_to(m.skew_y(), 0.0)
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Bounds { pub struct Bounds {
pub nw: Point, pub nw: Point,
pub ne: Point, pub ne: Point,
@ -95,9 +110,9 @@ impl Bounds {
self.sw = mtx.map_point(self.sw); self.sw = mtx.map_point(self.sw);
} }
// pub fn box_bounds(&self, other: &Self) -> Option<Self> { pub fn box_bounds(&self, other: &Self) -> Option<Self> {
// self.from_points(other.points()) self.from_points(other.points())
// } }
pub fn from_points(&self, points: Vec<Point>) -> Option<Self> { pub fn from_points(&self, points: Vec<Point>) -> Option<Self> {
let hv = self.horizontal_vec(); let hv = self.horizontal_vec();

View file

@ -690,7 +690,19 @@ impl RenderState {
if element.is_recursive() { if element.is_recursive() {
let children_clip_bounds = node_render_state let children_clip_bounds = node_render_state
.get_children_clip_bounds(element, modifiers.get(&element.id)); .get_children_clip_bounds(element, modifiers.get(&element.id));
for child_id in element.children_ids().iter().rev() {
let mut children_ids = element.children_ids();
// Z-index ordering on Layouts
if element.has_layout() {
children_ids.sort_by(|id1, id2| {
let z1 = tree.get(id1).map_or_else(|| 0, |s| s.z_index());
let z2 = tree.get(id2).map_or_else(|| 0, |s| s.z_index());
z1.cmp(&z2)
});
}
for child_id in children_ids.iter().rev() {
self.pending_nodes.push(NodeRenderState { self.pending_nodes.push(NodeRenderState {
id: *child_id, id: *child_id,
visited_children: false, visited_children: false,

View file

@ -722,6 +722,20 @@ impl Shape {
} }
} }
pub fn is_absolute(&self) -> bool {
match &self.layout_item {
Some(LayoutItem { is_absolute, .. }) => *is_absolute,
_ => false,
}
}
pub fn z_index(&self) -> i32 {
match &self.layout_item {
Some(LayoutItem { z_index, .. }) => *z_index,
_ => 0,
}
}
pub fn is_layout_vertical_auto(&self) -> bool { pub fn is_layout_vertical_auto(&self) -> bool {
match &self.layout_item { match &self.layout_item {
Some(LayoutItem { v_sizing, .. }) => v_sizing == &Sizing::Auto, Some(LayoutItem { v_sizing, .. }) => v_sizing == &Sizing::Auto,

View file

@ -1,5 +1,4 @@
use std::collections::{HashMap, HashSet, VecDeque}; use std::collections::{HashMap, HashSet, VecDeque};
mod common; mod common;
mod constraints; mod constraints;
mod flex_layout; mod flex_layout;
@ -9,7 +8,7 @@ use uuid::Uuid;
use common::GetBounds; use common::GetBounds;
use crate::math::{Bounds, Matrix, Point}; use crate::math::{identitish, Bounds, Matrix, Point};
use crate::shapes::{ use crate::shapes::{
ConstraintH, ConstraintV, Frame, Group, Layout, Modifier, Shape, TransformEntry, Type, ConstraintH, ConstraintV, Frame, Group, Layout, Modifier, Shape, TransformEntry, Type,
}; };
@ -23,7 +22,7 @@ fn propagate_children(
transform: Matrix, transform: Matrix,
bounds: &HashMap<Uuid, Bounds>, bounds: &HashMap<Uuid, Bounds>,
) -> VecDeque<Modifier> { ) -> VecDeque<Modifier> {
if shape.children.len() == 0 { if shape.children.len() == 0 || identitish(transform) {
return VecDeque::new(); return VecDeque::new();
} }
@ -39,7 +38,13 @@ fn propagate_children(
let constraint_h = match &shape.shape_type { let constraint_h = match &shape.shape_type {
Type::Frame(Frame { Type::Frame(Frame {
layout: Some(_), .. layout: Some(_), ..
}) => ConstraintH::Left, }) => {
if child.is_absolute() {
child.constraint_h(ConstraintH::Left)
} else {
ConstraintH::Left
}
}
Type::Frame(_) => child.constraint_h(ConstraintH::Left), Type::Frame(_) => child.constraint_h(ConstraintH::Left),
_ => child.constraint_h(ConstraintH::Scale), _ => child.constraint_h(ConstraintH::Scale),
}; };
@ -47,7 +52,13 @@ fn propagate_children(
let constraint_v = match &shape.shape_type { let constraint_v = match &shape.shape_type {
Type::Frame(Frame { Type::Frame(Frame {
layout: Some(_), .. layout: Some(_), ..
}) => ConstraintV::Top, }) => {
if child.is_absolute() {
child.constraint_v(ConstraintV::Top)
} else {
ConstraintV::Top
}
}
Type::Frame(_) => child.constraint_v(ConstraintV::Top), Type::Frame(_) => child.constraint_v(ConstraintV::Top),
_ => child.constraint_v(ConstraintV::Scale), _ => child.constraint_v(ConstraintV::Scale),
}; };
@ -108,7 +119,6 @@ pub fn propagate_modifiers(state: &State, modifiers: Vec<TransformEntry>) -> Vec
while let Some(modifier) = entries.pop_front() { while let Some(modifier) = entries.pop_front() {
match modifier { match modifier {
Modifier::Transform(entry) => { Modifier::Transform(entry) => {
// println!("Transform {}", entry.id);
let Some(shape) = state.shapes.get(&entry.id) else { let Some(shape) = state.shapes.get(&entry.id) else {
continue; continue;
}; };
@ -154,8 +164,22 @@ pub fn propagate_modifiers(state: &State, modifiers: Vec<TransformEntry>) -> Vec
layout: Some(_), .. layout: Some(_), ..
}) => { }) => {
if !reflown.contains(&id) { if !reflown.contains(&id) {
let mut skip_reflow = false;
if shape.is_layout_horizontal_fill()
|| shape.is_layout_vertical_fill()
{
if let Some(parent_id) = shape.parent_id {
if !reflown.contains(&parent_id) {
// If this is a fill layout but the parent has not been reflown yet
// we wait for the next iteration for reflow
skip_reflow = true;
}
}
}
if !skip_reflow {
layout_reflows.push(id); layout_reflows.push(id);
reflown.insert(id); }
} }
} }
Type::Group(Group { masked: true }) => { Type::Group(Group { masked: true }) => {
@ -196,6 +220,10 @@ pub fn propagate_modifiers(state: &State, modifiers: Vec<TransformEntry>) -> Vec
} }
for id in layout_reflows.iter() { for id in layout_reflows.iter() {
if reflown.contains(&id) {
continue;
}
let Some(shape) = state.shapes.get(&id) else { let Some(shape) = state.shapes.get(&id) else {
continue; continue;
}; };
@ -203,10 +231,14 @@ pub fn propagate_modifiers(state: &State, modifiers: Vec<TransformEntry>) -> Vec
let Type::Frame(frame_data) = &shape.shape_type else { let Type::Frame(frame_data) = &shape.shape_type else {
continue; continue;
}; };
if let Some(Layout::FlexLayout(layout_data, flex_data)) = &frame_data.layout { if let Some(Layout::FlexLayout(layout_data, flex_data)) = &frame_data.layout {
let mut children = let mut children = flex_layout::reflow_flex_layout(
flex_layout::reflow_flex_layout(shape, layout_data, flex_data, shapes, &bounds); shape,
layout_data,
flex_data,
shapes,
&mut bounds,
);
entries.append(&mut children); entries.append(&mut children);
} }
@ -215,6 +247,7 @@ pub fn propagate_modifiers(state: &State, modifiers: Vec<TransformEntry>) -> Vec
grid_layout::reflow_grid_layout(shape, layout_data, grid_data, shapes, &bounds); grid_layout::reflow_grid_layout(shape, layout_data, grid_data, shapes, &bounds);
entries.append(&mut children); entries.append(&mut children);
} }
reflown.insert(*id);
} }
layout_reflows = Vec::new(); layout_reflows = Vec::new();
} }

View file

@ -116,8 +116,8 @@ struct ChildAxis {
max_across_size: f32, max_across_size: f32,
is_fill_main: bool, is_fill_main: bool,
is_fill_across: bool, is_fill_across: bool,
is_absolute: bool,
z_index: i32, z_index: i32,
bounds: Bounds,
} }
impl ChildAxis { impl ChildAxis {
@ -139,8 +139,8 @@ impl ChildAxis {
max_across_size: layout_item.and_then(|i| i.max_h).unwrap_or(MAX_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_main: child.is_layout_horizontal_fill(),
is_fill_across: child.is_layout_vertical_fill(), is_fill_across: child.is_layout_vertical_fill(),
is_absolute: layout_item.map(|i| i.is_absolute).unwrap_or(false),
z_index: layout_item.map(|i| i.z_index).unwrap_or(0), z_index: layout_item.map(|i| i.z_index).unwrap_or(0),
bounds: child_bounds.clone(),
} }
} else { } else {
Self { Self {
@ -157,8 +157,8 @@ impl ChildAxis {
max_main_size: layout_item.and_then(|i| i.max_h).unwrap_or(MAX_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_main: child.is_layout_vertical_fill(),
is_fill_across: child.is_layout_horizontal_fill(), is_fill_across: child.is_layout_horizontal_fill(),
is_absolute: layout_item.map(|i| i.is_absolute).unwrap_or(false),
z_index: layout_item.map(|i| i.z_index).unwrap_or(0), z_index: layout_item.map(|i| i.z_index).unwrap_or(0),
bounds: child_bounds.clone(),
} }
}; };
@ -174,6 +174,7 @@ impl ChildAxis {
fn initialize_tracks( fn initialize_tracks(
shape: &Shape, shape: &Shape,
layout_bounds: &Bounds,
layout_axis: &LayoutAxis, layout_axis: &LayoutAxis,
layout_data: &LayoutData, layout_data: &LayoutData,
flex_data: &FlexData, flex_data: &FlexData,
@ -194,15 +195,28 @@ fn initialize_tracks(
continue; continue;
}; };
let child_bounds = bounds.find(child); 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, layout_data); let child_axis = ChildAxis::new(child, &child_bounds, layout_data);
let child_main_size = if child_axis.is_fill_main { 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 child_axis.min_main_size
} else { } else {
child_axis.main_size child_axis.main_size
}; };
let child_across_size = if child_axis.is_fill_across {
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 child_axis.min_across_size
} else { } else {
child_axis.across_size child_axis.across_size
@ -271,6 +285,7 @@ fn distribute_fill_main_space(layout_axis: &LayoutAxis, tracks: &mut Vec<TrackDa
f32::min(child.max_main_size, child.main_size + current) - child.main_size; f32::min(child.max_main_size, child.main_size + current) - child.main_size;
child.main_size = child.main_size + delta; child.main_size = child.main_size + delta;
left_space = left_space - delta; left_space = left_space - delta;
track.main_size = track.main_size + delta;
if (child.main_size - child.max_main_size).abs() < MIN_SIZE { if (child.main_size - child.max_main_size).abs() < MIN_SIZE {
to_resize_children.remove(i); to_resize_children.remove(i);
@ -315,9 +330,10 @@ fn distribute_fill_across_space(layout_axis: &LayoutAxis, tracks: &mut Vec<Track
for child in track.shapes.iter_mut() { for child in track.shapes.iter_mut() {
if child.is_fill_across { if child.is_fill_across {
child.across_size = track let mut size =
.across_size track.across_size - child.margin_across_start - child.margin_across_end;
.clamp(child.min_across_size, child.max_across_size); size = size.clamp(child.min_across_size, child.max_across_size);
child.across_size = size;
} }
} }
} }
@ -446,7 +462,15 @@ fn calculate_track_data(
bounds: &HashMap<Uuid, Bounds>, bounds: &HashMap<Uuid, Bounds>,
) -> Vec<TrackData> { ) -> Vec<TrackData> {
let layout_axis = LayoutAxis::new(shape, layout_bounds, layout_data, flex_data); let layout_axis = LayoutAxis::new(shape, layout_bounds, layout_data, flex_data);
let mut tracks = initialize_tracks(shape, &layout_axis, layout_data, flex_data, shapes, bounds); let mut tracks = initialize_tracks(
shape,
layout_bounds,
&layout_axis,
layout_data,
flex_data,
shapes,
bounds,
);
if !layout_axis.is_auto_main { if !layout_axis.is_auto_main {
distribute_fill_main_space(&layout_axis, &mut tracks); distribute_fill_main_space(&layout_axis, &mut tracks);
@ -510,10 +534,12 @@ fn next_anchor(
prev_anchor: Point, prev_anchor: Point,
total_shapes_size: f32, total_shapes_size: f32,
) -> Point { ) -> Point {
let delta = match layout_data.justify_content { let delta = child_axis.margin_main_start
+ child_axis.margin_main_end
+ match layout_data.justify_content {
JustifyContent::SpaceBetween => { JustifyContent::SpaceBetween => {
let effective_gap = let effective_gap = (layout_axis.main_space() - total_shapes_size)
(layout_axis.main_space() - total_shapes_size) / (track.shapes.len() - 1) as f32; / (track.shapes.len() - 1) as f32;
child_axis.main_size + f32::max(layout_axis.gap_main, effective_gap) child_axis.main_size + f32::max(layout_axis.gap_main, effective_gap)
} }
JustifyContent::SpaceAround => { JustifyContent::SpaceAround => {
@ -522,8 +548,8 @@ fn next_anchor(
child_axis.main_size + f32::max(layout_axis.gap_main, effective_gap) child_axis.main_size + f32::max(layout_axis.gap_main, effective_gap)
} }
JustifyContent::SpaceEvenly => { JustifyContent::SpaceEvenly => {
let effective_gap = let effective_gap = (layout_axis.main_space() - total_shapes_size)
(layout_axis.main_space() - total_shapes_size) / (track.shapes.len() + 1) as f32; / (track.shapes.len() + 1) as f32;
child_axis.main_size + f32::max(layout_axis.gap_main, effective_gap) child_axis.main_size + f32::max(layout_axis.gap_main, effective_gap)
} }
_ => child_axis.main_size + layout_axis.gap_main, _ => child_axis.main_size + layout_axis.gap_main,
@ -539,7 +565,8 @@ fn child_position(
child_axis: &ChildAxis, child_axis: &ChildAxis,
track: &TrackData, track: &TrackData,
) -> Point { ) -> Point {
let delta = match child.layout_item { let delta = child_axis.margin_across_start
+ match child.layout_item {
Some(LayoutItem { Some(LayoutItem {
align_self: Some(align_self), align_self: Some(align_self),
.. ..
@ -554,7 +581,7 @@ fn child_position(
_ => 0.0, _ => 0.0,
}, },
}; };
shape_anchor + layout_axis.across_v * delta shape_anchor + layout_axis.main_v * child_axis.margin_main_start + layout_axis.across_v * delta
} }
pub fn reflow_flex_layout( pub fn reflow_flex_layout(
@ -562,7 +589,7 @@ pub fn reflow_flex_layout(
layout_data: &LayoutData, layout_data: &LayoutData,
flex_data: &FlexData, flex_data: &FlexData,
shapes: &HashMap<Uuid, Shape>, shapes: &HashMap<Uuid, Shape>,
bounds: &HashMap<Uuid, Bounds>, bounds: &mut HashMap<Uuid, Bounds>,
) -> VecDeque<Modifier> { ) -> VecDeque<Modifier> {
let mut result = VecDeque::new(); let mut result = VecDeque::new();
let layout_bounds = &bounds.find(&shape); let layout_bounds = &bounds.find(&shape);
@ -587,7 +614,7 @@ pub fn reflow_flex_layout(
child_axis, child_axis,
track, track,
); );
let child_bounds = bounds.find(child); let child_bounds = &child_axis.bounds;
let delta_v = Vector::new_points(&child_bounds.nw, &position); let delta_v = Vector::new_points(&child_bounds.nw, &position);
let (new_width, new_height) = if layout_data.is_row() { let (new_width, new_height) = if layout_data.is_row() {
@ -695,7 +722,9 @@ pub fn reflow_flex_layout(
scale.pre_translate(-origin); scale.pre_translate(-origin);
scale.pre_concat(&parent_transform_inv); scale.pre_concat(&parent_transform_inv);
let layout_bounds_after = layout_bounds.transform(&scale);
result.push_back(Modifier::parent(shape.id, scale)); result.push_back(Modifier::parent(shape.id, scale));
bounds.insert(shape.id, layout_bounds_after);
} }
result result
} }