Skip to main content

rosace_layout/widgets/
wrap.rs

1//! [`Wrap`] — a wrapping layout that flows children into multiple rows.
2
3use rosace_core::child_container::ChildContainer;
4use rosace_core::element::{Element, NativeElement};
5#[cfg(debug_assertions)]
6use rosace_core::render_object::AxisBound;
7use rosace_core::types::{Point, Size};
8#[cfg(debug_assertions)]
9use rosace_trace::{
10    event::{ComponentId, RosaceTrace, TraceConstraints},
11    trace,
12};
13
14use crate::constraints::Constraints;
15use crate::layout_result::LayoutResult;
16
17/// A widget that lays out children left-to-right and wraps to a new row when
18/// the available width is exhausted.
19///
20/// - [`spacing`](Self::spacing) controls horizontal space between items.
21/// - [`run_spacing`](Self::run_spacing) controls vertical space between rows.
22#[derive(Debug, Clone)]
23pub struct Wrap {
24    children: Vec<Element>,
25    spacing: f32,
26    run_spacing: f32,
27}
28
29impl Wrap {
30    /// Create a new `Wrap` with zero spacing and run-spacing.
31    pub fn new() -> Self {
32        Self {
33            children: Vec::new(),
34            spacing: 0.0,
35            run_spacing: 0.0,
36        }
37    }
38
39    /// Set the horizontal gap in logical pixels between items in the same row.
40    pub fn spacing(mut self, s: f32) -> Self {
41        self.spacing = s;
42        self
43    }
44
45    /// Set the vertical gap in logical pixels between rows.
46    pub fn run_spacing(mut self, s: f32) -> Self {
47        self.run_spacing = s;
48        self
49    }
50
51    /// Perform the Measure + Place passes and return a [`LayoutResult`].
52    ///
53    /// Items are placed left-to-right; when the next item would exceed the
54    /// constraint's maximum width the layout wraps to a new row.
55    ///
56    /// Emits [`RosaceTrace::LayoutStart`] and [`RosaceTrace::LayoutEnd`] events.
57    pub fn layout(&self, constraints: Constraints, child_sizes: &[Size]) -> LayoutResult {
58        #[cfg(debug_assertions)]
59        let start = std::time::Instant::now();
60
61        #[cfg(debug_assertions)]
62        trace!(RosaceTrace::LayoutStart {
63            component: ComponentId(0),
64            constraints: TraceConstraints {
65                min_width: constraints.min_width,
66                max_width: match &constraints.max_width {
67                    AxisBound::Bounded(v) => Some(*v),
68                    _ => None,
69                },
70                min_height: constraints.min_height,
71                max_height: match &constraints.max_height {
72                    AxisBound::Bounded(v) => Some(*v),
73                    _ => None,
74                },
75            },
76        });
77
78        let max_w = constraints.max_width_f32();
79        let n = child_sizes.len();
80        let mut positions = Vec::with_capacity(n);
81
82        // Current insertion point within the active row.
83        let mut cursor_x = 0.0_f32;
84        // Top of the active row.
85        let mut cursor_y = 0.0_f32;
86        // Tallest item in the active row.
87        let mut row_height = 0.0_f32;
88        // Whether the active row is empty.
89        let mut is_first_in_row = true;
90
91        for size in child_sizes {
92            // Width needed to place this item (including gap if not first).
93            let x_with_gap = if is_first_in_row {
94                0.0
95            } else {
96                cursor_x + self.spacing
97            };
98
99            // Wrap if the item would overflow the row (but always place if the
100            // row is empty — otherwise infinitely wide items would loop forever).
101            if !is_first_in_row && x_with_gap + size.width > max_w {
102                cursor_y += row_height + self.run_spacing;
103                cursor_x = 0.0;
104                row_height = 0.0;
105                is_first_in_row = true;
106            }
107
108            let pos_x = if is_first_in_row {
109                0.0
110            } else {
111                cursor_x + self.spacing
112            };
113
114            positions.push(Point { x: pos_x, y: cursor_y });
115            cursor_x = pos_x + size.width;
116            row_height = row_height.max(size.height);
117            is_first_in_row = false;
118        }
119
120        let total_height = cursor_y + row_height;
121        // Use the constrained maximum width as the container width when finite.
122        let total_width = if max_w.is_finite() { max_w } else { cursor_x };
123        let size = constraints.constrain(Size {
124            width: total_width,
125            height: total_height,
126        });
127
128        let result = LayoutResult {
129            size,
130            child_positions: positions,
131        };
132
133        #[cfg(debug_assertions)]
134        trace!(RosaceTrace::LayoutEnd {
135            component: ComponentId(0),
136            size: result.size,
137            duration: start.elapsed(),
138        });
139
140        result
141    }
142}
143
144impl Default for Wrap {
145    fn default() -> Self {
146        Self::new()
147    }
148}
149
150impl ChildContainer for Wrap {
151    fn child(mut self, element: impl Into<Element>) -> Self {
152        self.children.push(element.into());
153        self
154    }
155
156    fn children<E: Into<Element>>(mut self, elements: Vec<E>) -> Self {
157        self.children
158            .extend(elements.into_iter().map(Into::into));
159        self
160    }
161
162    fn prepend(mut self, element: impl Into<Element>) -> Self {
163        self.children.insert(0, element.into());
164        self
165    }
166}
167
168impl From<Wrap> for Element {
169    fn from(w: Wrap) -> Self {
170        Element::Native(NativeElement {
171            tag: "Wrap",
172            payload: None,
173            children: w.children,
174            key: None,
175        })
176    }
177}