1use 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
202 #[test]
203 fn space_direction_default() {
204 assert_eq!(SpaceDirection::default(), SpaceDirection::Horizontal);
205 }
206
207 #[test]
208 fn space_direction_variants() {
209 assert_eq!(SpaceDirection::Horizontal, SpaceDirection::Horizontal);
210 assert_eq!(SpaceDirection::Vertical, SpaceDirection::Vertical);
211 assert_ne!(SpaceDirection::Horizontal, SpaceDirection::Vertical);
212 }
213
214 #[test]
215 fn space_direction_clone_and_copy() {
216 let dir1 = SpaceDirection::Horizontal;
217 let dir2 = dir1; assert_eq!(dir1, dir2);
219 }
220
221 #[test]
222 fn space_direction_debug() {
223 let debug_str = format!("{:?}", SpaceDirection::Horizontal);
224 assert!(debug_str.contains("Horizontal"));
225
226 let debug_str2 = format!("{:?}", SpaceDirection::Vertical);
227 assert!(debug_str2.contains("Vertical"));
228 }
229
230 #[test]
231 fn space_align_default() {
232 assert_eq!(SpaceAlign::default(), SpaceAlign::Start);
233 }
234
235 #[test]
236 fn space_align_variants() {
237 assert_eq!(SpaceAlign::Start, SpaceAlign::Start);
238 assert_eq!(SpaceAlign::End, SpaceAlign::End);
239 assert_eq!(SpaceAlign::Center, SpaceAlign::Center);
240 assert_eq!(SpaceAlign::Baseline, SpaceAlign::Baseline);
241 assert_ne!(SpaceAlign::Start, SpaceAlign::End);
242 assert_ne!(SpaceAlign::Start, SpaceAlign::Center);
243 assert_ne!(SpaceAlign::Start, SpaceAlign::Baseline);
244 }
245
246 #[test]
247 fn space_align_clone_and_copy() {
248 let align1 = SpaceAlign::Center;
249 let align2 = align1; assert_eq!(align1, align2);
251 }
252
253 #[test]
254 fn space_align_debug() {
255 let debug_str = format!("{:?}", SpaceAlign::Start);
256 assert!(debug_str.contains("Start"));
257
258 let debug_str2 = format!("{:?}", SpaceAlign::Baseline);
259 assert!(debug_str2.contains("Baseline"));
260 }
261
262 #[test]
263 fn space_size_default() {
264 assert_eq!(SpaceSize::default(), SpaceSize::Middle);
265 }
266
267 #[test]
268 fn space_size_variants() {
269 assert_eq!(SpaceSize::Small, SpaceSize::Small);
270 assert_eq!(SpaceSize::Middle, SpaceSize::Middle);
271 assert_eq!(SpaceSize::Large, SpaceSize::Large);
272 assert_ne!(SpaceSize::Small, SpaceSize::Middle);
273 assert_ne!(SpaceSize::Middle, SpaceSize::Large);
274 assert_ne!(SpaceSize::Small, SpaceSize::Large);
275 }
276
277 #[test]
278 fn space_size_clone_and_copy() {
279 let size1 = SpaceSize::Large;
280 let size2 = size1; assert_eq!(size1, size2);
282 }
283
284 #[test]
285 fn space_size_debug() {
286 let debug_str = format!("{:?}", SpaceSize::Small);
287 assert!(debug_str.contains("Small"));
288
289 let debug_str2 = format!("{:?}", SpaceSize::Large);
290 assert!(debug_str2.contains("Large"));
291 }
292
293 #[test]
294 fn space_size_to_gap_preset_conversion() {
295 let _gap1: GapPreset = SpaceSize::Small.into();
298 let _gap2: GapPreset = SpaceSize::Middle.into();
299 let _gap3: GapPreset = SpaceSize::Large.into();
300 assert!(true);
302 }
303
304 #[test]
305 fn space_size_into_gap_preset_all_variants() {
306 let _gap1: GapPreset = SpaceSize::Small.into();
308 let _gap2: GapPreset = SpaceSize::Middle.into();
309 let _gap3: GapPreset = SpaceSize::Large.into();
310 assert!(true);
312 }
313
314 #[test]
315 fn space_props_defaults() {
316 let direction_default = SpaceDirection::default();
320 assert_eq!(direction_default, SpaceDirection::Horizontal);
321
322 let size_default = SpaceSize::default();
323 assert_eq!(size_default, SpaceSize::Middle);
324
325 let align_default = SpaceAlign::default();
326 assert_eq!(align_default, SpaceAlign::Start);
327
328 let compact_default = false;
329 assert_eq!(compact_default, false);
330 }
331
332 #[test]
333 fn space_props_optional_fields() {
334 let _gap: Option<f32> = None;
336 let _gap_cross: Option<f32> = None;
337 let _wrap: Option<bool> = None;
338 let _split: Option<Element> = None;
339 let _class: Option<String> = None;
340 let _style: Option<String> = None;
341 assert!(true);
343 }
344}