From 00ca9026013b4f50907d22675efbd1c699bc9743 Mon Sep 17 00:00:00 2001 From: Marc Espin Date: Sun, 10 Nov 2024 12:34:23 +0100 Subject: [PATCH] feat: flex support (#920) * feat: flex support * move flex to content * flex tests * flex docs * fmt --- .../elements/src/_docs/attributes/content.md | 6 +- crates/elements/src/_docs/size_unit.rs | 27 ++ crates/state/src/values/content.rs | 1 + crates/state/src/values/size.rs | 10 + crates/torin/src/measure.rs | 111 +++++- crates/torin/src/values/content.rs | 6 + crates/torin/src/values/size.rs | 22 +- crates/torin/tests/flex.rs | 321 ++++++++++++++++++ examples/flex.rs | 53 +++ examples/flex_2.rs | 34 ++ 10 files changed, 570 insertions(+), 21 deletions(-) create mode 100644 crates/torin/tests/flex.rs create mode 100644 examples/flex.rs create mode 100644 examples/flex_2.rs diff --git a/crates/elements/src/_docs/attributes/content.md b/crates/elements/src/_docs/attributes/content.md index a26b154ed..d1d723c00 100644 --- a/crates/elements/src/_docs/attributes/content.md +++ b/crates/elements/src/_docs/attributes/content.md @@ -4,10 +4,12 @@ Accepted values: - `normal` (default): Uses parent bounds. - `fit`: Uses parent bounds but later shrunks to the size of the biggest element inside. +- `flex`: Marks the container as flex container, children of this element will be able to use `size`/`size(n)` in their `width` and `height` attributes. -The `fit` mode will allow the inner elements using `width: fill-min` to expand to the biggest element inside this element. -### Example +### `fit` + +The `fit` mode will allow the inner elements using `width: fill-min` to expand to the biggest element inside this element. ```rust, no_run # use freya::prelude::*; diff --git a/crates/elements/src/_docs/size_unit.rs b/crates/elements/src/_docs/size_unit.rs index 15a341bdb..ac2de49a9 100644 --- a/crates/elements/src/_docs/size_unit.rs +++ b/crates/elements/src/_docs/size_unit.rs @@ -102,3 +102,30 @@ //! ) //! } //! ``` +//! +//! #### Flex Factor +//! +//! When being a children of an element with `content: flex` you may change the growth factor of the size attributes. +//! +//! ```rust, no_run +//! # use freya::prelude::*; +//! fn app() -> Element { +//! rsx!( +//! rect { +//! content: "flex", +//! width: "200", +//! height: "200", +//! rect { +//! height: "flex(1)", +//! width: "100%", +//! background: "red" +//! } +//! rect { +//! height: "flex(3)", +//! width: "100%", +//! background: "blue" +//! } +//! } +//! ) +//! } +//! ``` diff --git a/crates/state/src/values/content.rs b/crates/state/src/values/content.rs index 7cd26f0a1..099599ba5 100644 --- a/crates/state/src/values/content.rs +++ b/crates/state/src/values/content.rs @@ -9,6 +9,7 @@ impl Parse for Content { fn parse(value: &str) -> Result { Ok(match value { "fit" => Content::Fit, + "flex" => Content::Flex, _ => Content::Normal, }) } diff --git a/crates/state/src/values/size.rs b/crates/state/src/values/size.rs index ae856e511..12148087a 100644 --- a/crates/state/src/values/size.rs +++ b/crates/state/src/values/size.rs @@ -15,6 +15,16 @@ impl Parse for Size { fn parse(value: &str) -> Result { if value == "auto" { Ok(Size::Inner) + } else if value == "flex" { + Ok(Size::Flex(Length::new(1.0))) + } else if value.contains("flex") { + Ok(Size::Flex(Length::new( + value + .replace("flex(", "") + .replace(')', "") + .parse::() + .map_err(|_| ParseError)?, + ))) } else if value == "fill" { Ok(Size::Fill) } else if value == "fill-min" { diff --git a/crates/torin/src/measure.rs b/crates/torin/src/measure.rs index 4ba77a479..78e17611e 100644 --- a/crates/torin/src/measure.rs +++ b/crates/torin/src/measure.rs @@ -20,6 +20,7 @@ use crate::{ AreaModel, DirectionMode, LayoutMetadata, + Length, Torin, }, }; @@ -308,8 +309,9 @@ where ) { let children = self.dom_adapter.children_of(parent_node_id); + let mut initial_phase_flex_grows = FxHashMap::default(); let mut initial_phase_sizes = FxHashMap::default(); - let mut initial_phase_inner_sizes = *inner_sizes; + let mut initial_phase_inner_sizes = Size2D::default(); // Used to calculate the spacing and some alignments let (non_absolute_children_len, first_child, last_child) = if parent_node.spacing.get() > 0. @@ -342,16 +344,18 @@ where ) }; - // Initial phase: Measure the size and position of the children if the parent has a - // non-start cross alignment, non-start main aligment of a fit-content. - if parent_node.cross_alignment.is_not_start() + let needs_initial_phase = parent_node.cross_alignment.is_not_start() || parent_node.main_alignment.is_not_start() || parent_node.content.is_fit() - { - let mut initial_phase_area = *area; - let mut initial_phase_inner_area = *inner_area; - let mut initial_phase_available_area = *available_area; + || parent_node.content.is_flex(); + let mut initial_phase_area = *area; + let mut initial_phase_inner_area = *inner_area; + let mut initial_phase_available_area = *available_area; + + // Initial phase: Measure the size and position of the children if the parent has a + // non-start cross alignment, non-start main aligment of a fit-content. + if needs_initial_phase { // Measure the children for child_id in children.iter() { let Some(child_data) = self.dom_adapter.get_node(child_id) else { @@ -382,11 +386,13 @@ where Self::stack_child( &mut initial_phase_available_area, parent_node, + &child_data, &mut initial_phase_area, &mut initial_phase_inner_area, &mut initial_phase_inner_sizes, &child_areas.area, is_last_child, + Phase::Initial, ); if parent_node.cross_alignment.is_not_start() @@ -394,8 +400,60 @@ where { initial_phase_sizes.insert(*child_id, child_areas.area.size); } + + if parent_node.content.is_flex() { + match parent_node.direction { + DirectionMode::Vertical => { + if let Some(ff) = child_data.height.flex_grow() { + initial_phase_flex_grows.insert(*child_id, ff); + } + } + DirectionMode::Horizontal => { + if let Some(ff) = child_data.width.flex_grow() { + initial_phase_flex_grows.insert(*child_id, ff); + } + } + } + } } + } + + let initial_available_area = *available_area; + let flex_grows = initial_phase_flex_grows + .values() + .cloned() + .reduce(|acc, v| acc + v) + .unwrap_or_default() + .max(Length::new(1.0)); + + let flex_axis = AlignAxis::new(&parent_node.direction, AlignmentDirection::Main); + + let flex_available_width = initial_available_area.width() - initial_phase_inner_sizes.width; + let flex_available_height = + initial_available_area.height() - initial_phase_inner_sizes.height; + + let initial_phase_inner_sizes_with_flex = + initial_phase_flex_grows + .values() + .fold(initial_phase_inner_sizes, |mut acc, f| { + let flex_grow_per = f.get() / flex_grows.get() * 100.; + + match flex_axis { + AlignAxis::Height => { + let size = flex_available_height / 100. * flex_grow_per; + acc.height += size; + } + AlignAxis::Width => { + let size = flex_available_width / 100. * flex_grow_per; + acc.width += size; + } + } + + acc + }); + + if needs_initial_phase { if parent_node.main_alignment.is_not_start() { // Adjust the available and inner areas of the Main axis Self::shrink_area_to_fit_when_unbounded( @@ -410,7 +468,7 @@ where Self::align_content( available_area, &initial_phase_inner_area, - &initial_phase_inner_sizes, + &initial_phase_inner_sizes_with_flex, &parent_node.main_alignment, &parent_node.direction, AlignmentDirection::Main, @@ -442,6 +500,25 @@ where let mut adapted_available_area = *available_area; + if parent_node.content.is_flex() { + let flex_grow = initial_phase_flex_grows.get(&child_id); + + if let Some(flex_grow) = flex_grow { + let flex_grow_per = flex_grow.get() / flex_grows.get() * 100.; + + match flex_axis { + AlignAxis::Height => { + let size = flex_available_height / 100. * flex_grow_per; + adapted_available_area.size.height = size; + } + AlignAxis::Width => { + let size = flex_available_width / 100. * flex_grow_per; + adapted_available_area.size.width = size; + } + } + } + } + // Only the stacked children will be aligned if parent_node.main_alignment.is_spaced() && !child_data.position.is_absolute() { // Align the Main axis if necessary @@ -449,7 +526,7 @@ where AlignmentDirection::Main, &mut adapted_available_area, &initial_available_area, - &initial_phase_inner_sizes, + &initial_phase_inner_sizes_with_flex, &parent_node.main_alignment, &parent_node.direction, non_absolute_children_len, @@ -492,11 +569,13 @@ where Self::stack_child( available_area, parent_node, + &child_data, area, inner_area, inner_sizes, &child_areas.area, is_last_child, + Phase::Final, ); } @@ -612,11 +691,13 @@ where fn stack_child( available_area: &mut Area, parent_node: &Node, + child_node: &Node, parent_area: &mut Area, inner_area: &mut Area, inner_sizes: &mut Size2D, child_area: &Area, is_last_sibiling: bool, + phase: Phase, ) { // Only apply the spacing to elements after `i > 0` and `i < len - 1` let spacing = (!is_last_sibiling) @@ -630,7 +711,10 @@ where available_area.size.width -= child_area.size.width + spacing.get(); inner_sizes.height = child_area.height().max(inner_sizes.height); - inner_sizes.width += child_area.width() + spacing.get(); + inner_sizes.width += spacing.get(); + if !child_node.width.is_flex() || phase == Phase::Final { + inner_sizes.width += child_area.width(); + } // Keep the biggest height if parent_node.height.inner_sized() { @@ -656,7 +740,10 @@ where available_area.size.height -= child_area.size.height + spacing.get(); inner_sizes.width = child_area.width().max(inner_sizes.width); - inner_sizes.height += child_area.height() + spacing.get(); + inner_sizes.height += spacing.get(); + if !child_node.height.is_flex() || phase == Phase::Final { + inner_sizes.height += child_area.height(); + } // Keep the biggest width if parent_node.width.inner_sized() { diff --git a/crates/torin/src/values/content.rs b/crates/torin/src/values/content.rs index a6193e8b8..c96b38afb 100644 --- a/crates/torin/src/values/content.rs +++ b/crates/torin/src/values/content.rs @@ -3,12 +3,17 @@ pub enum Content { #[default] Normal, Fit, + Flex, } impl Content { pub fn is_fit(&self) -> bool { self == &Self::Fit } + + pub fn is_flex(&self) -> bool { + self == &Self::Flex + } } impl Content { @@ -16,6 +21,7 @@ impl Content { match self { Self::Normal => "normal".to_owned(), Self::Fit => "fit".to_owned(), + Self::Flex => "flex".to_owned(), } } } diff --git a/crates/torin/src/values/size.rs b/crates/torin/src/values/size.rs index 2c1c347b2..066cd97ac 100644 --- a/crates/torin/src/values/size.rs +++ b/crates/torin/src/values/size.rs @@ -21,6 +21,7 @@ pub enum Size { RootPercentage(Length), InnerPercentage(Length), DynamicCalculations(Box>), + Flex(Length), } impl Default for Size { @@ -30,6 +31,17 @@ impl Default for Size { } impl Size { + pub fn flex_grow(&self) -> Option { + match self { + Self::Flex(f) => Some(*f), + _ => None, + } + } + + pub fn is_flex(&self) -> bool { + matches!(self, Self::Flex(_)) + } + pub fn inner_sized(&self) -> bool { matches!( self, @@ -58,6 +70,7 @@ impl Size { Size::FillMinimum => "fill-min".to_string(), Size::RootPercentage(p) => format!("{}% of root", p.get()), Size::InnerPercentage(p) => format!("{}% of auto", p.get()), + Size::Flex(f) => format!("flex({}", f.get()), } } @@ -76,14 +89,9 @@ impl Size { run_calculations(calculations.deref(), parent_value, root_value).unwrap_or(0.0), ), Size::Fill => Some(available_parent_value), - Size::FillMinimum => { - if phase == Phase::Initial { - None - } else { - Some(available_parent_value) - } - } + Size::FillMinimum if phase == Phase::Final => Some(available_parent_value), Size::RootPercentage(per) => Some(root_value / 100.0 * per.get()), + Size::Flex(_) if phase == Phase::Final => Some(available_parent_value), _ => None, } } diff --git a/crates/torin/tests/flex.rs b/crates/torin/tests/flex.rs new file mode 100644 index 000000000..d5fcd0902 --- /dev/null +++ b/crates/torin/tests/flex.rs @@ -0,0 +1,321 @@ +use euclid::Length; +use torin::{ + prelude::*, + test_utils::*, +}; + +#[test] +pub fn flex_generic() { + let (mut layout, mut measurer) = test_utils(); + + let mut mocked_dom = TestingDOM::default(); + mocked_dom.add( + 0, + None, + vec![1, 2, 3, 4], + Node::from_size_and_content( + Size::Pixels(Length::new(200.0)), + Size::Pixels(Length::new(200.0)), + Content::Flex, + ), + ); + mocked_dom.add( + 1, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Percentage(Length::new(10.)), + DirectionMode::Vertical, + ), + ); + mocked_dom.add( + 2, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(1.0)), + DirectionMode::Vertical, + ), + ); + mocked_dom.add( + 3, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Pixels(Length::new(50.0)), + DirectionMode::Vertical, + ), + ); + mocked_dom.add( + 4, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(3.0)), + DirectionMode::Vertical, + ), + ); + + layout.measure( + 0, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(1000.0, 1000.0)), + &mut measurer, + &mut mocked_dom, + ); + + assert_eq!( + layout.get(0).unwrap().area, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(200.0, 200.0)), + ); + + assert_eq!( + layout.get(1).unwrap().area, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(100.0, 20.0)), + ); + assert_eq!( + layout.get(2).unwrap().area, + Rect::new(Point2D::new(0.0, 20.0), Size2D::new(100.0, 32.5)), + ); + assert_eq!( + layout.get(3).unwrap().area, + Rect::new(Point2D::new(0.0, 52.5), Size2D::new(100.0, 50.0)), + ); + assert_eq!( + layout.get(4).unwrap().area, + Rect::new(Point2D::new(0.0, 102.5), Size2D::new(100.0, 97.5)), + ); +} + +#[test] +pub fn flex_under_1_flex_grow() { + let (mut layout, mut measurer) = test_utils(); + + let mut mocked_dom = TestingDOM::default(); + mocked_dom.add( + 0, + None, + vec![1, 2], + Node::from_size_and_content( + Size::Pixels(Length::new(200.0)), + Size::Pixels(Length::new(200.0)), + Content::Flex, + ), + ); + mocked_dom.add( + 1, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(0.2)), + DirectionMode::Vertical, + ), + ); + mocked_dom.add( + 2, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(0.5)), + DirectionMode::Vertical, + ), + ); + + layout.measure( + 0, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(1000.0, 1000.0)), + &mut measurer, + &mut mocked_dom, + ); + + assert_eq!( + layout.get(0).unwrap().area, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(200.0, 200.0)), + ); + + assert_eq!( + layout.get(1).unwrap().area, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(100.0, 40.0)), + ); + assert_eq!( + layout.get(2).unwrap().area, + Rect::new(Point2D::new(0.0, 40.0), Size2D::new(100.0, 100.0)), + ); +} + +#[test] +pub fn flex_grow_balance() { + let (mut layout, mut measurer) = test_utils(); + + let mut mocked_dom = TestingDOM::default(); + mocked_dom.add( + 0, + None, + vec![1, 2, 3, 4], + Node::from_size_and_content( + Size::Pixels(Length::new(200.0)), + Size::Pixels(Length::new(200.0)), + Content::Flex, + ), + ); + mocked_dom.add( + 1, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(1.0)), + DirectionMode::Vertical, + ), + ); + mocked_dom.add( + 2, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(2.0)), + DirectionMode::Vertical, + ), + ); + mocked_dom.add( + 3, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(3.0)), + DirectionMode::Vertical, + ), + ); + mocked_dom.add( + 4, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(4.0)), + DirectionMode::Vertical, + ), + ); + + layout.measure( + 0, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(1000.0, 1000.0)), + &mut measurer, + &mut mocked_dom, + ); + + assert_eq!( + layout.get(0).unwrap().area, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(200.0, 200.0)), + ); + + assert_eq!( + layout.get(1).unwrap().area, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(100.0, 20.0)), + ); + assert_eq!( + layout.get(2).unwrap().area, + Rect::new(Point2D::new(0.0, 20.0), Size2D::new(100.0, 40.0)), + ); + assert_eq!( + layout.get(3).unwrap().area.round(), + Rect::new(Point2D::new(0.0, 60.0), Size2D::new(100.0, 60.0)), + ); + assert_eq!( + layout.get(4).unwrap().area.round(), + Rect::new(Point2D::new(0.0, 120.0), Size2D::new(100.0, 80.0)), + ); +} + +#[test] +pub fn flex_large_grow_balance() { + let (mut layout, mut measurer) = test_utils(); + + let mut mocked_dom = TestingDOM::default(); + mocked_dom.add( + 0, + None, + vec![1, 2, 3, 4], + Node::from_size_and_content( + Size::Pixels(Length::new(200.0)), + Size::Pixels(Length::new(200.0)), + Content::Flex, + ), + ); + mocked_dom.add( + 1, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(5.0)), + DirectionMode::Vertical, + ), + ); + mocked_dom.add( + 2, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(65.0)), + DirectionMode::Vertical, + ), + ); + mocked_dom.add( + 3, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(30.0)), + DirectionMode::Vertical, + ), + ); + mocked_dom.add( + 4, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Flex(Length::new(100.0)), + DirectionMode::Vertical, + ), + ); + + layout.measure( + 0, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(1000.0, 1000.0)), + &mut measurer, + &mut mocked_dom, + ); + + assert_eq!( + layout.get(0).unwrap().area, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(200.0, 200.0)), + ); + + assert_eq!( + layout.get(1).unwrap().area, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(100.0, 5.0)), + ); + assert_eq!( + layout.get(2).unwrap().area, + Rect::new(Point2D::new(0.0, 5.0), Size2D::new(100.0, 65.0)), + ); + assert_eq!( + layout.get(3).unwrap().area.round(), + Rect::new(Point2D::new(0.0, 70.0), Size2D::new(100.0, 30.0)), + ); + assert_eq!( + layout.get(4).unwrap().area.round(), + Rect::new(Point2D::new(0.0, 100.0), Size2D::new(100.0, 100.0)), + ); +} diff --git a/examples/flex.rs b/examples/flex.rs new file mode 100644 index 000000000..a091c705b --- /dev/null +++ b/examples/flex.rs @@ -0,0 +1,53 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +use freya::prelude::*; + +fn main() { + launch(app); +} + +fn app() -> Element { + rsx!( + rect { + height: "100%", + width: "100%", + direction: "horizontal", + content: "flex", + spacing: "4", + padding: "4", + rect { + height: "100%", + width: "10%", + background: "red", + } + rect { + width: "flex", + height: "100%", + background: "orange", + } + rect { + height: "100%", + width: "25", + background: "black", + } + rect { + width: "flex(3)", + height: "100%", + background: "yellow", + } + rect { + width: "flex", + height: "100%", + background: "green", + } + rect { + height: "100%", + width: "30%", + background: "blue", + } + } + ) +} diff --git a/examples/flex_2.rs b/examples/flex_2.rs new file mode 100644 index 000000000..d07c3126e --- /dev/null +++ b/examples/flex_2.rs @@ -0,0 +1,34 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +use freya::prelude::*; + +fn main() { + launch(app); +} + +fn app() -> Element { + rsx!( + rect { + width: "100%", + height: "fill", + direction: "horizontal", + main_align: "space-around", + content: "flex", + + rect { + width: "flex(0.5)", + height: "fill", + background: "red", + } + + rect { + width: "120", + height: "fill", + background: "orange", + } + } + ) +}