From fa0da3a69558422bc836a4d8ef63ea38989cfd9d Mon Sep 17 00:00:00 2001 From: Alonso Torres Date: Mon, 17 Mar 2025 10:46:32 +0100 Subject: [PATCH] :sparkles: Flex layout modifiers wasm implementation * :sparkles: Flex layout modifiers wasm implementation * :sparkles: Flex auto modifiers propagation --- frontend/src/app/render_wasm/api.cljs | 26 +- frontend/src/app/render_wasm/shape.cljs | 1 + render-wasm/docs/serialization.md | 10 + render-wasm/src/main.rs | 17 + render-wasm/src/math.rs | 90 ++- render-wasm/src/shapes.rs | 51 +- render-wasm/src/shapes/layouts.rs | 60 +- render-wasm/src/shapes/modifiers.rs | 425 ++++++----- render-wasm/src/shapes/modifiers/common.rs | 17 + .../src/shapes/modifiers/constraints.rs | 164 ++++ .../src/shapes/modifiers/flex_layout.rs | 701 ++++++++++++++++++ .../src/shapes/modifiers/grid_layout.rs | 15 + render-wasm/src/shapes/transform.rs | 42 +- 13 files changed, 1400 insertions(+), 219 deletions(-) create mode 100644 render-wasm/src/shapes/modifiers/common.rs create mode 100644 render-wasm/src/shapes/modifiers/constraints.rs create mode 100644 render-wasm/src/shapes/modifiers/flex_layout.rs create mode 100644 render-wasm/src/shapes/modifiers/grid_layout.rs diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 65e2372b6..1656e547f 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -108,6 +108,15 @@ (aget buffer 2) (aget buffer 3)))) +(defn set-parent-id + [id] + (let [buffer (uuid/get-u32 id)] + (h/call internal-module "_set_parent" + (aget buffer 0) + (aget buffer 1) + (aget buffer 2) + (aget buffer 3)))) + (defn set-shape-clip-content [clip-content] (h/call internal-module "_set_shape_clip_content" clip-content)) @@ -614,6 +623,15 @@ :fix 1 :auto 2)) +(defn translate-align-self + [value] + (when value + (case value + :start 0 + :end 1 + :center 2 + :stretch 3))) + (defn set-layout-child [shape] (let [margins (dm/get-prop shape :layout-item-margin) @@ -624,6 +642,7 @@ h-sizing (-> (dm/get-prop shape :layout-item-h-sizing) (or :auto) translate-layout-sizing) v-sizing (-> (dm/get-prop shape :layout-item-v-sizing) (or :auto) translate-layout-sizing) + align-self (-> (dm/get-prop shape :layout-item-align-self) translate-align-self) max-h (dm/get-prop shape :layout-item-max-h) has-max-h (some? max-h) @@ -651,6 +670,8 @@ (or max-w 0) has-min-w (or min-w 0) + (some? align-self) + (or align-self 0) is-absolute z-index))) @@ -794,6 +815,7 @@ (if (< index total-shapes) (let [shape (nth shapes index) id (dm/get-prop shape :id) + parent-id (dm/get-prop shape :parent-id) type (dm/get-prop shape :type) masked (dm/get-prop shape :masked-group) selrect (dm/get-prop shape :selrect) @@ -824,6 +846,7 @@ shadows (dm/get-prop shape :shadow)] (use-shape id) + (set-parent-id parent-id) (set-shape-type type) (set-shape-clip-content clip-content) (set-shape-selrect selrect) @@ -850,7 +873,8 @@ (when (and (= type :text) (some? content)) (set-shape-text-content content)) - (when (ctl/any-layout-immediate-child? objects shape) + (when (or (ctl/any-layout? shape) + (ctl/any-layout-immediate-child? objects shape)) (set-layout-child shape)) (when (ctl/flex-layout? shape) diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs index dd16951d2..d38bdd1aa 100644 --- a/frontend/src/app/render_wasm/shape.cljs +++ b/frontend/src/app/render_wasm/shape.cljs @@ -111,6 +111,7 @@ (when ^boolean shape/*wasm-sync* (api/use-shape (:id self)) (case k + :parent-id (api/set-parent-id v) :type (api/set-shape-type v) :bool-type (api/set-shape-bool-type v) :bool-content (api/set-shape-bool-content v) diff --git a/render-wasm/docs/serialization.md b/render-wasm/docs/serialization.md index b4ea254d1..35fdab183 100644 --- a/render-wasm/docs/serialization.md +++ b/render-wasm/docs/serialization.md @@ -162,6 +162,16 @@ Shadow styles are serialized as `u8`: | 3 | Stretch | | \_ | error | +### Align self + +| Value | Field | +| ----- | ------- | +| 0 | Start | +| 1 | End | +| 2 | Center | +| 3 | Stretch | +| \_ | error | + ### Align Content | Value | Field | diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index b49ff216a..9b53011fb 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -167,6 +167,15 @@ pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) { }); } +#[no_mangle] +pub unsafe extern "C" fn set_parent(a: u32, b: u32, c: u32, d: u32) { + let state = unsafe { STATE.as_mut() }.expect("Got an invalid state pointer"); + let id = uuid_from_u32_quartet(a, b, c, d); + if let Some(shape) = state.current_shape() { + shape.set_parent(id); + } +} + #[no_mangle] pub extern "C" fn set_shape_masked_group(masked: bool) { with_current_shape!(state, |shape: &mut Shape| { @@ -697,6 +706,8 @@ pub extern "C" fn set_layout_child_data( max_w: f32, has_min_w: bool, min_w: f32, + has_align_self: bool, + align_self: u8, is_absolute: bool, z_index: i32, ) { @@ -706,6 +717,11 @@ pub extern "C" fn set_layout_child_data( let min_h = if has_min_h { Some(min_h) } else { None }; let max_w = if has_max_w { Some(max_w) } else { None }; let min_w = if has_min_w { Some(min_w) } else { None }; + let align_self = if has_align_self { + shapes::AlignSelf::from_u8(align_self) + } else { + None + }; with_current_shape!(state, |shape: &mut Shape| { shape.set_flex_layout_child_data( @@ -719,6 +735,7 @@ pub extern "C" fn set_layout_child_data( min_h, max_w, min_w, + align_self, is_absolute, z_index, ); diff --git a/render-wasm/src/math.rs b/render-wasm/src/math.rs index 4b5f30ca8..ada204936 100644 --- a/render-wasm/src/math.rs +++ b/render-wasm/src/math.rs @@ -24,7 +24,7 @@ pub struct Bounds { pub sw: Point, } -fn vec_min_max(arr: &[Option]) -> Option<(f32, f32)> { +fn vec_min_max(arr: &Vec>) -> Option<(f32, f32)> { let mut minv: Option = None; let mut maxv: Option = None; @@ -95,26 +95,30 @@ impl Bounds { self.sw = mtx.map_point(self.sw); } - pub fn box_bounds(&self, other: &Self) -> Option { + // pub fn box_bounds(&self, other: &Self) -> Option { + // self.from_points(other.points()) + // } + + pub fn from_points(&self, points: Vec) -> Option { let hv = self.horizontal_vec(); let vv = self.vertical_vec(); let hr = Ray::new(self.nw, hv); let vr = Ray::new(self.nw, vv); - let (min_ht, max_ht) = vec_min_max(&[ - intersect_rays_t(&hr, &Ray::new(other.nw, vv)), - intersect_rays_t(&hr, &Ray::new(other.ne, vv)), - intersect_rays_t(&hr, &Ray::new(other.sw, vv)), - intersect_rays_t(&hr, &Ray::new(other.se, vv)), - ])?; + let (min_ht, max_ht) = vec_min_max( + &points + .iter() + .map(|p| intersect_rays_t(&hr, &Ray::new(*p, vv))) + .collect(), + )?; - let (min_vt, max_vt) = vec_min_max(&[ - intersect_rays_t(&vr, &Ray::new(other.nw, hv)), - intersect_rays_t(&vr, &Ray::new(other.ne, hv)), - intersect_rays_t(&vr, &Ray::new(other.sw, hv)), - intersect_rays_t(&vr, &Ray::new(other.se, hv)), - ])?; + let (min_vt, max_vt) = vec_min_max( + &points + .iter() + .map(|p| intersect_rays_t(&vr, &Ray::new(*p, hv))) + .collect(), + )?; let nw = intersect_rays(&Ray::new(hr.t(min_ht), vv), &Ray::new(vr.t(min_vt), hv))?; let ne = intersect_rays(&Ray::new(hr.t(max_ht), vv), &Ray::new(vr.t(min_vt), hv))?; @@ -124,6 +128,10 @@ impl Bounds { Some(Self { nw, ne, se, sw }) } + pub fn points(&self) -> Vec { + vec![self.nw, self.ne, self.se, self.sw] + } + pub fn left(&self, p: Point) -> f32 { let hr = Ray::new(p, self.horizontal_vec()); let vr = Ray::new(self.nw, self.vertical_vec()); @@ -224,6 +232,11 @@ impl Bounds { result } + pub fn center(&self) -> Point { + // Calculates the centroid of the four points + Point::new(self.nw.x + self.se.x / 2.0, self.nw.y + self.se.y / 2.0) + } + pub fn transform_matrix(&self) -> Option { let w2 = self.width() / 2.0; let h2 = self.height() / 2.0; @@ -320,6 +333,40 @@ pub fn intersect_rays(ray1: &Ray, ray2: &Ray) -> Option { } } +/* + * Creates a resizing matrix with width/height relative to the parent + * box and keepin the same transform as the parent. + */ +pub fn resize_matrix( + parent_bounds: &Bounds, + child_bounds: &Bounds, + new_width: f32, + new_height: f32, +) -> Matrix { + let mut result = Matrix::default(); + let scale_width = new_width / child_bounds.width(); + let scale_height = new_height / child_bounds.height(); + + let center = child_bounds.center(); + let mut parent_transform = parent_bounds + .transform_matrix() + .unwrap_or(Matrix::default()); + + parent_transform.post_translate(center); + parent_transform.pre_translate(-center); + + let parent_transform_inv = &parent_transform.invert().unwrap(); + let origin = parent_transform_inv.map_point(child_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); + result.post_concat(&scale); + result +} + #[cfg(test)] mod tests { use super::*; @@ -364,12 +411,19 @@ mod tests { #[test] fn test_vec_min_max() { - assert_eq!(None, vec_min_max(&[])); - assert_eq!(None, vec_min_max(&[None, None])); - assert_eq!(Some((1.0, 1.0)), vec_min_max(&[None, Some(1.0)])); + assert_eq!(None, vec_min_max(&vec![])); + assert_eq!(None, vec_min_max(&vec![None, None])); + assert_eq!(Some((1.0, 1.0)), vec_min_max(&vec![None, Some(1.0)])); assert_eq!( Some((0.0, 1.0)), - vec_min_max(&[Some(0.3), None, Some(0.0), Some(0.7), Some(1.0), Some(0.1)]) + vec_min_max(&vec![ + Some(0.3), + None, + Some(0.0), + Some(0.7), + Some(1.0), + Some(0.1) + ]) ); } diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index fd21ccc49..cc7494500 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -153,9 +153,9 @@ impl ConstraintV { pub type Color = skia::Color; #[derive(Debug, Clone)] -#[allow(dead_code)] pub struct Shape { pub id: Uuid, + pub parent_id: Option, pub shape_type: Type, pub children: Vec, pub selrect: math::Rect, @@ -180,6 +180,7 @@ impl Shape { pub fn new(id: Uuid) -> Self { Self { id, + parent_id: None, shape_type: Type::Rect(Rect::default()), children: Vec::::new(), selrect: math::Rect::new_empty(), @@ -201,14 +202,32 @@ impl Shape { } } + pub fn set_parent(&mut self, id: Uuid) { + self.parent_id = Some(id); + } + pub fn set_shape_type(&mut self, shape_type: Type) { self.shape_type = shape_type; } + #[allow(dead_code)] pub fn is_frame(&self) -> bool { matches!(self.shape_type, Type::Frame(_)) } + pub fn is_group_like(&self) -> bool { + matches!(self.shape_type, Type::Group(_)) || matches!(self.shape_type, Type::Bool(_)) + } + + pub fn has_layout(&self) -> bool { + match self.shape_type { + Type::Frame(Frame { + layout: Some(_), .. + }) => true, + _ => false, + } + } + pub fn set_selrect(&mut self, left: f32, top: f32, right: f32, bottom: f32) { self.selrect.set_ltrb(left, top, right, bottom); match self.shape_type { @@ -276,6 +295,7 @@ impl Shape { min_h: Option, max_w: Option, min_w: Option, + align_self: Option, is_absolute: bool, z_index: i32, ) { @@ -292,6 +312,7 @@ impl Shape { min_w, is_absolute, z_index, + align_self, }); } @@ -654,6 +675,34 @@ impl Shape { _ => {} } } + + pub fn is_layout_vertical_auto(&self) -> bool { + match &self.layout_item { + Some(LayoutItem { v_sizing, .. }) => v_sizing == &Sizing::Auto, + _ => false, + } + } + + pub fn is_layout_vertical_fill(&self) -> bool { + match &self.layout_item { + Some(LayoutItem { v_sizing, .. }) => v_sizing == &Sizing::Fill, + _ => false, + } + } + + pub fn is_layout_horizontal_auto(&self) -> bool { + match &self.layout_item { + Some(LayoutItem { h_sizing, .. }) => h_sizing == &Sizing::Auto, + _ => false, + } + } + + pub fn is_layout_horizontal_fill(&self) -> bool { + match &self.layout_item { + Some(LayoutItem { h_sizing, .. }) => h_sizing == &Sizing::Fill, + _ => false, + } + } } #[cfg(test)] diff --git a/render-wasm/src/shapes/layouts.rs b/render-wasm/src/shapes/layouts.rs index d6741b0b8..3dcdf2e16 100644 --- a/render-wasm/src/shapes/layouts.rs +++ b/render-wasm/src/shapes/layouts.rs @@ -1,6 +1,5 @@ -#![allow(dead_code)] - #[derive(Debug, Clone, PartialEq)] +#[allow(dead_code)] pub enum Layout { FlexLayout(LayoutData, FlexData), GridLayout(LayoutData, GridData), @@ -137,7 +136,7 @@ impl WrapType { #[derive(Debug, Clone, PartialEq)] pub struct GridTrack {} -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Copy)] pub enum Sizing { Fill, Fix, @@ -168,6 +167,49 @@ pub struct LayoutData { pub padding_left: f32, } +impl LayoutData { + pub fn is_reverse(&self) -> bool { + match &self.direction { + Direction::RowReverse | Direction::ColumnReverse => true, + _ => false, + } + } + pub fn is_row(&self) -> bool { + match &self.direction { + Direction::RowReverse | Direction::Row => true, + _ => false, + } + } + + #[allow(dead_code)] + pub fn is_column(&self) -> bool { + match &self.direction { + Direction::ColumnReverse | Direction::Column => true, + _ => false, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum AlignSelf { + Start, + End, + Center, + Stretch, +} + +impl AlignSelf { + pub fn from_u8(value: u8) -> Option { + match value { + 0 => Some(Self::Start), + 1 => Some(Self::End), + 2 => Some(Self::Center), + 3 => Some(Self::Stretch), + _ => None, + } + } +} + #[derive(Debug, Clone, PartialEq)] pub struct FlexData { pub row_gap: f32, @@ -175,6 +217,15 @@ pub struct FlexData { pub wrap_type: WrapType, } +impl FlexData { + pub fn is_wrap(&self) -> bool { + match self.wrap_type { + WrapType::Wrap => true, + _ => false, + } + } +} + #[derive(Debug, Clone, PartialEq)] pub struct GridData { pub rows: Vec, @@ -182,7 +233,7 @@ pub struct GridData { // layout-grid-cells ;; map of id->grid-cell } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Copy)] pub struct LayoutItem { pub margin_top: f32, pub margin_right: f32, @@ -196,4 +247,5 @@ pub struct LayoutItem { pub min_w: Option, pub is_absolute: bool, pub z_index: i32, + pub align_self: Option, } diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index f11ed0214..2ca02ffaa 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -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, +fn propagate_children( shape: &Shape, + shapes: &HashMap, + parent_bounds_before: &Bounds, + parent_bounds_after: &Bounds, transform: Matrix, -) -> Vec { + bounds: &HashMap, +) -> VecDeque { 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, + bounds: &HashMap, +) -> Option { + let shape_bounds = bounds.find(&shape); + let mut result = Vec::::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) -> Vec { - let mut entries = modifiers.clone(); - let mut processed = HashSet::::new(); - let mut result = Vec::::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::::new(); + let mut bounds = HashMap::::new(); + + let mut reflown = HashSet::::new(); + let mut layout_reflows = Vec::::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::::new(), + ); assert_eq!(result.len(), 1); } + + #[test] + fn test_group_bounds() { + let mut shapes = HashMap::::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::::new()).unwrap(); + + assert_eq!(bounds.width(), 3.0); + assert_eq!(bounds.height(), 3.0); + } } diff --git a/render-wasm/src/shapes/modifiers/common.rs b/render-wasm/src/shapes/modifiers/common.rs new file mode 100644 index 000000000..9324adb90 --- /dev/null +++ b/render-wasm/src/shapes/modifiers/common.rs @@ -0,0 +1,17 @@ +use std::collections::HashMap; +use uuid::Uuid; + +use crate::math::Bounds; +use crate::shapes::Shape; + +pub trait GetBounds { + fn find(&self, shape: &Shape) -> Bounds; +} + +impl GetBounds for HashMap { + fn find(&self, shape: &Shape) -> Bounds { + self.get(&shape.id) + .map(|b| b.clone()) + .unwrap_or(shape.bounds()) + } +} diff --git a/render-wasm/src/shapes/modifiers/constraints.rs b/render-wasm/src/shapes/modifiers/constraints.rs new file mode 100644 index 000000000..ae59cf8cd --- /dev/null +++ b/render-wasm/src/shapes/modifiers/constraints.rs @@ -0,0 +1,164 @@ +use crate::math::{Bounds, Matrix}; +use crate::shapes::{ConstraintH, ConstraintV}; + +pub 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)) + } +} + +pub 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)) + } +} + +pub fn propagate_shape_constraints( + parent_bounds_before: &Bounds, + parent_bounds_after: &Bounds, + child_bounds_before: &Bounds, + constraint_h: ConstraintH, + constraint_v: ConstraintV, + transform: Matrix, +) -> Matrix { + // 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() + { + return transform; + } + + let mut transform = transform; + let mut child_bounds_after = child_bounds_before.transform(&transform); + + // 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_bounds_before.center(); + + let mut parent_transform = parent_bounds_after + .transform_matrix() + .unwrap_or(Matrix::default()); + parent_transform.post_translate(center); + parent_transform.pre_translate(-center); + + 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)); + } + + transform +} diff --git a/render-wasm/src/shapes/modifiers/flex_layout.rs b/render-wasm/src/shapes/modifiers/flex_layout.rs new file mode 100644 index 000000000..10ec4caff --- /dev/null +++ b/render-wasm/src/shapes/modifiers/flex_layout.rs @@ -0,0 +1,701 @@ +#![allow(dead_code)] +use crate::math::{self as math, Bounds, Matrix, Point, Vector, VectorExt}; +use crate::shapes::{ + AlignContent, AlignItems, AlignSelf, FlexData, JustifyContent, LayoutData, LayoutItem, + Modifier, Shape, +}; +use std::collections::{HashMap, VecDeque}; +use uuid::Uuid; + +use super::common::GetBounds; + +const MIN_SIZE: f32 = 0.01; +const MAX_SIZE: f32 = f32::INFINITY; + +#[derive(Debug)] +struct TrackData { + main_size: f32, + across_size: f32, + max_across_size: f32, + is_fill_across: bool, + shapes: Vec, + anchor: Point, +} + +impl TrackData { + fn default() -> Self { + Self { + main_size: MIN_SIZE, + across_size: MIN_SIZE, + max_across_size: MAX_SIZE, + is_fill_across: false, + shapes: Vec::new(), + anchor: Point::default(), + } + } +} + +#[derive(Debug)] +struct LayoutAxis { + main_size: f32, + across_size: f32, + main_v: Vector, + across_v: Vector, + padding_main_start: f32, + padding_main_end: f32, + padding_across_start: f32, + padding_across_end: f32, + gap_main: f32, + gap_across: f32, + is_auto_main: bool, + is_auto_across: bool, +} + +impl LayoutAxis { + fn main_space(&self) -> f32 { + self.main_size - self.padding_main_start - self.padding_main_end + } + fn across_space(&self) -> f32 { + self.across_size - self.padding_across_start - self.padding_across_end + } +} + +impl LayoutAxis { + fn new( + shape: &Shape, + layout_bounds: &Bounds, + layout_data: &LayoutData, + flex_data: &FlexData, + ) -> Self { + if layout_data.is_row() { + Self { + main_size: layout_bounds.width(), + across_size: layout_bounds.height(), + main_v: layout_bounds.hv(1.0), + across_v: layout_bounds.vv(1.0), + padding_main_start: layout_data.padding_left, + padding_main_end: layout_data.padding_right, + padding_across_start: layout_data.padding_top, + padding_across_end: layout_data.padding_bottom, + gap_main: flex_data.column_gap, + gap_across: flex_data.row_gap, + is_auto_main: shape.is_layout_horizontal_auto(), + is_auto_across: shape.is_layout_vertical_auto(), + } + } else { + Self { + main_size: layout_bounds.height(), + across_size: layout_bounds.width(), + main_v: layout_bounds.vv(1.0), + across_v: layout_bounds.hv(1.0), + padding_main_start: layout_data.padding_top, + padding_main_end: layout_data.padding_bottom, + padding_across_start: layout_data.padding_left, + padding_across_end: layout_data.padding_right, + gap_main: flex_data.row_gap, + gap_across: flex_data.column_gap, + is_auto_main: shape.is_layout_vertical_auto(), + is_auto_across: shape.is_layout_horizontal_auto(), + } + } + } +} + +#[derive(Debug, Copy, Clone)] +struct ChildAxis { + id: Uuid, + main_size: f32, + across_size: f32, + margin_main_start: f32, + margin_main_end: f32, + margin_across_start: f32, + margin_across_end: f32, + min_main_size: f32, + max_main_size: f32, + min_across_size: f32, + max_across_size: f32, + is_fill_main: bool, + is_fill_across: bool, + is_absolute: bool, + z_index: i32, +} + +impl ChildAxis { + fn new(child: &Shape, child_bounds: &Bounds, layout_data: &LayoutData) -> Self { + let id = child.id; + let layout_item = child.layout_item; + let mut result = if layout_data.is_row() { + Self { + id, + main_size: child_bounds.width(), + across_size: child_bounds.height(), + margin_main_start: layout_item.map(|i| i.margin_left).unwrap_or(0.0), + margin_main_end: layout_item.map(|i| i.margin_right).unwrap_or(0.0), + margin_across_start: layout_item.map(|i| i.margin_top).unwrap_or(0.0), + margin_across_end: layout_item.map(|i| i.margin_bottom).unwrap_or(0.0), + min_main_size: layout_item.and_then(|i| i.min_w).unwrap_or(MIN_SIZE), + max_main_size: layout_item.and_then(|i| i.max_w).unwrap_or(MAX_SIZE), + min_across_size: layout_item.and_then(|i| i.min_h).unwrap_or(MIN_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_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), + } + } else { + Self { + id, + across_size: child_bounds.width(), + main_size: child_bounds.height(), + margin_across_start: layout_item.map(|i| i.margin_left).unwrap_or(0.0), + margin_across_end: layout_item.map(|i| i.margin_right).unwrap_or(0.0), + margin_main_start: layout_item.map(|i| i.margin_top).unwrap_or(0.0), + margin_main_end: layout_item.map(|i| i.margin_bottom).unwrap_or(0.0), + min_across_size: layout_item.and_then(|i| i.min_w).unwrap_or(MIN_SIZE), + max_across_size: layout_item.and_then(|i| i.max_w).unwrap_or(MAX_SIZE), + min_main_size: layout_item.and_then(|i| i.min_h).unwrap_or(MIN_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_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), + } + }; + + if result.is_fill_main { + result.main_size = result.min_main_size; + } + if result.is_fill_across { + result.across_size = result.min_across_size; + } + result + } +} + +fn initialize_tracks( + shape: &Shape, + layout_axis: &LayoutAxis, + layout_data: &LayoutData, + flex_data: &FlexData, + shapes: &HashMap, + bounds: &HashMap, +) -> Vec { + let mut tracks = Vec::::new(); + let mut current_track = TrackData::default(); + let mut children = shape.children.clone(); + let mut first = true; + + if !layout_data.is_reverse() { + children.reverse(); + } + + for child_id in children.iter() { + let Some(child) = shapes.get(child_id) else { + continue; + }; + + let child_bounds = bounds.find(child); + let child_axis = ChildAxis::new(child, &child_bounds, layout_data); + + let child_main_size = if child_axis.is_fill_main { + child_axis.min_main_size + } else { + child_axis.main_size + }; + let child_across_size = if child_axis.is_fill_across { + child_axis.min_across_size + } else { + child_axis.across_size + }; + + let child_max_across_size = if child_axis.is_fill_across { + child_axis.max_across_size + } else { + child_axis.across_size + }; + + let gap_main = if first { 0.0 } else { layout_axis.gap_main }; + let next_main_size = current_track.main_size + child_main_size + gap_main; + + if !layout_axis.is_auto_main + && flex_data.is_wrap() + && (next_main_size > layout_axis.main_space()) + { + tracks.push(current_track); + + current_track = TrackData { + main_size: child_main_size, + across_size: child_across_size, + shapes: Vec::from([child_axis]), + is_fill_across: child_axis.is_fill_across, + anchor: Point::default(), + max_across_size: child_max_across_size, + }; + } else { + // Update current track + current_track.main_size = next_main_size; + current_track.across_size = f32::max(child_across_size, current_track.across_size); + current_track.shapes.push(child_axis); + current_track.is_fill_across = + current_track.is_fill_across || child_axis.is_fill_across; + current_track.max_across_size = + f32::max(current_track.max_across_size, child_max_across_size); + } + + first = false; + } + + // Finalize current track + tracks.push(current_track); + + tracks +} + +// Resize main axis fill +fn distribute_fill_main_space(layout_axis: &LayoutAxis, tracks: &mut Vec) { + for track in tracks.iter_mut() { + let mut left_space = layout_axis.main_space() - track.main_size; + let mut to_resize_children: Vec<&mut ChildAxis> = Vec::new(); + + for child in track.shapes.iter_mut() { + if child.is_fill_main && child.main_size < child.max_main_size { + to_resize_children.push(child); + } + } + + while left_space > MIN_SIZE && !to_resize_children.is_empty() { + let current = left_space / to_resize_children.len() as f32; + for i in (0..to_resize_children.len()).rev() { + let child = &mut to_resize_children[i]; + let delta = + f32::min(child.max_main_size, child.main_size + current) - child.main_size; + child.main_size = child.main_size + delta; + left_space = left_space - delta; + + if (child.main_size - child.max_main_size).abs() < MIN_SIZE { + to_resize_children.remove(i); + } + } + } + } +} + +fn distribute_fill_across_space(layout_axis: &LayoutAxis, tracks: &mut Vec) { + let total_across_size = tracks.iter().map(|t| t.across_size).sum::() + + (tracks.len() - 1) as f32 * layout_axis.gap_across; + let mut left_space = layout_axis.across_space() - total_across_size; + let mut to_resize_tracks: Vec<&mut TrackData> = Vec::new(); + + for track in tracks.iter_mut() { + if track.is_fill_across && track.across_size < track.max_across_size { + to_resize_tracks.push(track); + } + } + + while left_space > MIN_SIZE && !to_resize_tracks.is_empty() { + let current = left_space / to_resize_tracks.len() as f32; + for i in (0..to_resize_tracks.len()).rev() { + let track = &mut to_resize_tracks[i]; + let delta = + f32::min(track.max_across_size, track.across_size + current) - track.across_size; + track.across_size = track.across_size + delta; + left_space = left_space - delta; + + if (track.across_size - track.max_across_size).abs() < MIN_SIZE { + to_resize_tracks.remove(i); + } + } + } + + // After assigning the across size to the tracks we can assing the size to the shapes + for track in tracks.iter_mut() { + if !track.is_fill_across { + continue; + } + + for child in track.shapes.iter_mut() { + if child.is_fill_across { + child.across_size = track + .across_size + .clamp(child.min_across_size, child.max_across_size); + } + } + } +} + +fn stretch_tracks_sizes( + layout_axis: &LayoutAxis, + tracks: &mut Vec, + total_across_size: f32, +) { + let total_across_size = total_across_size + (tracks.len() - 1) as f32 * layout_axis.gap_across; + let left_space = layout_axis.across_space() - total_across_size; + let delta = left_space / tracks.len() as f32; + + for track in tracks.iter_mut() { + track.across_size = track.across_size + delta; + } +} + +fn calculate_track_positions( + layout_data: &LayoutData, + layout_axis: &LayoutAxis, + layout_bounds: &Bounds, + tracks: &mut Vec, + total_across_size: f32, +) { + let mut align_content = &layout_data.align_content; + + if layout_axis.is_auto_across { + align_content = &AlignContent::Start; + } + + 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( + shape: &Shape, + layout_data: &LayoutData, + flex_data: &FlexData, + layout_bounds: &Bounds, + shapes: &HashMap, + bounds: &HashMap, +) -> Vec { + 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); + + if !layout_axis.is_auto_main { + distribute_fill_main_space(&layout_axis, &mut tracks); + } + + if !layout_axis.is_auto_across { + distribute_fill_across_space(&layout_axis, &mut tracks); + } + + let total_across_size = tracks.iter().map(|t| t.across_size).sum::(); + + if !layout_axis.is_auto_across && layout_data.align_content == AlignContent::Stretch { + stretch_tracks_sizes(&layout_axis, &mut tracks, total_across_size); + } + + calculate_track_positions( + &layout_data, + &layout_axis, + layout_bounds, + &mut tracks, + total_across_size, + ); + tracks +} + +fn first_anchor( + layout_data: &LayoutData, + layout_axis: &LayoutAxis, + track: &TrackData, + total_shapes_size: f32, +) -> Point { + if layout_axis.is_auto_main { + return track.anchor + layout_axis.main_v * layout_axis.padding_main_start; + } + + let delta = match layout_data.justify_content { + JustifyContent::Center => (layout_axis.main_size - track.main_size) / 2.0, + JustifyContent::End => { + layout_axis.main_size - layout_axis.padding_main_end - track.main_size + } + JustifyContent::SpaceAround => { + let effective_gap = + (layout_axis.main_space() - total_shapes_size) / (track.shapes.len()) as f32; + layout_axis.padding_main_end + f32::max(layout_axis.gap_main, effective_gap / 2.0) + } + JustifyContent::SpaceEvenly => { + let effective_gap = + (layout_axis.main_space() - total_shapes_size) / (track.shapes.len() + 1) as f32; + layout_axis.padding_main_end + f32::max(layout_axis.gap_main, effective_gap) + } + _ => layout_axis.padding_main_start, + }; + track.anchor + layout_axis.main_v * delta +} + +fn next_anchor( + layout_data: &LayoutData, + layout_axis: &LayoutAxis, + child_axis: &ChildAxis, + track: &TrackData, + prev_anchor: Point, + total_shapes_size: f32, +) -> Point { + let delta = match layout_data.justify_content { + JustifyContent::SpaceBetween => { + let effective_gap = + (layout_axis.main_space() - total_shapes_size) / (track.shapes.len() - 1) as f32; + child_axis.main_size + f32::max(layout_axis.gap_main, effective_gap) + } + JustifyContent::SpaceAround => { + let effective_gap = + (layout_axis.main_space() - total_shapes_size) / (track.shapes.len()) as f32; + child_axis.main_size + f32::max(layout_axis.gap_main, effective_gap) + } + JustifyContent::SpaceEvenly => { + let effective_gap = + (layout_axis.main_space() - total_shapes_size) / (track.shapes.len() + 1) as f32; + child_axis.main_size + f32::max(layout_axis.gap_main, effective_gap) + } + _ => child_axis.main_size + layout_axis.gap_main, + }; + prev_anchor + layout_axis.main_v * delta +} + +fn child_position( + child: &Shape, + shape_anchor: Point, + layout_data: &LayoutData, + layout_axis: &LayoutAxis, + child_axis: &ChildAxis, + track: &TrackData, +) -> Point { + let delta = match child.layout_item { + Some(LayoutItem { + align_self: Some(align_self), + .. + }) => match align_self { + AlignSelf::Center => (track.across_size - child_axis.across_size) / 2.0, + AlignSelf::End => track.across_size - child_axis.across_size, + _ => 0.0, + }, + _ => match layout_data.align_items { + AlignItems::Center => (track.across_size - child_axis.across_size) / 2.0, + AlignItems::End => track.across_size - child_axis.across_size, + _ => 0.0, + }, + }; + shape_anchor + layout_axis.across_v * delta +} + +pub fn reflow_flex_layout( + shape: &Shape, + layout_data: &LayoutData, + flex_data: &FlexData, + shapes: &HashMap, + bounds: &HashMap, +) -> VecDeque { + let mut result = VecDeque::new(); + let layout_bounds = &bounds.find(&shape); + let layout_axis = LayoutAxis::new(shape, layout_bounds, layout_data, flex_data); + let tracks = calculate_track_data(shape, layout_data, flex_data, layout_bounds, shapes, bounds); + + for track in tracks.iter() { + let total_shapes_size = track.shapes.iter().map(|s| s.main_size).sum::(); + let mut shape_anchor = first_anchor(&layout_data, &layout_axis, track, total_shapes_size); + + for child_axis in track.shapes.iter() { + let child_id = child_axis.id; + let Some(child) = shapes.get(&child_id) else { + continue; + }; + + let position = child_position( + child, + shape_anchor, + layout_data, + &layout_axis, + child_axis, + track, + ); + let child_bounds = bounds.find(child); + let delta_v = Vector::new_points(&child_bounds.nw, &position); + + let (new_width, new_height) = if layout_data.is_row() { + (child_axis.main_size, child_axis.across_size) + } else { + (child_axis.across_size, child_axis.main_size) + }; + + 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, + )); + } + + if delta_v.x.abs() > MIN_SIZE || delta_v.y.abs() > MIN_SIZE { + transform.post_concat(&Matrix::translate(delta_v)); + } + + result.push_back(Modifier::transform(child.id, transform)); + + shape_anchor = next_anchor( + &layout_data, + &layout_axis, + &child_axis, + &track, + shape_anchor, + total_shapes_size, + ); + } + } + + if layout_axis.is_auto_across || layout_axis.is_auto_main { + let width = layout_bounds.width(); + let height = layout_bounds.height(); + + let auto_across_size = if layout_axis.is_auto_across { + tracks.iter().map(|track| track.across_size).sum::() + + (tracks.len() - 1) as f32 * layout_axis.gap_across + + layout_axis.padding_main_start + + layout_axis.padding_main_end + } else { + 0.0 + }; + + let auto_main_size = if layout_axis.is_auto_main { + tracks + .iter() + .map(|track| { + track.shapes.iter().map(|s| s.main_size).sum::() + + (track.shapes.len() - 1) as f32 * layout_axis.gap_main + }) + .reduce(f32::max) + .unwrap_or(0.01) + + layout_axis.padding_across_start + + layout_axis.padding_across_end + } else { + 0.0 + }; + + let (scale_width, scale_height) = if layout_data.is_row() { + ( + if layout_axis.is_auto_main { + auto_main_size / width + } else { + 1.0 + }, + if layout_axis.is_auto_across { + auto_across_size / height + } else { + 1.0 + }, + ) + } else { + ( + if layout_axis.is_auto_across { + auto_across_size / width + } else { + 1.0 + }, + if layout_axis.is_auto_main { + auto_main_size / height + } else { + 1.0 + }, + ) + }; + + 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); + + result.push_back(Modifier::parent(shape.id, scale)); + } + result +} diff --git a/render-wasm/src/shapes/modifiers/grid_layout.rs b/render-wasm/src/shapes/modifiers/grid_layout.rs new file mode 100644 index 000000000..26714cbb0 --- /dev/null +++ b/render-wasm/src/shapes/modifiers/grid_layout.rs @@ -0,0 +1,15 @@ +use crate::math::Bounds; +use crate::shapes::{GridData, LayoutData, Modifier, Shape}; +use std::collections::{HashMap, VecDeque}; +use uuid::Uuid; + +pub fn reflow_grid_layout( + _shape: &Shape, + _layout_data: &LayoutData, + _grid_data: &GridData, + _shapes: &HashMap, + _bounds: &HashMap, +) -> VecDeque { + // TODO + VecDeque::new() +} diff --git a/render-wasm/src/shapes/transform.rs b/render-wasm/src/shapes/transform.rs index 9808fdf96..bbfd8f77d 100644 --- a/render-wasm/src/shapes/transform.rs +++ b/render-wasm/src/shapes/transform.rs @@ -5,16 +5,46 @@ use crate::mem::SerializableResult; use crate::utils::{uuid_from_u32_quartet, uuid_to_u32_quartet}; use skia::Matrix; +#[derive(PartialEq, Debug, Clone)] +pub enum Modifier { + Transform(TransformEntry), + Reflow(Uuid), +} + +impl Modifier { + pub fn transform(id: Uuid, transform: Matrix) -> Self { + Modifier::Transform(TransformEntry::new(id, transform)) + } + pub fn parent(id: Uuid, transform: Matrix) -> Self { + Modifier::Transform(TransformEntry::parent(id, transform)) + } + pub fn reflow(id: Uuid) -> Self { + Modifier::Reflow(id) + } +} + #[derive(PartialEq, Debug, Clone)] #[repr(C)] pub struct TransformEntry { pub id: Uuid, pub transform: Matrix, + pub propagate: bool, } impl TransformEntry { pub fn new(id: Uuid, transform: Matrix) -> Self { - TransformEntry { id, transform } + TransformEntry { + id, + transform, + propagate: true, + } + } + pub fn parent(id: Uuid, transform: Matrix) -> Self { + TransformEntry { + id, + transform, + propagate: false, + } } } @@ -40,7 +70,7 @@ impl SerializableResult for TransformEntry { 0.0, 1.0, ); - TransformEntry { id, transform } + TransformEntry::new(id, transform) } fn as_bytes(&self) -> Self::BytesType { @@ -75,10 +105,10 @@ mod tests { #[test] fn test_serialization() { - let entry = TransformEntry { - id: uuid!("550e8400-e29b-41d4-a716-446655440000"), - transform: Matrix::new_all(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 0.0, 0.0, 1.0), - }; + let entry = TransformEntry::new( + uuid!("550e8400-e29b-41d4-a716-446655440000"), + Matrix::new_all(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 0.0, 0.0, 1.0), + ); let bytes = entry.as_bytes();