mirror of
https://github.com/penpot/penpot.git
synced 2025-07-16 19:15:16 +02:00
✨ Flex layout modifiers wasm implementation
* ✨ Flex layout modifiers wasm implementation * ✨ Flex auto modifiers propagation
This commit is contained in:
parent
fa9d8a9b15
commit
fa0da3a695
13 changed files with 1400 additions and 219 deletions
|
@ -1,220 +1,228 @@
|
|||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
|
||||
use skia::Matrix;
|
||||
use skia_safe as skia;
|
||||
mod common;
|
||||
mod constraints;
|
||||
mod flex_layout;
|
||||
mod grid_layout;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::math::Bounds;
|
||||
use crate::shapes::{ConstraintH, ConstraintV, Shape, TransformEntry};
|
||||
use common::GetBounds;
|
||||
|
||||
use crate::math::{Bounds, Matrix, Point};
|
||||
use crate::shapes::{
|
||||
ConstraintH, ConstraintV, Frame, Group, Layout, Modifier, Shape, TransformEntry, Type,
|
||||
};
|
||||
use crate::state::State;
|
||||
|
||||
fn calculate_resize(
|
||||
constraint_h: ConstraintH,
|
||||
constraint_v: ConstraintV,
|
||||
parent_before: &Bounds,
|
||||
parent_after: &Bounds,
|
||||
child_before: &Bounds,
|
||||
child_after: &Bounds,
|
||||
) -> Option<(f32, f32)> {
|
||||
let scale_width = match constraint_h {
|
||||
ConstraintH::Left | ConstraintH::Right | ConstraintH::Center => {
|
||||
parent_before.width() / parent_after.width()
|
||||
}
|
||||
ConstraintH::LeftRight => {
|
||||
let left = parent_before.left(child_before.nw);
|
||||
let right = parent_before.right(child_before.ne);
|
||||
let target_width = parent_after.width() - left - right;
|
||||
target_width / child_after.width()
|
||||
}
|
||||
_ => 1.0,
|
||||
};
|
||||
|
||||
let scale_height = match constraint_v {
|
||||
ConstraintV::Top | ConstraintV::Bottom | ConstraintV::Center => {
|
||||
parent_before.height() / parent_after.height()
|
||||
}
|
||||
ConstraintV::TopBottom => {
|
||||
let top = parent_before.top(child_before.nw);
|
||||
let bottom = parent_before.bottom(child_before.sw);
|
||||
let target_height = parent_after.height() - top - bottom;
|
||||
target_height / child_after.height()
|
||||
}
|
||||
_ => 1.0,
|
||||
};
|
||||
|
||||
if (scale_width - 1.0).abs() < f32::EPSILON && (scale_height - 1.0).abs() < f32::EPSILON {
|
||||
None
|
||||
} else {
|
||||
Some((scale_width, scale_height))
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_displacement(
|
||||
constraint_h: ConstraintH,
|
||||
constraint_v: ConstraintV,
|
||||
parent_before: &Bounds,
|
||||
parent_after: &Bounds,
|
||||
child_before: &Bounds,
|
||||
child_after: &Bounds,
|
||||
) -> Option<(f32, f32)> {
|
||||
let delta_x = match constraint_h {
|
||||
ConstraintH::Left | ConstraintH::LeftRight => {
|
||||
let target_left = parent_before.left(child_before.nw);
|
||||
let current_left = parent_after.left(child_after.nw);
|
||||
target_left - current_left
|
||||
}
|
||||
ConstraintH::Right => {
|
||||
let target_right = parent_before.right(child_before.ne);
|
||||
let current_right = parent_after.right(child_after.ne);
|
||||
current_right - target_right
|
||||
}
|
||||
ConstraintH::Center => {
|
||||
let delta_width = parent_after.width() - parent_before.width();
|
||||
let target_left = parent_before.left(child_before.nw);
|
||||
let current_left = parent_after.left(child_after.nw);
|
||||
target_left - current_left + delta_width / 2.0
|
||||
}
|
||||
_ => 0.0,
|
||||
};
|
||||
|
||||
let delta_y = match constraint_v {
|
||||
ConstraintV::Top | ConstraintV::TopBottom => {
|
||||
let target_top = parent_before.top(child_before.nw);
|
||||
let current_top = parent_after.top(child_after.nw);
|
||||
target_top - current_top
|
||||
}
|
||||
ConstraintV::Bottom => {
|
||||
let target_bottom = parent_before.bottom(child_before.ne);
|
||||
let current_bottom = parent_after.bottom(child_after.ne);
|
||||
current_bottom - target_bottom
|
||||
}
|
||||
ConstraintV::Center => {
|
||||
let delta_height = parent_after.height() - parent_before.height();
|
||||
let target_top = parent_before.top(child_before.nw);
|
||||
let current_top = parent_after.top(child_after.nw);
|
||||
target_top - current_top + delta_height / 2.0
|
||||
}
|
||||
_ => 0.0,
|
||||
};
|
||||
|
||||
if delta_x.abs() < f32::EPSILON && delta_y.abs() < f32::EPSILON {
|
||||
None
|
||||
} else {
|
||||
Some((delta_x, delta_y))
|
||||
}
|
||||
}
|
||||
|
||||
fn propagate_shape(
|
||||
shapes: &HashMap<Uuid, Shape>,
|
||||
fn propagate_children(
|
||||
shape: &Shape,
|
||||
shapes: &HashMap<Uuid, Shape>,
|
||||
parent_bounds_before: &Bounds,
|
||||
parent_bounds_after: &Bounds,
|
||||
transform: Matrix,
|
||||
) -> Vec<TransformEntry> {
|
||||
bounds: &HashMap<Uuid, Bounds>,
|
||||
) -> VecDeque<Modifier> {
|
||||
if shape.children.len() == 0 {
|
||||
return vec![];
|
||||
return VecDeque::new();
|
||||
}
|
||||
|
||||
let parent_bounds_before = shape.bounds();
|
||||
let parent_bounds_after = parent_bounds_before.transform(&transform);
|
||||
let mut result = Vec::new();
|
||||
let mut result = VecDeque::new();
|
||||
|
||||
for child_id in shape.children.iter() {
|
||||
if let Some(child) = shapes.get(child_id) {
|
||||
let constraint_h = child.constraint_h(if shape.is_frame() {
|
||||
ConstraintH::Left
|
||||
} else {
|
||||
ConstraintH::Scale
|
||||
});
|
||||
let Some(child) = shapes.get(child_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let constraint_v = child.constraint_v(if shape.is_frame() {
|
||||
ConstraintV::Top
|
||||
} else {
|
||||
ConstraintV::Scale
|
||||
});
|
||||
// if the constrains are scale & scale or the transform has only moves we
|
||||
// can propagate as is
|
||||
if (constraint_h == ConstraintH::Scale && constraint_v == ConstraintV::Scale)
|
||||
|| transform.is_translate()
|
||||
{
|
||||
result.push(TransformEntry::new(child_id.clone(), transform));
|
||||
continue;
|
||||
}
|
||||
let child_bounds = bounds.find(child);
|
||||
|
||||
if let Some(child_bounds_before) = parent_bounds_before.box_bounds(&child.bounds()) {
|
||||
let mut transform = transform;
|
||||
let mut child_bounds_after = child_bounds_before.transform(&transform);
|
||||
let constraint_h = match &shape.shape_type {
|
||||
Type::Frame(Frame {
|
||||
layout: Some(_), ..
|
||||
}) => ConstraintH::Left,
|
||||
Type::Frame(_) => child.constraint_h(ConstraintH::Left),
|
||||
_ => child.constraint_h(ConstraintH::Scale),
|
||||
};
|
||||
|
||||
// Scale shape
|
||||
if let Some((scale_width, scale_height)) = calculate_resize(
|
||||
constraint_h,
|
||||
constraint_v,
|
||||
&parent_bounds_before,
|
||||
&parent_bounds_after,
|
||||
&child_bounds_before,
|
||||
&child_bounds_after,
|
||||
) {
|
||||
let center = child.center();
|
||||
let constraint_v = match &shape.shape_type {
|
||||
Type::Frame(Frame {
|
||||
layout: Some(_), ..
|
||||
}) => ConstraintV::Top,
|
||||
Type::Frame(_) => child.constraint_v(ConstraintV::Top),
|
||||
_ => child.constraint_v(ConstraintV::Scale),
|
||||
};
|
||||
|
||||
let mut parent_transform = parent_bounds_after
|
||||
.transform_matrix()
|
||||
.unwrap_or(Matrix::default());
|
||||
parent_transform.post_translate(center);
|
||||
parent_transform.pre_translate(-center);
|
||||
let transform = constraints::propagate_shape_constraints(
|
||||
&parent_bounds_before,
|
||||
&parent_bounds_after,
|
||||
&child_bounds,
|
||||
constraint_h,
|
||||
constraint_v,
|
||||
transform,
|
||||
);
|
||||
|
||||
let parent_transform_inv = &parent_transform.invert().unwrap();
|
||||
let origin = parent_transform_inv.map_point(child_bounds_after.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);
|
||||
|
||||
child_bounds_after.transform_mut(&scale);
|
||||
transform.post_concat(&scale);
|
||||
}
|
||||
|
||||
// Translate position
|
||||
if let Some((delta_x, delta_y)) = calculate_displacement(
|
||||
constraint_h,
|
||||
constraint_v,
|
||||
&parent_bounds_before,
|
||||
&parent_bounds_after,
|
||||
&child_bounds_before,
|
||||
&child_bounds_after,
|
||||
) {
|
||||
let th = parent_bounds_after.hv(delta_x);
|
||||
let tv = parent_bounds_after.vv(delta_y);
|
||||
transform.post_concat(&Matrix::translate(th + tv));
|
||||
}
|
||||
|
||||
result.push(TransformEntry::new(child_id.clone(), transform));
|
||||
}
|
||||
}
|
||||
result.push_back(Modifier::transform(*child_id, transform));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn calculate_group_bounds(
|
||||
shape: &Shape,
|
||||
shapes: &HashMap<Uuid, Shape>,
|
||||
bounds: &HashMap<Uuid, Bounds>,
|
||||
) -> Option<Bounds> {
|
||||
let shape_bounds = bounds.find(&shape);
|
||||
let mut result = Vec::<Point>::new();
|
||||
for child_id in shape.children.iter() {
|
||||
let Some(child) = shapes.get(child_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let child_bounds = bounds.find(child);
|
||||
result.append(&mut child_bounds.points());
|
||||
}
|
||||
|
||||
shape_bounds.from_points(result)
|
||||
}
|
||||
|
||||
pub fn propagate_modifiers(state: &State, modifiers: Vec<TransformEntry>) -> Vec<TransformEntry> {
|
||||
let mut entries = modifiers.clone();
|
||||
let mut processed = HashSet::<Uuid>::new();
|
||||
let mut result = Vec::<TransformEntry>::new();
|
||||
let shapes = &state.shapes;
|
||||
|
||||
// Propagate the transform to children
|
||||
while let Some(entry) = entries.pop() {
|
||||
if !processed.contains(&entry.id) {
|
||||
if let Some(shape) = state.shapes.get(&entry.id) {
|
||||
let mut children = propagate_shape(&state.shapes, shape, entry.transform);
|
||||
entries.append(&mut children);
|
||||
processed.insert(entry.id);
|
||||
result.push(entry.clone());
|
||||
let mut entries: VecDeque<_> = modifiers
|
||||
.iter()
|
||||
.map(|entry| Modifier::Transform(entry.clone()))
|
||||
.collect();
|
||||
let mut modifiers = HashMap::<Uuid, Matrix>::new();
|
||||
let mut bounds = HashMap::<Uuid, Bounds>::new();
|
||||
|
||||
let mut reflown = HashSet::<Uuid>::new();
|
||||
let mut layout_reflows = Vec::<Uuid>::new();
|
||||
|
||||
// We first propagate the transforms to the children and then after
|
||||
// recalculate the layouts. The layout can create further transforms that
|
||||
// we need to re-propagate.
|
||||
// In order for loop to eventualy finish, we limit the flex reflow to just
|
||||
// one (the reflown set).
|
||||
while !entries.is_empty() {
|
||||
while let Some(modifier) = entries.pop_front() {
|
||||
match modifier {
|
||||
Modifier::Transform(entry) => {
|
||||
// println!("Transform {}", entry.id);
|
||||
let Some(shape) = state.shapes.get(&entry.id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let shape_bounds_before = bounds.find(&shape);
|
||||
let shape_bounds_after = shape_bounds_before.transform(&entry.transform);
|
||||
|
||||
if entry.propagate {
|
||||
let mut children = propagate_children(
|
||||
shape,
|
||||
shapes,
|
||||
&shape_bounds_before,
|
||||
&shape_bounds_after,
|
||||
entry.transform,
|
||||
&bounds,
|
||||
);
|
||||
|
||||
entries.append(&mut children);
|
||||
}
|
||||
|
||||
bounds.insert(shape.id, shape_bounds_after);
|
||||
|
||||
let default_matrix = Matrix::default();
|
||||
let mut shape_modif =
|
||||
modifiers.get(&shape.id).unwrap_or(&default_matrix).clone();
|
||||
shape_modif.post_concat(&entry.transform);
|
||||
modifiers.insert(shape.id, shape_modif);
|
||||
|
||||
if let Some(parent) = shape.parent_id.and_then(|id| shapes.get(&id)) {
|
||||
if parent.has_layout() || parent.is_group_like() {
|
||||
entries.push_back(Modifier::reflow(parent.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Modifier::Reflow(id) => {
|
||||
let Some(shape) = state.shapes.get(&id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match &shape.shape_type {
|
||||
Type::Frame(Frame {
|
||||
layout: Some(_), ..
|
||||
}) => {
|
||||
if !reflown.contains(&id) {
|
||||
layout_reflows.push(id);
|
||||
reflown.insert(id);
|
||||
}
|
||||
}
|
||||
Type::Group(Group { masked: true }) => {
|
||||
if let Some(child) = shapes.get(&shape.children[0]) {
|
||||
let child_bounds = bounds.find(&child);
|
||||
bounds.insert(shape.id, child_bounds);
|
||||
}
|
||||
}
|
||||
Type::Group(_) => {
|
||||
if let Some(shape_bounds) =
|
||||
calculate_group_bounds(shape, shapes, &bounds)
|
||||
{
|
||||
bounds.insert(shape.id, shape_bounds);
|
||||
}
|
||||
}
|
||||
Type::Bool(_) => {
|
||||
// TODO: How to calculate from rust the new box? we need to calculate the
|
||||
// new path... impossible right now. I'm going to use for the moment the group
|
||||
// calculation
|
||||
if let Some(shape_bounds) =
|
||||
calculate_group_bounds(shape, shapes, &bounds)
|
||||
{
|
||||
bounds.insert(shape.id, shape_bounds);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Other shapes don't have to be reflown
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(parent) = shape.parent_id.and_then(|id| shapes.get(&id)) {
|
||||
if parent.has_layout() || parent.is_group_like() {
|
||||
entries.push_back(Modifier::reflow(parent.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for id in layout_reflows.iter() {
|
||||
let Some(shape) = state.shapes.get(&id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Type::Frame(frame_data) = &shape.shape_type else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(Layout::FlexLayout(layout_data, flex_data)) = &frame_data.layout {
|
||||
let mut children =
|
||||
flex_layout::reflow_flex_layout(shape, layout_data, flex_data, shapes, &bounds);
|
||||
entries.append(&mut children);
|
||||
}
|
||||
|
||||
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);
|
||||
entries.append(&mut children);
|
||||
}
|
||||
}
|
||||
layout_reflows = Vec::new();
|
||||
}
|
||||
|
||||
result
|
||||
modifiers
|
||||
.iter()
|
||||
.map(|(key, val)| TransformEntry::new(*key, *val))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -246,8 +254,47 @@ mod tests {
|
|||
transform.post_translate(Point::new(x, y));
|
||||
transform.pre_translate(Point::new(-x, -y));
|
||||
|
||||
let result = propagate_shape(&shapes, &parent, transform);
|
||||
let bounds_before = parent.bounds();
|
||||
let bounds_after = bounds_before.transform(&transform);
|
||||
|
||||
let result = propagate_children(
|
||||
&parent,
|
||||
&shapes,
|
||||
&bounds_before,
|
||||
&bounds_after,
|
||||
transform,
|
||||
&HashMap::<Uuid, Bounds>::new(),
|
||||
);
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_group_bounds() {
|
||||
let mut shapes = HashMap::<Uuid, Shape>::new();
|
||||
|
||||
let child1_id = Uuid::new_v4();
|
||||
let mut child1 = Shape::new(child1_id);
|
||||
child1.set_selrect(3.0, 3.0, 2.0, 2.0);
|
||||
shapes.insert(child1_id, child1);
|
||||
|
||||
let child2_id = Uuid::new_v4();
|
||||
let mut child2 = Shape::new(child2_id);
|
||||
child2.set_selrect(0.0, 0.0, 1.0, 1.0);
|
||||
shapes.insert(child2_id, child2);
|
||||
|
||||
let parent_id = Uuid::new_v4();
|
||||
let mut parent = Shape::new(parent_id);
|
||||
parent.set_shape_type(Type::Group(Group::default()));
|
||||
parent.add_child(child1_id);
|
||||
parent.add_child(child2_id);
|
||||
parent.set_selrect(0.0, 0.0, 3.0, 3.0);
|
||||
shapes.insert(parent_id, parent.clone());
|
||||
|
||||
let bounds =
|
||||
calculate_group_bounds(&parent, &shapes, HashMap::<Uuid, Bounds>::new()).unwrap();
|
||||
|
||||
assert_eq!(bounds.width(), 3.0);
|
||||
assert_eq!(bounds.height(), 3.0);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue