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/grid.rs
1//! Grid widget for 2D layout
2use crate::widget::{generate_id, Widget, WidgetId};
3use std::any::Any;
4use strato_core::{
5 event::{Event, EventResult},
6 layout::{Constraints, Layout, Size},
7};
8use strato_renderer::batch::RenderBatch;
9 
10/// Unit for grid tracks (rows/columns)
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum GridUnit {
13 /// Fixed size in pixels
14 Pixel(f32),
15 /// Fraction of available space (fr)
16 Fraction(f32),
17 /// Auto size (fits content)
18 Auto,
19}
20 
21/// Grid widget for 2D layout
22#[derive(Debug)]
23pub struct Grid {
24 id: WidgetId,
25 children: Vec<Box<dyn Widget>>,
26 rows: Vec<GridUnit>,
27 cols: Vec<GridUnit>,
28 row_gap: f32,
29 col_gap: f32,
30 // Store layout results for rendering
31 cached_child_layouts: Vec<Layout>,
32}
33 
34impl Grid {
35 /// Create a new grid
36 pub fn new() -> Self {
37 Self {
38 id: generate_id(),
39 children: Vec::new(),
40 rows: Vec::new(),
41 cols: Vec::new(),
42 row_gap: 0.0,
43 col_gap: 0.0,
44 cached_child_layouts: Vec::new(),
45 }
46 }
47 
48 /// Set columns template
49 pub fn columns(mut self, cols: Vec<GridUnit>) -> Self {
50 self.cols = cols;
51 self
52 }
53 
54 /// Set rows template
55 pub fn rows(mut self, rows: Vec<GridUnit>) -> Self {
56 self.rows = rows;
57 self
58 }
59 
60 /// Set gap between rows
61 pub fn row_gap(mut self, gap: f32) -> Self {
62 self.row_gap = gap;
63 self
64 }
65 
66 /// Set gap between columns
67 pub fn col_gap(mut self, gap: f32) -> Self {
68 self.col_gap = gap;
69 self
70 }
71 
72 /// Add children
73 pub fn children(mut self, children: Vec<Box<dyn Widget>>) -> Self {
74 self.children = children;
75 self
76 }
77 
78 /// Add a single child
79 pub fn child(mut self, child: Box<dyn Widget>) -> Self {
80 self.children.push(child);
81 self
82 }
83}
84 
85impl Widget for Grid {
86 fn id(&self) -> WidgetId {
87 self.id
88 }
89 
90 fn layout(&mut self, constraints: Constraints) -> Size {
91 // If no columns defined, default to 1 column auto
92 if self.cols.is_empty() {
93 self.cols.push(GridUnit::Auto);
94 }
95 // If no rows defined, we will implicitly add auto rows as needed
96 
97 let num_cols = self.cols.len();
98 let num_children = self.children.len();
99 let implicit_rows_needed = (num_children as f32 / num_cols as f32).ceil() as usize;
100 
101 // Final rows list including implicit ones
102 let mut final_rows = self.rows.clone();
103 while final_rows.len() < implicit_rows_needed {
104 final_rows.push(GridUnit::Auto);
105 }
106 let num_rows = final_rows.len();
107 
108 // 1. Calculate available space
109 let available_width = constraints.max_width;
110 let available_height = constraints.max_height;
111 
112 // 2. Resolve Track Sizes (simplistic implementation)
113 // First pass: Calculate fixed and auto sizes
114 let mut col_widths = vec![0.0; num_cols];
115 let mut row_heights = vec![0.0; num_rows];
116 
117 // Helper to get child at (row, col)
118 let get_child_idx = |r, c| r * num_cols + c;
119 
120 // Measure AUTO tracks
121 for r in 0..num_rows {
122 for c in 0..num_cols {
123 let idx = get_child_idx(r, c);
124 if idx >= self.children.len() {
125 continue;
126 }
127 
128 let is_col_auto = matches!(self.cols[c], GridUnit::Auto);
129 let is_row_auto = matches!(final_rows[r], GridUnit::Auto);
130 
131 if is_col_auto || is_row_auto {
132 // Measure content
133 // TODO: This is naive. True grid layout is complex.
134 // We measure with loose constraints to get content size.
135 let measure_constraints = Constraints::loose(available_width, available_height);
136 let size = self.children[idx].layout(measure_constraints);
137 
138 if is_col_auto {
139 col_widths[c] = f32::max(col_widths[c], size.width);
140 }
141 if is_row_auto {
142 row_heights[r] = f32::max(row_heights[r], size.height);
143 }
144 }
145 }
146 }
147 
148 // Measure FIXED tracks
149 for (c, unit) in self.cols.iter().enumerate() {
150 if let GridUnit::Pixel(px) = unit {
151 col_widths[c] = *px;
152 }
153 }
154 for (r, unit) in final_rows.iter().enumerate() {
155 if let GridUnit::Pixel(px) = unit {
156 row_heights[r] = *px;
157 }
158 }
159 
160 // Measure FRACTION tracks
161 let used_width: f32 =
162 col_widths.iter().sum::<f32>() + (num_cols.saturating_sub(1) as f32 * self.col_gap);
163 let remaining_width = (available_width - used_width).max(0.0);
164 let total_col_fr: f32 = self.cols.iter().fold(0.0, |acc, u| {
165 if let GridUnit::Fraction(fr) = u {
166 acc + fr
167 } else {
168 acc
169 }
170 });
171 
172 if total_col_fr > 0.0 {
173 for (c, unit) in self.cols.iter().enumerate() {
174 if let GridUnit::Fraction(fr) = unit {
175 col_widths[c] = (fr / total_col_fr) * remaining_width;
176 }
177 }
178 }
179 
180 // For rows, we often don't have a fixed height container, so fractions might be tricky.
181 // If we have infinite height constraint, fractions might resolve to 0 or behave like Auto.
182 // Here we assume if height is constrained, we distribute.
183 if available_height.is_finite() {
184 let used_height: f32 = row_heights.iter().sum::<f32>()
185 + (num_rows.saturating_sub(1) as f32 * self.row_gap);
186 let remaining_height = (available_height - used_height).max(0.0);
187 let total_row_fr: f32 = final_rows.iter().fold(0.0, |acc, u| {
188 if let GridUnit::Fraction(fr) = u {
189 acc + fr
190 } else {
191 acc
192 }
193 });
194 
195 if total_row_fr > 0.0 {
196 for (r, unit) in final_rows.iter().enumerate() {
197 if let GridUnit::Fraction(fr) = unit {
198 row_heights[r] = (fr / total_row_fr) * remaining_height;
199 }
200 }
201 }
202 }
203 // If height is infinite, treat fractions as auto or 0?
204 // For now, let's treat as 0 or maybe min size. In real CSS grid they collapse to content if height is indefinite.
205 // We leave them as 0 if not calculated above, unless we implement content-based minimums for fr tracks.
206 
207 // 3. Position Children and Re-layout with precise constraints
208 self.cached_child_layouts.clear();
209 let mut total_width = 0.0f32;
210 let mut total_height = 0.0f32;
211 
212 let mut current_y = 0.0;
213 for r in 0..num_rows {
214 let mut current_x = 0.0;
215 let row_h = row_heights[r];
216 
217 for c in 0..num_cols {
218 let idx = get_child_idx(r, c);
219 let col_w = col_widths[c];
220 
221 if idx < self.children.len() {
222 let cell_x = current_x;
223 let cell_y = current_y;
224 
225 // Re-layout child with exact cell size
226 // We force the child to fit the cell? Or align it?
227 // Typically grid items stretch to fill cell unless aligned.
228 // We'll enforce loose constraints up to cell size, but tight might be better for stretch.
229 // Let's use tight for compatibility with "stretch" default behavior.
230 let cell_constraints = Constraints::tight(col_w, row_h);
231 // Note: If row_h is 0 (e.g. empty fr track), this hides the child.
232 
233 self.children[idx].layout(cell_constraints);
234 
235 self.cached_child_layouts.push(Layout::new(
236 glam::Vec2::new(cell_x, cell_y),
237 Size::new(col_w, row_h),
238 ));
239 }
240 
241 current_x += col_w + self.col_gap;
242 }
243 
244 total_width = total_width.max(current_x - self.col_gap); // remove last gap
245 current_y += row_h + self.row_gap;
246 }
247 total_height = current_y - self.row_gap; // remove last gap
248 
249 Size::new(total_width, total_height)
250 }
251 
252 fn render(&self, batch: &mut RenderBatch, layout: Layout) {
253 for (i, child) in self.children.iter().enumerate() {
254 if let Some(child_layout) = self.cached_child_layouts.get(i) {
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 
262 fn handle_event(&mut self, event: &Event) -> EventResult {
263 for child in &mut self.children {
264 if child.handle_event(event) == EventResult::Handled {
265 return EventResult::Handled;
266 }
267 }
268 EventResult::Ignored
269 }
270 
271 fn children(&self) -> Vec<&(dyn Widget + '_)> {
272 self.children.iter().map(|c| c.as_ref()).collect()
273 }
274 
275 fn children_mut<'a>(&'a mut self) -> Vec<&'a mut (dyn Widget + 'a)> {
276 self.children
277 .iter_mut()
278 .map(|c| c.as_mut() as &'a mut (dyn Widget + 'a))
279 .collect()
280 }
281 
282 fn as_any(&self) -> &dyn Any {
283 self
284 }
285 
286 fn as_any_mut(&mut self) -> &mut dyn Any {
287 self
288 }
289 
290 fn clone_widget(&self) -> Box<dyn Widget> {
291 Box::new(Grid {
292 id: generate_id(),
293 children: self.children.iter().map(|c| c.clone_widget()).collect(),
294 rows: self.rows.clone(),
295 cols: self.cols.clone(),
296 row_gap: self.row_gap,
297 col_gap: self.col_gap,
298 cached_child_layouts: Vec::new(),
299 })
300 }
301}
302