Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-widgets/src/wrap.rs
1//! Wrap widget for flow layout
2use crate::widget::{generate_id, Widget, WidgetId};
3use std::any::Any;
4use strato_core::{
5 event::{Event, EventResult},
6 layout::{
7 AlignContent, AlignItems, Constraints, FlexContainer, FlexDirection, FlexItem, FlexWrap,
8 Gap, JustifyContent, Layout, Size,
9 },
10};
11use strato_renderer::batch::RenderBatch;
12 
13/// Alignment for wrap layout
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum WrapAlignment {
16 Start,
17 Center,
18 End,
19 SpaceBetween,
20 SpaceAround,
21 SpaceEvenly,
22}
23 
24/// Cross axis alignment for items in a run
25#[derive(Debug, Clone, Copy, PartialEq)]
26pub enum WrapCrossAlignment {
27 Start,
28 Center,
29 End,
30}
31 
32/// A widget that displays its children in multiple horizontal or vertical runs.
33#[derive(Debug)]
34pub struct Wrap {
35 id: WidgetId,
36 children: Vec<Box<dyn Widget>>,
37 direction: FlexDirection,
38 alignment: WrapAlignment,
39 cross_alignment: WrapCrossAlignment,
40 run_alignment: WrapAlignment,
41 spacing: f32,
42 run_spacing: f32,
43 // Layout cache computed during layout()
44 cached_child_sizes: Vec<Size>,
45}
46 
47impl Wrap {
48 /// Create a new wrap widget
49 pub fn new() -> Self {
50 Self {
51 id: generate_id(),
52 children: Vec::new(),
53 direction: FlexDirection::Row,
54 alignment: WrapAlignment::Start,
55 cross_alignment: WrapCrossAlignment::Start,
56 run_alignment: WrapAlignment::Start,
57 spacing: 0.0,
58 run_spacing: 0.0,
59 cached_child_sizes: Vec::new(),
60 }
61 }
62 
63 /// Add children widgets
64 pub fn children(mut self, children: Vec<Box<dyn Widget>>) -> Self {
65 self.children = children;
66 self
67 }
68 
69 /// Add a single child
70 pub fn child(mut self, child: Box<dyn Widget>) -> Self {
71 self.children.push(child);
72 self
73 }
74 
75 /// Set direction
76 pub fn direction(mut self, direction: FlexDirection) -> Self {
77 self.direction = direction;
78 self
79 }
80 
81 /// Set main axis alignment
82 pub fn alignment(mut self, alignment: WrapAlignment) -> Self {
83 self.alignment = alignment;
84 self
85 }
86 
87 /// Set cross axis alignment (for items in a run)
88 pub fn cross_alignment(mut self, alignment: WrapCrossAlignment) -> Self {
89 self.cross_alignment = alignment;
90 self
91 }
92 
93 /// Set run alignment (how runs are placed in the cross axis)
94 pub fn run_alignment(mut self, alignment: WrapAlignment) -> Self {
95 self.run_alignment = alignment;
96 self
97 }
98 
99 /// Set spacing between items in the main axis
100 pub fn spacing(mut self, spacing: f32) -> Self {
101 self.spacing = spacing;
102 self
103 }
104 
105 /// Set spacing between runs in the cross axis
106 pub fn run_spacing(mut self, spacing: f32) -> Self {
107 self.run_spacing = spacing;
108 self
109 }
110}
111 
112impl Widget for Wrap {
113 fn id(&self) -> WidgetId {
114 self.id
115 }
116 
117 fn layout(&mut self, constraints: Constraints) -> Size {
118 let engine = strato_core::layout::LayoutEngine::new();
119 
120 // Relax constraints for children measurement
121 // Wrap children can be any size, they force a wrap if they exceed width
122 let child_constraints = Constraints {
123 min_width: 0.0,
124 max_width: constraints.max_width, // Individual items shouldn't exceed container
125 min_height: 0.0,
126 max_height: constraints.max_height,
127 };
128 
129 // Calculate child sizes
130 let mut child_data = Vec::new();
131 let mut sizes = Vec::with_capacity(self.children.len());
132 for child in &mut self.children {
133 let child_size = child.layout(child_constraints);
134 sizes.push(child_size);
135 
136 // Wrap treats all items as non-flex by default in terms of growing to fill line
137 // But we can support flex basis if needed. For now, we assume simple flow.
138 let flex_item = FlexItem::default();
139 child_data.push((flex_item, child_size));
140 }
141 self.cached_child_sizes = sizes;
142 
143 // Map widget props to core layout props
144 let justify_content = match self.alignment {
145 WrapAlignment::Start => JustifyContent::FlexStart,
146 WrapAlignment::Center => JustifyContent::Center,
147 WrapAlignment::End => JustifyContent::FlexEnd,
148 WrapAlignment::SpaceBetween => JustifyContent::SpaceBetween,
149 WrapAlignment::SpaceAround => JustifyContent::SpaceAround,
150 WrapAlignment::SpaceEvenly => JustifyContent::SpaceEvenly,
151 };
152 
153 let align_items = match self.cross_alignment {
154 WrapCrossAlignment::Start => AlignItems::FlexStart,
155 WrapCrossAlignment::Center => AlignItems::Center,
156 WrapCrossAlignment::End => AlignItems::FlexEnd,
157 };
158 
159 let align_content = match self.run_alignment {
160 WrapAlignment::Start => AlignContent::FlexStart,
161 WrapAlignment::Center => AlignContent::Center,
162 WrapAlignment::End => AlignContent::FlexEnd,
163 WrapAlignment::SpaceBetween => AlignContent::SpaceBetween,
164 WrapAlignment::SpaceAround => AlignContent::SpaceAround,
165 WrapAlignment::SpaceEvenly => AlignContent::SpaceEvenly,
166 };
167 
168 let container = FlexContainer {
169 direction: self.direction,
170 wrap: FlexWrap::Wrap,
171 justify_content,
172 align_items,
173 align_content,
174 gap: Gap {
175 row: self.run_spacing, // Gap between runs (lines)
176 column: self.spacing, // Gap between items
177 },
178 ..Default::default()
179 };
180 
181 let layouts = engine.calculate_flex_layout(&container, &child_data, constraints);
182 
183 // Calculate total size based on returned layouts
184 let mut max_x = 0.0f32;
185 let mut max_y = 0.0f32;
186 for l in layouts {
187 max_x = max_x.max(l.position.x + l.size.width);
188 max_y = max_y.max(l.position.y + l.size.height);
189 }
190 
191 Size::new(max_x, max_y)
192 }
193 
194 fn render(&self, batch: &mut RenderBatch, layout: Layout) {
195 let engine = strato_core::layout::LayoutEngine::new();
196 
197 // Reconstruct child data from cache
198 let mut child_data = Vec::new();
199 for (i, _) in self.children.iter().enumerate() {
200 let child_size = self
201 .cached_child_sizes
202 .get(i)
203 .copied()
204 .unwrap_or(Size::zero());
205 child_data.push((FlexItem::default(), child_size));
206 }
207 
208 // Map props again (should factor this out if it gets complex)
209 let justify_content = match self.alignment {
210 WrapAlignment::Start => JustifyContent::FlexStart,
211 WrapAlignment::Center => JustifyContent::Center,
212 WrapAlignment::End => JustifyContent::FlexEnd,
213 WrapAlignment::SpaceBetween => JustifyContent::SpaceBetween,
214 WrapAlignment::SpaceAround => JustifyContent::SpaceAround,
215 WrapAlignment::SpaceEvenly => JustifyContent::SpaceEvenly,
216 };
217 
218 let align_items = match self.cross_alignment {
219 WrapCrossAlignment::Start => AlignItems::FlexStart,
220 WrapCrossAlignment::Center => AlignItems::Center,
221 WrapCrossAlignment::End => AlignItems::FlexEnd,
222 };
223 
224 let align_content = match self.run_alignment {
225 WrapAlignment::Start => AlignContent::FlexStart,
226 WrapAlignment::Center => AlignContent::Center,
227 WrapAlignment::End => AlignContent::FlexEnd,
228 WrapAlignment::SpaceBetween => AlignContent::SpaceBetween,
229 WrapAlignment::SpaceAround => AlignContent::SpaceAround,
230 WrapAlignment::SpaceEvenly => AlignContent::SpaceEvenly,
231 };
232 
233 let container = FlexContainer {
234 direction: self.direction,
235 wrap: FlexWrap::Wrap,
236 justify_content,
237 align_items,
238 align_content,
239 gap: Gap {
240 row: self.run_spacing,
241 column: self.spacing,
242 },
243 ..Default::default()
244 };
245 
246 // Recalculate layout inside bounds
247 let layouts = engine.calculate_flex_layout(
248 &container,
249 &child_data,
250 Constraints::tight(layout.size.width, layout.size.height), // Use actual assigned size
251 );
252 
253 // Render children
254 for (child, child_layout) in self.children.iter().zip(layouts.iter()) {
255 let absolute_layout =
256 Layout::new(layout.position + child_layout.position, child_layout.size);
257 child.render(batch, absolute_layout);
258 }
259 }
260 
261 fn handle_event(&mut self, event: &Event) -> EventResult {
262 for child in &mut self.children {
263 if child.handle_event(event) == EventResult::Handled {
264 return EventResult::Handled;
265 }
266 }
267 EventResult::Ignored
268 }
269 
270 fn children(&self) -> Vec<&(dyn Widget + '_)> {
271 self.children.iter().map(|c| c.as_ref()).collect()
272 }
273 
274 fn children_mut<'a>(&'a mut self) -> Vec<&'a mut (dyn Widget + 'a)> {
275 self.children
276 .iter_mut()
277 .map(|c| c.as_mut() as &'a mut (dyn Widget + 'a))
278 .collect()
279 }
280 
281 fn as_any(&self) -> &dyn Any {
282 self
283 }
284 
285 fn as_any_mut(&mut self) -> &mut dyn Any {
286 self
287 }
288 
289 fn clone_widget(&self) -> Box<dyn Widget> {
290 Box::new(Wrap {
291 id: generate_id(),
292 children: self.children.iter().map(|c| c.clone_widget()).collect(),
293 direction: self.direction,
294 alignment: self.alignment,
295 cross_alignment: self.cross_alignment,
296 run_alignment: self.run_alignment,
297 spacing: self.spacing,
298 run_spacing: self.run_spacing,
299 cached_child_sizes: Vec::new(),
300 })
301 }
302}
303