use std::collections::{HashMap, HashSet, VecDeque}; mod constraints; mod flex_layout; pub mod common; pub mod grid_layout; use crate::math::{self as math, bools, identitish, is_close_to, Bounds, Matrix, Point}; use common::GetBounds; use crate::shapes::{ ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, Shape, TransformEntry, TransformEntrySource, Type, }; use crate::state::{ShapesPoolRef, State}; use crate::uuid::Uuid; #[allow(clippy::too_many_arguments)] fn propagate_children( shape: &Shape, shapes: ShapesPoolRef, parent_bounds_before: &Bounds, parent_bounds_after: &Bounds, transform: Matrix, bounds: &HashMap, ) -> VecDeque { if identitish(&transform) { return VecDeque::new(); } let mut result = VecDeque::new(); for child_id in shape.children_ids_iter(true) { let Some(child) = shapes.get(child_id) else { continue; }; let child_bounds = bounds.find(child); let constraint_h = match &shape.shape_type { Type::Frame(Frame { layout: Some(_), .. }) => { if child.is_absolute() { child.constraint_h(ConstraintH::Left) } else { ConstraintH::Left } } Type::Frame(_) => child.constraint_h(ConstraintH::Left), _ => child.constraint_h(ConstraintH::Scale), }; let constraint_v = match &shape.shape_type { Type::Frame(Frame { layout: Some(_), .. }) => { if child.is_absolute() { child.constraint_v(ConstraintV::Top) } else { ConstraintV::Top } } Type::Frame(_) => child.constraint_v(ConstraintV::Top), _ => child.constraint_v(ConstraintV::Scale), }; let transform = constraints::propagate_shape_constraints( parent_bounds_before, parent_bounds_after, &child_bounds, constraint_h, constraint_v, transform, child.ignore_constraints, ); result.push_back(Modifier::transform_propagate(*child_id, transform)); } result } fn calculate_group_bounds( shape: &Shape, shapes: ShapesPoolRef, bounds: &HashMap, ) -> Option { let shape_bounds = bounds.find(shape); let mut result = Vec::::new(); for child_id in shape.children_ids_iter(true) { let Some(child) = shapes.get(child_id) else { continue; }; let child_bounds = bounds.find(child); result.append(&mut child_bounds.points()); } shape_bounds.with_points(result) } fn calculate_bool_bounds( shape: &Shape, shapes: ShapesPoolRef, bounds: &HashMap, modifiers: &HashMap, ) -> Option { let shape_bounds = bounds.find(shape); let children_ids = shape.children_ids(true); let Type::Bool(bool_data) = &shape.shape_type else { return Some(shape_bounds); }; let mut subtree = shapes.subtree(&shape.id); subtree.set_modifiers(modifiers.clone()); let path = bools::bool_from_shapes(bool_data.bool_type, &children_ids, &subtree); let result = path.bounds(); Some(result) } fn set_pixel_precision(transform: &mut Matrix, bounds: &mut Bounds) { let tr = bounds.transform_matrix().unwrap_or_default(); let tr_inv = tr.invert().unwrap_or_default(); let x = bounds.min_x().round(); let y = bounds.min_y().round(); let width = bounds.width(); let height = bounds.height(); let scale_width = if width > 0.1 { f32::max(0.01, bounds.width().round() / bounds.width()) } else { 1.0 }; let scale_height = if height > 0.1 { f32::max(0.01, bounds.height().round() / bounds.height()) } else { 1.0 }; if f32::is_finite(scale_width) && f32::is_finite(scale_height) && (!math::is_close_to(scale_width, 1.0) || !math::is_close_to(scale_height, 1.0)) { let mut round_transform = Matrix::scale((scale_width, scale_height)); round_transform.post_concat(&tr); round_transform.pre_concat(&tr_inv); transform.post_concat(&round_transform); bounds.transform_mut(&round_transform); } let dx = x - bounds.min_x(); let dy = y - bounds.min_y(); if f32::is_finite(dx) && f32::is_finite(dy) { let round_transform = Matrix::translate((dx, dy)); transform.post_concat(&round_transform); bounds.transform_mut(&round_transform); } } fn propagate_transform( entry: TransformEntry, pixel_precision: bool, state: &State, entries: &mut VecDeque, bounds: &mut HashMap, modifiers: &mut HashMap, ) { let Some(shape) = state.shapes.get(&entry.id) else { return; }; let shapes = &state.shapes; let shape_bounds_before = bounds.find(shape); let mut shape_bounds_after = shape_bounds_before.transform(&entry.transform); let mut transform = entry.transform; // Only check the text layout when the width/height changes if !is_close_to(shape_bounds_before.width(), shape_bounds_after.width()) || !is_close_to(shape_bounds_before.height(), shape_bounds_after.height()) { if let Type::Text(text_content) = &mut shape.shape_type.clone() { let resized_selrect = math::Rect::from_xywh( shape.selrect.left(), shape.selrect.top(), shape_bounds_after.width(), shape_bounds_after.height(), ); match text_content.grow_type() { GrowType::AutoHeight => { // For auto-height, always update layout when width changes // because the new width affects how text wraps let width_changed = !is_close_to(shape_bounds_before.width(), shape_bounds_after.width()); if width_changed || text_content.needs_update_layout() { text_content.update_layout(resized_selrect); } let height = text_content.size.height; let resize_transform = math::resize_matrix( &shape_bounds_after, &shape_bounds_after, shape_bounds_after.width(), height, ); shape_bounds_after = shape_bounds_after.transform(&resize_transform); transform.post_concat(&resize_transform); } GrowType::AutoWidth => { // For auto-width, always update layout when height changes // because the new height affects how text flows let height_changed = !is_close_to(shape_bounds_before.height(), shape_bounds_after.height()); if height_changed || text_content.needs_update_layout() { text_content.update_layout(resized_selrect); } let width = text_content.width(); let height = text_content.size.height; let resize_transform = math::resize_matrix( &shape_bounds_after, &shape_bounds_after, width, height, ); shape_bounds_after = shape_bounds_after.transform(&resize_transform); transform.post_concat(&resize_transform); } GrowType::Fixed => {} } } } if pixel_precision { set_pixel_precision(&mut transform, &mut shape_bounds_after); } if entry.propagate { let mut children = propagate_children( shape, shapes, &shape_bounds_before, &shape_bounds_after, transform, bounds, ); entries.append(&mut children); } bounds.insert(shape.id, shape_bounds_after); let mut shape_modif = modifiers.get(&shape.id).copied().unwrap_or_default(); shape_modif.post_concat(&transform); modifiers.insert(shape.id, shape_modif); let is_resize = !math::is_move_only_matrix(&transform); let is_propagate = entry.source == TransformEntrySource::Propagate; // If this is a layout and we're only moving don't need to reflow if shape.has_layout() && is_resize { entries.push_back(Modifier::reflow(shape.id, false)); } if let Some(parent) = shape.parent_id.and_then(|id| shapes.get(&id)) { // When the parent is either a group or a layout we only mark for reflow // if the current transformation is not a move propagation. // If it's a move propagation we don't need to reflow, the parent is already changed. if (parent.has_layout() || parent.is_group_like()) && (is_resize || !is_propagate) { entries.push_back(Modifier::reflow(parent.id, false)); } } } fn propagate_reflow( id: &Uuid, state: &State, entries: &mut VecDeque, bounds: &mut HashMap, layout_reflows: &mut HashSet, reflown: &mut HashSet, modifiers: &HashMap, ) { let Some(shape) = state.shapes.get(id) else { return; }; let shapes = &state.shapes; if reflown.contains(id) { return; } match &shape.shape_type { Type::Frame(Frame { layout: Some(_), .. }) => { 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 parent_id != Uuid::nil() && !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.insert(*id); } } Type::Group(Group { masked: true }) => { let children_ids = shape.children_ids(true); if let Some(child) = shapes.get(&children_ids[0]) { let child_bounds = bounds.find(child); bounds.insert(shape.id, child_bounds); } reflown.insert(*id); } Type::Group(_) => { if let Some(shape_bounds) = calculate_group_bounds(shape, shapes, bounds) { bounds.insert(shape.id, shape_bounds); } reflown.insert(*id); } Type::Bool(_) => { if let Some(shape_bounds) = calculate_bool_bounds(shape, shapes, bounds, modifiers) { bounds.insert(shape.id, shape_bounds); } reflown.insert(*id); } _ => {} } 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, false)); } } } fn reflow_shape( id: &Uuid, state: &State, reflown: &mut HashSet, entries: &mut VecDeque, bounds: &mut HashMap, ) { let Some(shape) = state.shapes.get(id) else { return; }; let shapes = &state.shapes; let Type::Frame(frame_data) = &shape.shape_type else { return; }; 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); } else 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); } reflown.insert(*id); } pub fn propagate_modifiers( state: &State, modifiers: &[TransformEntry], pixel_precision: bool, ) -> Vec { let mut entries: VecDeque<_> = modifiers .iter() .map(|entry| { // If we receive a identity matrix we force a reflow if math::identitish(&entry.transform) { Modifier::Reflow(entry.id, false) } else { Modifier::Transform(*entry) } }) .collect(); let shapes = &state.shapes; let mut modifiers = HashMap::::new(); let mut bounds = HashMap::::new(); let mut reflown = HashSet::::new(); let mut layout_reflows = HashSet::::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) => propagate_transform( entry, pixel_precision, state, &mut entries, &mut bounds, &mut modifiers, ), Modifier::Reflow(id, force_reflow) => { if force_reflow { reflown.remove(&id); } propagate_reflow( &id, state, &mut entries, &mut bounds, &mut layout_reflows, &mut reflown, &modifiers, ) } } } // We sort the reflows so they are processed deepest-first in the // tree structure. This way we can be sure that the children layouts // are already reflowed before their parents. let mut layout_reflows_vec: Vec = std::mem::take(&mut layout_reflows).into_iter().collect(); layout_reflows_vec.sort_unstable_by(|id_a, id_b| { let da = shapes.get_depth(id_a); let db = shapes.get_depth(id_b); db.cmp(&da) }); // This temporary bounds is necesary so the layouts can be calculated // correctly but will be discarded before the next iteration for the // bounds to be calculated properly with the modifiers. let mut bounds_temp = bounds.clone(); for id in &layout_reflows_vec { if reflown.contains(id) { continue; } reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds_temp); } layout_reflows = HashSet::new(); } #[allow(dead_code)] modifiers .iter() .map(|(key, val)| TransformEntry::from_input(*key, *val)) .collect() } #[cfg(test)] mod tests { use super::*; use crate::math::{Matrix, Point}; use crate::shapes::*; use crate::state::ShapesPool; #[test] fn test_propagate_shape() { let parent_id = Uuid::new_v4(); let shapes = { let mut shapes = ShapesPool::new(); shapes.initialize(10); let child_id = Uuid::new_v4(); let child = shapes.add_shape(child_id); child.set_selrect(3.0, 3.0, 2.0, 2.0); let parent = shapes.add_shape(parent_id); parent.set_shape_type(Type::Group(Group::default())); parent.add_child(child_id); parent.set_selrect(1.0, 1.0, 5.0, 5.0); shapes }; let parent = shapes.get(&parent_id).unwrap(); let mut transform = Matrix::scale((2.0, 1.5)); let x = parent.selrect.x(); let y = parent.selrect.y(); transform.post_translate(Point::new(x, y)); transform.pre_translate(Point::new(-x, -y)); 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 parent_id = Uuid::new_v4(); let shapes = { let mut shapes = ShapesPool::new(); shapes.initialize(10); let child1_id = Uuid::new_v4(); let child1 = shapes.add_shape(child1_id); child1.set_selrect(3.0, 3.0, 2.0, 2.0); let child2_id = Uuid::new_v4(); let child2 = shapes.add_shape(child2_id); child2.set_selrect(0.0, 0.0, 1.0, 1.0); let parent = shapes.add_shape(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 }; let parent = shapes.get(&parent_id).unwrap(); let bounds = calculate_group_bounds(parent, &shapes, &HashMap::new()).unwrap(); assert_eq!(bounds.width(), 3.0); assert_eq!(bounds.height(), 3.0); } }