diff --git a/crates/components/src/dropdown.rs b/crates/components/src/dropdown.rs index 6bd2aa8af..fee1750cf 100644 --- a/crates/components/src/dropdown.rs +++ b/crates/components/src/dropdown.rs @@ -243,6 +243,7 @@ where let DropdownTheme { width, + margin, font_theme, dropdown_background, background_button, @@ -267,7 +268,7 @@ where onmouseleave, onclick, onkeydown, - margin: "4", + margin: "{margin}", focus_id, background: "{button_background}", color: "{font_theme.color}", @@ -299,7 +300,7 @@ where onglobalclick, onkeydown, layer: "-99", - margin: "4", + margin: "{margin}", border: "1 solid {border_fill}", overflow: "clip", corner_radius: "8", diff --git a/crates/components/src/link.rs b/crates/components/src/link.rs index 15ecb295b..08848c504 100644 --- a/crates/components/src/link.rs +++ b/crates/components/src/link.rs @@ -279,7 +279,7 @@ mod test { // Go to the "Somewhere" route utils.push_event(PlatformEvent::Mouse { name: EventName::Click, - cursor: (5., 70.).into(), + cursor: (5., 60.).into(), button: Some(MouseButton::Left), }); diff --git a/crates/components/src/scroll_views/scroll_view.rs b/crates/components/src/scroll_views/scroll_view.rs index 6e7243a0f..6312f393b 100644 --- a/crates/components/src/scroll_views/scroll_view.rs +++ b/crates/components/src/scroll_views/scroll_view.rs @@ -130,6 +130,7 @@ pub fn ScrollView(props: ScrollViewProps) -> Element { let theme = use_applied_theme!(&props.theme, scroll_view); let scrollbar_theme = use_applied_theme!(&props.scrollbar_theme, scroll_bar); + let spacing = &theme.spacing; let padding = &theme.padding; let user_container_width = &theme.width; let user_container_height = &theme.height; @@ -359,6 +360,7 @@ pub fn ScrollView(props: ScrollViewProps) -> Element { height: "{container_height}", rect { overflow: "clip", + spacing: "{spacing}", padding: "{padding}", height: "100%", width: "100%", diff --git a/crates/components/src/sidebar.rs b/crates/components/src/sidebar.rs index 4c606b9e9..f72acced0 100644 --- a/crates/components/src/sidebar.rs +++ b/crates/components/src/sidebar.rs @@ -29,6 +29,7 @@ pub fn Sidebar( sidebar: Element, ) -> Element { let SidebarTheme { + spacing, font_theme, background, } = use_applied_theme!(&theme, sidebar); @@ -48,6 +49,7 @@ pub fn Sidebar( ScrollView { theme: theme_with!(ScrollViewTheme { padding: "8".into(), + spacing: spacing, }), {sidebar} } @@ -74,6 +76,7 @@ pub fn SidebarItem( onclick: Option>, ) -> Element { let SidebarItemTheme { + margin, hover_background, background, font_theme, @@ -113,7 +116,7 @@ pub fn SidebarItem( rsx!( rect { overflow: "clip", - margin: "4 0", + margin: "{margin}", onclick, onmouseenter, onmouseleave, diff --git a/crates/components/src/switch.rs b/crates/components/src/switch.rs index e4eeb21e0..86dbf31da 100644 --- a/crates/components/src/switch.rs +++ b/crates/components/src/switch.rs @@ -148,7 +148,7 @@ pub fn Switch(props: SwitchProps) -> Element { rsx!( rect { - margin: "1.5", + margin: "{theme.margin}", width: "50", height: "25", padding: "1", diff --git a/crates/core/src/dom/dom_adapter.rs b/crates/core/src/dom/dom_adapter.rs index b085274ac..8ae793b60 100644 --- a/crates/core/src/dom/dom_adapter.rs +++ b/crates/core/src/dom/dom_adapter.rs @@ -62,6 +62,7 @@ impl DOMAdapter for DioxusDOMAdapter<'_> { position: layout.position, content: layout.content, contains_text, + spacing: layout.spacing, }; node.scale(self.scale_factor); diff --git a/crates/elements/src/_docs/attributes/spacing.md b/crates/elements/src/_docs/attributes/spacing.md new file mode 100644 index 000000000..1d802d24b --- /dev/null +++ b/crates/elements/src/_docs/attributes/spacing.md @@ -0,0 +1,35 @@ +Specify a space between the inner elements. Think it as a margin for every element but defined by its parent. +It only applies to the side of the direction. + +### Example + +```rust, no_run +# use freya::prelude::*; +fn app() -> Element { + rsx!( + rect { + direction: "vertical", + spacing: "20", + // Not before + rect { + width: "100", + height: "100", + background: "red", + } + // There will be a space between these two elements of 20 pixels + rect { + width: "100", + height: "100", + background: "blue", + } + // Here as well + rect { + width: "100", + height: "100", + background: "green", + } + // But not after + } + ) +} +``` \ No newline at end of file diff --git a/crates/elements/src/definitions.rs b/crates/elements/src/definitions.rs index fe1b21176..d76f9e177 100644 --- a/crates/elements/src/definitions.rs +++ b/crates/elements/src/definitions.rs @@ -230,6 +230,8 @@ builder_constructors! { content: String, #[doc = include_str!("_docs/attributes/line_height.md")] line_height: String, + #[doc = include_str!("_docs/attributes/spacing.md")] + spacing: String, name: String, focusable: String, diff --git a/crates/hooks/src/theming/dark.rs b/crates/hooks/src/theming/dark.rs index 97b9288fa..afb09f28a 100644 --- a/crates/hooks/src/theming/dark.rs +++ b/crates/hooks/src/theming/dark.rs @@ -47,6 +47,7 @@ pub const DARK_THEME: Theme = Theme { shadow: LIGHT_THEME.input.shadow, }, switch: SwitchTheme { + margin: LIGHT_THEME.input.margin, background: cow_borrowed!("rgb(60, 60, 60)"), thumb_background: cow_borrowed!("rgb(200, 200, 200)"), enabled_background: cow_borrowed!("rgb(255, 95, 0)"), @@ -65,6 +66,7 @@ pub const DARK_THEME: Theme = Theme { height: LIGHT_THEME.scroll_view.height, width: LIGHT_THEME.scroll_view.width, padding: LIGHT_THEME.scroll_view.padding, + spacing: LIGHT_THEME.scroll_view.spacing, }, tooltip: TooltipTheme { background: cow_borrowed!("rgb(35,35,35)"), @@ -73,6 +75,7 @@ pub const DARK_THEME: Theme = Theme { }, dropdown: DropdownTheme { width: LIGHT_THEME.dropdown.width, + margin: LIGHT_THEME.dropdown.margin, dropdown_background: cow_borrowed!("rgb(25, 25, 25)"), background_button: cow_borrowed!("rgb(35, 35, 35)"), hover_background: cow_borrowed!("rgb(45, 45, 45)"), @@ -140,12 +143,14 @@ pub const DARK_THEME: Theme = Theme { margin: LIGHT_THEME.icon.margin, }, sidebar: SidebarTheme { + spacing: LIGHT_THEME.sidebar.spacing, background: cow_borrowed!("rgb(20, 20, 20)"), font_theme: FontTheme { color: cow_borrowed!("white"), }, }, sidebar_item: SidebarItemTheme { + margin: LIGHT_THEME.sidebar_item.margin, background: cow_borrowed!("transparent"), hover_background: cow_borrowed!("rgb(45, 45, 45)"), font_theme: FontTheme { diff --git a/crates/hooks/src/theming/light.rs b/crates/hooks/src/theming/light.rs index f19bc32c1..6febf61f3 100644 --- a/crates/hooks/src/theming/light.rs +++ b/crates/hooks/src/theming/light.rs @@ -26,7 +26,7 @@ pub const LIGHT_THEME: Theme = Theme { focus_border_fill: cow_borrowed!("rgb(180, 180, 180)"), shadow: cow_borrowed!("0 4 5 0 rgb(0, 0, 0, 0.1)"), padding: cow_borrowed!("8 12"), - margin: cow_borrowed!("4"), + margin: cow_borrowed!("0"), corner_radius: cow_borrowed!("8"), width: cow_borrowed!("auto"), height: cow_borrowed!("auto"), @@ -42,11 +42,12 @@ pub const LIGHT_THEME: Theme = Theme { }, border_fill: cow_borrowed!("rgb(210, 210, 210)"), width: cow_borrowed!("150"), - margin: cow_borrowed!("4"), + margin: cow_borrowed!("0"), corner_radius: cow_borrowed!("10"), shadow: cow_borrowed!("0 4 5 0 rgb(0, 0, 0, 0.1)"), }, switch: SwitchTheme { + margin: cow_borrowed!("0"), background: cow_borrowed!("rgb(121, 116, 126)"), thumb_background: cow_borrowed!("rgb(231, 224, 236)"), enabled_background: cow_borrowed!("rgb(103, 80, 164)"), @@ -65,6 +66,7 @@ pub const LIGHT_THEME: Theme = Theme { height: cow_borrowed!("fill"), width: cow_borrowed!("fill"), padding: cow_borrowed!("0"), + spacing: cow_borrowed!("0"), }, tooltip: TooltipTheme { background: cow_borrowed!("rgb(245, 245, 245)"), @@ -73,6 +75,7 @@ pub const LIGHT_THEME: Theme = Theme { }, dropdown: DropdownTheme { width: cow_borrowed!("auto"), + margin: cow_borrowed!("0"), dropdown_background: cow_borrowed!("white"), background_button: cow_borrowed!("rgb(245, 245, 245)"), hover_background: cow_borrowed!("rgb(235, 235, 235)"), @@ -137,15 +140,17 @@ pub const LIGHT_THEME: Theme = Theme { icon: IconTheme { width: cow_borrowed!("10"), height: cow_borrowed!("10"), - margin: cow_borrowed!("none"), + margin: cow_borrowed!("0"), }, sidebar: SidebarTheme { + spacing: cow_borrowed!("4"), background: cow_borrowed!("rgb(245, 245, 245)"), font_theme: FontTheme { color: cow_borrowed!("rgb(10, 10, 10)"), }, }, sidebar_item: SidebarItemTheme { + margin: cow_borrowed!("0"), background: cow_borrowed!("transparent"), hover_background: cow_borrowed!("rgb(230, 230, 230)"), font_theme: FontTheme { diff --git a/crates/hooks/src/theming/mod.rs b/crates/hooks/src/theming/mod.rs index 6fe924c38..4b327481b 100644 --- a/crates/hooks/src/theming/mod.rs +++ b/crates/hooks/src/theming/mod.rs @@ -225,6 +225,7 @@ define_theme! { pub Dropdown { %[cows] width: str, + margin: str, dropdown_background: str, background_button: str, hover_background: str, @@ -295,6 +296,7 @@ define_theme! { %[component] pub Switch { %[cows] + margin: str, background: str, thumb_background: str, enabled_background: str, @@ -323,6 +325,7 @@ define_theme! { %[cows] height: str, width: str, + spacing: str, padding: str, } } @@ -455,6 +458,7 @@ define_theme! { %[component] pub Sidebar { %[cows] + spacing: str, background: str, %[subthemes] font_theme: FontTheme, @@ -465,6 +469,7 @@ define_theme! { %[component] pub SidebarItem { %[cows] + margin: str, background: str, hover_background: str, %[subthemes] diff --git a/crates/native-core/src/attributes.rs b/crates/native-core/src/attributes.rs index d853942f4..d5ebb3083 100644 --- a/crates/native-core/src/attributes.rs +++ b/crates/native-core/src/attributes.rs @@ -66,6 +66,7 @@ pub enum AttributeName { ImageData, SvgData, SvgContent, + Spacing, } impl FromStr for AttributeName { @@ -137,6 +138,7 @@ impl FromStr for AttributeName { "image_data" => Ok(AttributeName::ImageData), "svg_data" => Ok(AttributeName::SvgData), "svg_content" => Ok(AttributeName::SvgContent), + "spacing" => Ok(AttributeName::Spacing), _ => Err(format!("{attr} not supported.")), } } diff --git a/crates/state/src/layout.rs b/crates/state/src/layout.rs index b8be67a31..3aceb5b05 100644 --- a/crates/state/src/layout.rs +++ b/crates/state/src/layout.rs @@ -48,6 +48,7 @@ pub struct LayoutState { pub content: Content, pub node_ref: Option, pub node_id: NodeId, + pub spacing: Length, } impl ParseAttribute for LayoutState { @@ -167,6 +168,11 @@ impl ParseAttribute for LayoutState { self.node_ref = Some(reference.clone()); } } + AttributeName::Spacing => { + if let Some(value) = attr.value.as_text() { + self.spacing = Length::new(value.parse::().map_err(|_| ParseError)?); + } + } _ => {} } Ok(()) @@ -203,6 +209,7 @@ impl State for LayoutState { AttributeName::PositionBottom, AttributeName::PositionLeft, AttributeName::Content, + AttributeName::Spacing, ])); fn update<'a>( diff --git a/crates/torin/src/measure.rs b/crates/torin/src/measure.rs index c9d6b803f..fd4a4bb76 100644 --- a/crates/torin/src/measure.rs +++ b/crates/torin/src/measure.rs @@ -307,7 +307,7 @@ where let mut initial_phase_available_area = *available_area; // Measure the children - for child_id in &children { + for (child_n, child_id) in children.iter().enumerate() { let Some(child_data) = self.dom_adapter.get_node(child_id) else { continue; }; @@ -339,6 +339,8 @@ where &mut initial_phase_inner_sizes, &child_areas.area, &child_data, + children.len(), + child_n, ); if parent_node.cross_alignment.is_not_start() @@ -382,6 +384,7 @@ where } let initial_available_area = *available_area; + let children_len = children.len(); // Final phase: measure the children with all the axis and sizes adjusted for (child_n, child_id) in children.into_iter().enumerate() { @@ -444,6 +447,8 @@ where inner_sizes, &child_areas.area, &child_data, + children_len, + child_n, ); // Cache the child layout if it was mutated and children must be cached @@ -476,7 +481,6 @@ where AlignAxis::Height => match alignment { Alignment::Center => { let new_origin_y = (inner_area.height() / 2.0) - (contents_size.height / 2.0); - available_area.origin.y = inner_area.min_y() + new_origin_y; } Alignment::End => { @@ -562,6 +566,7 @@ where } /// Stack a child Node into its parent + #[allow(clippy::too_many_arguments)] fn stack_child( available_area: &mut Area, parent_node: &Node, @@ -570,20 +575,27 @@ where inner_sizes: &mut Size2D, child_area: &Area, child_node: &Node, + siblings_len: usize, + child_position: usize, ) { // No need to stack a node that is positioned absolutely if child_node.position.is_absolute() { return; } + // Only apply the spacing to elements after `i > 0` and `i < len - 1` + let spacing = (child_position < siblings_len - 1) + .then_some(parent_node.spacing) + .unwrap_or_default(); + match parent_node.direction { DirectionMode::Horizontal => { // Move the available area - available_area.origin.x = child_area.max_x(); - available_area.size.width -= child_area.size.width; + available_area.origin.x = child_area.max_x() + spacing.get(); + 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(); + inner_sizes.width += child_area.width() + spacing.get(); // Keep the biggest height if parent_node.height.inner_sized() { @@ -600,16 +612,16 @@ where // Accumulate width if parent_node.width.inner_sized() { - parent_area.size.width += child_area.size.width; + parent_area.size.width += child_area.size.width + spacing.get(); } } DirectionMode::Vertical => { // Move the available area - available_area.origin.y = child_area.max_y(); - available_area.size.height -= child_area.size.height; + available_area.origin.y = child_area.max_y() + spacing.get(); + 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(); + inner_sizes.height += child_area.height() + spacing.get(); // Keep the biggest width if parent_node.width.inner_sized() { @@ -626,7 +638,7 @@ where // Accumulate height if parent_node.height.inner_sized() { - parent_area.size.height += child_area.size.height; + parent_area.size.height += child_area.size.height + spacing.get(); } } } diff --git a/crates/torin/src/node.rs b/crates/torin/src/node.rs index 204c4978b..259cd37ff 100644 --- a/crates/torin/src/node.rs +++ b/crates/torin/src/node.rs @@ -54,6 +54,8 @@ pub struct Node { pub has_layout_references: bool, pub contains_text: bool, + + pub spacing: Length, } impl Scaled for Node { @@ -69,6 +71,7 @@ impl Scaled for Node { self.offset_x *= scale_factor; self.offset_y *= scale_factor; self.position.scale(scale_factor); + self.spacing *= scale_factor; } } @@ -198,6 +201,22 @@ impl Node { } } + /// Construct a new Node given a size and spacing + pub fn from_size_and_direction_and_spacing( + width: Size, + height: Size, + direction: DirectionMode, + spacing: Length, + ) -> Self { + Self { + width, + height, + direction, + spacing, + ..Default::default() + } + } + /// Has properties that depend on the inner Nodes? pub fn does_depend_on_inner(&self) -> bool { self.width.inner_sized() diff --git a/crates/torin/tests/spacing.rs b/crates/torin/tests/spacing.rs new file mode 100644 index 000000000..9cbb67e41 --- /dev/null +++ b/crates/torin/tests/spacing.rs @@ -0,0 +1,90 @@ +use torin::{ + prelude::*, + test_utils::*, +}; + +#[test] +pub fn spacing() { + 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_direction_and_spacing( + Size::Fill, + Size::Fill, + DirectionMode::Vertical, + Length::new(40.0), + ), + ); + mocked_dom.add( + 1, + Some(0), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(200.0)), + Size::Pixels(Length::new(200.0)), + DirectionMode::Horizontal, + ), + ); + mocked_dom.add( + 2, + Some(0), + vec![3, 4], + Node::from_size_and_direction_and_spacing( + Size::Pixels(Length::new(600.0)), + Size::Pixels(Length::new(600.0)), + DirectionMode::Horizontal, + Length::new(50.0), + ), + ); + mocked_dom.add( + 3, + Some(2), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(300.0)), + Size::Pixels(Length::new(300.0)), + DirectionMode::Horizontal, + ), + ); + mocked_dom.add( + 4, + Some(2), + vec![], + Node::from_size_and_direction( + Size::Pixels(Length::new(200.0)), + Size::Pixels(Length::new(200.0)), + DirectionMode::Horizontal, + ), + ); + + 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(1).unwrap().area, + Rect::new(Point2D::new(0.0, 0.0), Size2D::new(200.0, 200.0)), + ); + + assert_eq!( + layout.get(2).unwrap().area, + Rect::new(Point2D::new(0.0, 240.0), Size2D::new(600.0, 600.0)), + ); + + assert_eq!( + layout.get(3).unwrap().area, + Rect::new(Point2D::new(0.0, 240.0), Size2D::new(300.0, 300.0)), + ); + + assert_eq!( + layout.get(4).unwrap().area, + Rect::new(Point2D::new(350.0, 240.0), Size2D::new(200.0, 200.0)), + ); +} diff --git a/examples/counter.rs b/examples/counter.rs index 25160e129..5e19d480a 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -33,6 +33,7 @@ fn app() -> Element { main_align: "center", cross_align: "center", direction: "horizontal", + spacing: "8", Button { onclick: move |_| count += 1, label { "Increase" } diff --git a/examples/popup.rs b/examples/popup.rs index 43bf7b436..e0bc54f1f 100644 --- a/examples/popup.rs +++ b/examples/popup.rs @@ -30,21 +30,24 @@ fn app() -> Element { } } PopupContent { - label { - "Change the input value:" - } - Input { - value, - onchange: move |text| { - value.set(text); - } - } - Button { - onclick: move |_| { - show_popup.set(false) - }, + rect { + spacing: "10", label { - "Submit" + "Change the input value:" + } + Input { + value, + onchange: move |text| { + value.set(text); + } + } + Button { + onclick: move |_| { + show_popup.set(false) + }, + label { + "Submit" + } } } } diff --git a/examples/spacing.rs b/examples/spacing.rs new file mode 100644 index 000000000..cab01b470 --- /dev/null +++ b/examples/spacing.rs @@ -0,0 +1,51 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +use freya::prelude::*; + +fn main() { + launch_with_title(app, "Spacing"); +} + +fn app() -> Element { + rsx!( + rect { + direction: "horizontal", + main_align: "center", + cross_align: "center", + spacing: "5", + rect { + direction: "vertical", + cross_align: "center", + spacing: "20", + rect { + background: "red", + width: "100", + height: "100" + } + rect { + background: "green", + width: "100", + height: "100" + } + } + rect { + direction: "vertical", + main_align: "center", + spacing: "50", + rect { + background: "blue", + width: "100", + height: "100" + } + rect { + background: "black", + width: "100", + height: "100" + } + } + } + ) +}