StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | //! Wrap widget for flow layout |
| 2 | use crate::widget::{generate_id, Widget, WidgetId}; |
| 3 | use std::any::Any; |
| 4 | use strato_core::{ |
| 5 | event::{Event, EventResult}, |
| 6 | layout::{ |
| 7 | AlignContent, AlignItems, Constraints, FlexContainer, FlexDirection, FlexItem, FlexWrap, |
| 8 | Gap, JustifyContent, Layout, Size, |
| 9 | }, |
| 10 | }; |
| 11 | use strato_renderer::batch::RenderBatch; |
| 12 | |
| 13 | /// Alignment for wrap layout |
| 14 | #[derive(Debug, Clone, Copy, PartialEq)] |
| 15 | pub 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)] |
| 26 | pub 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)] |
| 34 | pub 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 | |
| 47 | impl 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 | |
| 112 | impl 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 |