adui_dioxus/components/
space.rs1use super::layout_utils::{GapPreset, compose_gap_style, push_gap_preset_class};
2use dioxus::core::DynamicNode;
3use dioxus::prelude::*;
4
5#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
7pub enum SpaceDirection {
8 #[default]
9 Horizontal,
10 Vertical,
11}
12
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15pub enum SpaceAlign {
16 #[default]
17 Start,
18 End,
19 Center,
20 Baseline,
21}
22
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
25pub enum SpaceSize {
26 Small,
27 #[default]
28 Middle,
29 Large,
30}
31
32impl From<SpaceSize> for GapPreset {
33 fn from(value: SpaceSize) -> Self {
34 match value {
35 SpaceSize::Small => GapPreset::Small,
36 SpaceSize::Middle => GapPreset::Middle,
37 SpaceSize::Large => GapPreset::Large,
38 }
39 }
40}
41
42#[derive(Props, Clone, PartialEq)]
44pub struct SpaceProps {
45 #[props(default)]
46 pub direction: SpaceDirection,
47 #[props(default)]
48 pub size: SpaceSize,
49 #[props(optional)]
50 pub gap: Option<f32>,
51 #[props(optional)]
52 pub gap_cross: Option<f32>,
53 #[props(optional)]
54 pub wrap: Option<bool>,
55 #[props(default)]
56 pub align: SpaceAlign,
57 #[props(default)]
58 pub compact: bool,
59 #[props(optional)]
60 pub split: Option<Element>,
61 #[props(optional)]
62 pub class: Option<String>,
63 #[props(optional)]
64 pub style: Option<String>,
65 pub children: Element,
66}
67
68#[component]
71pub fn Space(props: SpaceProps) -> Element {
72 let SpaceProps {
73 direction,
74 size,
75 gap,
76 gap_cross,
77 wrap,
78 align,
79 compact,
80 split,
81 class,
82 style,
83 children,
84 } = props;
85
86 let nodes = collect_children(children)?;
87
88 let should_wrap = wrap.unwrap_or(matches!(direction, SpaceDirection::Horizontal));
89
90 let mut class_list = vec!["adui-space".to_string()];
91 class_list.push(match direction {
92 SpaceDirection::Horizontal => "adui-space-horizontal".into(),
93 SpaceDirection::Vertical => "adui-space-vertical".into(),
94 });
95 if should_wrap && matches!(direction, SpaceDirection::Horizontal) {
96 class_list.push("adui-space-wrap".into());
97 }
98 class_list.push(match align {
99 SpaceAlign::Start => "adui-space-align-start".into(),
100 SpaceAlign::End => "adui-space-align-end".into(),
101 SpaceAlign::Center => "adui-space-align-center".into(),
102 SpaceAlign::Baseline => "adui-space-align-baseline".into(),
103 });
104 if compact {
105 class_list.push("adui-space-compact".into());
106 } else if gap.is_none() && gap_cross.is_none() {
107 push_gap_preset_class(&mut class_list, "adui-space-size", Some(size.into()));
108 }
109 if let Some(extra) = class.as_ref() {
110 class_list.push(extra.clone());
111 }
112 let class_attr = class_list.join(" ");
113 let row_gap_override = if matches!(direction, SpaceDirection::Horizontal) {
114 gap_cross
115 } else {
116 None
117 };
118 let column_gap_override = if matches!(direction, SpaceDirection::Vertical) {
119 gap_cross
120 } else {
121 None
122 };
123 let style_attr = compose_gap_style(style, gap, row_gap_override, column_gap_override);
124
125 if let Some(separator) = split {
126 let sep_node: VNode = separator?;
127 let total = nodes.len();
128 return rsx! {
129 div {
130 class: "{class_attr}",
131 style: "{style_attr}",
132 {
133 nodes
134 .into_iter()
135 .enumerate()
136 .flat_map(move |(idx, child)| {
137 let mut group = vec![child];
138 if idx + 1 != total {
139 group.push(sep_node.clone());
140 }
141 group
142 })
143 .map(|node| rsx!({node}))
144 }
145 }
146 };
147 }
148
149 rsx! {
150 div {
151 class: "{class_attr}",
152 style: "{style_attr}",
153 {nodes.into_iter().map(|node| rsx!({node}))}
154 }
155 }
156}
157
158fn collect_children(children: Element) -> Result<Vec<VNode>, RenderError> {
159 let vnode = children?;
160 if let Some(fragment) = vnode.template.roots.iter().find_map(|root| {
161 root.dynamic_id()
162 .and_then(|id| match &vnode.dynamic_nodes[id] {
163 DynamicNode::Fragment(list) => Some(list.clone()),
164 _ => None,
165 })
166 }) {
167 return Ok(fragment);
168 }
169
170 Ok(vec![vnode])
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn collect_children_handles_static_roots_with_fallback() {
179 let nodes = collect_children(rsx! {
180 div { "one" }
181 div { "two" }
182 })
183 .unwrap();
184 assert_eq!(nodes.len(), 1);
185 }
186
187 #[test]
188 fn collect_children_handles_single_node() {
189 let nodes = collect_children(rsx!(div { "one" })).unwrap();
190 assert_eq!(nodes.len(), 1);
191 }
192
193 #[test]
194 fn collect_children_extracts_dynamic_fragment() {
195 let nodes = collect_children(rsx! {
196 {(0..2).map(|idx| rsx!(div { "{idx}" }))}
197 })
198 .unwrap();
199 assert_eq!(nodes.len(), 2);
200 }
201}