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-core/src/layout.rs
StratoSDK / crates / strato-core / src / layout.rs
1//! Flexbox-based layout engine for StratoUI
2//!
3//! This module provides a comprehensive flexbox layout system that supports
4//! all major flexbox properties including direction, wrap, alignment, and gaps.
5 
6use glam::Vec2;
7use std::fmt::Debug;
8 
9/// Layout constraints for widgets
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct Constraints {
12 pub min_width: f32,
13 pub max_width: f32,
14 pub min_height: f32,
15 pub max_height: f32,
16}
17 
18/// Type alias for backward compatibility
19pub type LayoutConstraints = Constraints;
20 
21impl Constraints {
22 /// Create constraints with no limits
23 pub fn none() -> Self {
24 Self {
25 min_width: 0.0,
26 max_width: f32::INFINITY,
27 min_height: 0.0,
28 max_height: f32::INFINITY,
29 }
30 }
31 
32 /// Create tight constraints (fixed size)
33 pub fn tight(width: f32, height: f32) -> Self {
34 Self {
35 min_width: width,
36 max_width: width,
37 min_height: height,
38 max_height: height,
39 }
40 }
41 
42 /// Create loose constraints (maximum size)
43 pub fn loose(width: f32, height: f32) -> Self {
44 Self {
45 min_width: 0.0,
46 max_width: width,
47 min_height: 0.0,
48 max_height: height,
49 }
50 }
51 
52 /// Constrain a size to these constraints
53 pub fn constrain(&self, size: Size) -> Size {
54 Size {
55 width: size.width.clamp(self.min_width, self.max_width),
56 height: size.height.clamp(self.min_height, self.max_height),
57 }
58 }
59 
60 /// Check if a size satisfies these constraints
61 pub fn is_satisfied_by(&self, size: Size) -> bool {
62 size.width >= self.min_width
63 && size.width <= self.max_width
64 && size.height >= self.min_height
65 && size.height <= self.max_height
66 }
67}
68 
69/// Size representation
70#[derive(Debug, Clone, Copy, PartialEq, Default)]
71pub struct Size {
72 pub width: f32,
73 pub height: f32,
74}
75 
76impl Size {
77 /// Create a new size
78 pub fn new(width: f32, height: f32) -> Self {
79 Self { width, height }
80 }
81 
82 /// Create a zero size
83 pub fn zero() -> Self {
84 Self::new(0.0, 0.0)
85 }
86 
87 /// Convert to Vec2
88 pub fn to_vec2(self) -> Vec2 {
89 Vec2::new(self.width, self.height)
90 }
91}
92 
93impl From<Vec2> for Size {
94 fn from(vec: Vec2) -> Self {
95 Self::new(vec.x, vec.y)
96 }
97}
98 
99/// Flex direction
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum FlexDirection {
102 Row,
103 RowReverse,
104 Column,
105 ColumnReverse,
106}
107 
108impl FlexDirection {
109 /// Check if this is a row direction
110 pub fn is_row(&self) -> bool {
111 matches!(self, FlexDirection::Row | FlexDirection::RowReverse)
112 }
113 
114 /// Check if this is a column direction
115 pub fn is_column(&self) -> bool {
116 !self.is_row()
117 }
118 
119 /// Check if this is reversed
120 pub fn is_reverse(&self) -> bool {
121 matches!(
122 self,
123 FlexDirection::RowReverse | FlexDirection::ColumnReverse
124 )
125 }
126}
127 
128/// Flex wrap behavior
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum FlexWrap {
131 NoWrap,
132 Wrap,
133 WrapReverse,
134}
135 
136/// Main axis alignment (along flex direction)
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum JustifyContent {
139 FlexStart,
140 FlexEnd,
141 Center,
142 SpaceBetween,
143 SpaceAround,
144 SpaceEvenly,
145}
146 
147/// Cross axis alignment (perpendicular to flex direction)
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum AlignItems {
150 FlexStart,
151 FlexEnd,
152 Center,
153 Stretch,
154 Baseline,
155}
156 
157/// Alignment for wrapped lines
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub enum AlignContent {
160 FlexStart,
161 FlexEnd,
162 Center,
163 SpaceBetween,
164 SpaceAround,
165 SpaceEvenly,
166 Stretch,
167}
168 
169/// Individual item alignment override
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub enum AlignSelf {
172 Auto,
173 FlexStart,
174 FlexEnd,
175 Center,
176 Stretch,
177 Baseline,
178}
179 
180/// Flex properties for a widget
181#[derive(Debug, Clone, Copy)]
182pub struct FlexItem {
183 pub flex_grow: f32,
184 pub flex_shrink: f32,
185 pub flex_basis: f32,
186 pub align_self: AlignSelf,
187 pub margin: EdgeInsets,
188}
189 
190impl Default for FlexItem {
191 fn default() -> Self {
192 Self {
193 flex_grow: 0.0,
194 flex_shrink: 1.0,
195 flex_basis: 0.0,
196 align_self: AlignSelf::Auto,
197 margin: EdgeInsets::default(),
198 }
199 }
200}
201 
202impl FlexItem {
203 /// Create a flex item with grow factor
204 pub fn grow(flex_grow: f32) -> Self {
205 Self {
206 flex_grow,
207 ..Default::default()
208 }
209 }
210 
211 /// Create a flex item with shrink factor
212 pub fn shrink(flex_shrink: f32) -> Self {
213 Self {
214 flex_shrink,
215 ..Default::default()
216 }
217 }
218 
219 /// Create a flex item with basis
220 pub fn basis(flex_basis: f32) -> Self {
221 Self {
222 flex_basis,
223 ..Default::default()
224 }
225 }
226}
227 
228/// Edge insets (padding/margin)
229#[derive(Debug, Clone, Copy, PartialEq, Default)]
230pub struct EdgeInsets {
231 pub top: f32,
232 pub right: f32,
233 pub bottom: f32,
234 pub left: f32,
235}
236 
237impl EdgeInsets {
238 /// Create uniform insets
239 pub fn all(value: f32) -> Self {
240 Self {
241 top: value,
242 right: value,
243 bottom: value,
244 left: value,
245 }
246 }
247 
248 /// Create symmetric insets
249 pub fn symmetric(horizontal: f32, vertical: f32) -> Self {
250 Self {
251 top: vertical,
252 right: horizontal,
253 bottom: vertical,
254 left: horizontal,
255 }
256 }
257 
258 /// Get total horizontal insets
259 pub fn horizontal(&self) -> f32 {
260 self.left + self.right
261 }
262 
263 /// Get total vertical insets
264 pub fn vertical(&self) -> f32 {
265 self.top + self.bottom
266 }
267}
268 
269/// Gap properties for flex containers
270#[derive(Debug, Clone, Copy, Default)]
271pub struct Gap {
272 pub row: f32,
273 pub column: f32,
274}
275 
276impl Gap {
277 /// Create uniform gap
278 pub fn all(value: f32) -> Self {
279 Self {
280 row: value,
281 column: value,
282 }
283 }
284 
285 /// Create gap with different row and column values
286 pub fn new(row: f32, column: f32) -> Self {
287 Self { row, column }
288 }
289}
290 
291/// Flex container properties
292#[derive(Debug, Clone, Copy)]
293pub struct FlexContainer {
294 pub direction: FlexDirection,
295 pub wrap: FlexWrap,
296 pub justify_content: JustifyContent,
297 pub align_items: AlignItems,
298 pub align_content: AlignContent,
299 pub gap: Gap,
300 pub padding: EdgeInsets,
301}
302 
303impl Default for FlexContainer {
304 fn default() -> Self {
305 Self {
306 direction: FlexDirection::Row,
307 wrap: FlexWrap::NoWrap,
308 justify_content: JustifyContent::FlexStart,
309 align_items: AlignItems::Stretch,
310 align_content: AlignContent::Stretch,
311 gap: Gap::default(),
312 padding: EdgeInsets::default(),
313 }
314 }
315}
316 
317/// Layout result for a widget
318#[derive(Debug, Clone, Copy)]
319pub struct Layout {
320 pub position: Vec2,
321 pub size: Size,
322}
323 
324impl Layout {
325 /// Create a new layout
326 pub fn new(position: Vec2, size: Size) -> Self {
327 Self { position, size }
328 }
329 
330 /// Get the bounds as (x, y, width, height)
331 pub fn bounds(&self) -> (f32, f32, f32, f32) {
332 (
333 self.position.x,
334 self.position.y,
335 self.size.width,
336 self.size.height,
337 )
338 }
339 
340 /// Check if a point is within this layout
341 pub fn contains(&self, point: Vec2) -> bool {
342 point.x >= self.position.x
343 && point.x <= self.position.x + self.size.width
344 && point.y >= self.position.y
345 && point.y <= self.position.y + self.size.height
346 }
347}
348 
349/// Flex line (row of items in a flex container)
350#[derive(Debug)]
351struct FlexLine {
352 items: Vec<usize>,
353 main_size: f32,
354 cross_size: f32,
355}
356 
357/// Layout engine for calculating widget positions
358pub struct LayoutEngine {
359 // Allow dead code for cache field as it's part of future optimization plans
360 #[allow(dead_code)]
361 cache: dashmap::DashMap<u64, Layout>,
362}
363 
364impl LayoutEngine {
365 /// Create a new layout engine
366 pub fn new() -> Self {
367 Self {
368 cache: dashmap::DashMap::new(),
369 }
370 }
371 
372 /// Calculate flex layout for a container and its children
373 pub fn calculate_flex_layout(
374 &self,
375 container: &FlexContainer,
376 children: &[(FlexItem, Size)],
377 constraints: Constraints,
378 ) -> Vec<Layout> {
379 if children.is_empty() {
380 return Vec::new();
381 }
382 
383 // Calculate available space after padding
384 let content_constraints = Constraints {
385 min_width: (constraints.min_width - container.padding.horizontal()).max(0.0),
386 max_width: (constraints.max_width - container.padding.horizontal()).max(0.0),
387 min_height: (constraints.min_height - container.padding.vertical()).max(0.0),
388 max_height: (constraints.max_height - container.padding.vertical()).max(0.0),
389 };
390 
391 // Determine main and cross axis dimensions
392 let (main_size, cross_size) = if container.direction.is_row() {
393 (
394 content_constraints.max_width,
395 content_constraints.max_height,
396 )
397 } else {
398 (
399 content_constraints.max_height,
400 content_constraints.max_width,
401 )
402 };
403 
404 // Create flex lines
405 let lines = self.create_flex_lines(container, children, main_size);
406 
407 // Calculate layouts for each line
408 let mut layouts = Vec::with_capacity(children.len());
409 let mut cross_position = container.padding.top;
410 
411 for line in &lines {
412 let line_layouts =
413 self.calculate_line_layout(container, children, line, main_size, cross_position);
414 layouts.extend(line_layouts);
415 cross_position += line.cross_size + container.gap.row;
416 }
417 
418 // Apply align-content for multiple lines
419 if lines.len() > 1 {
420 self.apply_align_content(container, &mut layouts, &lines, cross_size);
421 }
422 
423 layouts
424 }
425 
426 /// Create flex lines based on wrap behavior
427 fn create_flex_lines(
428 &self,
429 container: &FlexContainer,
430 children: &[(FlexItem, Size)],
431 main_size: f32,
432 ) -> Vec<FlexLine> {
433 let mut lines = Vec::new();
434 let mut current_line = FlexLine {
435 items: Vec::new(),
436 main_size: 0.0,
437 cross_size: 0.0,
438 };
439 
440 for (i, (item, size)) in children.iter().enumerate() {
441 let item_main_size = if container.direction.is_row() {
442 size.width + item.margin.horizontal()
443 } else {
444 size.height + item.margin.vertical()
445 };
446 
447 let item_cross_size = if container.direction.is_row() {
448 size.height + item.margin.vertical()
449 } else {
450 size.width + item.margin.horizontal()
451 };
452 
453 // Check if we need to wrap
454 let needs_wrap = container.wrap != FlexWrap::NoWrap
455 && !current_line.items.is_empty()
456 && current_line.main_size + item_main_size + container.gap.column > main_size;
457 
458 if needs_wrap {
459 lines.push(current_line);
460 current_line = FlexLine {
461 items: Vec::new(),
462 main_size: 0.0,
463 cross_size: 0.0,
464 };
465 }
466 
467 current_line.items.push(i);
468 current_line.main_size += item_main_size;
469 if !current_line.items.is_empty() {
470 current_line.main_size += container.gap.column;
471 }
472 current_line.cross_size = current_line.cross_size.max(item_cross_size);
473 }
474 
475 if !current_line.items.is_empty() {
476 lines.push(current_line);
477 }
478 
479 lines
480 }
481 
482 /// Calculate layout for a single flex line
483 fn calculate_line_layout(
484 &self,
485 container: &FlexContainer,
486 children: &[(FlexItem, Size)],
487 line: &FlexLine,
488 main_size: f32,
489 cross_position: f32,
490 ) -> Vec<Layout> {
491 let mut layouts = Vec::new();
492 
493 // Calculate flex grow/shrink
494 let total_flex_grow: f32 = line.items.iter().map(|&i| children[i].0.flex_grow).sum();
495 
496 let total_flex_shrink: f32 = line.items.iter().map(|&i| children[i].0.flex_shrink).sum();
497 
498 // Calculate available space
499 let used_space = line.main_size - container.gap.column * (line.items.len() - 1) as f32;
500 let free_space = main_size - used_space;
501 
502 // Distribute free space
503 let mut main_position = container.padding.left;
504 
505 // Apply justify-content
506 match container.justify_content {
507 JustifyContent::FlexEnd => main_position += free_space,
508 JustifyContent::Center => main_position += free_space / 2.0,
509 JustifyContent::SpaceBetween if line.items.len() > 1 => {
510 // Space will be distributed between items
511 }
512 JustifyContent::SpaceAround => {
513 let space_per_item = free_space / line.items.len() as f32;
514 main_position += space_per_item / 2.0;
515 }
516 JustifyContent::SpaceEvenly => {
517 let space_per_gap = free_space / (line.items.len() + 1) as f32;
518 main_position += space_per_gap;
519 }
520 _ => {}
521 }
522 
523 for (idx, &item_idx) in line.items.iter().enumerate() {
524 let (item, size) = &children[item_idx];
525 
526 // Calculate item main size with flex
527 let mut item_main_size = if container.direction.is_row() {
528 size.width
529 } else {
530 size.height
531 };
532 
533 if free_space > 0.0 && total_flex_grow > 0.0 {
534 item_main_size += (item.flex_grow / total_flex_grow) * free_space;
535 } else if free_space < 0.0 && total_flex_shrink > 0.0 {
536 item_main_size += (item.flex_shrink / total_flex_shrink) * free_space;
537 }
538 
539 let mut item_cross_size = if container.direction.is_row() {
540 size.height
541 } else {
542 size.width
543 };
544 
545 // Apply align-items/align-self
546 let align = if item.align_self != AlignSelf::Auto {
547 match item.align_self {
548 AlignSelf::FlexStart => AlignItems::FlexStart,
549 AlignSelf::FlexEnd => AlignItems::FlexEnd,
550 AlignSelf::Center => AlignItems::Center,
551 AlignSelf::Stretch => AlignItems::Stretch,
552 AlignSelf::Baseline => AlignItems::Baseline,
553 AlignSelf::Auto => container.align_items,
554 }
555 } else {
556 container.align_items
557 };
558 
559 let mut item_cross_position = cross_position;
560 match align {
561 AlignItems::FlexEnd => {
562 item_cross_position += line.cross_size
563 - (item_cross_size
564 + if container.direction.is_row() {
565 item.margin.vertical()
566 } else {
567 item.margin.horizontal()
568 })
569 }
570 AlignItems::Center => {
571 item_cross_position += (line.cross_size
572 - (item_cross_size
573 + if container.direction.is_row() {
574 item.margin.vertical()
575 } else {
576 item.margin.horizontal()
577 }))
578 / 2.0
579 }
580 AlignItems::Stretch => {
581 // Stretch to fill cross axis
582 let margin = if container.direction.is_row() {
583 item.margin.vertical()
584 } else {
585 item.margin.horizontal()
586 };
587 item_cross_size = (line.cross_size - margin).max(0.0);
588 }
589 _ => {}
590 }
591 
592 // Create layout based on direction
593 let layout = if container.direction.is_row() {
594 Layout::new(
595 Vec2::new(
596 main_position + item.margin.left,
597 item_cross_position + item.margin.top,
598 ),
599 Size::new(item_main_size, item_cross_size),
600 )
601 } else {
602 Layout::new(
603 Vec2::new(
604 item_cross_position + item.margin.left,
605 main_position + item.margin.top,
606 ),
607 Size::new(item_cross_size, item_main_size),
608 )
609 };
610 
611 layouts.push(layout);
612 
613 // Update position for next item
614 main_position += item_main_size + item.margin.horizontal() + container.gap.column;
615 
616 // Apply justify-content spacing
617 match container.justify_content {
618 JustifyContent::SpaceBetween
619 if line.items.len() > 1 && idx < line.items.len() - 1 =>
620 {
621 main_position += free_space / (line.items.len() - 1) as f32;
622 }
623 JustifyContent::SpaceAround => {
624 let space_per_item = free_space / line.items.len() as f32;
625 main_position += space_per_item;
626 }
627 JustifyContent::SpaceEvenly if idx < line.items.len() - 1 => {
628 let space_per_gap = free_space / (line.items.len() + 1) as f32;
629 main_position += space_per_gap;
630 }
631 _ => {}
632 }
633 }
634 
635 layouts
636 }
637 
638 /// Apply align-content for multiple lines
639 fn apply_align_content(
640 &self,
641 container: &FlexContainer,
642 layouts: &mut [Layout],
643 lines: &[FlexLine],
644 cross_size: f32,
645 ) {
646 let total_cross_size: f32 = lines.iter().map(|line| line.cross_size).sum();
647 let total_gaps = container.gap.row * (lines.len() - 1) as f32;
648 let free_cross_space = cross_size - total_cross_size - total_gaps;
649 
650 let mut cross_offset = 0.0;
651 match container.align_content {
652 AlignContent::FlexEnd => cross_offset = free_cross_space,
653 AlignContent::Center => cross_offset = free_cross_space / 2.0,
654 AlignContent::SpaceBetween if lines.len() > 1 => {
655 // Space will be distributed between lines
656 }
657 AlignContent::SpaceAround => {
658 cross_offset = free_cross_space / (lines.len() * 2) as f32;
659 }
660 AlignContent::SpaceEvenly => {
661 cross_offset = free_cross_space / (lines.len() + 1) as f32;
662 }
663 _ => return,
664 }
665 
666 // Apply offset to all layouts
667 let mut item_idx = 0;
668 for (line_idx, line) in lines.iter().enumerate() {
669 let line_offset = match container.align_content {
670 AlignContent::SpaceBetween if lines.len() > 1 => {
671 (free_cross_space / (lines.len() - 1) as f32) * line_idx as f32
672 }
673 AlignContent::SpaceAround => {
674 cross_offset + (free_cross_space / lines.len() as f32) * line_idx as f32
675 }
676 AlignContent::SpaceEvenly => cross_offset * (line_idx + 1) as f32,
677 _ => cross_offset,
678 };
679 
680 for _ in 0..line.items.len() {
681 if container.direction.is_row() {
682 layouts[item_idx].position.y += line_offset;
683 } else {
684 layouts[item_idx].position.x += line_offset;
685 }
686 item_idx += 1;
687 }
688 }
689 }
690 
691 /// Clear the layout cache
692 pub fn clear_cache(&self) {
693 self.cache.clear();
694 }
695}
696 
697impl Default for LayoutEngine {
698 fn default() -> Self {
699 Self::new()
700 }
701}
702 
703#[cfg(test)]
704mod tests {
705 use super::*;
706 
707 #[test]
708 fn test_constraints() {
709 let constraints = Constraints::tight(100.0, 100.0);
710 let size = Size::new(150.0, 50.0);
711 let constrained = constraints.constrain(size);
712 assert_eq!(constrained.width, 100.0);
713 assert_eq!(constrained.height, 100.0);
714 }
715 
716 #[test]
717 fn test_flex_layout() {
718 let engine = LayoutEngine::new();
719 let children = vec![
720 (FlexItem::grow(1.0), Size::new(0.0, 50.0)),
721 (FlexItem::grow(2.0), Size::new(0.0, 50.0)),
722 ];
723 
724 let container = FlexContainer {
725 direction: FlexDirection::Row,
726 justify_content: JustifyContent::FlexStart,
727 align_items: AlignItems::FlexStart,
728 ..Default::default()
729 };
730 
731 let layouts =
732 engine.calculate_flex_layout(&container, &children, Constraints::loose(300.0, 100.0));
733 
734 assert_eq!(layouts.len(), 2);
735 assert_eq!(layouts[0].size.width, 100.0);
736 assert_eq!(layouts[1].size.width, 200.0);
737 }
738}
739