diff --git a/book/src/guides/layout.md b/book/src/guides/layout.md index ff6d85cc6..9eac5905c 100644 --- a/book/src/guides/layout.md +++ b/book/src/guides/layout.md @@ -13,8 +13,9 @@ Learn how the layout attributes work. - [`fill`](#fill) - [`direction`](#direction) - [`padding`](#padding) -- [`margin`](#margin) - [`main_align & cross_align`](#main_align--cross_align) +- [`margin`](#margin) +- [`position`](#position) > ⚠️ Freya's layout is still somewhat limited. @@ -255,4 +256,41 @@ fn app(cx: Scope) -> Element { } ) } +``` + +### position + +Specify how you want the element to be positioned inside it's parent Area + +Possible values for `position`: +- `stacked` (default) +- `absolute` + +When using the `absolute` mode, you can also combine it with the following attributes: +- `position_top` +- `position_right` +- `position_bottom` +- `position_left` + +These only support pixels. + +Example: + +```rust, no_run +fn app(cx: Scope) -> Element { + render!( + rect { + width: "100%", + height: "100%", + rect { + position: "absolute", + position_bottom: "15", + position_right: "15", + background: "black", + width: "100", + height: "100", + } + } + ) +} ``` \ No newline at end of file diff --git a/crates/dom/src/dom_adapter.rs b/crates/dom/src/dom_adapter.rs index dbd95c85a..5f7430b9b 100644 --- a/crates/dom/src/dom_adapter.rs +++ b/crates/dom/src/dom_adapter.rs @@ -31,29 +31,30 @@ impl<'a> DioxusDOMAdapter<'a> { impl DOMAdapter for DioxusDOMAdapter<'_> { fn get_node(&self, node_id: &NodeId) -> Option { let node = self.rdom.get(*node_id)?; - let mut size = node.get::().unwrap().clone(); + let mut layout = node.get::().unwrap().clone(); // The root node expands by default if *node_id == self.rdom.root_id() { - size.width = Size::Percentage(Length::new(100.0)); - size.height = Size::Percentage(Length::new(100.0)); + layout.width = Size::Percentage(Length::new(100.0)); + layout.height = Size::Percentage(Length::new(100.0)); } Some(Node { - width: size.width, - height: size.height, - minimum_width: size.minimum_width, - minimum_height: size.minimum_height, - maximum_width: size.maximum_width, - maximum_height: size.maximum_height, - direction: size.direction, - padding: size.padding, - margin: size.margin, - main_alignment: size.main_alignment, - cross_alignment: size.cross_alignment, - offset_x: Length::new(size.offset_x), - offset_y: Length::new(size.offset_y), - has_layout_references: size.node_ref.is_some(), + width: layout.width, + height: layout.height, + minimum_width: layout.minimum_width, + minimum_height: layout.minimum_height, + maximum_width: layout.maximum_width, + maximum_height: layout.maximum_height, + direction: layout.direction, + padding: layout.padding, + margin: layout.margin, + main_alignment: layout.main_alignment, + cross_alignment: layout.cross_alignment, + offset_x: Length::new(layout.offset_x), + offset_y: Length::new(layout.offset_y), + has_layout_references: layout.node_ref.is_some(), + position: layout.position, }) } diff --git a/crates/elements/src/elements.rs b/crates/elements/src/elements.rs index 524a71d0a..78da875c9 100644 --- a/crates/elements/src/elements.rs +++ b/crates/elements/src/elements.rs @@ -191,6 +191,11 @@ builder_constructors! { name: String, focusable: String, margin: String, + position: String, + position_top: String, + position_right: String, + position_bottom: String, + position_left: String, }; label { color: String, diff --git a/crates/state/src/layout.rs b/crates/state/src/layout.rs index 0870e90cc..e459acc0e 100644 --- a/crates/state/src/layout.rs +++ b/crates/state/src/layout.rs @@ -30,6 +30,7 @@ pub struct LayoutState { pub offset_x: f32, pub main_alignment: Alignment, pub cross_alignment: Alignment, + pub position: Position, pub node_ref: Option>, } @@ -57,6 +58,11 @@ impl State for LayoutState { "cross_align", "reference", "margin", + "position", + "position_top", + "position_right", + "position_bottom", + "position_left", ])) .with_tag() .with_text(); @@ -190,6 +196,43 @@ impl State for LayoutState { } } } + "position" => { + if let Some(value) = attr.value.as_text() { + if let Ok(position) = Position::parse(value) { + if layout.position.is_empty() { + layout.position = position; + } + } + } + } + "position_top" => { + if let Some(value) = attr.value.as_text() { + if let Ok(top) = value.parse::() { + layout.position.set_top(top * scale_factor); + } + } + } + "position_right" => { + if let Some(value) = attr.value.as_text() { + if let Ok(right) = value.parse::() { + layout.position.set_right(right * scale_factor); + } + } + } + "position_bottom" => { + if let Some(value) = attr.value.as_text() { + if let Ok(bottom) = value.parse::() { + layout.position.set_bottom(bottom * scale_factor); + } + } + } + "position_left" => { + if let Some(value) = attr.value.as_text() { + if let Ok(left) = value.parse::() { + layout.position.set_left(left * scale_factor); + } + } + } "reference" => { if let OwnedAttributeValue::Custom(CustomAttributeValues::Reference( reference, @@ -217,7 +260,8 @@ impl State for LayoutState { || (layout.offset_x != self.offset_x) || (layout.offset_y != self.offset_y) || (layout.main_alignment != self.main_alignment) - || (layout.cross_alignment != self.cross_alignment); + || (layout.cross_alignment != self.cross_alignment) + || (layout.position != self.position); if changed { torin_layout.lock().unwrap().invalidate(node_view.node_id()); diff --git a/crates/state/src/values/mod.rs b/crates/state/src/values/mod.rs index 3891a5e41..6d851aaaf 100644 --- a/crates/state/src/values/mod.rs +++ b/crates/state/src/values/mod.rs @@ -10,6 +10,7 @@ mod font; mod gaps; mod gradient; mod overflow; +mod position; mod shadow; mod size; mod text_shadow; @@ -26,6 +27,7 @@ pub use font::*; pub use gaps::*; pub use gradient::*; pub use overflow::*; +pub use position::*; pub use shadow::*; pub use size::*; pub use text_shadow::*; diff --git a/crates/state/src/values/position.rs b/crates/state/src/values/position.rs new file mode 100644 index 000000000..928a6d1dc --- /dev/null +++ b/crates/state/src/values/position.rs @@ -0,0 +1,16 @@ +use crate::Parse; +use torin::position::Position; + +#[derive(Debug, PartialEq, Eq)] +pub struct ParsePositionError; + +impl Parse for Position { + type Err = ParsePositionError; + + fn parse(value: &str) -> Result { + Ok(match value { + "absolute" => Position::new_absolute(), + _ => Position::Stacked, + }) + } +} diff --git a/crates/torin/src/measure.rs b/crates/torin/src/measure.rs index 7b41b4909..cd6181f89 100644 --- a/crates/torin/src/measure.rs +++ b/crates/torin/src/measure.rs @@ -29,15 +29,12 @@ pub fn measure_node( ) -> (bool, NodeAreas) { let must_run = layout.dirty.contains(&node_id) || layout.results.get(&node_id).is_none(); if must_run { - // 1. Create the initial Node area - let mut area = Rect::new( - available_parent_area.origin, - Size2D::new(node.padding.horizontal(), node.padding.vertical()), - ); + // 1. Create the initial Node area size + let mut area_size = Size2D::new(node.padding.horizontal(), node.padding.vertical()); // 2. Compute the width and height given the size, the minimum size, the maximum size and margins - area.size.width = node.width.min_max( - area.size.width, + area_size.width = node.width.min_max( + area_size.width, parent_area.size.width, available_parent_area.size.width, node.margin.left(), @@ -45,8 +42,8 @@ pub fn measure_node( &node.minimum_width, &node.maximum_width, ); - area.size.height = node.height.min_max( - area.size.height, + area_size.height = node.height.min_max( + area_size.height, parent_area.size.height, available_parent_area.size.height, node.margin.top(), @@ -55,14 +52,21 @@ pub fn measure_node( &node.maximum_height, ); - // 3. If available, run a custom layout measure function + // 3. Compute the origin of the area + let area_origin = node + .position + .get_origin(available_parent_area, parent_area, &area_size); + + let mut area = Rect::new(area_origin, area_size); + + // 4. If available, run a custom layout measure function // This is useful when you use third-party libraries (e.g. rust-skia, cosmic-text) to measure text layouts // When a Node is measured by a custom measurer function the inner children will be skipped let measure_inner_children = if let Some(measurer) = measurer { let custom_area = measurer.measure(node_id, node, &area, parent_area, available_parent_area); - // 3.1. Compute the width and height again using the new custom area sizes + // 4.1. Compute the width and height again using the new custom area sizes if let Some(custom_area) = custom_area { if Size::Inner == node.width { area.size.width = node.width.min_max( @@ -94,11 +98,11 @@ pub fn measure_node( true }; - // 4. Compute the inner area of the Node, which is basically the area inside the margins and paddings + // 5. Compute the inner area of the Node, which is basically the area inside the margins and paddings let mut inner_area = { let mut inner_area = area; - // 4.1. When having an unsized bound we set it to whatever is still available in the parent's area + // 5.1. When having an unsized bound we set it to whatever is still available in the parent's area if Size::Inner == node.width { inner_area.size.width = node.width.min_max( available_parent_area.width(), @@ -130,10 +134,10 @@ pub fn measure_node( let mut inner_sizes = Size2D::default(); if measure_inner_children { - // 5. Create an area containing the available space inside the inner area + // 6. Create an area containing the available space inside the inner area let mut available_area = inner_area; - // 5.1. Adjust the available area with the node offsets (mainly used by scrollviews) + // 6.1. Adjust the available area with the node offsets (mainly used by scrollviews) available_area.move_with_offsets(&node.offset_x, &node.offset_y); let mut measurement_mode = MeasureMode::ParentIsNotCached { @@ -141,7 +145,7 @@ pub fn measure_node( inner_area: &mut inner_area, }; - // 6. Measure the layout of this Node's children + // 7. Measure the layout of this Node's children measure_inner_nodes( &node_id, node, @@ -259,7 +263,13 @@ pub fn measure_inner_nodes( ); // Stack the child into its parent - mode.stack_into_node(parent_node, available_area, &child_areas.area, inner_sizes); + mode.stack_into_node( + parent_node, + available_area, + &child_areas.area, + inner_sizes, + &child_data, + ); // Cache the child layout if it was mutated and inner nodes must be cache if child_revalidated && must_cache_inner_nodes { diff --git a/crates/torin/src/measure_mode.rs b/crates/torin/src/measure_mode.rs index 78cd6c464..e67af89c9 100644 --- a/crates/torin/src/measure_mode.rs +++ b/crates/torin/src/measure_mode.rs @@ -134,7 +134,12 @@ impl<'a> MeasureMode<'a> { available_area: &mut Area, content_area: &Area, inner_sizes: &mut Size2D, + node_data: &Node, ) { + if node_data.position.is_absolute() { + return; + } + match parent_node.direction { DirectionMode::Horizontal => { // Move the available area diff --git a/crates/torin/src/node.rs b/crates/torin/src/node.rs index cd439cc7d..d296553ac 100644 --- a/crates/torin/src/node.rs +++ b/crates/torin/src/node.rs @@ -1,7 +1,8 @@ pub use euclid::Rect; use crate::{ - alignment::Alignment, direction::DirectionMode, gaps::Gaps, geometry::Length, size::Size, + alignment::Alignment, direction::DirectionMode, gaps::Gaps, geometry::Length, + prelude::Position, size::Size, }; /// Node layout configuration @@ -36,6 +37,8 @@ pub struct Node { /// Direction in which it's inner Nodes will be stacked pub direction: DirectionMode, + pub position: Position, + /// A Node might depend on inner sizes but have a fixed position, like scroll views. pub has_layout_references: bool, } @@ -146,6 +149,16 @@ impl Node { } } + /// Construct a new Node given a size and a position + pub fn from_size_and_position(width: Size, height: Size, position: Position) -> Self { + Self { + width, + height, + position, + ..Default::default() + } + } + /// Has properties that depend on the inner Nodes? pub fn does_depend_on_inner(&self) -> bool { Size::Inner == self.width diff --git a/crates/torin/src/values/mod.rs b/crates/torin/src/values/mod.rs index 24128385b..4f5c75fbc 100644 --- a/crates/torin/src/values/mod.rs +++ b/crates/torin/src/values/mod.rs @@ -1,11 +1,13 @@ pub mod alignment; pub mod direction; pub mod gaps; +pub mod position; pub mod size; pub mod prelude { pub use crate::alignment::*; pub use crate::direction::*; pub use crate::gaps::*; + pub use crate::position::*; pub use crate::size::*; } diff --git a/crates/torin/src/values/position.rs b/crates/torin/src/values/position.rs new file mode 100644 index 000000000..3dca243f9 --- /dev/null +++ b/crates/torin/src/values/position.rs @@ -0,0 +1,114 @@ +use crate::prelude::{Area, Point2D, Size2D}; + +#[derive(Default, PartialEq, Debug, Clone)] +pub enum Position { + #[default] + Stacked, + + Absolute { + top: Option, + right: Option, + bottom: Option, + left: Option, + }, +} + +impl Position { + pub fn is_empty(&self) -> bool { + match self { + Self::Absolute { + top, + right, + bottom, + left, + } => top.is_some() && right.is_some() && bottom.is_some() && left.is_some(), + Self::Stacked => true, + } + } + + pub fn new_absolute() -> Self { + Self::Absolute { + top: None, + right: None, + bottom: None, + left: None, + } + } + + pub fn is_absolute(&self) -> bool { + matches!(self, Self::Absolute { .. }) + } + + pub fn set_top(&mut self, value: f32) { + if !self.is_absolute() { + *self = Self::new_absolute(); + } + if let Self::Absolute { top, .. } = self { + *top = Some(value) + } + } + + pub fn set_right(&mut self, value: f32) { + if !self.is_absolute() { + *self = Self::new_absolute(); + } + if let Self::Absolute { right, .. } = self { + *right = Some(value) + } + } + + pub fn set_bottom(&mut self, value: f32) { + if !self.is_absolute() { + *self = Self::new_absolute(); + } + if let Self::Absolute { bottom, .. } = self { + *bottom = Some(value) + } + } + + pub fn set_left(&mut self, value: f32) { + if !self.is_absolute() { + *self = Self::new_absolute(); + } + if let Self::Absolute { left, .. } = self { + *left = Some(value) + } + } + + pub fn get_origin( + &self, + available_parent_area: &Area, + parent_area: &Area, + area_size: &Size2D, + ) -> Point2D { + match self { + Position::Stacked => available_parent_area.origin, + Position::Absolute { + top, + right, + bottom, + left, + } => { + let y = { + let mut y = parent_area.min_y(); + if let Some(top) = top { + y += top; + } else if let Some(bottom) = bottom { + y = parent_area.max_y() - bottom - area_size.height; + } + y + }; + let x = { + let mut x = parent_area.min_x(); + if let Some(left) = left { + x += left; + } else if let Some(right) = right { + x = parent_area.max_x() - right - area_size.width; + } + x + }; + Point2D::new(x, y) + } + } + } +} diff --git a/crates/torin/tests/position.rs b/crates/torin/tests/position.rs new file mode 100644 index 000000000..a9817675c --- /dev/null +++ b/crates/torin/tests/position.rs @@ -0,0 +1,113 @@ +#[cfg(test)] +use torin::{prelude::*, test_utils::*}; + +#[test] +pub fn position() { + let (mut layout, mut measurer) = test_utils(); + + let mut mocked_dom = TestingDOM::default(); + mocked_dom.add( + 0, + None, + vec![1], + Node::from_size_and_padding( + Size::Percentage(Length::new(100.0)), + Size::Percentage(Length::new(100.0)), + Gaps::new(20.0, 20.0, 20.0, 20.0), + ), + ); + mocked_dom.add( + 1, + Some(0), + vec![2, 3, 4, 5], + Node::from_size_and_padding( + Size::Percentage(Length::new(100.0)), + Size::Percentage(Length::new(100.0)), + Gaps::new(30.0, 30.0, 30.0, 30.0), + ), + ); + mocked_dom.add( + 2, + Some(1), + vec![], + Node::from_size_and_position( + Size::Pixels(Length::new(200.0)), + Size::Pixels(Length::new(200.0)), + Position::Absolute { + top: Some(100.0), + right: None, + bottom: None, + left: Some(50.0), + }, + ), + ); + mocked_dom.add( + 3, + Some(1), + vec![], + Node::from_size_and_position( + Size::Pixels(Length::new(200.0)), + Size::Pixels(Length::new(200.0)), + Position::Absolute { + top: Some(100.0), + right: Some(50.0), + bottom: None, + left: None, + }, + ), + ); + mocked_dom.add( + 4, + Some(1), + vec![], + Node::from_size_and_position( + Size::Pixels(Length::new(200.0)), + Size::Pixels(Length::new(200.0)), + Position::Absolute { + top: None, + right: Some(50.0), + bottom: Some(100.0), + left: None, + }, + ), + ); + mocked_dom.add( + 5, + Some(1), + vec![], + Node::from_size_and_position( + Size::Pixels(Length::new(200.0)), + Size::Pixels(Length::new(200.0)), + Position::Absolute { + top: None, + right: None, + bottom: Some(100.0), + left: Some(50.0), + }, + ), + ); + + 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(2).unwrap().area, + Rect::new(Point2D::new(100.0, 150.0), Size2D::new(200.0, 200.0)), + ); + assert_eq!( + layout.get(3).unwrap().area.round(), + Rect::new(Point2D::new(700.0, 150.0), Size2D::new(200.0, 200.0)), + ); + assert_eq!( + layout.get(4).unwrap().area.round(), + Rect::new(Point2D::new(700.0, 650.0), Size2D::new(200.0, 200.0)), + ); + assert_eq!( + layout.get(5).unwrap().area.round(), + Rect::new(Point2D::new(100.0, 650.0), Size2D::new(200.0, 200.0)), + ); +} diff --git a/examples/position.rs b/examples/position.rs new file mode 100644 index 000000000..8f5c06525 --- /dev/null +++ b/examples/position.rs @@ -0,0 +1,51 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +use freya::prelude::*; + +fn main() { + launch_with_props(app, "Position", (400.0, 350.0)); +} + +fn app(cx: Scope) -> Element { + render!( + rect { + height: "100%", + width: "100%", + rect { + height: "20%", + width: "20%", + background: "black", + position: "absolute", + position_top: "10", + position_left: "10", + } + rect { + height: "20%", + width: "20%", + background: "black", + position: "absolute", + position_top: "10", + position_right: "10", + } + rect { + height: "20%", + width: "20%", + background: "black", + position: "absolute", + position_bottom: "10", + position_right: "10", + } + rect { + height: "20%", + width: "20%", + background: "black", + position: "absolute", + position_bottom: "10", + position_left: "10", + } + } + ) +}