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}