diff --git a/crates/freya-core/src/elements/extensions.rs b/crates/freya-core/src/elements/extensions.rs index 6fbda8220..35c6f1e7a 100644 --- a/crates/freya-core/src/elements/extensions.rs +++ b/crates/freya-core/src/elements/extensions.rs @@ -19,6 +19,7 @@ use torin::{ Alignment, Direction, Length, + Order, Position, VisibleSize, }, @@ -490,6 +491,24 @@ where self.get_layout().layout.content = content; self } + + /// Set the order children are stacked in along the direction axis. See [`Order`]. + fn order(mut self, order: impl Into) -> Self { + self.get_layout().layout.order = order.into(); + self + } + + /// Stack children in their natural order along the direction axis. Shorthand for [`order`](Self::order) set to [`Order::Forward`]. + fn forward_order(mut self) -> Self { + self.get_layout().layout.order = Order::forward(); + self + } + + /// Stack children in reverse order along the direction axis. Shorthand for [`order`](Self::order) set to [`Order::Backward`]. + fn backward_order(mut self) -> Self { + self.get_layout().layout.order = Order::backward(); + self + } /// Center children on both axes. Shorthand for [`main_align`](Self::main_align) and [`cross_align`](Self::cross_align) set to [`Alignment::Center`]. fn center(mut self) -> Self { self.get_layout().layout.main_alignment = Alignment::Center; diff --git a/crates/freya-devtools-app/src/components/attribute.rs b/crates/freya-devtools-app/src/components/attribute.rs index 41cf62be6..ea74e45a8 100644 --- a/crates/freya-devtools-app/src/components/attribute.rs +++ b/crates/freya-devtools-app/src/components/attribute.rs @@ -45,6 +45,7 @@ pub fn attribute_element(name: &str, attribute: AttributeType<'_>) -> Element { .into(), AttributeType::Text(text) => Property::new(name, text).into(), AttributeType::Direction(direction) => Property::new(name, direction.pretty()).into(), + AttributeType::Order(order) => Property::new(name, order.pretty()).into(), AttributeType::Position(position) => Property::new(name, position.pretty()).into(), AttributeType::Content(content) => Property::new(name, content.pretty()).into(), AttributeType::Alignment(alignment) => Property::new(name, alignment.pretty()).into(), diff --git a/crates/freya-devtools/src/node_info.rs b/crates/freya-devtools/src/node_info.rs index abc4811d6..29e298a9a 100644 --- a/crates/freya-devtools/src/node_info.rs +++ b/crates/freya-devtools/src/node_info.rs @@ -23,6 +23,7 @@ use torin::{ direction::Direction, gaps::Gaps, geometry::Length, + order::Order, prelude::{ Area, AreaOf, @@ -83,6 +84,7 @@ impl NodeState { "direction", AttributeType::Direction(&self.layout.direction), ), + ("order", AttributeType::Order(&self.layout.order)), ("padding", AttributeType::Measures(self.layout.padding)), ("margin", AttributeType::Measures(self.layout.margin)), ("position", AttributeType::Position(&self.layout.position)), @@ -190,6 +192,7 @@ pub enum AttributeType<'a> { Measures(Gaps), CornerRadius(CornerRadius), Direction(&'a Direction), + Order(&'a Order), Position(&'a Position), Content(&'a Content), Alignment(&'a Alignment), diff --git a/crates/freya/src/lib.rs b/crates/freya/src/lib.rs index 703aead2b..489e29e71 100644 --- a/crates/freya/src/lib.rs +++ b/crates/freya/src/lib.rs @@ -147,6 +147,7 @@ pub mod prelude { CursorPoint, Size2D, }, + order::Order, position::Position, size::Size, visible_size::VisibleSize, diff --git a/crates/torin/src/measure.rs b/crates/torin/src/measure.rs index 0dbd1a7c9..0631268a7 100644 --- a/crates/torin/src/measure.rs +++ b/crates/torin/src/measure.rs @@ -543,7 +543,10 @@ where // Parent Node is dirty. parent_is_dirty: bool, ) { - let children = self.tree_adapter.children_of(parent_node_id); + let mut children = self.tree_adapter.children_of(parent_node_id); + if parent_node.order.is_backward() { + children.reverse(); + } let initial_area = *inner_area; diff --git a/crates/torin/src/node.rs b/crates/torin/src/node.rs index f7b157d9f..bbed74e0f 100644 --- a/crates/torin/src/node.rs +++ b/crates/torin/src/node.rs @@ -5,6 +5,7 @@ use crate::{ direction::Direction, gaps::Gaps, geometry::Length, + order::Order, prelude::{ Content, Position, @@ -51,6 +52,9 @@ pub struct Node { /// Direction in which it's inner Nodes will be stacked pub direction: Direction, + /// Order in which it's inner Nodes will be stacked along the direction axis + pub order: Order, + /// Position config pub position: Position, @@ -104,6 +108,7 @@ impl Node { && self.padding == other.padding && self.margin == other.margin && self.direction == other.direction + && self.order == other.order && self.position == other.position && self.content == other.content && self.has_layout_references == other.has_layout_references @@ -126,6 +131,22 @@ impl Node { } } + /// Construct a new Node given a size, a direction and an order + pub fn from_size_and_direction_and_order( + width: Size, + height: Size, + direction: Direction, + order: Order, + ) -> Self { + Self { + width, + height, + direction, + order, + ..Default::default() + } + } + /// Construct a new Node given some sizes pub fn from_sizes( width: Size, diff --git a/crates/torin/src/values/mod.rs b/crates/torin/src/values/mod.rs index 07288819a..473ce8392 100644 --- a/crates/torin/src/values/mod.rs +++ b/crates/torin/src/values/mod.rs @@ -2,6 +2,7 @@ pub mod alignment; pub mod content; pub mod direction; pub mod gaps; +pub mod order; pub mod position; pub mod size; pub mod visible_size; @@ -12,6 +13,7 @@ pub mod prelude { content::*, direction::*, gaps::*, + order::*, position::*, size::*, visible_size::*, diff --git a/crates/torin/src/values/order.rs b/crates/torin/src/values/order.rs new file mode 100644 index 000000000..52c46f573 --- /dev/null +++ b/crates/torin/src/values/order.rs @@ -0,0 +1,33 @@ +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(PartialEq, Eq, Clone, Debug, Default, Copy)] +pub enum Order { + /// Lay children out in their natural order. This is the default. + #[default] + Forward, + /// Lay children out in reverse order along the direction axis. + Backward, +} + +impl Order { + /// Use a [`Forward`](Order::Forward) order. + pub fn forward() -> Order { + Order::Forward + } + + /// Use a [`Backward`](Order::Backward) order. + pub fn backward() -> Order { + Order::Backward + } + + /// Whether children are laid out in reverse order. + pub fn is_backward(&self) -> bool { + matches!(self, Self::Backward) + } + + pub fn pretty(&self) -> String { + match self { + Self::Forward => "forward".to_string(), + Self::Backward => "backward".to_string(), + } + } +} diff --git a/crates/torin/tests/order.rs b/crates/torin/tests/order.rs new file mode 100644 index 000000000..48a1602af --- /dev/null +++ b/crates/torin/tests/order.rs @@ -0,0 +1,218 @@ +use torin::{ + prelude::*, + test_utils::*, +}; + +fn stacked_tree(parent: Node) -> (Torin, Option, TestingTree) { + let (layout, measurer) = test_utils(); + + let mut mocked_tree = TestingTree::default(); + mocked_tree.add(0, None, vec![1, 2, 3], parent); + for id in 1..=3 { + mocked_tree.add( + id, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(100.0)), + Size::Pixels(Length::new(100.0)), + Direction::Vertical, + ), + ); + } + + (layout, measurer, mocked_tree) +} + +#[test] +pub fn backward_order_stacks_horizontal_children_in_reverse() { + let (mut layout, mut measurer, mut mocked_tree) = + stacked_tree(Node::from_size_and_direction_and_order( + Size::Pixels(Length::new(300.0)), + Size::Pixels(Length::new(100.0)), + Direction::Horizontal, + Order::Backward, + )); + + layout.measure( + 0, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(300.0, 100.0)), + &mut measurer, + &mut mocked_tree, + ); + + assert_eq!(layout.get(&3).unwrap().area.origin, Point2D::new(0.0, 0.0)); + assert_eq!( + layout.get(&2).unwrap().area.origin, + Point2D::new(100.0, 0.0) + ); + assert_eq!( + layout.get(&1).unwrap().area.origin, + Point2D::new(200.0, 0.0) + ); +} + +#[test] +pub fn backward_order_stacks_vertical_children_in_reverse() { + let (mut layout, mut measurer, mut mocked_tree) = + stacked_tree(Node::from_size_and_direction_and_order( + Size::Pixels(Length::new(100.0)), + Size::Pixels(Length::new(300.0)), + Direction::Vertical, + Order::Backward, + )); + + layout.measure( + 0, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(100.0, 300.0)), + &mut measurer, + &mut mocked_tree, + ); + + assert_eq!(layout.get(&3).unwrap().area.origin, Point2D::new(0.0, 0.0)); + assert_eq!( + layout.get(&2).unwrap().area.origin, + Point2D::new(0.0, 100.0) + ); + assert_eq!( + layout.get(&1).unwrap().area.origin, + Point2D::new(0.0, 200.0) + ); +} + +#[test] +pub fn backward_order_composes_with_center_alignment() { + let (mut layout, mut measurer) = test_utils(); + + let mut parent = Node::from_size_and_alignments_and_direction( + Size::Pixels(Length::new(300.0)), + Size::Pixels(Length::new(100.0)), + Alignment::Center, + Alignment::Start, + Direction::Horizontal, + ); + parent.order = Order::Backward; + + let mut mocked_tree = TestingTree::default(); + mocked_tree.add(0, None, vec![1, 2], parent); + for id in 1..=2 { + mocked_tree.add( + id, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(50.0)), + Size::Pixels(Length::new(50.0)), + Direction::Vertical, + ), + ); + } + + layout.measure( + 0, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(300.0, 100.0)), + &mut measurer, + &mut mocked_tree, + ); + + assert_eq!( + layout.get(&2).unwrap().area.origin, + Point2D::new(100.0, 0.0) + ); + assert_eq!( + layout.get(&1).unwrap().area.origin, + Point2D::new(150.0, 0.0) + ); +} + +#[test] +pub fn backward_order_keeps_spacing_between_children() { + let mut parent = Node::from_size_and_alignments_and_direction_and_spacing( + Size::Pixels(Length::new(320.0)), + Size::Pixels(Length::new(100.0)), + Alignment::Start, + Alignment::Start, + Direction::Horizontal, + Length::new(10.0), + ); + parent.order = Order::Backward; + + let (mut layout, mut measurer, mut mocked_tree) = stacked_tree(parent); + + layout.measure( + 0, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(320.0, 100.0)), + &mut measurer, + &mut mocked_tree, + ); + + assert_eq!(layout.get(&3).unwrap().area.origin, Point2D::new(0.0, 0.0)); + assert_eq!( + layout.get(&2).unwrap().area.origin, + Point2D::new(110.0, 0.0) + ); + assert_eq!( + layout.get(&1).unwrap().area.origin, + Point2D::new(220.0, 0.0) + ); +} + +#[test] +pub fn backward_order_composes_with_flex() { + let (mut layout, mut measurer) = test_utils(); + + let mut mocked_tree = TestingTree::default(); + let mut root = Node::from_size_and_content( + Size::Pixels(Length::new(300.0)), + Size::Pixels(Length::new(100.0)), + Content::Flex, + ); + root.direction = Direction::Horizontal; + root.order = Order::Backward; + mocked_tree.add(0, None, vec![1, 2, 3], root); + mocked_tree.add( + 1, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(50.0)), + Size::Pixels(Length::new(100.0)), + Direction::Vertical, + ), + ); + mocked_tree.add( + 2, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Flex(Length::new(1.0)), + Size::Pixels(Length::new(100.0)), + Direction::Vertical, + ), + ); + mocked_tree.add( + 3, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(50.0)), + Size::Pixels(Length::new(100.0)), + Direction::Vertical, + ), + ); + + layout.measure( + 0, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(300.0, 100.0)), + &mut measurer, + &mut mocked_tree, + ); + + assert_eq!(layout.get(&3).unwrap().area.origin, Point2D::new(0.0, 0.0)); + assert_eq!(layout.get(&2).unwrap().area.width(), 200.0); + assert_eq!(layout.get(&2).unwrap().area.origin, Point2D::new(50.0, 0.0)); + assert_eq!( + layout.get(&1).unwrap().area.origin, + Point2D::new(250.0, 0.0) + ); +} diff --git a/examples/layout_reverse_direction.rs b/examples/layout_reverse_direction.rs new file mode 100644 index 000000000..a7a35d366 --- /dev/null +++ b/examples/layout_reverse_direction.rs @@ -0,0 +1,58 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] +use freya::prelude::*; + +fn main() { + launch(LaunchConfig::new().with_window(WindowConfig::new(app))) +} + +fn cards() -> [Element; 3] { + [1, 2, 3].map(|index| { + Button::new() + .on_press(move |_| println!("Pressed item {index}")) + .child(format!("Item {index}")) + .into_element() + }) +} + +fn row(title: &str, content: Element) -> Element { + rect() + .width(Size::fill()) + .spacing(5.) + .child(title.to_string()) + .child(content) + .into() +} + +fn app() -> impl IntoElement { + ScrollView::new() + .spacing(20.) + .child(row( + "horizontal", + rect().horizontal().spacing(5.).children(cards()).into(), + )) + .child(row( + "horizontal + reversed", + rect() + .horizontal() + .backward_order() + .spacing(5.) + .children(cards()) + .into(), + )) + .child(row( + "vertical", + rect().vertical().spacing(5.).children(cards()).into(), + )) + .child(row( + "vertical + reversed", + rect() + .vertical() + .backward_order() + .spacing(5.) + .children(cards()) + .into(), + )) +}