⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/browser/
flexbox.rs

1//! CSS Flexbox Layout
2//!
3//! Implements the CSS Flexible Box Layout algorithm. All dimensions
4//! use 26.6 fixed-point arithmetic (i32) consistent with Phase A.
5
6#![allow(dead_code)]
7
8use alloc::vec::Vec;
9
10use super::events::NodeId;
11
12// ---------------------------------------------------------------------------
13// 26.6 fixed-point helpers
14// ---------------------------------------------------------------------------
15
16/// 26.6 fixed-point type
17pub type FixedPoint = i32;
18
19/// Shift for 26.6 fixed-point
20pub const FP_SHIFT: u32 = 6;
21
22/// Convert integer to 26.6 fixed-point
23#[inline]
24pub const fn fp(v: i32) -> FixedPoint {
25    v << FP_SHIFT
26}
27
28/// Convert 26.6 fixed-point to integer (truncate)
29#[inline]
30pub const fn fp_int(v: FixedPoint) -> i32 {
31    v >> FP_SHIFT
32}
33
34/// Multiply two 26.6 fixed-point values
35#[inline]
36pub const fn fp_mul(a: FixedPoint, b: FixedPoint) -> FixedPoint {
37    ((a as i64 * b as i64) >> FP_SHIFT) as i32
38}
39
40/// Divide two 26.6 fixed-point values
41#[inline]
42pub fn fp_div(a: FixedPoint, b: FixedPoint) -> FixedPoint {
43    if b == 0 {
44        return 0;
45    }
46    (((a as i64) << FP_SHIFT) / (b as i64)) as i32
47}
48
49/// Zero in 26.6
50pub const FP_ZERO: FixedPoint = 0;
51
52/// One in 26.6
53pub const FP_ONE: FixedPoint = 1 << FP_SHIFT;
54
55// ---------------------------------------------------------------------------
56// Flex properties
57// ---------------------------------------------------------------------------
58
59/// Flex direction (main axis orientation)
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
61pub enum FlexDirection {
62    #[default]
63    Row,
64    RowReverse,
65    Column,
66    ColumnReverse,
67}
68
69impl FlexDirection {
70    /// Whether the main axis is horizontal
71    pub fn is_row(self) -> bool {
72        matches!(self, Self::Row | Self::RowReverse)
73    }
74
75    /// Whether the direction is reversed
76    pub fn is_reverse(self) -> bool {
77        matches!(self, Self::RowReverse | Self::ColumnReverse)
78    }
79}
80
81/// Flex wrapping behavior
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
83pub enum FlexWrap {
84    #[default]
85    NoWrap,
86    Wrap,
87    WrapReverse,
88}
89
90/// Cross-axis alignment for flex items
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
92pub enum AlignItems {
93    FlexStart,
94    FlexEnd,
95    Center,
96    #[default]
97    Stretch,
98    Baseline,
99}
100
101/// Main-axis content distribution
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
103pub enum JustifyContent {
104    #[default]
105    FlexStart,
106    FlexEnd,
107    Center,
108    SpaceBetween,
109    SpaceAround,
110    SpaceEvenly,
111}
112
113/// Alignment for multiple flex lines
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
115pub enum AlignContent {
116    FlexStart,
117    FlexEnd,
118    Center,
119    #[default]
120    Stretch,
121    SpaceBetween,
122    SpaceAround,
123}
124
125/// Per-item alignment override
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
127pub enum AlignSelf {
128    #[default]
129    Auto,
130    FlexStart,
131    FlexEnd,
132    Center,
133    Stretch,
134    Baseline,
135}
136
137// ---------------------------------------------------------------------------
138// Flex item
139// ---------------------------------------------------------------------------
140
141/// A flex item with its computed properties
142#[derive(Debug, Clone)]
143pub struct FlexItem {
144    /// DOM node ID
145    pub node_id: NodeId,
146    /// CSS order property
147    pub order: i32,
148    /// flex-grow factor (26.6 fixed-point, e.g., fp(1) = 1.0)
149    pub flex_grow: FixedPoint,
150    /// flex-shrink factor
151    pub flex_shrink: FixedPoint,
152    /// flex-basis (26.6 fixed-point pixels)
153    pub flex_basis: FixedPoint,
154    /// Minimum main-axis size
155    pub min_main: FixedPoint,
156    /// Maximum main-axis size (0 = none)
157    pub max_main: FixedPoint,
158    /// Minimum cross-axis size
159    pub min_cross: FixedPoint,
160    /// Maximum cross-axis size (0 = none)
161    pub max_cross: FixedPoint,
162    /// Hypothetical main size (from content or flex-basis)
163    pub hyp_main: FixedPoint,
164    /// Hypothetical cross size
165    pub hyp_cross: FixedPoint,
166    /// Item self-alignment override
167    pub align_self: AlignSelf,
168
169    // -- Output (computed layout position) --
170    /// Main-axis offset from container start
171    pub main_offset: FixedPoint,
172    /// Cross-axis offset from line start
173    pub cross_offset: FixedPoint,
174    /// Final main-axis size
175    pub main_size: FixedPoint,
176    /// Final cross-axis size
177    pub cross_size: FixedPoint,
178    /// Whether this item was frozen during flexible length resolution
179    frozen: bool,
180    /// Target main size during resolution
181    target_main: FixedPoint,
182}
183
184impl Default for FlexItem {
185    fn default() -> Self {
186        Self {
187            node_id: 0,
188            order: 0,
189            flex_grow: FP_ZERO,
190            flex_shrink: FP_ONE,
191            flex_basis: FP_ZERO,
192            min_main: FP_ZERO,
193            max_main: 0,
194            min_cross: FP_ZERO,
195            max_cross: 0,
196            hyp_main: FP_ZERO,
197            hyp_cross: FP_ZERO,
198            align_self: AlignSelf::Auto,
199            main_offset: FP_ZERO,
200            cross_offset: FP_ZERO,
201            main_size: FP_ZERO,
202            cross_size: FP_ZERO,
203            frozen: false,
204            target_main: FP_ZERO,
205        }
206    }
207}
208
209impl FlexItem {
210    pub fn new(node_id: NodeId) -> Self {
211        Self {
212            node_id,
213            ..Default::default()
214        }
215    }
216
217    /// Clamp the target main size to min/max constraints
218    fn clamp_main(&self, size: FixedPoint) -> FixedPoint {
219        let clamped = if self.min_main > 0 {
220            size.max(self.min_main)
221        } else {
222            size
223        };
224        if self.max_main > 0 {
225            clamped.min(self.max_main)
226        } else {
227            clamped
228        }
229    }
230}
231
232// ---------------------------------------------------------------------------
233// Flex line (for wrapping)
234// ---------------------------------------------------------------------------
235
236/// A single line of flex items (created during wrapping)
237#[derive(Debug, Clone, Default)]
238struct FlexLine {
239    /// Indices into the items vec
240    item_indices: Vec<usize>,
241    /// Cross-axis size of this line
242    cross_size: FixedPoint,
243    /// Cross-axis offset of this line
244    cross_offset: FixedPoint,
245}
246
247// ---------------------------------------------------------------------------
248// Flex container / layout engine
249// ---------------------------------------------------------------------------
250
251/// Flex container properties
252#[derive(Debug, Clone, Default)]
253pub struct FlexContainerStyle {
254    pub direction: FlexDirection,
255    pub wrap: FlexWrap,
256    pub justify_content: JustifyContent,
257    pub align_items: AlignItems,
258    pub align_content: AlignContent,
259}
260
261/// Flex layout engine
262pub struct FlexLayout {
263    /// Container style
264    pub style: FlexContainerStyle,
265    /// Available main-axis size (26.6 fixed-point)
266    pub available_main: FixedPoint,
267    /// Available cross-axis size
268    pub available_cross: FixedPoint,
269}
270
271impl FlexLayout {
272    pub fn new(
273        style: FlexContainerStyle,
274        available_main: FixedPoint,
275        available_cross: FixedPoint,
276    ) -> Self {
277        Self {
278            style,
279            available_main,
280            available_cross,
281        }
282    }
283
284    /// Run the full flexbox layout algorithm on the given items.
285    /// Modifies items in-place with computed positions and sizes.
286    pub fn layout(&self, items: &mut [FlexItem]) {
287        if items.is_empty() {
288            return;
289        }
290
291        // Sort by CSS order
292        items.sort_by_key(|item| item.order);
293
294        // If direction is reversed, reverse the item order
295        if self.style.direction.is_reverse() {
296            items.reverse();
297        }
298
299        // Collect items into flex lines
300        let lines = self.collect_into_lines(items);
301
302        // Resolve flexible lengths per line
303        for line in &lines {
304            self.resolve_flexible_lengths(items, &line.item_indices);
305        }
306
307        // Compute cross sizes
308        let mut computed_lines = self.compute_cross_sizes(items, &lines);
309
310        // Distribute cross-axis space
311        self.distribute_cross_space(&mut computed_lines, items);
312
313        // Position items on main axis per line
314        for line in &computed_lines {
315            self.position_main_axis(items, &line.item_indices);
316        }
317
318        // Position items on cross axis
319        for line in &computed_lines {
320            self.position_cross_axis(items, line);
321        }
322    }
323
324    /// Break items into flex lines (wrapping)
325    fn collect_into_lines(&self, items: &[FlexItem]) -> Vec<FlexLine> {
326        let mut lines = Vec::new();
327        let mut current_line = FlexLine::default();
328        let mut line_main_size: FixedPoint = FP_ZERO;
329
330        for (i, item) in items.iter().enumerate() {
331            let item_main = if item.flex_basis > 0 {
332                item.flex_basis
333            } else {
334                item.hyp_main
335            };
336
337            if self.style.wrap != FlexWrap::NoWrap
338                && !current_line.item_indices.is_empty()
339                && line_main_size + item_main > self.available_main
340            {
341                lines.push(current_line);
342                current_line = FlexLine::default();
343                line_main_size = FP_ZERO;
344            }
345
346            current_line.item_indices.push(i);
347            line_main_size += item_main;
348        }
349
350        if !current_line.item_indices.is_empty() {
351            lines.push(current_line);
352        }
353
354        if self.style.wrap == FlexWrap::WrapReverse {
355            lines.reverse();
356        }
357
358        lines
359    }
360
361    /// Resolve flexible lengths using the grow/shrink algorithm
362    fn resolve_flexible_lengths(&self, items: &mut [FlexItem], indices: &[usize]) {
363        // Initialize: set each item's target to its flex basis
364        for &idx in indices {
365            items[idx].frozen = false;
366            items[idx].target_main = if items[idx].flex_basis > 0 {
367                items[idx].flex_basis
368            } else {
369                items[idx].hyp_main
370            };
371        }
372
373        // Calculate used space
374        let used: FixedPoint = indices.iter().map(|&i| items[i].target_main).sum();
375
376        let free_space = self.available_main - used;
377        let growing = free_space > 0;
378
379        // Freeze items that cannot flex
380        for &idx in indices {
381            let item = &mut items[idx];
382            if (growing && item.flex_grow == 0) || (!growing && item.flex_shrink == 0) {
383                item.frozen = true;
384                item.main_size = item.target_main;
385            }
386        }
387
388        // Iterative resolution (up to 10 iterations to prevent infinite loops)
389        for _ in 0..10 {
390            let unfrozen: Vec<usize> = indices
391                .iter()
392                .copied()
393                .filter(|&i| !items[i].frozen)
394                .collect();
395
396            if unfrozen.is_empty() {
397                break;
398            }
399
400            // Recalculate free space considering frozen items
401            let frozen_size: FixedPoint = indices
402                .iter()
403                .filter(|&&i| items[i].frozen)
404                .map(|&i| items[i].main_size)
405                .sum();
406            let unfrozen_basis: FixedPoint = unfrozen.iter().map(|&i| items[i].target_main).sum();
407            let remaining = self.available_main - frozen_size - unfrozen_basis;
408
409            if growing {
410                let total_grow: FixedPoint = unfrozen.iter().map(|&i| items[i].flex_grow).sum();
411                if total_grow > 0 {
412                    for &idx in &unfrozen {
413                        // Single-step division avoids intermediate fp_div truncation
414                        let addition = ((remaining as i64 * items[idx].flex_grow as i64)
415                            / total_grow as i64) as i32;
416                        items[idx].target_main += addition;
417                    }
418                }
419            } else {
420                let total_shrink: FixedPoint = unfrozen
421                    .iter()
422                    .map(|&i| fp_mul(items[i].flex_shrink, items[i].target_main))
423                    .sum();
424                if total_shrink > 0 {
425                    for &idx in &unfrozen {
426                        let scaled = fp_mul(items[idx].flex_shrink, items[idx].target_main);
427                        let ratio = fp_div(scaled, total_shrink);
428                        let reduction = fp_mul(remaining.abs(), ratio);
429                        items[idx].target_main -= reduction;
430                    }
431                }
432            }
433
434            // Clamp and freeze items that hit min/max
435            let mut all_ok = true;
436            for &idx in &unfrozen {
437                let clamped = items[idx].clamp_main(items[idx].target_main);
438                if clamped != items[idx].target_main {
439                    items[idx].target_main = clamped;
440                    items[idx].frozen = true;
441                    all_ok = false;
442                }
443            }
444
445            // Freeze all remaining if nothing was clamped
446            if all_ok {
447                for &idx in &unfrozen {
448                    items[idx].frozen = true;
449                }
450                break;
451            }
452        }
453
454        // Apply computed sizes
455        for &idx in indices {
456            items[idx].main_size = items[idx].clamp_main(items[idx].target_main);
457        }
458    }
459
460    /// Compute cross sizes for each line
461    fn compute_cross_sizes(&self, items: &[FlexItem], lines: &[FlexLine]) -> Vec<FlexLine> {
462        let mut result = Vec::with_capacity(lines.len());
463        for line in lines {
464            let mut max_cross = FP_ZERO;
465            for &idx in &line.item_indices {
466                let cross = if items[idx].hyp_cross > 0 {
467                    items[idx].hyp_cross
468                } else {
469                    items[idx].main_size // fallback: square
470                };
471                if cross > max_cross {
472                    max_cross = cross;
473                }
474            }
475            result.push(FlexLine {
476                item_indices: line.item_indices.clone(),
477                cross_size: max_cross,
478                cross_offset: FP_ZERO,
479            });
480        }
481        result
482    }
483
484    /// Distribute cross-axis space among lines
485    fn distribute_cross_space(&self, lines: &mut [FlexLine], _items: &[FlexItem]) {
486        let total_cross: FixedPoint = lines.iter().map(|l| l.cross_size).sum();
487        let free_cross = self.available_cross - total_cross;
488
489        match self.style.align_content {
490            AlignContent::FlexStart => {
491                let mut offset = FP_ZERO;
492                for line in lines.iter_mut() {
493                    line.cross_offset = offset;
494                    offset += line.cross_size;
495                }
496            }
497            AlignContent::FlexEnd => {
498                let mut offset = free_cross.max(FP_ZERO);
499                for line in lines.iter_mut() {
500                    line.cross_offset = offset;
501                    offset += line.cross_size;
502                }
503            }
504            AlignContent::Center => {
505                let mut offset = (free_cross / 2).max(FP_ZERO);
506                for line in lines.iter_mut() {
507                    line.cross_offset = offset;
508                    offset += line.cross_size;
509                }
510            }
511            AlignContent::Stretch => {
512                let extra_per_line = if !lines.is_empty() && free_cross > 0 {
513                    free_cross / lines.len() as i32
514                } else {
515                    FP_ZERO
516                };
517                let mut offset = FP_ZERO;
518                for line in lines.iter_mut() {
519                    line.cross_offset = offset;
520                    line.cross_size += extra_per_line;
521                    offset += line.cross_size;
522                }
523            }
524            AlignContent::SpaceBetween => {
525                let gap = if lines.len() > 1 && free_cross > 0 {
526                    free_cross / (lines.len() as i32 - 1)
527                } else {
528                    FP_ZERO
529                };
530                let mut offset = FP_ZERO;
531                for line in lines.iter_mut() {
532                    line.cross_offset = offset;
533                    offset += line.cross_size + gap;
534                }
535            }
536            AlignContent::SpaceAround => {
537                let gap = if !lines.is_empty() && free_cross > 0 {
538                    free_cross / lines.len() as i32
539                } else {
540                    FP_ZERO
541                };
542                let mut offset = gap / 2;
543                for line in lines.iter_mut() {
544                    line.cross_offset = offset;
545                    offset += line.cross_size + gap;
546                }
547            }
548        }
549    }
550
551    /// Position items along the main axis within a line
552    fn position_main_axis(&self, items: &mut [FlexItem], indices: &[usize]) {
553        let total_main: FixedPoint = indices.iter().map(|&i| items[i].main_size).sum();
554        let free = self.available_main - total_main;
555        let count = indices.len() as i32;
556
557        let (mut offset, gap) = match self.style.justify_content {
558            JustifyContent::FlexStart => (FP_ZERO, FP_ZERO),
559            JustifyContent::FlexEnd => (free.max(FP_ZERO), FP_ZERO),
560            JustifyContent::Center => ((free / 2).max(FP_ZERO), FP_ZERO),
561            JustifyContent::SpaceBetween => {
562                let g = if count > 1 && free > 0 {
563                    free / (count - 1)
564                } else {
565                    FP_ZERO
566                };
567                (FP_ZERO, g)
568            }
569            JustifyContent::SpaceAround => {
570                let g = if count > 0 && free > 0 {
571                    free / count
572                } else {
573                    FP_ZERO
574                };
575                (g / 2, g)
576            }
577            JustifyContent::SpaceEvenly => {
578                let g = if count > 0 && free > 0 {
579                    free / (count + 1)
580                } else {
581                    FP_ZERO
582                };
583                (g, g)
584            }
585        };
586
587        for &idx in indices {
588            items[idx].main_offset = offset;
589            offset += items[idx].main_size + gap;
590        }
591    }
592
593    /// Position items along the cross axis within a line
594    fn position_cross_axis(&self, items: &mut [FlexItem], line: &FlexLine) {
595        for &idx in &line.item_indices {
596            let align = match items[idx].align_self {
597                AlignSelf::Auto => self.style.align_items,
598                AlignSelf::FlexStart => AlignItems::FlexStart,
599                AlignSelf::FlexEnd => AlignItems::FlexEnd,
600                AlignSelf::Center => AlignItems::Center,
601                AlignSelf::Stretch => AlignItems::Stretch,
602                AlignSelf::Baseline => AlignItems::Baseline,
603            };
604
605            let item_cross = if items[idx].hyp_cross > 0 {
606                items[idx].hyp_cross
607            } else {
608                line.cross_size
609            };
610
611            match align {
612                AlignItems::FlexStart => {
613                    items[idx].cross_offset = line.cross_offset;
614                    items[idx].cross_size = item_cross;
615                }
616                AlignItems::FlexEnd => {
617                    items[idx].cross_offset = line.cross_offset + line.cross_size - item_cross;
618                    items[idx].cross_size = item_cross;
619                }
620                AlignItems::Center => {
621                    items[idx].cross_offset =
622                        line.cross_offset + (line.cross_size - item_cross) / 2;
623                    items[idx].cross_size = item_cross;
624                }
625                AlignItems::Stretch => {
626                    items[idx].cross_offset = line.cross_offset;
627                    items[idx].cross_size = line.cross_size;
628                }
629                AlignItems::Baseline => {
630                    // Baseline alignment falls back to flex-start
631                    items[idx].cross_offset = line.cross_offset;
632                    items[idx].cross_size = item_cross;
633                }
634            }
635        }
636    }
637
638    /// Get the final bounding rectangle for a flex item.
639    /// Returns (x, y, width, height) in 26.6 fixed-point.
640    pub fn item_rect(
641        item: &FlexItem,
642        direction: FlexDirection,
643    ) -> (FixedPoint, FixedPoint, FixedPoint, FixedPoint) {
644        if direction.is_row() {
645            (
646                item.main_offset,
647                item.cross_offset,
648                item.main_size,
649                item.cross_size,
650            )
651        } else {
652            (
653                item.cross_offset,
654                item.main_offset,
655                item.cross_size,
656                item.main_size,
657            )
658        }
659    }
660}
661
662// ---------------------------------------------------------------------------
663// Tests
664// ---------------------------------------------------------------------------
665
666#[cfg(test)]
667mod tests {
668    #[allow(unused_imports)]
669    use alloc::vec;
670
671    use super::*;
672
673    fn make_item(node_id: NodeId, basis: i32, grow: i32, shrink: i32) -> FlexItem {
674        FlexItem {
675            node_id,
676            flex_basis: fp(basis),
677            flex_grow: fp(grow),
678            flex_shrink: fp(shrink),
679            hyp_main: fp(basis),
680            hyp_cross: fp(30),
681            ..Default::default()
682        }
683    }
684
685    #[test]
686    fn test_fp_helpers() {
687        assert_eq!(fp(10), 640);
688        assert_eq!(fp_int(fp(10)), 10);
689        assert_eq!(fp_int(fp_mul(fp(3), fp(4))), 12);
690    }
691
692    #[test]
693    fn test_fp_div() {
694        assert_eq!(fp_int(fp_div(fp(10), fp(2))), 5);
695        assert_eq!(fp_div(fp(1), fp(0)), 0);
696    }
697
698    #[test]
699    fn test_direction_is_row() {
700        assert!(FlexDirection::Row.is_row());
701        assert!(FlexDirection::RowReverse.is_row());
702        assert!(!FlexDirection::Column.is_row());
703    }
704
705    #[test]
706    fn test_direction_is_reverse() {
707        assert!(!FlexDirection::Row.is_reverse());
708        assert!(FlexDirection::RowReverse.is_reverse());
709        assert!(FlexDirection::ColumnReverse.is_reverse());
710    }
711
712    #[test]
713    fn test_single_item_fills_container() {
714        let style = FlexContainerStyle {
715            direction: FlexDirection::Row,
716            justify_content: JustifyContent::FlexStart,
717            align_items: AlignItems::Stretch,
718            ..Default::default()
719        };
720        let layout = FlexLayout::new(style, fp(300), fp(100));
721        let mut items = vec![make_item(0, 100, 1, 0)];
722        layout.layout(&mut items);
723        // Single item with grow=1 should fill 300
724        assert_eq!(fp_int(items[0].main_size), 300);
725    }
726
727    #[test]
728    fn test_two_items_equal_grow() {
729        let style = FlexContainerStyle::default();
730        let layout = FlexLayout::new(style, fp(200), fp(100));
731        let mut items = vec![make_item(0, 50, 1, 0), make_item(1, 50, 1, 0)];
732        layout.layout(&mut items);
733        // Free space 100, split equally
734        assert_eq!(fp_int(items[0].main_size), 100);
735        assert_eq!(fp_int(items[1].main_size), 100);
736    }
737
738    #[test]
739    fn test_unequal_grow() {
740        let style = FlexContainerStyle::default();
741        let layout = FlexLayout::new(style, fp(300), fp(100));
742        let mut items = vec![make_item(0, 0, 1, 0), make_item(1, 0, 2, 0)];
743        layout.layout(&mut items);
744        // grow 1:2 ratio on 300px
745        assert_eq!(fp_int(items[0].main_size), 100);
746        assert_eq!(fp_int(items[1].main_size), 200);
747    }
748
749    #[test]
750    fn test_no_grow() {
751        let style = FlexContainerStyle::default();
752        let layout = FlexLayout::new(style, fp(300), fp(100));
753        let mut items = vec![make_item(0, 80, 0, 0), make_item(1, 60, 0, 0)];
754        layout.layout(&mut items);
755        assert_eq!(fp_int(items[0].main_size), 80);
756        assert_eq!(fp_int(items[1].main_size), 60);
757    }
758
759    #[test]
760    fn test_justify_center() {
761        let style = FlexContainerStyle {
762            justify_content: JustifyContent::Center,
763            ..Default::default()
764        };
765        let layout = FlexLayout::new(style, fp(300), fp(100));
766        let mut items = vec![make_item(0, 100, 0, 0)];
767        layout.layout(&mut items);
768        // 100px item centered in 300px => offset = 100
769        assert_eq!(fp_int(items[0].main_offset), 100);
770    }
771
772    #[test]
773    fn test_justify_flex_end() {
774        let style = FlexContainerStyle {
775            justify_content: JustifyContent::FlexEnd,
776            ..Default::default()
777        };
778        let layout = FlexLayout::new(style, fp(300), fp(100));
779        let mut items = vec![make_item(0, 100, 0, 0)];
780        layout.layout(&mut items);
781        assert_eq!(fp_int(items[0].main_offset), 200);
782    }
783
784    #[test]
785    fn test_justify_space_between() {
786        let style = FlexContainerStyle {
787            justify_content: JustifyContent::SpaceBetween,
788            ..Default::default()
789        };
790        let layout = FlexLayout::new(style, fp(300), fp(100));
791        let mut items = vec![
792            make_item(0, 50, 0, 0),
793            make_item(1, 50, 0, 0),
794            make_item(2, 50, 0, 0),
795        ];
796        layout.layout(&mut items);
797        // 300 - 150 = 150 free, 2 gaps = 75 each
798        assert_eq!(fp_int(items[0].main_offset), 0);
799        assert_eq!(fp_int(items[1].main_offset), 125); // 50 + 75
800        assert_eq!(fp_int(items[2].main_offset), 250); // 50 + 75 + 50 + 75
801    }
802
803    #[test]
804    fn test_justify_space_evenly() {
805        let style = FlexContainerStyle {
806            justify_content: JustifyContent::SpaceEvenly,
807            ..Default::default()
808        };
809        let layout = FlexLayout::new(style, fp(400), fp(100));
810        let mut items = vec![make_item(0, 50, 0, 0), make_item(1, 50, 0, 0)];
811        layout.layout(&mut items);
812        // 400 - 100 = 300 free, 3 gaps = 100 each
813        assert_eq!(fp_int(items[0].main_offset), 100);
814        assert_eq!(fp_int(items[1].main_offset), 250);
815    }
816
817    #[test]
818    fn test_align_items_center() {
819        let style = FlexContainerStyle {
820            align_items: AlignItems::Center,
821            ..Default::default()
822        };
823        let layout = FlexLayout::new(style, fp(300), fp(100));
824        let mut items = vec![make_item(0, 100, 0, 0)];
825        items[0].hyp_cross = fp(40);
826        layout.layout(&mut items);
827        // Cross size 40, line cross 40, centered in 40 => 0
828        // But the line cross_size = max(hyp_cross) = 40
829        // Then distribute_cross_space stretches to available_cross=100
830        // With Stretch align_content (default), line gets 100px cross
831        // Center: offset = (100 - 40)/2 = 30
832        assert_eq!(fp_int(items[0].cross_offset), 30);
833    }
834
835    #[test]
836    fn test_align_items_stretch() {
837        let style = FlexContainerStyle {
838            align_items: AlignItems::Stretch,
839            ..Default::default()
840        };
841        let layout = FlexLayout::new(style, fp(300), fp(100));
842        let mut items = vec![make_item(0, 100, 0, 0)];
843        items[0].hyp_cross = fp(40);
844        layout.layout(&mut items);
845        // Stretch: item cross = line cross = 100 (after stretch)
846        assert_eq!(fp_int(items[0].cross_size), 100);
847    }
848
849    #[test]
850    fn test_wrap_basic() {
851        let style = FlexContainerStyle {
852            wrap: FlexWrap::Wrap,
853            ..Default::default()
854        };
855        let layout = FlexLayout::new(style, fp(200), fp(200));
856        let mut items = vec![make_item(0, 120, 0, 0), make_item(1, 120, 0, 0)];
857        layout.layout(&mut items);
858        // 120 + 120 > 200, so wrap to 2 lines
859        // Item 0 on first line, item 1 on second line
860        assert_eq!(fp_int(items[0].main_offset), 0);
861        assert_eq!(fp_int(items[1].main_offset), 0);
862        // Item 1 should be on a different cross line
863        assert!(items[1].cross_offset > items[0].cross_offset);
864    }
865
866    #[test]
867    fn test_nowrap() {
868        let style = FlexContainerStyle {
869            wrap: FlexWrap::NoWrap,
870            ..Default::default()
871        };
872        let layout = FlexLayout::new(style, fp(200), fp(100));
873        let mut items = vec![make_item(0, 120, 0, 1), make_item(1, 120, 0, 1)];
874        layout.layout(&mut items);
875        // No wrap: both on same line, may overflow or shrink
876        // With shrink=1, items shrink proportionally
877    }
878
879    #[test]
880    fn test_order() {
881        let style = FlexContainerStyle::default();
882        let layout = FlexLayout::new(style, fp(300), fp(100));
883        let mut items = vec![
884            {
885                let mut item = make_item(0, 50, 0, 0);
886                item.order = 2;
887                item
888            },
889            {
890                let mut item = make_item(1, 50, 0, 0);
891                item.order = 1;
892                item
893            },
894        ];
895        layout.layout(&mut items);
896        // Order 1 comes first
897        assert!(items[0].order <= items[1].order);
898    }
899
900    #[test]
901    fn test_reverse() {
902        let style = FlexContainerStyle {
903            direction: FlexDirection::RowReverse,
904            ..Default::default()
905        };
906        let layout = FlexLayout::new(style, fp(300), fp(100));
907        let mut items = vec![make_item(0, 100, 0, 0), make_item(1, 100, 0, 0)];
908        layout.layout(&mut items);
909        // Items reversed: original 1 is now at offset 0
910    }
911
912    #[test]
913    fn test_column_direction() {
914        let style = FlexContainerStyle {
915            direction: FlexDirection::Column,
916            ..Default::default()
917        };
918        let layout = FlexLayout::new(style, fp(400), fp(300));
919        let mut items = vec![make_item(0, 100, 1, 0), make_item(1, 100, 1, 0)];
920        layout.layout(&mut items);
921        // Column: main = vertical, so items split 400 height
922        assert_eq!(fp_int(items[0].main_size), 200);
923        assert_eq!(fp_int(items[1].main_size), 200);
924    }
925
926    #[test]
927    fn test_item_rect_row() {
928        let mut item = FlexItem::new(0);
929        item.main_offset = fp(10);
930        item.cross_offset = fp(20);
931        item.main_size = fp(100);
932        item.cross_size = fp(50);
933        let (x, y, w, h) = FlexLayout::item_rect(&item, FlexDirection::Row);
934        assert_eq!(fp_int(x), 10);
935        assert_eq!(fp_int(y), 20);
936        assert_eq!(fp_int(w), 100);
937        assert_eq!(fp_int(h), 50);
938    }
939
940    #[test]
941    fn test_item_rect_column() {
942        let mut item = FlexItem::new(0);
943        item.main_offset = fp(10);
944        item.cross_offset = fp(20);
945        item.main_size = fp(100);
946        item.cross_size = fp(50);
947        let (x, y, w, h) = FlexLayout::item_rect(&item, FlexDirection::Column);
948        // Column: x=cross_offset, y=main_offset
949        assert_eq!(fp_int(x), 20);
950        assert_eq!(fp_int(y), 10);
951        assert_eq!(fp_int(w), 50);
952        assert_eq!(fp_int(h), 100);
953    }
954
955    #[test]
956    fn test_shrink() {
957        let style = FlexContainerStyle::default();
958        let layout = FlexLayout::new(style, fp(100), fp(50));
959        let mut items = vec![make_item(0, 80, 0, 1), make_item(1, 80, 0, 1)];
960        layout.layout(&mut items);
961        // 160 > 100, shrink by 60 total
962        // Equal shrink factors, equal basis => 50 each
963        assert_eq!(fp_int(items[0].main_size), 50);
964        assert_eq!(fp_int(items[1].main_size), 50);
965    }
966
967    #[test]
968    fn test_min_max_clamp() {
969        let style = FlexContainerStyle::default();
970        let layout = FlexLayout::new(style, fp(300), fp(100));
971        let mut items = vec![{
972            let mut item = make_item(0, 50, 1, 0);
973            item.max_main = fp(200);
974            item
975        }];
976        layout.layout(&mut items);
977        // Would grow to 300, but clamped to 200
978        assert_eq!(fp_int(items[0].main_size), 200);
979    }
980
981    #[test]
982    fn test_align_self_override() {
983        let style = FlexContainerStyle {
984            align_items: AlignItems::FlexStart,
985            ..Default::default()
986        };
987        let layout = FlexLayout::new(style, fp(300), fp(100));
988        let mut items = vec![{
989            let mut item = make_item(0, 100, 0, 0);
990            item.align_self = AlignSelf::FlexEnd;
991            item.hyp_cross = fp(30);
992            item
993        }];
994        layout.layout(&mut items);
995        // AlignSelf::FlexEnd overrides FlexStart
996        // Line cross = 100 (stretched), item cross = 30
997        // FlexEnd: offset = 100 - 30 = 70
998        assert_eq!(fp_int(items[0].cross_offset), 70);
999    }
1000
1001    #[test]
1002    fn test_empty_items() {
1003        let style = FlexContainerStyle::default();
1004        let layout = FlexLayout::new(style, fp(300), fp(100));
1005        let mut items: Vec<FlexItem> = Vec::new();
1006        layout.layout(&mut items);
1007        // Should not panic
1008    }
1009
1010    #[test]
1011    fn test_flex_defaults() {
1012        let item = FlexItem::default();
1013        assert_eq!(item.flex_grow, FP_ZERO);
1014        assert_eq!(item.flex_shrink, FP_ONE);
1015        assert_eq!(item.order, 0);
1016    }
1017
1018    #[test]
1019    fn test_justify_space_around() {
1020        let style = FlexContainerStyle {
1021            justify_content: JustifyContent::SpaceAround,
1022            ..Default::default()
1023        };
1024        let layout = FlexLayout::new(style, fp(300), fp(100));
1025        let mut items = vec![make_item(0, 50, 0, 0), make_item(1, 50, 0, 0)];
1026        layout.layout(&mut items);
1027        // Free = 200, 2 items, gap = 100, start = 50
1028        assert_eq!(fp_int(items[0].main_offset), 50);
1029        assert_eq!(fp_int(items[1].main_offset), 200);
1030    }
1031}