adui_dioxus/components/
flex.rs

1use super::layout_utils::{GapPreset, compose_gap_style, push_gap_preset_class};
2use dioxus::prelude::*;
3
4/// Orientation helper used by design tokens.
5#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
6pub enum FlexOrientation {
7    #[default]
8    Horizontal,
9    Vertical,
10}
11
12/// Root element type for the flex container.
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
14pub enum FlexComponent {
15    #[default]
16    Div,
17    Section,
18    Article,
19    Nav,
20    Span,
21}
22
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
24pub enum FlexDirection {
25    #[default]
26    Row,
27    RowReverse,
28    Column,
29    ColumnReverse,
30}
31
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
33pub enum FlexJustify {
34    #[default]
35    Start,
36    End,
37    Center,
38    Between,
39    Around,
40    Evenly,
41}
42
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
44pub enum FlexAlign {
45    Start,
46    End,
47    Center,
48    #[default]
49    Stretch,
50    Baseline,
51}
52
53#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
54pub enum FlexWrap {
55    #[default]
56    NoWrap,
57    Wrap,
58    WrapReverse,
59}
60
61/// Preset gap sizes aligned with design tokens.
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63pub enum FlexGap {
64    Small,
65    Middle,
66    Large,
67}
68
69impl From<FlexGap> for GapPreset {
70    fn from(value: FlexGap) -> Self {
71        match value {
72            FlexGap::Small => GapPreset::Small,
73            FlexGap::Middle => GapPreset::Middle,
74            FlexGap::Large => GapPreset::Large,
75        }
76    }
77}
78
79/// Shared configuration provided via context.
80#[derive(Clone, Debug, Default, PartialEq)]
81pub struct FlexSharedConfig {
82    pub class: Option<String>,
83    pub style: Option<String>,
84    pub vertical: Option<bool>,
85}
86
87/// Provide flex configuration to descendants (mirrors Ant Design ConfigProvider.flex).
88#[derive(Props, Clone, PartialEq)]
89pub struct FlexConfigProviderProps {
90    pub value: FlexSharedConfig,
91    pub children: Element,
92}
93
94#[component]
95pub fn FlexConfigProvider(props: FlexConfigProviderProps) -> Element {
96    let FlexConfigProviderProps { value, children } = props;
97    use_context_provider(|| value);
98    children
99}
100
101#[derive(Props, Clone, PartialEq)]
102pub struct FlexProps {
103    #[props(default)]
104    pub direction: FlexDirection,
105    #[props(default)]
106    pub justify: FlexJustify,
107    #[props(default)]
108    pub align: FlexAlign,
109    #[props(default)]
110    pub wrap: FlexWrap,
111    #[props(optional)]
112    pub orientation: Option<FlexOrientation>,
113    #[props(default)]
114    pub vertical: bool,
115    #[props(default)]
116    pub component: FlexComponent,
117    #[props(optional)]
118    pub gap: Option<f32>,
119    #[props(optional)]
120    pub row_gap: Option<f32>,
121    #[props(optional)]
122    pub column_gap: Option<f32>,
123    #[props(optional)]
124    pub gap_size: Option<FlexGap>,
125    #[props(optional)]
126    pub class: Option<String>,
127    #[props(optional)]
128    pub style: Option<String>,
129    pub children: Element,
130}
131
132/// Flexible box container with configurable alignment and wrapping.
133#[component]
134pub fn Flex(props: FlexProps) -> Element {
135    let FlexProps {
136        direction,
137        justify,
138        align,
139        wrap,
140        orientation,
141        vertical,
142        component,
143        gap,
144        row_gap,
145        column_gap,
146        gap_size,
147        class,
148        style,
149        children,
150    } = props;
151
152    let inherited = try_use_context::<FlexSharedConfig>();
153    let inherited_vertical = inherited.as_ref().and_then(|ctx| ctx.vertical);
154    let resolved_direction =
155        compute_direction(direction, orientation, vertical, inherited_vertical);
156
157    let mut class_list = base_classes(resolved_direction, wrap, justify, align);
158    if let Some(ctx) = inherited.as_ref()
159        && let Some(extra) = ctx.class.as_ref()
160    {
161        class_list.push(extra.clone());
162    }
163    if let Some(extra) = class.as_ref() {
164        class_list.push(extra.clone());
165    }
166    if gap.is_none() && row_gap.is_none() && column_gap.is_none() {
167        let preset = gap_size.map(Into::into);
168        push_gap_preset_class(&mut class_list, "adui-flex-gap", preset);
169    }
170    let class_attr = class_list.join(" ");
171
172    let mut base_style = String::new();
173    if let Some(ctx) = inherited.as_ref()
174        && let Some(st) = ctx.style.as_ref()
175    {
176        base_style.push_str(st);
177    }
178    if let Some(extra) = style {
179        base_style.push_str(&extra);
180    }
181    let base_style_opt = if base_style.is_empty() {
182        None
183    } else {
184        Some(base_style)
185    };
186    let style_attr = compose_gap_style(base_style_opt, gap, row_gap, column_gap);
187
188    render_component(component, &class_attr, &style_attr, &children)
189}
190
191fn render_component(
192    component: FlexComponent,
193    class_attr: &str,
194    style_attr: &str,
195    children: &Element,
196) -> Element {
197    match component {
198        FlexComponent::Div => {
199            rsx!(div { class: "{class_attr}", style: "{style_attr}", {children.clone()} })
200        }
201        FlexComponent::Section => {
202            rsx!(section { class: "{class_attr}", style: "{style_attr}", {children.clone()} })
203        }
204        FlexComponent::Article => {
205            rsx!(article { class: "{class_attr}", style: "{style_attr}", {children.clone()} })
206        }
207        FlexComponent::Nav => {
208            rsx!(nav { class: "{class_attr}", style: "{style_attr}", {children.clone()} })
209        }
210        FlexComponent::Span => {
211            rsx!(span { class: "{class_attr}", style: "{style_attr}", {children.clone()} })
212        }
213    }
214}
215
216fn compute_direction(
217    explicit: FlexDirection,
218    orientation: Option<FlexOrientation>,
219    vertical_flag: bool,
220    inherited_vertical: Option<bool>,
221) -> FlexDirection {
222    if let Some(orientation) = orientation {
223        return match orientation {
224            FlexOrientation::Horizontal => FlexDirection::Row,
225            FlexOrientation::Vertical => FlexDirection::Column,
226        };
227    }
228
229    if let Some(flag) = inherited_vertical
230        && flag
231        && matches!(explicit, FlexDirection::Row | FlexDirection::RowReverse)
232    {
233        return FlexDirection::Column;
234    }
235
236    if vertical_flag && matches!(explicit, FlexDirection::Row | FlexDirection::RowReverse) {
237        return FlexDirection::Column;
238    }
239
240    explicit
241}
242
243fn base_classes(
244    direction: FlexDirection,
245    wrap: FlexWrap,
246    justify: FlexJustify,
247    align: FlexAlign,
248) -> Vec<String> {
249    let mut classes = vec!["adui-flex".to_string()];
250    match direction {
251        FlexDirection::Row => classes.push("adui-flex-horizontal".into()),
252        FlexDirection::RowReverse => {
253            classes.push("adui-flex-horizontal".into());
254            classes.push("adui-flex-row-reverse".into());
255        }
256        FlexDirection::Column => classes.push("adui-flex-vertical".into()),
257        FlexDirection::ColumnReverse => {
258            classes.push("adui-flex-vertical".into());
259            classes.push("adui-flex-column-reverse".into());
260        }
261    }
262
263    classes.push(match wrap {
264        FlexWrap::NoWrap => "adui-flex-wrap-nowrap".into(),
265        FlexWrap::Wrap => "adui-flex-wrap-wrap".into(),
266        FlexWrap::WrapReverse => "adui-flex-wrap-wrap-reverse".into(),
267    });
268
269    classes.push(match justify {
270        FlexJustify::Start => "adui-flex-justify-start".into(),
271        FlexJustify::End => "adui-flex-justify-end".into(),
272        FlexJustify::Center => "adui-flex-justify-center".into(),
273        FlexJustify::Between => "adui-flex-justify-between".into(),
274        FlexJustify::Around => "adui-flex-justify-around".into(),
275        FlexJustify::Evenly => "adui-flex-justify-evenly".into(),
276    });
277
278    classes.push(match align {
279        FlexAlign::Start => "adui-flex-align-start".into(),
280        FlexAlign::End => "adui-flex-align-end".into(),
281        FlexAlign::Center => "adui-flex-align-center".into(),
282        FlexAlign::Stretch => "adui-flex-align-stretch".into(),
283        FlexAlign::Baseline => "adui-flex-align-baseline".into(),
284    });
285
286    classes
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292
293    #[test]
294    fn flex_direction_variants() {
295        assert_eq!(FlexDirection::default(), FlexDirection::Row);
296        assert_ne!(FlexDirection::Row, FlexDirection::Column);
297        assert_ne!(FlexDirection::RowReverse, FlexDirection::ColumnReverse);
298    }
299
300    #[test]
301    fn flex_justify_variants() {
302        assert_eq!(FlexJustify::default(), FlexJustify::Start);
303        assert_ne!(FlexJustify::Start, FlexJustify::End);
304        assert_ne!(FlexJustify::Center, FlexJustify::Between);
305    }
306
307    #[test]
308    fn flex_align_variants() {
309        assert_eq!(FlexAlign::default(), FlexAlign::Stretch);
310        assert_ne!(FlexAlign::Start, FlexAlign::End);
311        assert_ne!(FlexAlign::Center, FlexAlign::Baseline);
312    }
313
314    #[test]
315    fn flex_wrap_variants() {
316        assert_eq!(FlexWrap::default(), FlexWrap::NoWrap);
317        assert_ne!(FlexWrap::NoWrap, FlexWrap::Wrap);
318        assert_ne!(FlexWrap::Wrap, FlexWrap::WrapReverse);
319    }
320
321    #[test]
322    fn flex_component_variants() {
323        assert_eq!(FlexComponent::default(), FlexComponent::Div);
324        assert_ne!(FlexComponent::Div, FlexComponent::Section);
325        assert_ne!(FlexComponent::Article, FlexComponent::Nav);
326    }
327
328    #[test]
329    fn flex_orientation_variants() {
330        assert_eq!(FlexOrientation::default(), FlexOrientation::Horizontal);
331        assert_ne!(FlexOrientation::Horizontal, FlexOrientation::Vertical);
332    }
333
334    #[test]
335    fn flex_gap_conversion() {
336        // Test that conversion works without panicking
337        let _small: GapPreset = FlexGap::Small.into();
338        let _middle: GapPreset = FlexGap::Middle.into();
339        let _large: GapPreset = FlexGap::Large.into();
340    }
341
342    #[test]
343    fn flex_shared_config_defaults() {
344        let config = FlexSharedConfig::default();
345        assert_eq!(config.class, None);
346        assert_eq!(config.style, None);
347        assert_eq!(config.vertical, None);
348    }
349
350    #[test]
351    fn compute_direction_with_orientation() {
352        // Orientation takes priority
353        assert_eq!(
354            compute_direction(
355                FlexDirection::Row,
356                Some(FlexOrientation::Vertical),
357                false,
358                None
359            ),
360            FlexDirection::Column
361        );
362        assert_eq!(
363            compute_direction(
364                FlexDirection::Column,
365                Some(FlexOrientation::Horizontal),
366                true,
367                None
368            ),
369            FlexDirection::Row
370        );
371    }
372
373    #[test]
374    fn compute_direction_with_vertical_flag() {
375        // Vertical flag converts Row to Column
376        assert_eq!(
377            compute_direction(FlexDirection::Row, None, true, None),
378            FlexDirection::Column
379        );
380        // But doesn't affect Column
381        assert_eq!(
382            compute_direction(FlexDirection::Column, None, true, None),
383            FlexDirection::Column
384        );
385    }
386
387    #[test]
388    fn compute_direction_with_inherited_vertical() {
389        // Inherited vertical flag converts Row to Column
390        assert_eq!(
391            compute_direction(FlexDirection::Row, None, false, Some(true)),
392            FlexDirection::Column
393        );
394        // But doesn't affect Column
395        assert_eq!(
396            compute_direction(FlexDirection::Column, None, false, Some(true)),
397            FlexDirection::Column
398        );
399    }
400
401    #[test]
402    fn base_classes_includes_flex_class() {
403        let classes = base_classes(
404            FlexDirection::Row,
405            FlexWrap::NoWrap,
406            FlexJustify::Start,
407            FlexAlign::Stretch,
408        );
409        assert!(classes.contains(&"adui-flex".to_string()));
410    }
411
412    #[test]
413    fn base_classes_direction_mapping() {
414        let row_classes = base_classes(
415            FlexDirection::Row,
416            FlexWrap::NoWrap,
417            FlexJustify::Start,
418            FlexAlign::Stretch,
419        );
420        assert!(row_classes.contains(&"adui-flex-horizontal".to_string()));
421
422        let col_classes = base_classes(
423            FlexDirection::Column,
424            FlexWrap::NoWrap,
425            FlexJustify::Start,
426            FlexAlign::Stretch,
427        );
428        assert!(col_classes.contains(&"adui-flex-vertical".to_string()));
429    }
430
431    #[test]
432    fn base_classes_wrap_mapping() {
433        let nowrap_classes = base_classes(
434            FlexDirection::Row,
435            FlexWrap::NoWrap,
436            FlexJustify::Start,
437            FlexAlign::Stretch,
438        );
439        assert!(nowrap_classes.contains(&"adui-flex-wrap-nowrap".to_string()));
440
441        let wrap_classes = base_classes(
442            FlexDirection::Row,
443            FlexWrap::Wrap,
444            FlexJustify::Start,
445            FlexAlign::Stretch,
446        );
447        assert!(wrap_classes.contains(&"adui-flex-wrap-wrap".to_string()));
448    }
449
450    #[test]
451    fn base_classes_justify_mapping() {
452        let start_classes = base_classes(
453            FlexDirection::Row,
454            FlexWrap::NoWrap,
455            FlexJustify::Start,
456            FlexAlign::Stretch,
457        );
458        assert!(start_classes.contains(&"adui-flex-justify-start".to_string()));
459
460        let center_classes = base_classes(
461            FlexDirection::Row,
462            FlexWrap::NoWrap,
463            FlexJustify::Center,
464            FlexAlign::Stretch,
465        );
466        assert!(center_classes.contains(&"adui-flex-justify-center".to_string()));
467    }
468
469    #[test]
470    fn base_classes_align_mapping() {
471        let stretch_classes = base_classes(
472            FlexDirection::Row,
473            FlexWrap::NoWrap,
474            FlexJustify::Start,
475            FlexAlign::Stretch,
476        );
477        assert!(stretch_classes.contains(&"adui-flex-align-stretch".to_string()));
478
479        let center_classes = base_classes(
480            FlexDirection::Row,
481            FlexWrap::NoWrap,
482            FlexJustify::Start,
483            FlexAlign::Center,
484        );
485        assert!(center_classes.contains(&"adui-flex-align-center".to_string()));
486    }
487}