StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | //! Grid widget for 2D layout |
| 2 | use crate::widget::{generate_id, Widget, WidgetId}; |
| 3 | use std::any::Any; |
| 4 | use strato_core::{ |
| 5 | event::{Event, EventResult}, |
| 6 | layout::{Constraints, Layout, Size}, |
| 7 | }; |
| 8 | use strato_renderer::batch::RenderBatch; |
| 9 | |
| 10 | /// Unit for grid tracks (rows/columns) |
| 11 | #[derive(Debug, Clone, Copy, PartialEq)] |
| 12 | pub 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)] |
| 23 | pub 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 | |
| 34 | impl 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 | |
| 85 | impl 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 |