Skip to main content

rosace_layout/widgets/
flex.rs

1//! [`Flex`] — the generic flex container used internally by [`Column`] and [`Row`].
2//!
3//! [`Column`]: crate::widgets::column::Column
4//! [`Row`]: crate::widgets::row::Row
5
6use rosace_core::child_container::ChildContainer;
7use rosace_core::element::{Element, NativeElement};
8#[cfg(debug_assertions)]
9use rosace_core::render_object::AxisBound;
10use rosace_core::types::{Point, Size};
11#[cfg(debug_assertions)]
12use rosace_trace::{
13    event::{ComponentId, RosaceTrace, TraceConstraints},
14    trace,
15};
16
17use crate::alignment::{CrossAxisAlignment, MainAxisAlignment};
18use crate::constraints::Constraints;
19use crate::layout_result::LayoutResult;
20
21/// The axis along which a [`Flex`] container arranges its children.
22#[derive(Debug, Clone, Copy, PartialEq)]
23pub enum FlexDirection {
24    /// Arrange children left-to-right (horizontal main axis).
25    Row,
26    /// Arrange children top-to-bottom (vertical main axis).
27    Column,
28}
29
30/// A generic flex container that drives the layout of [`Column`] and [`Row`].
31///
32/// Prefer the higher-level [`Column`] and [`Row`] widgets for typical use.
33/// Use `Flex` directly when you need runtime direction switching.
34///
35/// [`Column`]: crate::widgets::column::Column
36/// [`Row`]: crate::widgets::row::Row
37#[derive(Debug, Clone)]
38pub struct Flex {
39    /// The primary layout direction.
40    pub direction: FlexDirection,
41    /// Child elements in declaration order.
42    pub children: Vec<Element>,
43    /// How children are distributed along the main axis.
44    pub main_axis_alignment: MainAxisAlignment,
45    /// How children are aligned on the cross axis.
46    pub cross_axis_alignment: CrossAxisAlignment,
47    /// Pixels of gap between consecutive children.
48    pub spacing: f32,
49}
50
51impl Flex {
52    /// Create a new `Flex` with the given `direction` and default alignments.
53    pub fn new(direction: FlexDirection) -> Self {
54        Self {
55            direction,
56            children: Vec::new(),
57            main_axis_alignment: MainAxisAlignment::default(),
58            cross_axis_alignment: CrossAxisAlignment::default(),
59            spacing: 0.0,
60        }
61    }
62
63    /// Set the gap in logical pixels between consecutive children.
64    pub fn spacing(mut self, s: f32) -> Self {
65        self.spacing = s;
66        self
67    }
68
69    /// Set how children are distributed along the main axis.
70    pub fn main_axis_alignment(mut self, a: MainAxisAlignment) -> Self {
71        self.main_axis_alignment = a;
72        self
73    }
74
75    /// Set how children are aligned on the cross axis.
76    pub fn cross_axis_alignment(mut self, a: CrossAxisAlignment) -> Self {
77        self.cross_axis_alignment = a;
78        self
79    }
80
81    /// Perform the Measure + Place passes and return a [`LayoutResult`].
82    ///
83    /// `child_sizes` must be in the same order as [`Self::children`].
84    /// Emits [`RosaceTrace::LayoutStart`] / [`RosaceTrace::LayoutEnd`] events.
85    pub fn layout(&self, constraints: Constraints, child_sizes: &[Size]) -> LayoutResult {
86        #[cfg(debug_assertions)]
87        let start = std::time::Instant::now();
88
89        #[cfg(debug_assertions)]
90        trace!(RosaceTrace::LayoutStart {
91            component: ComponentId(0),
92            constraints: TraceConstraints {
93                min_width: constraints.min_width,
94                max_width: match &constraints.max_width {
95                    AxisBound::Bounded(v) => Some(*v),
96                    _ => None,
97                },
98                min_height: constraints.min_height,
99                max_height: match &constraints.max_height {
100                    AxisBound::Bounded(v) => Some(*v),
101                    _ => None,
102                },
103            },
104        });
105
106        let result = self.layout_inner(constraints, child_sizes);
107
108        #[cfg(debug_assertions)]
109        trace!(RosaceTrace::LayoutEnd {
110            component: ComponentId(0),
111            size: result.size,
112            duration: start.elapsed(),
113        });
114
115        result
116    }
117
118    /// Inner layout without trace emissions — used by [`Column`] and [`Row`]
119    /// which emit their own traces.
120    ///
121    /// [`Column`]: crate::widgets::column::Column
122    /// [`Row`]: crate::widgets::row::Row
123    pub(crate) fn layout_inner(
124        &self,
125        constraints: Constraints,
126        child_sizes: &[Size],
127    ) -> LayoutResult {
128        match self.direction {
129            FlexDirection::Column => layout_column(
130                constraints,
131                child_sizes,
132                self.main_axis_alignment,
133                self.cross_axis_alignment,
134                self.spacing,
135            ),
136            FlexDirection::Row => layout_row(
137                constraints,
138                child_sizes,
139                self.main_axis_alignment,
140                self.cross_axis_alignment,
141                self.spacing,
142            ),
143        }
144    }
145}
146
147/// Core column layout algorithm shared by [`Flex`] and [`Column`].
148pub fn layout_column(
149    constraints: Constraints,
150    child_sizes: &[Size],
151    main_axis_alignment: MainAxisAlignment,
152    cross_axis_alignment: CrossAxisAlignment,
153    spacing: f32,
154) -> LayoutResult {
155    let n = child_sizes.len();
156    if n == 0 {
157        return LayoutResult {
158            size: constraints.constrain(Size {
159                width: 0.0,
160                height: 0.0,
161            }),
162            child_positions: vec![],
163        };
164    }
165
166    let max_w = constraints.max_width_f32();
167    let max_h = constraints.max_height_f32();
168
169    // Cross axis (width): max of child widths, or full available if Stretch.
170    let content_width = child_sizes
171        .iter()
172        .map(|s| s.width)
173        .fold(0.0_f32, f32::max);
174    let container_width = match cross_axis_alignment {
175        CrossAxisAlignment::Stretch if max_w.is_finite() => max_w,
176        _ => content_width,
177    }
178    .max(constraints.min_width)
179    .min(max_w);
180
181    // Main axis (height): sum of heights + spacing.
182    let total_child_height: f32 = child_sizes.iter().map(|s| s.height).sum();
183    let total_spacing = spacing * (n - 1) as f32;
184    let content_height = total_child_height + total_spacing;
185    let container_height = content_height
186        .max(constraints.min_height)
187        .min(max_h);
188
189    let extra = (container_height - content_height).max(0.0);
190    let (initial_offset, between_gap) = distribute_extra(main_axis_alignment, extra, n);
191
192    let mut positions = Vec::with_capacity(n);
193    let mut y = initial_offset;
194
195    for (i, child_size) in child_sizes.iter().enumerate() {
196        let x = cross_offset(cross_axis_alignment, container_width, child_size.width);
197        positions.push(Point { x, y });
198        y += child_size.height;
199        if i + 1 < n {
200            y += spacing + between_gap;
201        }
202    }
203
204    LayoutResult {
205        size: Size {
206            width: container_width,
207            height: container_height,
208        },
209        child_positions: positions,
210    }
211}
212
213/// Core row layout algorithm shared by [`Flex`] and [`Row`].
214pub fn layout_row(
215    constraints: Constraints,
216    child_sizes: &[Size],
217    main_axis_alignment: MainAxisAlignment,
218    cross_axis_alignment: CrossAxisAlignment,
219    spacing: f32,
220) -> LayoutResult {
221    let n = child_sizes.len();
222    if n == 0 {
223        return LayoutResult {
224            size: constraints.constrain(Size {
225                width: 0.0,
226                height: 0.0,
227            }),
228            child_positions: vec![],
229        };
230    }
231
232    let max_w = constraints.max_width_f32();
233    let max_h = constraints.max_height_f32();
234
235    // Cross axis (height): max of child heights, or full available if Stretch.
236    let content_height = child_sizes
237        .iter()
238        .map(|s| s.height)
239        .fold(0.0_f32, f32::max);
240    let container_height = match cross_axis_alignment {
241        CrossAxisAlignment::Stretch if max_h.is_finite() => max_h,
242        _ => content_height,
243    }
244    .max(constraints.min_height)
245    .min(max_h);
246
247    // Main axis (width): sum of widths + spacing.
248    let total_child_width: f32 = child_sizes.iter().map(|s| s.width).sum();
249    let total_spacing = spacing * (n - 1) as f32;
250    let content_width = total_child_width + total_spacing;
251    let container_width = content_width
252        .max(constraints.min_width)
253        .min(max_w);
254
255    let extra = (container_width - content_width).max(0.0);
256    let (initial_offset, between_gap) = distribute_extra(main_axis_alignment, extra, n);
257
258    let mut positions = Vec::with_capacity(n);
259    let mut x = initial_offset;
260
261    for (i, child_size) in child_sizes.iter().enumerate() {
262        let y = cross_offset(cross_axis_alignment, container_height, child_size.height);
263        positions.push(Point { x, y });
264        x += child_size.width;
265        if i + 1 < n {
266            x += spacing + between_gap;
267        }
268    }
269
270    LayoutResult {
271        size: Size {
272            width: container_width,
273            height: container_height,
274        },
275        child_positions: positions,
276    }
277}
278
279/// Compute `(initial_offset, per_gap_extra)` for a given main-axis alignment.
280///
281/// - `extra`: total remaining space after placing children and fixed spacing.
282/// - `n`: number of children.
283pub(crate) fn distribute_extra(
284    alignment: MainAxisAlignment,
285    extra: f32,
286    n: usize,
287) -> (f32, f32) {
288    match alignment {
289        MainAxisAlignment::Start => (0.0, 0.0),
290        MainAxisAlignment::Center => (extra / 2.0, 0.0),
291        MainAxisAlignment::End => (extra, 0.0),
292        MainAxisAlignment::SpaceBetween => {
293            let gap = if n > 1 {
294                extra / (n - 1) as f32
295            } else {
296                0.0
297            };
298            (0.0, gap)
299        }
300        MainAxisAlignment::SpaceAround => {
301            let unit = if n > 0 { extra / n as f32 } else { 0.0 };
302            (unit / 2.0, unit)
303        }
304        MainAxisAlignment::SpaceEvenly => {
305            let unit = extra / (n + 1) as f32;
306            (unit, unit)
307        }
308    }
309}
310
311/// Compute the cross-axis offset for one child given the container size and child size.
312pub(crate) fn cross_offset(alignment: CrossAxisAlignment, container: f32, child: f32) -> f32 {
313    match alignment {
314        CrossAxisAlignment::Start | CrossAxisAlignment::Stretch | CrossAxisAlignment::Baseline => {
315            0.0
316        }
317        CrossAxisAlignment::Center => (container - child) / 2.0,
318        CrossAxisAlignment::End => container - child,
319    }
320}
321
322impl ChildContainer for Flex {
323    fn child(mut self, element: impl Into<Element>) -> Self {
324        self.children.push(element.into());
325        self
326    }
327
328    fn children<E: Into<Element>>(mut self, elements: Vec<E>) -> Self {
329        self.children
330            .extend(elements.into_iter().map(Into::into));
331        self
332    }
333
334    fn prepend(mut self, element: impl Into<Element>) -> Self {
335        self.children.insert(0, element.into());
336        self
337    }
338}
339
340impl From<Flex> for Element {
341    fn from(f: Flex) -> Self {
342        Element::Native(NativeElement {
343            tag: "Flex",
344            payload: None,
345            children: f.children,
346            key: None,
347        })
348    }
349}