Skip to main content

i_slint_core/
layout.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! Runtime support for layouts.
5
6// cspell:ignore coord
7
8use crate::items::{
9    CrossAxisAlignment, DialogButtonRole, FlexboxLayoutDirection, FlexboxLayoutWrap,
10    LayoutAlignment,
11};
12use crate::{Coord, SharedVector, slice::Slice};
13use alloc::format;
14use alloc::string::String;
15use alloc::vec::Vec;
16use num_traits::Float;
17
18pub use crate::items::Orientation;
19
20/// The constraint that applies to a layout
21// Also, the field needs to be in alphabetical order because how the generated code sort fields for struct
22#[repr(C)]
23#[derive(Clone, Copy, Debug, PartialEq)]
24pub struct LayoutInfo {
25    /// The maximum size for the item.
26    pub max: Coord,
27    /// The maximum size in percentage of the parent (value between 0 and 100).
28    pub max_percent: Coord,
29    /// The minimum size for this item.
30    pub min: Coord,
31    /// The minimum size in percentage of the parent (value between 0 and 100).
32    pub min_percent: Coord,
33    /// the preferred size
34    pub preferred: Coord,
35    /// the  stretch factor
36    pub stretch: f32,
37}
38
39impl Default for LayoutInfo {
40    fn default() -> Self {
41        LayoutInfo {
42            min: 0 as _,
43            max: Coord::MAX,
44            min_percent: 0 as _,
45            max_percent: 100 as _,
46            preferred: 0 as _,
47            stretch: 0 as _,
48        }
49    }
50}
51
52impl LayoutInfo {
53    // Note: This "logic" is duplicated in the cpp generator's generated code for merging layout
54    // infos, and in the compiler's const_propagation pass, which folds merges of constant
55    // layout infos at compile time.
56    #[must_use]
57    pub fn merge(&self, other: &LayoutInfo) -> Self {
58        Self {
59            min: self.min.max(other.min),
60            max: self.max.min(other.max),
61            min_percent: self.min_percent.max(other.min_percent),
62            max_percent: self.max_percent.min(other.max_percent),
63            preferred: self.preferred.max(other.preferred),
64            stretch: self.stretch.min(other.stretch),
65        }
66    }
67
68    /// Helper function to return a preferred size which is within the min/max constraints
69    #[must_use]
70    pub fn preferred_bounded(&self) -> Coord {
71        self.preferred.min(self.max).max(self.min)
72    }
73}
74
75impl core::ops::Add for LayoutInfo {
76    type Output = Self;
77
78    fn add(self, rhs: Self) -> Self::Output {
79        self.merge(&rhs)
80    }
81}
82
83/// Returns the logical min and max sizes given the provided layout constraints.
84pub fn min_max_size_for_layout_constraints(
85    constraints_horizontal: LayoutInfo,
86    constraints_vertical: LayoutInfo,
87) -> (Option<crate::api::LogicalSize>, Option<crate::api::LogicalSize>) {
88    let min_width = constraints_horizontal.min.min(constraints_horizontal.max) as f32;
89    let min_height = constraints_vertical.min.min(constraints_vertical.max) as f32;
90    let max_width = constraints_horizontal.max.max(constraints_horizontal.min) as f32;
91    let max_height = constraints_vertical.max.max(constraints_vertical.min) as f32;
92
93    //cfg!(target_arch = "wasm32") is there because wasm32 winit don't like when max size is None:
94    // panicked at 'Property is read only: JsValue(NoModificationAllowedError: CSSStyleDeclaration.removeProperty: Can't remove property 'max-width' from computed style
95
96    let min_size = if min_width > 0. || min_height > 0. || cfg!(target_arch = "wasm32") {
97        Some(crate::api::LogicalSize::new(min_width, min_height))
98    } else {
99        None
100    };
101
102    let max_size = if (max_width > 0.
103        && max_height > 0.
104        && (max_width < i32::MAX as f32 || max_height < i32::MAX as f32))
105        || cfg!(target_arch = "wasm32")
106    {
107        // maximum widget size for Qt and a workaround for the winit api not allowing partial constraints
108        let window_size_max = 16_777_215.;
109        Some(crate::api::LogicalSize::new(
110            max_width.min(window_size_max),
111            max_height.min(window_size_max),
112        ))
113    } else {
114        None
115    };
116
117    (min_size, max_size)
118}
119
120/// Implement a saturating_add version for both possible value of Coord.
121/// So that adding the max value does not overflow
122trait Saturating {
123    fn add(_: Self, _: Self) -> Self;
124}
125impl Saturating for i32 {
126    #[inline]
127    fn add(a: Self, b: Self) -> Self {
128        a.saturating_add(b)
129    }
130}
131impl Saturating for f32 {
132    #[inline]
133    fn add(a: Self, b: Self) -> Self {
134        a + b
135    }
136}
137
138mod grid_internal {
139    use super::*;
140
141    fn order_coord<T: PartialOrd>(a: &T, b: &T) -> core::cmp::Ordering {
142        a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)
143    }
144
145    #[derive(Debug, Clone)]
146    pub struct LayoutData {
147        // inputs
148        pub min: Coord,
149        pub max: Coord,
150        pub pref: Coord,
151        pub stretch: f32,
152
153        // outputs
154        pub pos: Coord,
155        pub size: Coord,
156    }
157
158    impl Default for LayoutData {
159        fn default() -> Self {
160            LayoutData {
161                min: 0 as _,
162                max: Coord::MAX,
163                pref: 0 as _,
164                stretch: f32::MAX,
165                pos: 0 as _,
166                size: 0 as _,
167            }
168        }
169    }
170
171    trait Adjust {
172        fn can_grow(_: &LayoutData) -> Coord;
173        fn to_distribute(expected_size: Coord, current_size: Coord) -> Coord;
174        fn distribute(_: &mut LayoutData, val: Coord);
175    }
176
177    struct Grow;
178    impl Adjust for Grow {
179        fn can_grow(it: &LayoutData) -> Coord {
180            it.max - it.size
181        }
182
183        fn to_distribute(expected_size: Coord, current_size: Coord) -> Coord {
184            expected_size - current_size
185        }
186
187        fn distribute(it: &mut LayoutData, val: Coord) {
188            it.size += val;
189        }
190    }
191
192    struct Shrink;
193    impl Adjust for Shrink {
194        fn can_grow(it: &LayoutData) -> Coord {
195            it.size - it.min
196        }
197
198        fn to_distribute(expected_size: Coord, current_size: Coord) -> Coord {
199            current_size - expected_size
200        }
201
202        fn distribute(it: &mut LayoutData, val: Coord) {
203            it.size -= val;
204        }
205    }
206
207    #[allow(clippy::unnecessary_cast)] // Coord
208    fn adjust_items<A: Adjust>(data: &mut [LayoutData], size_without_spacing: Coord) -> Option<()> {
209        loop {
210            let size_cannot_grow: Coord = data
211                .iter()
212                .filter(|it| A::can_grow(it) <= 0 as _)
213                .map(|it| it.size)
214                .fold(0 as Coord, Saturating::add);
215
216            let total_stretch: f32 =
217                data.iter().filter(|it| A::can_grow(it) > 0 as _).map(|it| it.stretch).sum();
218
219            let actual_stretch = |s: f32| if total_stretch <= 0. { 1. } else { s };
220
221            let max_grow = data
222                .iter()
223                .filter(|it| A::can_grow(it) > 0 as _)
224                .map(|it| A::can_grow(it) as f32 / actual_stretch(it.stretch))
225                .min_by(order_coord)?;
226
227            let current_size: Coord = data
228                .iter()
229                .filter(|it| A::can_grow(it) > 0 as _)
230                .map(|it| it.size)
231                .fold(0 as _, Saturating::add);
232
233            //let to_distribute = size_without_spacing - (size_cannot_grow + current_size);
234            let to_distribute =
235                A::to_distribute(size_without_spacing, size_cannot_grow + current_size) as f32;
236            if to_distribute <= 0. || max_grow <= 0. {
237                return Some(());
238            }
239
240            let grow = if total_stretch <= 0. {
241                to_distribute
242                    / (data.iter().filter(|it| A::can_grow(it) > 0 as _).count() as Coord) as f32
243            } else {
244                to_distribute / total_stretch
245            }
246            .min(max_grow);
247
248            let mut distributed = 0 as Coord;
249            for it in data.iter_mut().filter(|it| A::can_grow(it) > 0 as Coord) {
250                let val = (grow * actual_stretch(it.stretch)) as Coord;
251                A::distribute(it, val);
252                distributed += val;
253            }
254
255            if distributed <= 0 as Coord {
256                // This can happen when Coord is integer and there is less then a pixel to add to each elements
257                // just give the pixel to the one with the bigger stretch
258                if let Some(it) = data
259                    .iter_mut()
260                    .filter(|it| A::can_grow(it) > 0 as _)
261                    .max_by(|a, b| actual_stretch(a.stretch).total_cmp(&b.stretch))
262                {
263                    A::distribute(it, to_distribute as Coord);
264                }
265                return Some(());
266            }
267        }
268    }
269
270    pub fn layout_items(data: &mut [LayoutData], start_pos: Coord, size: Coord, spacing: Coord) {
271        let size_without_spacing = size - spacing * (data.len() - 1) as Coord;
272
273        let mut pref = 0 as Coord;
274        for it in data.iter_mut() {
275            it.size = it.pref;
276            pref += it.pref;
277        }
278        if size_without_spacing >= pref {
279            adjust_items::<Grow>(data, size_without_spacing);
280        } else if size_without_spacing < pref {
281            adjust_items::<Shrink>(data, size_without_spacing);
282        }
283
284        let mut pos = start_pos;
285        for it in data.iter_mut() {
286            it.pos = pos;
287            pos = Saturating::add(pos, Saturating::add(it.size, spacing));
288        }
289    }
290
291    #[test]
292    #[allow(clippy::float_cmp)] // We want bit-wise equality here
293    fn test_layout_items() {
294        let my_items = &mut [
295            LayoutData { min: 100., max: 200., pref: 100., stretch: 1., ..Default::default() },
296            LayoutData { min: 50., max: 300., pref: 100., stretch: 1., ..Default::default() },
297            LayoutData { min: 50., max: 150., pref: 100., stretch: 1., ..Default::default() },
298        ];
299
300        layout_items(my_items, 100., 650., 0.);
301        assert_eq!(my_items[0].size, 200.);
302        assert_eq!(my_items[1].size, 300.);
303        assert_eq!(my_items[2].size, 150.);
304
305        layout_items(my_items, 100., 200., 0.);
306        assert_eq!(my_items[0].size, 100.);
307        assert_eq!(my_items[1].size, 50.);
308        assert_eq!(my_items[2].size, 50.);
309
310        layout_items(my_items, 100., 300., 0.);
311        assert_eq!(my_items[0].size, 100.);
312        assert_eq!(my_items[1].size, 100.);
313        assert_eq!(my_items[2].size, 100.);
314    }
315
316    /// Create a vector of LayoutData (e.g. one per row if Vertical) based on the constraints and organized data
317    /// Used by both solve_grid_layout() and grid_layout_info()
318    pub fn to_layout_data(
319        organized_data: &GridLayoutOrganizedData,
320        constraints: Slice<LayoutItemInfo>,
321        orientation: Orientation,
322        repeater_indices: Slice<u32>,
323        repeater_steps: Slice<u32>,
324        spacing: Coord,
325        size: Option<Coord>,
326    ) -> Vec<LayoutData> {
327        assert!(organized_data.len().is_multiple_of(4));
328        let num = organized_data.max_value(
329            constraints.len(),
330            orientation,
331            &repeater_indices,
332            &repeater_steps,
333        );
334        if num < 1 {
335            return Default::default();
336        }
337        let marker_for_empty = -1.;
338        let mut layout_data = alloc::vec![grid_internal::LayoutData { max: 0 as Coord, stretch: marker_for_empty, ..Default::default() }; num];
339        let mut has_spans = false;
340        for (idx, cell_data) in constraints.iter().enumerate() {
341            let constraint = &cell_data.constraint;
342            let mut max = constraint.max;
343            if let Some(size) = size {
344                max = max.min(size * constraint.max_percent / 100 as Coord);
345            }
346            let (col_or_row, span) = organized_data.col_or_row_and_span(
347                idx,
348                orientation,
349                &repeater_indices,
350                &repeater_steps,
351            );
352            for c in 0..(span as usize) {
353                let cdata = &mut layout_data[col_or_row as usize + c];
354                // Initialize max/stretch to proper defaults on first item in this row/col
355                // so that empty rows/columns don't stretch.
356                if cdata.stretch == marker_for_empty {
357                    cdata.max = Coord::MAX;
358                    cdata.stretch = 1.;
359                }
360                cdata.max = cdata.max.min(max);
361            }
362            if span == 1 {
363                let mut min = constraint.min;
364                if let Some(size) = size {
365                    min = min.max(size * constraint.min_percent / 100 as Coord);
366                }
367                let pref = constraint.preferred.min(max).max(min);
368                let cdata = &mut layout_data[col_or_row as usize];
369                cdata.min = cdata.min.max(min);
370                cdata.pref = cdata.pref.max(pref);
371                cdata.stretch = cdata.stretch.min(constraint.stretch);
372            } else {
373                has_spans = true;
374            }
375        }
376        if has_spans {
377            for (idx, cell_data) in constraints.iter().enumerate() {
378                let constraint = &cell_data.constraint;
379                let (col_or_row, span) = organized_data.col_or_row_and_span(
380                    idx,
381                    orientation,
382                    &repeater_indices,
383                    &repeater_steps,
384                );
385                if span > 1 {
386                    let span_data = &mut layout_data
387                        [(col_or_row as usize)..(col_or_row as usize + span as usize)];
388
389                    // Adjust minimum sizes
390                    let mut min = constraint.min;
391                    if let Some(size) = size {
392                        min = min.max(size * constraint.min_percent / 100 as Coord);
393                    }
394                    grid_internal::layout_items(span_data, 0 as _, min, spacing);
395                    for cdata in span_data.iter_mut() {
396                        if cdata.min < cdata.size {
397                            cdata.min = cdata.size;
398                        }
399                    }
400
401                    // Adjust maximum sizes
402                    let mut max = constraint.max;
403                    if let Some(size) = size {
404                        max = max.min(size * constraint.max_percent / 100 as Coord);
405                    }
406                    grid_internal::layout_items(span_data, 0 as _, max, spacing);
407                    for cdata in span_data.iter_mut() {
408                        if cdata.max > cdata.size {
409                            cdata.max = cdata.size;
410                        }
411                    }
412
413                    // Adjust preferred sizes
414                    grid_internal::layout_items(span_data, 0 as _, constraint.preferred, spacing);
415                    for cdata in span_data.iter_mut() {
416                        cdata.pref = cdata.pref.max(cdata.size).min(cdata.max).max(cdata.min);
417                    }
418
419                    // Adjust stretches
420                    let total_stretch: f32 = span_data.iter().map(|c| c.stretch).sum();
421                    if total_stretch > constraint.stretch {
422                        for cdata in span_data.iter_mut() {
423                            cdata.stretch *= constraint.stretch / total_stretch;
424                        }
425                    }
426                }
427            }
428        }
429        for cdata in layout_data.iter_mut() {
430            if cdata.stretch == marker_for_empty {
431                cdata.stretch = 0.;
432            }
433            // A cell collapsed to a fixed zero size can pull the row/col's
434            // max below its min (#9724). The minimum is the hard constraint,
435            // so raise max to meet it; min == max now, so that's pref's only
436            // legal value too. Guarded: this must not touch rows without a
437            // min/max conflict, since to_layout_data's output also drives
438            // solve_grid_layout.
439            if cdata.max < cdata.min {
440                cdata.max = cdata.min;
441                cdata.pref = cdata.min;
442            }
443        }
444        layout_data
445    }
446}
447
448#[repr(C)]
449pub struct Constraint {
450    pub min: Coord,
451    pub max: Coord,
452}
453
454impl Default for Constraint {
455    fn default() -> Self {
456        Constraint { min: 0 as Coord, max: Coord::MAX }
457    }
458}
459
460#[repr(C)]
461#[derive(Copy, Clone, Debug, Default)]
462pub struct Padding {
463    pub begin: Coord,
464    pub end: Coord,
465}
466
467#[repr(C)]
468#[derive(Debug)]
469/// The horizontal or vertical data for all cells of a GridLayout, used as input to solve_grid_layout()
470pub struct GridLayoutData {
471    pub size: Coord,
472    pub spacing: Coord,
473    pub padding: Padding,
474    pub organized_data: GridLayoutOrganizedData,
475}
476
477/// The input data for a cell of a GridLayout, before row/col determination and before H/V split
478/// Used as input to organize_grid_layout()
479#[repr(C)]
480#[derive(Debug, Clone)]
481pub struct GridLayoutInputData {
482    /// whether this cell is the first one in a Row element
483    pub new_row: bool,
484    /// col and row number.
485    /// Only ROW_COL_AUTO and the u16 range are valid, values outside of
486    /// that will be clamped with a warning at runtime
487    pub col: f32,
488    pub row: f32,
489    /// colspan and rowspan
490    /// Only the u16 range is valid, values outside of that will be clamped with a warning at runtime
491    pub colspan: f32,
492    pub rowspan: f32,
493}
494
495impl Default for GridLayoutInputData {
496    fn default() -> Self {
497        Self {
498            new_row: false,
499            col: i_slint_common::ROW_COL_AUTO,
500            row: i_slint_common::ROW_COL_AUTO,
501            colspan: 1.0,
502            rowspan: 1.0,
503        }
504    }
505}
506
507/// The organized layout data for a GridLayout, after row/col determination:
508/// For each cell, stores col, colspan, row, rowspan
509pub type GridLayoutOrganizedData = SharedVector<u16>;
510
511impl GridLayoutOrganizedData {
512    fn push_cell(&mut self, col: u16, colspan: u16, row: u16, rowspan: u16) {
513        self.push(col);
514        self.push(colspan);
515        self.push(row);
516        self.push(rowspan);
517    }
518
519    fn col_or_row_and_span(
520        &self,
521        cell_number: usize,
522        orientation: Orientation,
523        repeater_indices: &Slice<u32>,
524        repeater_steps: &Slice<u32>,
525    ) -> (u16, u16) {
526        // For every cell, we have 4 entries, each at their own index
527        // But we also need to take into account indirections for repeated items
528
529        // Two-level indirection for repeated items:
530        //   jump_pos = (ri_start_cell - cell_nr_adj) * 4
531        //   data_base = self[jump_pos]        (base of this repeater's data)
532        //   stride    = self[jump_pos + 1]    (u16 entries per row = step * 4)
533        //   data_idx = data_base + row_in_rep * stride + col_in_rep * 4
534        let mut final_idx = 0;
535        let mut cell_nr_adj = 0i32; // needs to be signed in case we start with an empty repeater
536        let cell_number = cell_number as i32;
537        // repeater_indices is a list of (start_cell, count) pairs
538        for rep_idx in 0..(repeater_indices.len() / 2) {
539            let ri_start_cell = repeater_indices[rep_idx * 2] as i32;
540            if cell_number < ri_start_cell {
541                break;
542            }
543            let ri_cell_count = repeater_indices[rep_idx * 2 + 1] as i32;
544            let step = repeater_steps.get(rep_idx).copied().unwrap_or(1) as i32;
545            let cells_in_repeater = ri_cell_count * step;
546            if cells_in_repeater > 0
547                && cell_number >= ri_start_cell
548                && cell_number < ri_start_cell + cells_in_repeater
549            {
550                let cell_in_rep = cell_number - ri_start_cell;
551                let row_in_rep = cell_in_rep / step;
552                let col_in_rep = cell_in_rep % step;
553                let jump_pos = (ri_start_cell - cell_nr_adj) as usize * 4;
554                let data_base = self[jump_pos] as usize;
555                let stride = self[jump_pos + 1] as usize;
556                final_idx = data_base + row_in_rep as usize * stride + col_in_rep as usize * 4;
557                break;
558            }
559            // Each repeater occupies 1 jump cell in the static area but cells_in_repeater cells logically
560            // Note: -1 is correct for an empty repeater (e.g. if false), which occupies 1 jump cell, for 0 real cells
561            cell_nr_adj += cells_in_repeater - 1;
562        }
563        if final_idx == 0 {
564            final_idx = ((cell_number - cell_nr_adj) * 4) as usize;
565        }
566        let offset = if orientation == Orientation::Horizontal { 0 } else { 2 };
567        (self[final_idx + offset], self[final_idx + offset + 1])
568    }
569
570    fn max_value(
571        &self,
572        num_cells: usize,
573        orientation: Orientation,
574        repeater_indices: &Slice<u32>,
575        repeater_steps: &Slice<u32>,
576    ) -> usize {
577        let mut max = 0;
578        // This could be rewritten more efficiently to avoid a loop calling a loop, by keeping track of the repeaters we saw until now
579        // Not sure it's worth the complexity though
580        for idx in 0..num_cells {
581            let (col_or_row, span) =
582                self.col_or_row_and_span(idx, orientation, repeater_indices, repeater_steps);
583            // Widen to usize: a cell with an out-of-range row/col is clamped to u16::MAX, so adding
584            // the span here in u16 would overflow and under-size the layout vector.
585            max = max.max(col_or_row as usize + span.max(1) as usize);
586        }
587        max
588    }
589}
590
591/// Two-level indirection organized data generator for grid layouts with repeaters.
592/// Uses 2-level indirection: cache[cache[jump_pos] + ri * stride + col * 4]
593/// Each jump cell stores [data_base, stride, 0, 0] where stride = step * 4.
594///
595/// Layout: [static_cells (4 u16 each)] [jump_cells (4 u16 each, 1 per repeater)]
596///         [row_data (rep_count * step * 4 u16)] ... (repeated for each repeater)
597struct OrganizedDataGenerator<'a> {
598    // Input
599    repeater_indices: &'a [u32],
600    repeater_steps: &'a [u32],
601    // An always increasing counter, the index of the cell being added
602    counter: usize,
603    // The u16 position in result for the next repeater's data section
604    repeat_u16_offset: usize,
605    // The index/2 in repeater_indices (i.e. which repeater we're looking at next)
606    next_rep: usize,
607    // The cell index in result for the next non-repeated item (each cell = 4 u16)
608    current_offset: usize,
609    // Output
610    result: &'a mut GridLayoutOrganizedData,
611}
612
613impl<'a> OrganizedDataGenerator<'a> {
614    fn new(
615        repeater_indices: &'a [u32],
616        repeater_steps: &'a [u32],
617        static_cells: usize,
618        num_repeaters: usize,
619        total_repeated_cells_count: usize,
620        result: &'a mut GridLayoutOrganizedData,
621    ) -> Self {
622        result.resize((static_cells + num_repeaters + total_repeated_cells_count) * 4, 0 as _);
623        let repeat_u16_offset = (static_cells + num_repeaters) * 4;
624        Self {
625            repeater_indices,
626            repeater_steps,
627            counter: 0,
628            repeat_u16_offset,
629            next_rep: 0,
630            current_offset: 0,
631            result,
632        }
633    }
634    fn add(&mut self, col: u16, colspan: u16, row: u16, rowspan: u16) {
635        let res = self.result.make_mut_slice();
636        loop {
637            if let Some(nr) = self.repeater_indices.get(self.next_rep * 2) {
638                let nr = *nr as usize;
639                let step = self.repeater_steps.get(self.next_rep).copied().unwrap_or(1) as usize;
640                let rep_count = self.repeater_indices[self.next_rep * 2 + 1] as usize;
641
642                if nr == self.counter {
643                    // First cell of this repeater
644                    let data_u16_start = self.repeat_u16_offset;
645                    let stride = step * 4;
646
647                    // Write jump cell: [data_base, stride, 0, 0]
648                    res[self.current_offset * 4] = data_u16_start as _;
649                    res[self.current_offset * 4 + 1] = stride as _;
650                    self.current_offset += 1;
651                }
652                if self.counter >= nr {
653                    let cells_in_repeater = rep_count * step;
654                    if self.counter - nr == cells_in_repeater {
655                        // Past the end of this repeater — advance past data
656                        self.repeat_u16_offset += cells_in_repeater * 4;
657                        self.next_rep += 1;
658                        continue;
659                    }
660                    // Write data at the position determined by row/col within repeater
661                    let cell_in_rep = self.counter - nr;
662                    let row_in_rep = cell_in_rep / step;
663                    let col_in_rep = cell_in_rep % step;
664                    let data_u16_start = self.repeat_u16_offset;
665                    let u16_pos = data_u16_start + row_in_rep * step * 4 + col_in_rep * 4;
666                    res[u16_pos] = col;
667                    res[u16_pos + 1] = colspan;
668                    res[u16_pos + 2] = row;
669                    res[u16_pos + 3] = rowspan;
670                    self.counter += 1;
671                    return;
672                }
673            }
674            // Non-repeated cell
675            res[self.current_offset * 4] = col;
676            res[self.current_offset * 4 + 1] = colspan;
677            res[self.current_offset * 4 + 2] = row;
678            res[self.current_offset * 4 + 3] = rowspan;
679            self.current_offset += 1;
680            self.counter += 1;
681            return;
682        }
683    }
684}
685
686/// Given the cells of a layout of a Dialog, re-order the buttons according to the platform
687/// This function assume that the `roles` contains the roles of the button which are the first cells in `input_data`
688pub fn organize_dialog_button_layout(
689    input_data: Slice<GridLayoutInputData>,
690    dialog_button_roles: Slice<DialogButtonRole>,
691) -> GridLayoutOrganizedData {
692    let mut organized_data = GridLayoutOrganizedData::default();
693    organized_data.reserve(input_data.len() * 4);
694
695    #[cfg(feature = "std")]
696    fn is_kde() -> bool {
697        // assume some Unix, check if XDG_CURRENT_DESKTOP starts with K
698        std::env::var("XDG_CURRENT_DESKTOP")
699            .ok()
700            .and_then(|v| v.as_bytes().first().copied())
701            .is_some_and(|x| x.eq_ignore_ascii_case(&b'K'))
702    }
703    #[cfg(not(feature = "std"))]
704    let is_kde = || true;
705
706    let expected_order: &[DialogButtonRole] = match crate::detect_operating_system() {
707        crate::items::OperatingSystemType::Windows => {
708            &[
709                DialogButtonRole::Reset,
710                DialogButtonRole::None, // spacer
711                DialogButtonRole::Accept,
712                DialogButtonRole::Action,
713                DialogButtonRole::Reject,
714                DialogButtonRole::Apply,
715                DialogButtonRole::Help,
716            ]
717        }
718        crate::items::OperatingSystemType::Macos | crate::items::OperatingSystemType::Ios => {
719            &[
720                DialogButtonRole::Help,
721                DialogButtonRole::Reset,
722                DialogButtonRole::Apply,
723                DialogButtonRole::Action,
724                DialogButtonRole::None, // spacer
725                DialogButtonRole::Reject,
726                DialogButtonRole::Accept,
727            ]
728        }
729        _ if is_kde() => {
730            // KDE variant
731            &[
732                DialogButtonRole::Help,
733                DialogButtonRole::Reset,
734                DialogButtonRole::None, // spacer
735                DialogButtonRole::Action,
736                DialogButtonRole::Accept,
737                DialogButtonRole::Apply,
738                DialogButtonRole::Reject,
739            ]
740        }
741        _ => {
742            // GNOME variant and fallback for WASM build
743            &[
744                DialogButtonRole::Help,
745                DialogButtonRole::Reset,
746                DialogButtonRole::None, // spacer
747                DialogButtonRole::Action,
748                DialogButtonRole::Accept,
749                DialogButtonRole::Apply,
750                DialogButtonRole::Reject,
751            ]
752        }
753    };
754
755    // Reorder the actual buttons according to expected_order
756    let mut column_for_input: Vec<usize> = Vec::with_capacity(dialog_button_roles.len());
757    for role in expected_order.iter() {
758        if role == &DialogButtonRole::None {
759            column_for_input.push(usize::MAX); // empty column, ensure nothing will match
760            continue;
761        }
762        for (idx, r) in dialog_button_roles.as_slice().iter().enumerate() {
763            if *r == *role {
764                column_for_input.push(idx);
765            }
766        }
767    }
768
769    for (input_index, cell) in input_data.as_slice().iter().enumerate() {
770        let col = column_for_input.iter().position(|&x| x == input_index);
771        if let Some(col) = col {
772            organized_data.push_cell(col as _, cell.colspan as _, cell.row as _, cell.rowspan as _);
773        } else {
774            // This is used for the main window (which is the only cell which isn't a button)
775            // Given lower_dialog_layout(), this will always be a single cell at 0,0 with a colspan of number_of_buttons
776            organized_data.push_cell(
777                cell.col as _,
778                cell.colspan as _,
779                cell.row as _,
780                cell.rowspan as _,
781            );
782        }
783    }
784    organized_data
785}
786
787// GridLayout-specific
788fn total_repeated_cells<'a>(repeater_indices: &'a [u32], repeater_steps: &'a [u32]) -> usize {
789    repeater_indices
790        .chunks(2)
791        .enumerate()
792        .map(|(i, chunk)| {
793            let count = chunk.get(1).copied().unwrap_or(0) as usize;
794            let step = repeater_steps.get(i).copied().unwrap_or(1) as usize;
795            count * step
796        })
797        .sum()
798}
799
800type Errors = Vec<String>;
801
802pub fn organize_grid_layout(
803    input_data: Slice<GridLayoutInputData>,
804    repeater_indices: Slice<u32>,
805    repeater_steps: Slice<u32>,
806) -> GridLayoutOrganizedData {
807    let (organized_data, errors) =
808        organize_grid_layout_impl(input_data, repeater_indices, repeater_steps);
809    for error in errors {
810        crate::debug_log!("Slint layout error: {}", error);
811    }
812    organized_data
813}
814
815// Implement "auto" behavior for row/col numbers (unless specified in the slint file).
816fn organize_grid_layout_impl(
817    input_data: Slice<GridLayoutInputData>,
818    repeater_indices: Slice<u32>,
819    repeater_steps: Slice<u32>,
820) -> (GridLayoutOrganizedData, Errors) {
821    let mut organized_data = GridLayoutOrganizedData::default();
822    // Cache size: static_cells * 4 + num_repeaters * 4 (jump cells)
823    //              + per repeater: rep_count * step * 4 (data)
824    let num_repeaters = repeater_indices.len() / 2;
825    let total_repeated_cells =
826        total_repeated_cells(repeater_indices.as_slice(), repeater_steps.as_slice());
827    let static_cells = input_data.len() - total_repeated_cells;
828    let mut generator = OrganizedDataGenerator::new(
829        repeater_indices.as_slice(),
830        repeater_steps.as_slice(),
831        static_cells,
832        num_repeaters,
833        total_repeated_cells,
834        &mut organized_data,
835    );
836    let mut errors = Vec::new();
837
838    fn clamp_to_u16(value: f32, field_name: &str, errors: &mut Vec<String>) -> u16 {
839        if value < 0.0 {
840            errors.push(format!("cell {field_name} {value} is negative, clamping to 0"));
841            0
842        } else if value > u16::MAX as f32 {
843            errors
844                .push(format!("cell {field_name} {value} is too large, clamping to {}", u16::MAX));
845            u16::MAX
846        } else {
847            value as u16
848        }
849    }
850
851    let mut row = 0;
852    let mut col = 0;
853    let mut first = true;
854    for cell in input_data.as_slice().iter() {
855        if cell.new_row && !first {
856            row += 1;
857            col = 0;
858        }
859        first = false;
860
861        if cell.row != i_slint_common::ROW_COL_AUTO {
862            let cell_row = clamp_to_u16(cell.row, "row", &mut errors);
863            if row != cell_row {
864                row = cell_row;
865                col = 0;
866            }
867        }
868        if cell.col != i_slint_common::ROW_COL_AUTO {
869            col = clamp_to_u16(cell.col, "col", &mut errors);
870        }
871
872        let colspan = clamp_to_u16(cell.colspan, "colspan", &mut errors);
873        let rowspan = clamp_to_u16(cell.rowspan, "rowspan", &mut errors);
874        col = col.min(u16::MAX - colspan); // ensure col + colspan doesn't overflow
875        generator.add(col, colspan, row, rowspan);
876        col += colspan;
877    }
878    (organized_data, errors)
879}
880
881/// Layout cache generator for box layouts.
882/// The layout cache generator inserts the pos and size into the result array (which becomes the layout cache property),
883/// including the indirections for repeated items (so that the x,y,width,height properties for repeated items
884/// can point to indices known at compile time, those that contain the indirections)
885/// Example: for repeater_indices=[1,4] (meaning that item at index 1 is repeated 4 times),
886/// result=[0.0, 80.0, 4.0, 5.0, 80.0, 80.0, 160.0, 80.0, 240.0, 80.0, 320.0, 80.0]
887///  i.e. pos1, width1, jump to idx 4, jump to idx 5, pos2, width2, pos3, width3, pos4, width4, pos5, width5
888struct LayoutCacheGenerator<'a> {
889    // Input
890    repeater_indices: &'a [u32],
891    // An always increasing counter, the index of the cell being added
892    counter: usize,
893    // The index/2 in result in which we should add the next repeated item
894    repeat_offset: usize,
895    // The index/2 in repeater_indices
896    next_rep: usize,
897    // The index/2 in result in which we should add the next non-repeated item
898    current_offset: usize,
899    // Output
900    result: &'a mut SharedVector<Coord>,
901}
902
903impl<'a> LayoutCacheGenerator<'a> {
904    fn new(repeater_indices: &'a [u32], result: &'a mut SharedVector<Coord>) -> Self {
905        let total_repeated_cells: usize = repeater_indices
906            .chunks(2)
907            .map(|chunk| chunk.get(1).copied().unwrap_or(0) as usize)
908            .sum();
909        assert!(result.len() >= total_repeated_cells * 2);
910        let repeat_offset = result.len() / 2 - total_repeated_cells;
911        Self { repeater_indices, counter: 0, repeat_offset, next_rep: 0, current_offset: 0, result }
912    }
913    fn add(&mut self, pos: Coord, size: Coord) {
914        let res = self.result.make_mut_slice();
915        let o = loop {
916            if let Some(nr) = self.repeater_indices.get(self.next_rep * 2) {
917                let nr = *nr as usize;
918                if nr == self.counter {
919                    // Write jump entry
920                    for o in 0..2 {
921                        res[self.current_offset * 2 + o] = (self.repeat_offset * 2 + o) as _;
922                    }
923                    self.current_offset += 1;
924                }
925                if self.counter >= nr {
926                    let rep_count = self.repeater_indices[self.next_rep * 2 + 1] as usize;
927                    if self.counter - nr == rep_count {
928                        self.repeat_offset += rep_count;
929                        self.next_rep += 1;
930                        continue;
931                    }
932                    let offset = self.repeat_offset + (self.counter - nr);
933                    break offset;
934                }
935            }
936            self.current_offset += 1;
937            break self.current_offset - 1;
938        };
939        res[o * 2] = pos;
940        res[o * 2 + 1] = size;
941        self.counter += 1;
942    }
943}
944
945/// Two-level indirection layout cache generator for grid layouts with repeaters.
946/// Uses 2-level indirection: cache[cache[jump] + ri * stride + child_offset]
947/// Each jump cell stores [data_base, stride] where stride = step * 2.
948struct GridLayoutCacheGenerator<'a> {
949    // Input
950    repeater_indices: &'a [u32],
951    repeater_steps: &'a [u32],
952    // An always increasing counter, the index of the cell being added
953    counter: usize,
954    // The f32 position in result for the next repeater's dynamic data section
955    repeat_f32_offset: usize,
956    // The index/2 in repeater_indices
957    next_rep: usize,
958    // The cell index (index/2) in result for the next non-repeated item
959    current_offset: usize,
960    // Output
961    result: &'a mut SharedVector<Coord>,
962}
963
964impl<'a> GridLayoutCacheGenerator<'a> {
965    fn new(
966        repeater_indices: &'a [u32],
967        repeater_steps: &'a [u32],
968        static_cells: usize,
969        num_repeaters: usize,
970        total_repeated_cells_count: usize,
971        result: &'a mut SharedVector<Coord>,
972    ) -> Self {
973        result.resize((static_cells + num_repeaters + total_repeated_cells_count) * 2, 0 as _);
974        let repeat_f32_offset = (static_cells + num_repeaters) * 2;
975        Self {
976            repeater_indices,
977            repeater_steps,
978            counter: 0,
979            repeat_f32_offset,
980            next_rep: 0,
981            current_offset: 0,
982            result,
983        }
984    }
985    fn add(&mut self, pos: Coord, size: Coord) {
986        let res = self.result.make_mut_slice();
987        loop {
988            if let Some(nr) = self.repeater_indices.get(self.next_rep * 2) {
989                let nr = *nr as usize;
990                let step = self.repeater_steps.get(self.next_rep).copied().unwrap_or(1) as usize;
991                let rep_count = self.repeater_indices[self.next_rep * 2 + 1] as usize;
992
993                if nr == self.counter {
994                    // First cell of this repeater
995                    let data_f32_start = self.repeat_f32_offset;
996                    let stride = step * 2;
997
998                    // Write 1 jump cell (2 f32): [data_base, stride]
999                    res[self.current_offset * 2] = data_f32_start as _;
1000                    res[self.current_offset * 2 + 1] = stride as _;
1001                    self.current_offset += 1;
1002                }
1003                if self.counter >= nr {
1004                    let cells_in_repeater = rep_count * step;
1005                    if self.counter - nr == cells_in_repeater {
1006                        // Past the end of this repeater — advance past data
1007                        self.repeat_f32_offset += cells_in_repeater * 2;
1008                        self.next_rep += 1;
1009                        continue;
1010                    }
1011                    // Write data at the position determined by row/col within repeater
1012                    let cell_in_rep = self.counter - nr;
1013                    let row_in_rep = cell_in_rep / step;
1014                    let col_in_rep = cell_in_rep % step;
1015                    let data_f32_start = self.repeat_f32_offset;
1016                    let f32_pos = data_f32_start + row_in_rep * step * 2 + col_in_rep * 2;
1017                    res[f32_pos] = pos;
1018                    res[f32_pos + 1] = size;
1019                    self.counter += 1;
1020                    return;
1021                }
1022            }
1023            // Non-repeated cell
1024            res[self.current_offset * 2] = pos;
1025            res[self.current_offset * 2 + 1] = size;
1026            self.current_offset += 1;
1027            self.counter += 1;
1028            return;
1029        }
1030    }
1031}
1032
1033/// return, an array which is of size `data.cells.len() * 2` which for each cell stores:
1034/// pos (x or y), size (width or height)
1035pub fn solve_grid_layout(
1036    data: &GridLayoutData,
1037    constraints: Slice<LayoutItemInfo>,
1038    orientation: Orientation,
1039    repeater_indices: Slice<u32>,
1040    repeater_steps: Slice<u32>,
1041) -> SharedVector<Coord> {
1042    let mut layout_data = grid_internal::to_layout_data(
1043        &data.organized_data,
1044        constraints,
1045        orientation,
1046        repeater_indices,
1047        repeater_steps,
1048        data.spacing,
1049        Some(data.size),
1050    );
1051
1052    if layout_data.is_empty() {
1053        return Default::default();
1054    }
1055
1056    grid_internal::layout_items(
1057        &mut layout_data,
1058        data.padding.begin,
1059        data.size - (data.padding.begin + data.padding.end),
1060        data.spacing,
1061    );
1062
1063    let mut result = SharedVector::<Coord>::default();
1064    let num_repeaters = repeater_indices.len() / 2;
1065    let total_repeated_cells =
1066        total_repeated_cells(repeater_indices.as_slice(), repeater_steps.as_slice());
1067    let static_cells = constraints.len() - total_repeated_cells;
1068    let mut generator = GridLayoutCacheGenerator::new(
1069        repeater_indices.as_slice(),
1070        repeater_steps.as_slice(),
1071        static_cells,
1072        num_repeaters,
1073        total_repeated_cells,
1074        &mut result,
1075    );
1076
1077    for idx in 0..constraints.len() {
1078        let (col_or_row, span) = data.organized_data.col_or_row_and_span(
1079            idx,
1080            orientation,
1081            &repeater_indices,
1082            &repeater_steps,
1083        );
1084        let cdata = &layout_data[col_or_row as usize];
1085        let size = if span > 0 {
1086            let last_cell = &layout_data[col_or_row as usize + span as usize - 1];
1087            last_cell.pos + last_cell.size - cdata.pos
1088        } else {
1089            0 as Coord
1090        };
1091        generator.add(cdata.pos, size);
1092    }
1093    result
1094}
1095
1096pub fn grid_layout_info(
1097    organized_data: GridLayoutOrganizedData, // not & because the code generator doesn't support it in ExtraBuiltinFunctionCall
1098    constraints: Slice<LayoutItemInfo>,
1099    repeater_indices: Slice<u32>,
1100    repeater_steps: Slice<u32>,
1101    spacing: Coord,
1102    padding: &Padding,
1103    orientation: Orientation,
1104) -> LayoutInfo {
1105    let layout_data = grid_internal::to_layout_data(
1106        &organized_data,
1107        constraints,
1108        orientation,
1109        repeater_indices,
1110        repeater_steps,
1111        spacing,
1112        None,
1113    );
1114    if layout_data.is_empty() {
1115        let mut info = LayoutInfo::default();
1116        info.min = padding.begin + padding.end;
1117        info.preferred = info.min;
1118        info.max = info.min;
1119        return info;
1120    }
1121    let spacing_w = spacing * (layout_data.len() - 1) as Coord + padding.begin + padding.end;
1122    let min = layout_data.iter().map(|data| data.min).sum::<Coord>() + spacing_w;
1123    let max = layout_data.iter().map(|data| data.max).fold(spacing_w, Saturating::add);
1124    let preferred = layout_data.iter().map(|data| data.pref).sum::<Coord>() + spacing_w;
1125    let stretch = layout_data.iter().map(|data| data.stretch).sum::<f32>();
1126    LayoutInfo { min, max, min_percent: 0 as _, max_percent: 100 as _, preferred, stretch }
1127}
1128
1129#[repr(C)]
1130#[derive(Debug)]
1131/// The BoxLayoutData is used to represent both a Horizontal and Vertical layout.
1132/// The width/height x/y correspond to that of a horizontal layout.
1133/// For vertical layout, they are inverted
1134pub struct BoxLayoutData<'a> {
1135    pub size: Coord,
1136    pub spacing: Coord,
1137    pub padding: Padding,
1138    pub alignment: LayoutAlignment,
1139    pub cells: Slice<'a, LayoutItemInfo>,
1140}
1141
1142/// Input for `solve_box_layout_ortho`.
1143#[repr(C)]
1144#[derive(Debug)]
1145pub struct BoxLayoutOrthoData<'a> {
1146    pub size: Coord,
1147    pub padding: Padding,
1148    pub cross_axis_alignment: CrossAxisAlignment,
1149    pub cells: Slice<'a, LayoutItemInfo>,
1150}
1151
1152#[repr(C)]
1153#[derive(Debug)]
1154/// The FlexboxLayoutData is used for a flex layout.
1155pub struct FlexboxLayoutData<'a> {
1156    pub width: Coord,
1157    pub height: Coord,
1158    pub spacing_h: Coord,
1159    pub spacing_v: Coord,
1160    pub padding_h: Padding,
1161    pub padding_v: Padding,
1162    pub alignment: LayoutAlignment,
1163    pub direction: FlexboxLayoutDirection,
1164    pub cross_axis_line_alignment: LayoutAlignment,
1165    pub cross_axis_alignment: CrossAxisAlignment,
1166    pub flex_wrap: FlexboxLayoutWrap,
1167    /// Horizontal constraints (width) for each cell
1168    pub cells_h: Slice<'a, LayoutItemInfo>,
1169    /// Vertical constraints (height) for each cell
1170    pub cells_v: Slice<'a, LayoutItemInfo>,
1171    /// Per-item flex properties, one per cell (axis-independent)
1172    pub flex_props: Slice<'a, FlexItemProps>,
1173}
1174
1175#[repr(C)]
1176#[derive(Debug, Clone, Default)]
1177/// The information about a single item in a box or grid layout
1178pub struct LayoutItemInfo {
1179    pub constraint: LayoutInfo,
1180    /// Per-item cross-axis alignment override for box layouts
1181    /// (`Auto` = use the container's `cross-axis-alignment`)
1182    pub cross_axis_self_alignment: CrossAxisAlignment,
1183    /// Visual ordering of box layout cells (lower values appear first, default 0).
1184    /// Only [`solve_box_layout`] reads it; the cross-axis solve and both
1185    /// layout-info functions ignore it. A FlexboxLayout carries it in
1186    /// [`FlexItemProps`] instead, and a GridLayout orders its cells with
1187    /// `row`/`col`, so both leave this at 0.
1188    pub layout_order: i32,
1189}
1190
1191/// The per-item flex properties of a FlexboxLayout cell.
1192///
1193/// A cell's size constraint is per-axis (`cells_h`/`cells_v`), but these
1194/// properties apply to the item as a whole, so they live in a single parallel
1195/// array instead of being duplicated into both axes.
1196#[repr(C)]
1197#[derive(Debug, Clone, Copy, Default)]
1198pub struct FlexItemProps {
1199    /// Per-item cross-axis alignment override (Auto = use container's cross-axis-alignment)
1200    pub cross_axis_self_alignment: CrossAxisAlignment,
1201    /// Visual ordering of flex items (lower values appear first, default 0)
1202    pub layout_order: i32,
1203}
1204
1205#[repr(C)]
1206#[derive(Debug, Clone, Default)]
1207/// One flexbox cell's full layout info, read from a single item instance via the
1208/// item vtable. The bulk cell data in [`FlexboxLayoutData`] keeps the constraint
1209/// and [`FlexItemProps`] in separate parallel arrays; this bundles both for the
1210/// per-instance query.
1211pub struct FlexboxLayoutItemInfo {
1212    pub constraint: LayoutInfo,
1213    pub props: FlexItemProps,
1214}
1215
1216impl From<LayoutItemInfo> for FlexboxLayoutItemInfo {
1217    fn from(info: LayoutItemInfo) -> Self {
1218        Self {
1219            constraint: info.constraint,
1220            props: FlexItemProps {
1221                cross_axis_self_alignment: info.cross_axis_self_alignment,
1222                layout_order: info.layout_order,
1223            },
1224        }
1225    }
1226}
1227
1228/// Solve a BoxLayout
1229pub fn solve_box_layout(data: &BoxLayoutData, repeater_indices: Slice<u32>) -> SharedVector<Coord> {
1230    let mut result = SharedVector::<Coord>::default();
1231    // One element results into two coordinates in the result vector. 1. Position, 2. Size
1232    result.resize(data.cells.len() * 2 + repeater_indices.len(), 0 as _);
1233
1234    if data.cells.is_empty() {
1235        return result;
1236    }
1237
1238    let size_without_padding = data.size - data.padding.begin - data.padding.end;
1239    let num_spacings = (data.cells.len() - 1) as Coord;
1240    let spacings = data.spacing * num_spacings;
1241    let content_size = size_without_padding - spacings; // The size the cells can occupy without going outside of the layout
1242    let mut layout_data: Vec<_> = data
1243        .cells
1244        .iter()
1245        .map(|c| {
1246            let min = c.constraint.min.max(c.constraint.min_percent * content_size / 100 as Coord);
1247            let max = c.constraint.max.min(c.constraint.max_percent * content_size / 100 as Coord);
1248            grid_internal::LayoutData {
1249                min,
1250                max,
1251                pref: c.constraint.preferred.min(max).max(min),
1252                stretch: c.constraint.stretch,
1253                ..Default::default()
1254            }
1255        })
1256        .collect();
1257
1258    // `layout-order` reorders the cells like the CSS `order` property. Solve on
1259    // the reordered list; the results are written back in declaration order
1260    // below, as a cell's cache slot is fixed by its declaration index.
1261    let order_map: Vec<usize> = if data.cells.iter().any(|c| c.layout_order != 0) {
1262        let mut indices: Vec<usize> = (0..layout_data.len()).collect();
1263        // sort_by_key is a stable sort, so equal orders keep declaration order
1264        indices.sort_by_key(|&i| data.cells[i].layout_order);
1265        layout_data = indices.iter().map(|&i| layout_data[i].clone()).collect();
1266        indices
1267    } else {
1268        Vec::new()
1269    };
1270
1271    let pref_size: Coord = layout_data.iter().map(|it| it.pref).sum();
1272
1273    let align = match data.alignment {
1274        LayoutAlignment::Stretch => {
1275            grid_internal::layout_items(
1276                &mut layout_data,
1277                data.padding.begin,
1278                size_without_padding,
1279                data.spacing,
1280            );
1281            None
1282        }
1283        _ if size_without_padding <= pref_size + spacings => {
1284            grid_internal::layout_items(
1285                &mut layout_data,
1286                data.padding.begin,
1287                size_without_padding,
1288                data.spacing,
1289            );
1290            None
1291        }
1292        LayoutAlignment::Center => Some((
1293            data.padding.begin + (size_without_padding - pref_size - spacings) / 2 as Coord,
1294            data.spacing,
1295        )),
1296        LayoutAlignment::Start => Some((data.padding.begin, data.spacing)),
1297        LayoutAlignment::End => {
1298            Some((data.padding.begin + (size_without_padding - pref_size - spacings), data.spacing))
1299        }
1300        LayoutAlignment::SpaceBetween => {
1301            Some((data.padding.begin, (size_without_padding - pref_size) / num_spacings))
1302        }
1303        LayoutAlignment::SpaceAround => {
1304            let spacing = (size_without_padding - pref_size) / (num_spacings + 1 as Coord);
1305            Some((data.padding.begin + spacing / 2 as Coord, spacing))
1306        }
1307        LayoutAlignment::SpaceEvenly => {
1308            let spacing = (size_without_padding - pref_size) / (num_spacings + 2 as Coord);
1309            Some((data.padding.begin + spacing, spacing))
1310        }
1311    };
1312    if let Some((mut pos, spacing)) = align {
1313        for it in &mut layout_data {
1314            it.pos = pos;
1315            it.size = it.pref;
1316            pos += spacing + it.size;
1317        }
1318    }
1319
1320    let mut generator = LayoutCacheGenerator::new(&repeater_indices, &mut result);
1321    if order_map.is_empty() {
1322        for layout in layout_data.iter() {
1323            generator.add(layout.pos, layout.size);
1324        }
1325    } else {
1326        let mut geom = alloc::vec![(0 as Coord, 0 as Coord); layout_data.len()];
1327        for (sorted_idx, &declared_idx) in order_map.iter().enumerate() {
1328            let layout = &layout_data[sorted_idx];
1329            geom[declared_idx] = (layout.pos, layout.size);
1330        }
1331        for (pos, size) in geom {
1332            generator.add(pos, size);
1333        }
1334    }
1335    result
1336}
1337
1338/// Resolves the effective alignment of an item on the cross axis: `auto` on the
1339/// item uses the container's alignment, and `auto` on the container is `stretch`.
1340fn resolve_cross_axis_alignment(
1341    self_alignment: CrossAxisAlignment,
1342    container_alignment: CrossAxisAlignment,
1343) -> CrossAxisAlignment {
1344    let alignment = match self_alignment {
1345        CrossAxisAlignment::Auto => container_alignment,
1346        other => other,
1347    };
1348    match alignment {
1349        CrossAxisAlignment::Auto => CrossAxisAlignment::Stretch,
1350        other => other,
1351    }
1352}
1353
1354/// Cross-axis solve: returns (position, size) per cell, like [`solve_box_layout`].
1355pub fn solve_box_layout_ortho(
1356    data: &BoxLayoutOrthoData,
1357    repeater_indices: Slice<u32>,
1358) -> SharedVector<Coord> {
1359    let mut result = SharedVector::<Coord>::default();
1360    result.resize(data.cells.len() * 2 + repeater_indices.len(), 0 as _);
1361    if data.cells.is_empty() {
1362        return result;
1363    }
1364    let size_without_padding = data.size - data.padding.begin - data.padding.end;
1365    let mut generator = LayoutCacheGenerator::new(&repeater_indices, &mut result);
1366    for c in data.cells.iter() {
1367        let alignment =
1368            resolve_cross_axis_alignment(c.cross_axis_self_alignment, data.cross_axis_alignment);
1369        let min =
1370            c.constraint.min.max(c.constraint.min_percent * size_without_padding / 100 as Coord);
1371        let max =
1372            c.constraint.max.min(c.constraint.max_percent * size_without_padding / 100 as Coord);
1373        let size = match alignment {
1374            CrossAxisAlignment::Stretch => size_without_padding,
1375            _ => c.constraint.preferred,
1376        }
1377        .min(max)
1378        .max(min);
1379        let pos = match alignment {
1380            CrossAxisAlignment::Auto | CrossAxisAlignment::Stretch | CrossAxisAlignment::Start => {
1381                data.padding.begin
1382            }
1383            CrossAxisAlignment::End => data.padding.begin + size_without_padding - size,
1384            CrossAxisAlignment::Center => {
1385                data.padding.begin + (size_without_padding - size) / 2 as Coord
1386            }
1387        };
1388        generator.add(pos, size);
1389    }
1390    result
1391}
1392
1393/// Return the LayoutInfo for a BoxLayout with the given cells.
1394pub fn box_layout_info(
1395    cells: Slice<LayoutItemInfo>,
1396    spacing: Coord,
1397    padding: &Padding,
1398    alignment: LayoutAlignment,
1399) -> LayoutInfo {
1400    let count = cells.len();
1401    let is_stretch = alignment == LayoutAlignment::Stretch;
1402    if count < 1 {
1403        let mut info = LayoutInfo::default();
1404        info.min = padding.begin + padding.end;
1405        info.preferred = info.min;
1406        if is_stretch {
1407            info.max = info.min;
1408        }
1409        return info;
1410    };
1411    let extra_w = padding.begin + padding.end + spacing * (count - 1) as Coord;
1412    let min = cells.iter().map(|c| c.constraint.min).sum::<Coord>() + extra_w; // Minimum size of the complete layout
1413    let max = if is_stretch {
1414        (cells.iter().map(|c| c.constraint.max).fold(extra_w, Saturating::add)).max(min)
1415    } else {
1416        Coord::MAX
1417    }; // Maximum size of the complete layout
1418    let preferred = cells.iter().map(|c| c.constraint.preferred_bounded()).sum::<Coord>() + extra_w;
1419    let stretch = cells.iter().map(|c| c.constraint.stretch).sum::<f32>();
1420    LayoutInfo { min, max, min_percent: 0 as _, max_percent: 100 as _, preferred, stretch }
1421}
1422
1423pub fn box_layout_info_ortho(cells: Slice<LayoutItemInfo>, padding: &Padding) -> LayoutInfo {
1424    let extra_w = padding.begin + padding.end;
1425    let mut fold =
1426        cells.iter().fold(LayoutInfo { stretch: f32::MAX, ..Default::default() }, |a, b| {
1427            a.merge(&b.constraint)
1428        });
1429    fold.max = fold.max.max(fold.min);
1430    fold.preferred = fold.preferred.clamp(fold.min, fold.max);
1431    fold.min += extra_w;
1432    fold.max = Saturating::add(fold.max, extra_w);
1433    fold.preferred += extra_w;
1434    // Don't propagate children's percentage constraints to the parent.
1435    // Percentages are relative to the layout's own size, not the grandparent's.
1436    fold.min_percent = 0 as _;
1437    fold.max_percent = 100 as _;
1438    fold
1439}
1440
1441/// Helper module for taffy-based flexbox layout
1442mod flexbox_taffy {
1443    use super::{
1444        Coord, CrossAxisAlignment, FlexItemProps, FlexboxLayoutWrap as SlintFlexboxLayoutWrap,
1445        LayoutAlignment, LayoutInfo, LayoutItemInfo, Padding, Slice, resolve_cross_axis_alignment,
1446    };
1447    use alloc::vec::Vec;
1448    pub use taffy::prelude::FlexDirection as TaffyFlexDirection;
1449    use taffy::prelude::*;
1450
1451    /// Start/End map to FlexStart/FlexEnd to respect the flex direction (including reverse);
1452    /// AlignContent::Start/End would ignore the direction and always use the writing mode.
1453    fn to_align_content(alignment: LayoutAlignment) -> AlignContent {
1454        match alignment {
1455            LayoutAlignment::Stretch => AlignContent::Stretch,
1456            LayoutAlignment::Start => AlignContent::FlexStart,
1457            LayoutAlignment::End => AlignContent::FlexEnd,
1458            LayoutAlignment::Center => AlignContent::Center,
1459            LayoutAlignment::SpaceBetween => AlignContent::SpaceBetween,
1460            LayoutAlignment::SpaceAround => AlignContent::SpaceAround,
1461            LayoutAlignment::SpaceEvenly => AlignContent::SpaceEvenly,
1462        }
1463    }
1464
1465    /// How an item's cross-axis size is decided when building the taffy tree.
1466    #[derive(Copy, Clone, PartialEq, Eq)]
1467    pub enum CrossAxisSizing {
1468        /// The item's own preferred cross size, except where it stretches to
1469        /// fill its flex line.
1470        Preferred,
1471        /// `auto`, so the measure callback decides. A column's flex basis is
1472        /// `auto` too, so a height-for-width item is measured at the width
1473        /// taffy assigns it rather than at its preferred width.
1474        FromMeasure,
1475        /// `auto` for every item, so one whose measure reports nothing falls
1476        /// back to its min constraint. This is how a layout's minimum cross
1477        /// size is measured.
1478        Minimum,
1479    }
1480
1481    /// Parameters for FlexboxTaffyBuilder::new
1482    pub struct FlexboxLayoutParams<'a> {
1483        pub cells_h: &'a Slice<'a, LayoutItemInfo>,
1484        pub cells_v: &'a Slice<'a, LayoutItemInfo>,
1485        pub flex_props: &'a Slice<'a, FlexItemProps>,
1486        pub spacing_h: Coord,
1487        pub spacing_v: Coord,
1488        pub padding_h: &'a Padding,
1489        pub padding_v: &'a Padding,
1490        pub alignment: LayoutAlignment,
1491        pub cross_axis_line_alignment: LayoutAlignment,
1492        pub cross_axis_alignment: CrossAxisAlignment,
1493        pub flex_wrap: SlintFlexboxLayoutWrap,
1494        /// `flex-shrink` for every item. `1.` while wrapping, `0.` for the
1495        /// non-wrapping re-solve in `solve_flexbox_layout_with_measure`, whose
1496        /// point is to let the content overflow rather than be compressed.
1497        pub flex_shrink: f32,
1498        pub flex_direction: TaffyFlexDirection,
1499        pub container_width: Option<Coord>,
1500        pub container_height: Option<Coord>,
1501        pub cross_axis_sizing: CrossAxisSizing,
1502    }
1503
1504    /// Build a taffy tree from Slint layout constraints.
1505    /// The NodeContext (usize) stores the original child index for measure callbacks.
1506    pub struct FlexboxTaffyBuilder {
1507        pub taffy: TaffyTree<usize>,
1508        pub children: Vec<NodeId>,
1509        pub container: NodeId,
1510        /// Maps taffy child position -> original cell index (empty if no reordering needed)
1511        pub order_map: Vec<usize>,
1512    }
1513
1514    impl FlexboxTaffyBuilder {
1515        /// Create a new flexbox layout tree from item constraints
1516        pub fn new(params: FlexboxLayoutParams) -> Self {
1517            let mut taffy = TaffyTree::<usize>::new();
1518
1519            // The container's content box on each axis (the outer size minus that
1520            // axis' padding), against which percentage constraints resolve.
1521            // `None` when that axis has no finite size: either unknown (an
1522            // auto-sized container in the info path) or the `Coord::MAX`
1523            // "unbounded" sentinel the info path passes for a no-wrap main axis.
1524            // A percentage then has no basis and only the absolute constraint
1525            // applies — resolving against `MAX` would overflow the `_percent * s`
1526            // product (i32 build) or yield infinity (f32).
1527            let content_box = |size: Option<Coord>, pad: &Padding| -> Option<Coord> {
1528                size.filter(|s| *s < Coord::MAX).map(|s| (s - pad.begin - pad.end).max(0 as Coord))
1529            };
1530            let content_w = content_box(params.container_width, params.padding_h);
1531            let content_h = content_box(params.container_height, params.padding_v);
1532
1533            // Cross-axis upper bound: an item is never bigger than the
1534            // container's content box across the flex direction, so a
1535            // height-for-width item (e.g. wrapped `Text`) measures against a real
1536            // width, and the container can shrink to the size it reports as its
1537            // minimum.
1538            let (column_cross_cap, row_cross_cap) = match params.flex_direction {
1539                TaffyFlexDirection::Column | TaffyFlexDirection::ColumnReverse => (content_w, None),
1540                TaffyFlexDirection::Row | TaffyFlexDirection::RowReverse => (None, content_h),
1541            };
1542
1543            // Resolve a percentage min/max against the container's content box and
1544            // fold it into the absolute constraint, matching how the box layouts
1545            // handle percentages (see `solve_box_layout`). Applied only when the
1546            // percentage is non-default and the content size is known, so an item
1547            // without a percentage size keeps its plain min/max unchanged.
1548            let eff_min = |c: &LayoutInfo, content: Option<Coord>| -> Coord {
1549                match content {
1550                    Some(s) if c.min_percent > 0 as Coord => {
1551                        c.min.max(c.min_percent * s / 100 as Coord)
1552                    }
1553                    _ => c.min,
1554                }
1555            };
1556            let eff_max = |c: &LayoutInfo, content: Option<Coord>| -> Coord {
1557                match content {
1558                    Some(s) if c.max_percent < 100 as Coord => {
1559                        c.max.min(c.max_percent * s / 100 as Coord)
1560                    }
1561                    _ => c.max,
1562                }
1563            };
1564
1565            // Main-axis growth follows the box layouts: items grow only under
1566            // `alignment: stretch`, weighted by the main axis' `*-stretch`
1567            // factor. When every factor is 0 (e.g. a row of Buttons, whose
1568            // styles set `horizontal-stretch: 0`), fall back to weight 1 so
1569            // the line still fills, as it would in a HorizontalLayout. The
1570            // all-zero test is container-wide since taffy decides the lines.
1571            // The parity has known limits: taffy applies the factors per line
1572            // and never hands space to grow-0 items, so a line whose items all
1573            // have factor 0 next to a stretchy line won't grow, and space a
1574            // max-capped item cannot take stays free instead of going to its
1575            // stretch-0 siblings (a box layout redistributes both).
1576            let main_cells = match params.flex_direction {
1577                TaffyFlexDirection::Row | TaffyFlexDirection::RowReverse => params.cells_h,
1578                TaffyFlexDirection::Column | TaffyFlexDirection::ColumnReverse => params.cells_v,
1579            };
1580            let any_stretch = main_cells.iter().any(|c| c.constraint.stretch > 0.);
1581
1582            // Create child nodes from Slint constraints
1583            let mut children: Vec<NodeId> = params
1584                .cells_h
1585                .iter()
1586                .enumerate()
1587                .map(|(idx, cell_h)| {
1588                    let cell_v = params.cells_v.get(idx);
1589                    let flex = params.flex_props.get(idx).cloned().unwrap_or_default();
1590                    let h_constraint = &cell_h.constraint;
1591                    let v_constraint = cell_v.map(|c| &c.constraint);
1592
1593                    // Use preferred_bounded() which clamps preferred to min/max bounds
1594                    let preferred_width = h_constraint.preferred_bounded();
1595                    let preferred_height =
1596                        v_constraint.map(|vc| vc.preferred_bounded()).unwrap_or(0 as Coord);
1597
1598                    // The basis is the preferred size of the main axis, except
1599                    // where `auto` defers to the measure callback (below)
1600                    let flex_basis = match params.flex_direction {
1601                        TaffyFlexDirection::Row | TaffyFlexDirection::RowReverse => {
1602                            Dimension::length(preferred_width as _)
1603                        }
1604                        // For a column the main axis is the height, so pinning the
1605                        // basis to `preferred_height` would stop taffy from ever
1606                        // consulting the measure callback. `auto` lets it size the
1607                        // item from its content at the width it actually assigns —
1608                        // `preferred_height` was measured at the container width,
1609                        // which is too wide for an item that does not stretch.
1610                        TaffyFlexDirection::Column | TaffyFlexDirection::ColumnReverse
1611                            if params.cross_axis_sizing == CrossAxisSizing::FromMeasure =>
1612                        {
1613                            Dimension::auto()
1614                        }
1615                        TaffyFlexDirection::Column | TaffyFlexDirection::ColumnReverse => {
1616                            Dimension::length(preferred_height as _)
1617                        }
1618                    };
1619
1620                    let max_width = eff_max(h_constraint, content_w)
1621                        .min(column_cross_cap.unwrap_or(Coord::MAX));
1622                    let max_height = v_constraint
1623                        .map_or(Coord::MAX, |vc| eff_max(vc, content_h))
1624                        .min(row_cross_cap.unwrap_or(Coord::MAX));
1625                    let max_width_dim = if max_width < Coord::MAX {
1626                        Dimension::length(max_width as _)
1627                    } else {
1628                        Dimension::auto()
1629                    };
1630
1631                    // A stretching item gets `auto` on the cross axis, so it sizes
1632                    // to its flex *line's* cross size (the per-column width when
1633                    // wrapped), not the whole container. A per-item
1634                    // `cross-axis-self-alignment` overrides the container's
1635                    // alignment. `auto` is also what lets the measure callback
1636                    // decide the cross size, and what lets the minimum pass reach
1637                    // the item's min constraint.
1638                    let stretches = resolve_cross_axis_alignment(
1639                        flex.cross_axis_self_alignment,
1640                        params.cross_axis_alignment,
1641                    ) == CrossAxisAlignment::Stretch;
1642                    let cross_auto =
1643                        stretches || params.cross_axis_sizing != CrossAxisSizing::Preferred;
1644                    let definite_cross = |preferred: Coord| {
1645                        if cross_auto || preferred <= 0 as Coord {
1646                            Dimension::auto()
1647                        } else {
1648                            Dimension::length(preferred as _)
1649                        }
1650                    };
1651
1652                    taffy
1653                        .new_leaf_with_context(
1654                            Style {
1655                                flex_basis,
1656                                size: Size {
1657                                    width: match params.flex_direction {
1658                                        TaffyFlexDirection::Column
1659                                        | TaffyFlexDirection::ColumnReverse => {
1660                                            definite_cross(preferred_width)
1661                                        }
1662                                        _ => Dimension::auto(),
1663                                    },
1664                                    height: match params.flex_direction {
1665                                        TaffyFlexDirection::Row
1666                                        | TaffyFlexDirection::RowReverse => {
1667                                            definite_cross(preferred_height)
1668                                        }
1669                                        _ => Dimension::auto(),
1670                                    },
1671                                },
1672                                min_size: Size {
1673                                    width: Dimension::length(eff_min(h_constraint, content_w) as _),
1674                                    height: Dimension::length(
1675                                        v_constraint
1676                                            .map(|vc| eff_min(vc, content_h) as f32)
1677                                            .unwrap_or(0.0),
1678                                    ),
1679                                },
1680                                max_size: Size {
1681                                    width: max_width_dim,
1682                                    height: if max_height < Coord::MAX {
1683                                        Dimension::length(max_height as _)
1684                                    } else {
1685                                        Dimension::auto()
1686                                    },
1687                                },
1688                                flex_grow: if params.alignment == LayoutAlignment::Stretch {
1689                                    if any_stretch {
1690                                        main_cells.get(idx).map_or(0., |c| c.constraint.stretch)
1691                                    } else {
1692                                        1.
1693                                    }
1694                                } else {
1695                                    0.
1696                                },
1697                                // Under `wrap` this is 1: a line with two or
1698                                // more items never has negative free space (it
1699                                // would have wrapped), so shrinking only applies
1700                                // to a single item alone on its line — and that
1701                                // item must go down to its min whatever its
1702                                // stretch factor, as it would in a box layout.
1703                                // The non-wrapping re-solve passes 0, where a
1704                                // line does have negative free space.
1705                                flex_shrink: params.flex_shrink,
1706                                align_self: match flex.cross_axis_self_alignment {
1707                                    CrossAxisAlignment::Auto => None,
1708                                    CrossAxisAlignment::Stretch => Some(AlignSelf::Stretch),
1709                                    CrossAxisAlignment::Start => Some(AlignSelf::FlexStart),
1710                                    CrossAxisAlignment::End => Some(AlignSelf::FlexEnd),
1711                                    CrossAxisAlignment::Center => Some(AlignSelf::Center),
1712                                },
1713                                ..Default::default()
1714                            },
1715                            idx,
1716                        )
1717                        .unwrap() // cannot fail
1718                })
1719                .collect();
1720
1721            // Sort children by CSS `order` property if any item has a non-zero order.
1722            // Build a mapping from sorted position -> original index.
1723            let has_order = params.flex_props.iter().any(|f| f.layout_order != 0);
1724            let order_map: Vec<usize> = if has_order {
1725                let mut indices: Vec<usize> = (0..children.len()).collect();
1726                // sort_by_key is a stable sort, as required by CSS
1727                indices.sort_by_key(|&i| params.flex_props.get(i).map_or(0, |f| f.layout_order));
1728                let sorted_children: Vec<NodeId> = indices.iter().map(|&i| children[i]).collect();
1729                children = sorted_children;
1730                indices
1731            } else {
1732                Vec::new()
1733            };
1734
1735            // Create container node
1736            let container = taffy
1737                .new_with_children(
1738                    Style {
1739                        display: Display::Flex,
1740                        flex_direction: params.flex_direction,
1741                        flex_wrap: match params.flex_wrap {
1742                            SlintFlexboxLayoutWrap::Wrap => FlexWrap::Wrap,
1743                            SlintFlexboxLayoutWrap::NoWrap => FlexWrap::NoWrap,
1744                            SlintFlexboxLayoutWrap::WrapReverse => FlexWrap::WrapReverse,
1745                        },
1746                        justify_content: Some(to_align_content(params.alignment)),
1747                        align_items: Some(match params.cross_axis_alignment {
1748                            CrossAxisAlignment::Auto | CrossAxisAlignment::Stretch => {
1749                                AlignItems::Stretch
1750                            }
1751                            CrossAxisAlignment::Start => AlignItems::FlexStart,
1752                            CrossAxisAlignment::End => AlignItems::FlexEnd,
1753                            CrossAxisAlignment::Center => AlignItems::Center,
1754                        }),
1755                        align_content: Some(to_align_content(params.cross_axis_line_alignment)),
1756                        gap: Size {
1757                            width: LengthPercentage::length(params.spacing_h as _),
1758                            height: LengthPercentage::length(params.spacing_v as _),
1759                        },
1760                        padding: Rect {
1761                            left: LengthPercentage::length(params.padding_h.begin as _),
1762                            right: LengthPercentage::length(params.padding_h.end as _),
1763                            top: LengthPercentage::length(params.padding_v.begin as _),
1764                            bottom: LengthPercentage::length(params.padding_v.end as _),
1765                        },
1766                        size: Size {
1767                            width: params
1768                                .container_width
1769                                .map(|w| Dimension::length(w as _))
1770                                .unwrap_or(Dimension::auto()),
1771                            height: params
1772                                .container_height
1773                                .map(|h| Dimension::length(h as _))
1774                                .unwrap_or(Dimension::auto()),
1775                        },
1776                        ..Default::default()
1777                    },
1778                    &children,
1779                )
1780                .unwrap(); // cannot fail
1781
1782            Self { taffy, children, container, order_map }
1783        }
1784
1785        /// Compute the layout with the given available space.
1786        ///
1787        /// The `measure` callback is called by taffy for leaf nodes whose size
1788        /// it takes from their content (height-for-width).
1789        /// It receives `(child_index, known_width, known_height)` where `known_width`
1790        /// / `known_height` are `Some` if taffy has already determined that dimension,
1791        /// and returns `(width, height)`.
1792        pub fn compute_layout(
1793            &mut self,
1794            available_width: Coord,
1795            available_height: Coord,
1796            measure: &mut dyn FnMut(usize, Option<Coord>, Option<Coord>) -> (Coord, Coord),
1797        ) {
1798            let available_space = taffy::prelude::Size {
1799                width: if available_width < Coord::MAX {
1800                    AvailableSpace::Definite(available_width as _)
1801                } else {
1802                    AvailableSpace::MaxContent
1803                },
1804                height: if available_height < Coord::MAX {
1805                    AvailableSpace::Definite(available_height as _)
1806                } else {
1807                    AvailableSpace::MaxContent
1808                },
1809            };
1810            self.taffy
1811                .compute_layout_with_measure(
1812                    self.container,
1813                    available_space,
1814                    |known_dimensions, _available_space, _node_id, node_context, _style| {
1815                        // Only the container node has no context.
1816                        let Some(&mut child_index) = node_context else {
1817                            return taffy::prelude::Size::ZERO;
1818                        };
1819                        let known_w = known_dimensions.width.map(|w| w as Coord);
1820                        let known_h = known_dimensions.height.map(|h| h as Coord);
1821                        let (w, h) = measure(child_index, known_w, known_h);
1822                        taffy::prelude::Size { width: w as f32, height: h as f32 }
1823                    },
1824                )
1825                .unwrap_or_else(|e| {
1826                    crate::debug_log!("FlexboxLayout computation error: {}", e);
1827                });
1828        }
1829
1830        /// Get the computed container size
1831        pub fn container_size(&self) -> (Coord, Coord) {
1832            let layout = self.taffy.layout(self.container).unwrap();
1833            (layout.size.width as Coord, layout.size.height as Coord)
1834        }
1835
1836        /// Get the geometry for a specific child
1837        pub fn child_geometry(&self, idx: usize) -> (Coord, Coord, Coord, Coord) {
1838            let layout = self.taffy.layout(self.children[idx]).unwrap();
1839            (
1840                layout.location.x as Coord,
1841                layout.location.y as Coord,
1842                layout.size.width as Coord,
1843                layout.size.height as Coord,
1844            )
1845        }
1846
1847        /// Map a taffy child index to the original cell index (accounting for `order` sorting).
1848        pub fn original_index(&self, taffy_idx: usize) -> usize {
1849            if self.order_map.is_empty() { taffy_idx } else { self.order_map[taffy_idx] }
1850        }
1851    }
1852}
1853
1854/// A cache generator for FlexboxLayout that handles 4 values per item (x, y, width, height)
1855struct FlexboxLayoutCacheGenerator<'a> {
1856    // Input
1857    repeater_indices: &'a [u32],
1858    // An always increasing counter, the index of the cell being added
1859    counter: usize,
1860    // The index/4 in result in which we should add the next repeated item
1861    repeat_offset: usize,
1862    // The index/4 in repeater_indices
1863    next_rep: usize,
1864    // The index/4 in result in which we should add the next non-repeated item
1865    current_offset: usize,
1866    // Output
1867    result: &'a mut SharedVector<Coord>,
1868}
1869
1870impl<'a> FlexboxLayoutCacheGenerator<'a> {
1871    fn new(repeater_indices: &'a [u32], result: &'a mut SharedVector<Coord>) -> Self {
1872        // Calculate total repeated cells (count for each repeater)
1873        let total_repeated_cells: usize = repeater_indices
1874            .chunks(2)
1875            .map(|chunk| chunk.get(1).copied().unwrap_or(0) as usize)
1876            .sum();
1877        assert!(result.len() >= total_repeated_cells * 4);
1878        let repeat_offset = result.len() / 4 - total_repeated_cells;
1879        Self { repeater_indices, counter: 0, repeat_offset, next_rep: 0, current_offset: 0, result }
1880    }
1881
1882    fn add(&mut self, x: Coord, y: Coord, w: Coord, h: Coord) {
1883        let res = self.result.make_mut_slice();
1884        let o = loop {
1885            if let Some(nr) = self.repeater_indices.get(self.next_rep * 2) {
1886                let nr = *nr as usize;
1887                if nr == self.counter {
1888                    // Write jump entries for repeater start
1889                    // Store the base offset (index into the repeated data region)
1890                    res[self.current_offset * 4] = (self.repeat_offset * 4) as Coord;
1891                    res[self.current_offset * 4 + 1] = (self.repeat_offset * 4 + 1) as Coord;
1892                    res[self.current_offset * 4 + 2] = (self.repeat_offset * 4 + 2) as Coord;
1893                    res[self.current_offset * 4 + 3] = (self.repeat_offset * 4 + 3) as Coord;
1894                    self.current_offset += 1;
1895                }
1896                if self.counter >= nr {
1897                    let rep_count = self.repeater_indices[self.next_rep * 2 + 1] as usize;
1898                    if self.counter - nr == rep_count {
1899                        // Advance repeat_offset past this repeater's data before moving to next
1900                        self.repeat_offset += rep_count;
1901                        self.next_rep += 1;
1902                        continue;
1903                    }
1904                    // Calculate offset into repeated data
1905                    let cell_in_rep = self.counter - nr;
1906                    let offset = self.repeat_offset + cell_in_rep;
1907                    break offset;
1908                }
1909            }
1910            self.current_offset += 1;
1911            break self.current_offset - 1;
1912        };
1913        res[o * 4] = x;
1914        res[o * 4 + 1] = y;
1915        res[o * 4 + 2] = w;
1916        res[o * 4 + 3] = h;
1917        self.counter += 1;
1918    }
1919}
1920
1921/// Measure callback for height-for-width items in a FlexboxLayout.
1922///
1923/// Called by taffy during the flex solve for items that need dynamic sizing.
1924/// Receives `(child_index, width, height, known_width, known_height)` where
1925/// `known_width`/`known_height` say whether taffy has already determined that
1926/// dimension; a dimension it has not is pre-resolved to the cell's preferred
1927/// size. Returns `(width, height)`.
1928///
1929/// A call with neither dimension known is a content-size probe. Its result
1930/// must be a self-consistent pair (each dimension measured at the other):
1931/// taffy caches it and reuses one dimension for a later query with the other
1932/// one known.
1933///
1934/// `None` means the caller has no callback of its own and takes the entry
1935/// point's default: the item's preferred size for a solve, and nothing (so an
1936/// item taffy sizes from its content falls back to its min constraint) for the
1937/// minimum pass of the cross-axis info. Taffy itself is always given a
1938/// callback, so a forgotten one is a compile error rather than a silently
1939/// zero-sized item.
1940pub type FlexboxMeasureFn<'a> =
1941    Option<&'a mut dyn FnMut(usize, Coord, Coord, bool, bool) -> (Coord, Coord)>;
1942
1943/// The default measure: report back the sizes `resolve_measure_defaults`
1944/// pre-resolved from the cells data, i.e. the item's preferred size for any
1945/// dimension taffy has not pinned down.
1946fn identity_measure(_: usize, w: Coord, h: Coord, _: bool, _: bool) -> (Coord, Coord) {
1947    (w, h)
1948}
1949
1950/// The measure that reports nothing, so an item taffy sizes from its content
1951/// falls back to its min constraint. Unlike [`identity_measure`] this one is
1952/// taffy-facing: it does not go through [`resolve_measure_defaults`], which
1953/// would resolve the unknown dimensions to the preferred size only to have them
1954/// thrown away.
1955fn zero_measure(_: usize, _: Option<Coord>, _: Option<Coord>) -> (Coord, Coord) {
1956    (0 as Coord, 0 as Coord)
1957}
1958
1959/// Adapt a [`FlexboxMeasureFn`] to the taffy-facing closure: resolve the
1960/// dimensions taffy did not supply to the cell's preferred size, so the
1961/// callback always receives concrete sizes plus which ones were known.
1962fn resolve_measure_defaults<'a>(
1963    cells_h: &'a [LayoutItemInfo],
1964    cells_v: &'a [LayoutItemInfo],
1965    measure: &'a mut dyn FnMut(usize, Coord, Coord, bool, bool) -> (Coord, Coord),
1966) -> impl FnMut(usize, Option<Coord>, Option<Coord>) -> (Coord, Coord) + 'a {
1967    move |index, known_w, known_h| {
1968        let w = known_w.unwrap_or_else(|| {
1969            cells_h.get(index).map_or(0 as Coord, |c| c.constraint.preferred_bounded())
1970        });
1971        let h = known_h.unwrap_or_else(|| {
1972            cells_v.get(index).map_or(0 as Coord, |c| c.constraint.preferred_bounded())
1973        });
1974        measure(index, w, h, known_w.is_some(), known_h.is_some())
1975    }
1976}
1977
1978pub fn solve_flexbox_layout(
1979    data: &FlexboxLayoutData,
1980    repeater_indices: Slice<u32>,
1981) -> SharedVector<Coord> {
1982    solve_flexbox_layout_with_measure(data, repeater_indices, None)
1983}
1984
1985/// Solve a FlexboxLayout using Taffy
1986/// Returns: [x1, y1, w1, h1, x2, y2, w2, h2, ...] for each item
1987pub fn solve_flexbox_layout_with_measure(
1988    data: &FlexboxLayoutData,
1989    repeater_indices: Slice<u32>,
1990    measure: FlexboxMeasureFn<'_>,
1991) -> SharedVector<Coord> {
1992    // 4 values per item: x, y, width, height
1993    let mut result = SharedVector::<Coord>::default();
1994    result.resize(data.cells_h.len() * 4 + repeater_indices.len() * 2, 0 as _);
1995
1996    if data.cells_h.is_empty() {
1997        return result;
1998    }
1999
2000    let taffy_direction = match data.direction {
2001        FlexboxLayoutDirection::Row => flexbox_taffy::TaffyFlexDirection::Row,
2002        FlexboxLayoutDirection::RowReverse => flexbox_taffy::TaffyFlexDirection::RowReverse,
2003        FlexboxLayoutDirection::Column => flexbox_taffy::TaffyFlexDirection::Column,
2004        FlexboxLayoutDirection::ColumnReverse => flexbox_taffy::TaffyFlexDirection::ColumnReverse,
2005    };
2006
2007    let (container_width, container_height) = (
2008        if data.width > 0 as Coord { Some(data.width) } else { None },
2009        if data.height > 0 as Coord { Some(data.height) } else { None },
2010    );
2011
2012    let use_measure = measure.is_some();
2013    let build = |flex_wrap, flex_shrink| {
2014        flexbox_taffy::FlexboxTaffyBuilder::new(flexbox_taffy::FlexboxLayoutParams {
2015            cells_h: &data.cells_h,
2016            cells_v: &data.cells_v,
2017            flex_props: &data.flex_props,
2018            spacing_h: data.spacing_h,
2019            spacing_v: data.spacing_v,
2020            padding_h: &data.padding_h,
2021            padding_v: &data.padding_v,
2022            alignment: data.alignment,
2023            cross_axis_line_alignment: data.cross_axis_line_alignment,
2024            cross_axis_alignment: data.cross_axis_alignment,
2025            flex_wrap,
2026            flex_shrink,
2027            flex_direction: taffy_direction,
2028            container_width,
2029            container_height,
2030            cross_axis_sizing: if use_measure {
2031                flexbox_taffy::CrossAxisSizing::FromMeasure
2032            } else {
2033                flexbox_taffy::CrossAxisSizing::Preferred
2034            },
2035        })
2036    };
2037    let mut builder = build(data.flex_wrap, 1.);
2038
2039    let (available_width, available_height) = match data.direction {
2040        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => {
2041            (data.width, Coord::MAX)
2042        }
2043        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => {
2044            (Coord::MAX, data.height)
2045        }
2046    };
2047
2048    // The compiler omits the callback only for layouts without
2049    // height-for-width cells, so the pre-computed height is correct for
2050    // whatever width taffy assigns.
2051    let mut identity = identity_measure;
2052    let measure = measure.unwrap_or(&mut identity);
2053    let mut measure = resolve_measure_defaults(&data.cells_h, &data.cells_v, measure);
2054    builder.compute_layout(available_width, available_height, &mut measure);
2055
2056    // A column flex wraps by height, so its columns can be wider than the
2057    // width it was given: one column's when its height is not settled (see
2058    // `flexbox_layout_info_cross_axis`). Never overflow sideways into a
2059    // sibling: solve without wrapping instead, and let the content overflow
2060    // downward, like a wrapped Text given too little height.
2061    //
2062    // `WrapReverse` is left alone. It anchors its lines at the cross end, which
2063    // `NoWrap` does not, so re-solving would move the content to the other side;
2064    // and the container's own height still drives the wrapping, so an unbounded
2065    // available height does not stop it either. Such a flex keeps wrapping past
2066    // its width.
2067    let is_column = matches!(
2068        data.direction,
2069        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse
2070    );
2071    if is_column && data.flex_wrap == FlexboxLayoutWrap::Wrap && data.width > 0 as Coord {
2072        // taffy computes in `f32`, so ignore an overflow below half a pixel.
2073        // Integer coordinates are exact and need no tolerance.
2074        #[cfg(not(slint_int_coord))]
2075        const OVERFLOW_TOLERANCE: Coord = 0.5;
2076        #[cfg(slint_int_coord)]
2077        const OVERFLOW_TOLERANCE: Coord = 0;
2078        let (left, right) = (data.padding_h.begin, data.width - data.padding_h.end);
2079        // Taffy indices, not original cell ones: `order` may have sorted them.
2080        // Asking whether *any* child overflows does not care about the order.
2081        let overflows = (0..data.cells_h.len()).any(|idx| {
2082            let (x, _, w, _) = builder.child_geometry(idx);
2083            x < left - OVERFLOW_TOLERANCE || x + w > right + OVERFLOW_TOLERANCE
2084        });
2085        if overflows {
2086            // A second full solve, deliberately, and a common one: an
2087            // unsettled-height column flex is given one column's width, so any
2088            // wrapping at all overflows it. `cross-axis-line-alignment`
2089            // places the lines, so a single line wider than the flex lands at a
2090            // negative `x` under `center` or `end`, and clamping that one line
2091            // back is not enough for the multi-line case this exists for:
2092            // taffy has to lay the items out again without wrapping.
2093            // `flexbox_column_wrap_line_alignment.slint` covers both.
2094            //
2095            // Shrink only where the first solve did: items that fit one column
2096            // keep the shrinking `wrap` gives them, while content that needed
2097            // more than one column is meant to overflow rather than be
2098            // compressed into the height that made it wrap
2099            // (`flexbox_column_wrap_shrink.slint`). A lone item never wrapped,
2100            // however far it overflows.
2101            // `cells_h` is the array the children were built from, so it is
2102            // the child count; `cells_v` is its main-axis twin, one entry per
2103            // cell. The sum uses the cells' preferred sizes, which is what
2104            // taffy wraps on too, though a height-for-width cell it measured
2105            // may end up a little taller: close enough to tell one column from
2106            // several, which is all this decides.
2107            let one_column = data.cells_h.len() < 2
2108                || flexbox_layout_unwrapped_main(
2109                    Slice::from_slice(data.cells_v.as_slice()),
2110                    data.spacing_v,
2111                    &data.padding_v,
2112                ) <= data.height;
2113            builder = build(FlexboxLayoutWrap::NoWrap, if one_column { 1. } else { 0. });
2114            builder.compute_layout(available_width, available_height, &mut measure);
2115        }
2116    }
2117
2118    // Extract results using the cache generator to handle repeaters.
2119    // If `order` sorting was applied, we need to collect results by original index first,
2120    // because the cache generator expects items in their original declaration order.
2121    if builder.order_map.is_empty() {
2122        let mut generator = FlexboxLayoutCacheGenerator::new(&repeater_indices, &mut result);
2123        for idx in 0..data.cells_h.len() {
2124            let (x, y, w, h) = builder.child_geometry(idx);
2125            generator.add(x, y, w, h);
2126        }
2127    } else {
2128        let count = data.cells_h.len();
2129        let mut geom = alloc::vec![(0 as Coord, 0 as Coord, 0 as Coord, 0 as Coord); count];
2130        for taffy_idx in 0..count {
2131            let orig_idx = builder.original_index(taffy_idx);
2132            geom[orig_idx] = builder.child_geometry(taffy_idx);
2133        }
2134        let mut generator = FlexboxLayoutCacheGenerator::new(&repeater_indices, &mut result);
2135        for (x, y, w, h) in geom {
2136            generator.add(x, y, w, h);
2137        }
2138    }
2139
2140    result
2141}
2142
2143/// The flex's natural single-line (no-wrap) main-axis size: the size it
2144/// occupies when all items sit on one line. A perpendicular parent uses this
2145/// to give a non-stretch wrapping-flex cell its natural size (and only wrap
2146/// when the available cross size is smaller), instead of the compact
2147/// `sqrt`-area "square" that [`flexbox_layout_info_main_axis`] reports as
2148/// `preferred`.
2149pub fn flexbox_layout_unwrapped_main(
2150    cells: Slice<LayoutItemInfo>,
2151    spacing: Coord,
2152    padding: &Padding,
2153) -> Coord {
2154    let extra_pad = padding.begin + padding.end;
2155    if cells.is_empty() {
2156        return extra_pad;
2157    }
2158    let num_spacings = cells.len().saturating_sub(1) as Coord;
2159    cells.iter().map(|c| c.constraint.preferred_bounded()).sum::<Coord>()
2160        + spacing * num_spacings
2161        + extra_pad
2162}
2163
2164/// Return main-axis LayoutInfo for a FlexboxLayout.
2165/// Only needs the same-axis cells, avoiding a cross-axis binding loop.
2166/// The reported `max` is always unbounded, even when every item is max-capped:
2167/// unlike `box_layout_info`, wrapping makes a sum-of-maxes cap ill-defined.
2168pub fn flexbox_layout_info_main_axis(
2169    cells: Slice<LayoutItemInfo>,
2170    spacing: Coord,
2171    padding: &Padding,
2172    flex_wrap: FlexboxLayoutWrap,
2173) -> LayoutInfo {
2174    let extra_pad = padding.begin + padding.end;
2175    if cells.is_empty() {
2176        return LayoutInfo {
2177            min: extra_pad,
2178            preferred: extra_pad,
2179            max: extra_pad,
2180            ..Default::default()
2181        };
2182    }
2183    let num_spacings = cells.len().saturating_sub(1) as Coord;
2184    let min = if matches!(flex_wrap, FlexboxLayoutWrap::NoWrap) {
2185        cells.iter().map(|c| c.constraint.min).sum::<Coord>() + spacing * num_spacings + extra_pad
2186    } else {
2187        // Wrapping: the widest single item must fit
2188        cells.iter().map(|c| c.constraint.min).fold(0.0 as Coord, |a, b| a.max(b)) + extra_pad
2189    };
2190    let preferred = if matches!(flex_wrap, FlexboxLayoutWrap::NoWrap) {
2191        // No wrapping: all items on one line
2192        flexbox_layout_unwrapped_main(cells, spacing, padding)
2193    } else {
2194        // Wrapping: aim for a roughly square (pixel-area) arrangement, using only
2195        // main-axis sizes so this stays independent of the cross axis. The square
2196        // side is the sqrt of the total area; each item is approximated as a
2197        // (size + spacing) square, so the gaps count toward the area and the grid
2198        // stays roughly square as spacing grows (otherwise a spacing-blind target
2199        // is reached with fewer items, skewing the grid taller). Snap that up to a
2200        // whole number of items, so a line holds a clean grid row instead of
2201        // wrapping mid-item (which would over-count columns: e.g. 3 equal items
2202        // want `A B / C`, not `A B C`).
2203        // Accumulate the area in f64: with the integer Coord build, Coord-typed
2204        // products (and their sum) would overflow for large items.
2205        let total_area: f64 = cells
2206            .iter()
2207            .map(|c| c.constraint.preferred_bounded() as f64 + spacing as f64)
2208            .map(|w| w * w)
2209            .sum();
2210        let target = Float::sqrt(total_area as f32) as Coord;
2211        let mut acc = 0 as Coord;
2212        let mut started = false;
2213        for c in cells.iter() {
2214            // taffy breaks the lines on the hypothetical size (the preferred size,
2215            // before any growing), so the line fitting has to measure the items
2216            // the same way.
2217            let size = c.constraint.preferred_bounded();
2218            acc += if started { spacing + size } else { size };
2219            started = true;
2220            // `acc` is the real row width (no trailing gap), but `target` budgets
2221            // a gap per item, so add one back before comparing — else a row grabs
2222            // one item too many near an integer sqrt.
2223            if acc + spacing >= target {
2224                break;
2225            }
2226        }
2227        acc + extra_pad
2228    };
2229    let stretch = cells.iter().map(|c| c.constraint.stretch).sum::<f32>();
2230    LayoutInfo {
2231        min,
2232        max: Coord::MAX,
2233        min_percent: 0 as _,
2234        max_percent: 100 as _,
2235        preferred,
2236        stretch,
2237    }
2238}
2239
2240/// Return cross-axis LayoutInfo for a FlexboxLayout.
2241///
2242/// The minimum and the preferred cross size are two different measurements of
2243/// the same items, so this runs the flex algorithm twice: once with every item
2244/// at its min constraint, once at its preferred size.
2245///
2246/// `constraint_size` is the main-axis container dimension (width for row,
2247/// height for column). When valid (> 0 and < MAX), it's used as the taffy
2248/// constraint for accurate wrapping. When invalid (e.g. 0, negative, or
2249/// MAX — which can happen due to circular dependencies in nested
2250/// perpendicular flexboxes), falls back to a heuristic based on
2251/// `flexbox_layout_info_main_axis`.
2252#[allow(clippy::too_many_arguments)]
2253pub fn flexbox_layout_info_cross_axis(
2254    cells_h: Slice<LayoutItemInfo>,
2255    cells_v: Slice<LayoutItemInfo>,
2256    flex_props: Slice<FlexItemProps>,
2257    spacing_h: Coord,
2258    spacing_v: Coord,
2259    padding_h: &Padding,
2260    padding_v: &Padding,
2261    direction: FlexboxLayoutDirection,
2262    alignment: LayoutAlignment,
2263    flex_wrap: FlexboxLayoutWrap,
2264    constraint_size: Coord,
2265) -> LayoutInfo {
2266    flexbox_layout_info_cross_axis_with_measure(
2267        cells_h,
2268        cells_v,
2269        flex_props,
2270        spacing_h,
2271        spacing_v,
2272        padding_h,
2273        padding_v,
2274        direction,
2275        alignment,
2276        flex_wrap,
2277        constraint_size,
2278        None,
2279    )
2280}
2281
2282/// Same as [`flexbox_layout_info_cross_axis`], with a measure callback so
2283/// height-for-width cells (e.g. a nested wrapping flexbox) are measured at the
2284/// main-axis size taffy actually assigns them, not at the pre-computed cell
2285/// size (which was measured at the container width).
2286///
2287/// `alignment` is the container's `alignment`: under `stretch` the solve grows
2288/// the cells along the main axis, which changes a height-for-width cell's
2289/// cross size, so this measurement must grow them the same way.
2290#[allow(clippy::too_many_arguments)]
2291pub fn flexbox_layout_info_cross_axis_with_measure(
2292    cells_h: Slice<LayoutItemInfo>,
2293    cells_v: Slice<LayoutItemInfo>,
2294    flex_props: Slice<FlexItemProps>,
2295    spacing_h: Coord,
2296    spacing_v: Coord,
2297    padding_h: &Padding,
2298    padding_v: &Padding,
2299    direction: FlexboxLayoutDirection,
2300    alignment: LayoutAlignment,
2301    flex_wrap: FlexboxLayoutWrap,
2302    constraint_size: Coord,
2303    measure: FlexboxMeasureFn<'_>,
2304) -> LayoutInfo {
2305    debug_assert_eq!(cells_h.len(), cells_v.len());
2306    debug_assert_eq!(cells_h.len(), flex_props.len());
2307    if cells_h.is_empty() {
2308        assert!(cells_v.is_empty());
2309        let orientation = match direction {
2310            FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => {
2311                Orientation::Vertical
2312            }
2313            FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => {
2314                Orientation::Horizontal
2315            }
2316        };
2317        let padding = match orientation {
2318            Orientation::Horizontal => padding_h,
2319            Orientation::Vertical => padding_v,
2320        };
2321        let pad = padding.begin + padding.end;
2322        return LayoutInfo { min: pad, preferred: pad, max: pad, ..Default::default() };
2323    }
2324
2325    // Determine which axis is cross
2326    let cross_cells = match direction {
2327        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => &cells_v,
2328        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => &cells_h,
2329    };
2330
2331    // Compute the main-axis preferred size to use as the constraint for taffy,
2332    // using the same heuristic as flexbox_layout_info_main_axis.
2333    let (main_cells, main_spacing, main_padding) = match direction {
2334        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => {
2335            (&cells_h, spacing_h, padding_h)
2336        }
2337        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => {
2338            (&cells_v, spacing_v, padding_v)
2339        }
2340    };
2341    let main_extra_pad = main_padding.begin + main_padding.end;
2342    let main_axis_constraint = if constraint_size > 0 as Coord && constraint_size < Coord::MAX {
2343        // Use the actual container main-axis dimension (accurate)
2344        constraint_size
2345    } else if matches!(flex_wrap, FlexboxLayoutWrap::NoWrap) || constraint_size >= Coord::MAX {
2346        // No-wrap mode, or caller signalled "unconstrained" via MAX
2347        // (used when no real main-axis dimension is in scope, e.g.
2348        // a nested perpendicular flex queried via vtable): treat the
2349        // main axis as unbounded so items don't wrap. This gives the
2350        // natural max-cell-cross-axis result rather than the
2351        // sqrt(item-areas) heuristic.
2352        Coord::MAX
2353    } else {
2354        // Use actual item areas (main * cross) for the heuristic, since both
2355        // axes' cells are available here (unlike flexbox_layout_info_main_axis).
2356        // Accumulate in f64: with the integer Coord build, Coord-typed products
2357        // (and their sum) would overflow for large items.
2358        let total_area: f64 = main_cells
2359            .iter()
2360            .zip(cross_cells.iter())
2361            .map(|(m, c)| {
2362                m.constraint.preferred_bounded() as f64 * c.constraint.preferred_bounded() as f64
2363            })
2364            .sum();
2365        let count = main_cells.len();
2366        Float::sqrt(total_area as f32) as Coord
2367            + main_spacing * (count - 1) as Coord
2368            + main_extra_pad
2369    };
2370
2371    let taffy_direction = match direction {
2372        FlexboxLayoutDirection::Row => flexbox_taffy::TaffyFlexDirection::Row,
2373        FlexboxLayoutDirection::RowReverse => flexbox_taffy::TaffyFlexDirection::RowReverse,
2374        FlexboxLayoutDirection::Column => flexbox_taffy::TaffyFlexDirection::Column,
2375        FlexboxLayoutDirection::ColumnReverse => flexbox_taffy::TaffyFlexDirection::ColumnReverse,
2376    };
2377
2378    let (container_width, container_height) = match direction {
2379        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => {
2380            (Some(main_axis_constraint), None)
2381        }
2382        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => {
2383            (None, Some(main_axis_constraint))
2384        }
2385    };
2386
2387    let params = |cross_axis_sizing| flexbox_taffy::FlexboxLayoutParams {
2388        cells_h: &cells_h,
2389        cells_v: &cells_v,
2390        flex_props: &flex_props,
2391        spacing_h,
2392        spacing_v,
2393        padding_h,
2394        padding_v,
2395        alignment,
2396        cross_axis_line_alignment: LayoutAlignment::Stretch,
2397        cross_axis_alignment: CrossAxisAlignment::Stretch,
2398        flex_wrap,
2399        flex_shrink: 1.,
2400        flex_direction: taffy_direction,
2401        container_width,
2402        container_height,
2403        cross_axis_sizing,
2404    };
2405
2406    let (available_width, available_height) = match direction {
2407        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => {
2408            (main_axis_constraint, Coord::MAX)
2409        }
2410        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => {
2411            (Coord::MAX, main_axis_constraint)
2412        }
2413    };
2414
2415    let cross_of = |(width, height): (Coord, Coord)| match direction {
2416        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => height,
2417        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => width,
2418    };
2419
2420    let mut builder =
2421        flexbox_taffy::FlexboxTaffyBuilder::new(params(flexbox_taffy::CrossAxisSizing::Minimum));
2422    // Report nothing, so every item falls back to its min constraint.
2423    let mut zero = zero_measure;
2424    builder.compute_layout(available_width, available_height, &mut zero);
2425    let cross_size = cross_of(builder.container_size());
2426    let mut resolved = measure.map(|m| resolve_measure_defaults(&cells_h, &cells_v, m));
2427
2428    // The pass above resolved every `auto` cross size to the item's minimum.
2429    // Measure again with the identity callback, which resolves `auto` to the
2430    // item's preferred size instead, and sums the flex lines when the items
2431    // wrap.
2432    let preferred = {
2433        let mut builder = flexbox_taffy::FlexboxTaffyBuilder::new(params(
2434            flexbox_taffy::CrossAxisSizing::FromMeasure,
2435        ));
2436        let mut identity = identity_measure;
2437        let mut fallback = resolve_measure_defaults(&cells_h, &cells_v, &mut identity);
2438        builder.compute_layout(
2439            available_width,
2440            available_height,
2441            match resolved.as_mut() {
2442                Some(m) => m,
2443                None => &mut fallback,
2444            },
2445        );
2446        cross_of(builder.container_size())
2447    };
2448
2449    LayoutInfo {
2450        min: cross_size,
2451        max: Coord::MAX,
2452        min_percent: 0 as _,
2453        max_percent: 100 as _,
2454        preferred,
2455        stretch: 0.0,
2456    }
2457}
2458
2459#[cfg(feature = "ffi")]
2460pub(crate) mod ffi {
2461    #![allow(unsafe_code)]
2462
2463    use super::*;
2464
2465    #[unsafe(no_mangle)]
2466    pub extern "C" fn slint_organize_grid_layout(
2467        input_data: Slice<GridLayoutInputData>,
2468        repeater_indices: Slice<u32>,
2469        repeater_steps: Slice<u32>,
2470        result: &mut GridLayoutOrganizedData,
2471    ) {
2472        *result = super::organize_grid_layout(input_data, repeater_indices, repeater_steps);
2473    }
2474
2475    #[unsafe(no_mangle)]
2476    pub extern "C" fn slint_organize_dialog_button_layout(
2477        input_data: Slice<GridLayoutInputData>,
2478        dialog_button_roles: Slice<DialogButtonRole>,
2479        result: &mut GridLayoutOrganizedData,
2480    ) {
2481        *result = super::organize_dialog_button_layout(input_data, dialog_button_roles);
2482    }
2483
2484    #[unsafe(no_mangle)]
2485    pub extern "C" fn slint_solve_grid_layout(
2486        data: &GridLayoutData,
2487        constraints: Slice<LayoutItemInfo>,
2488        orientation: Orientation,
2489        repeater_indices: Slice<u32>,
2490        repeater_steps: Slice<u32>,
2491        result: &mut SharedVector<Coord>,
2492    ) {
2493        *result = super::solve_grid_layout(
2494            data,
2495            constraints,
2496            orientation,
2497            repeater_indices,
2498            repeater_steps,
2499        )
2500    }
2501
2502    #[unsafe(no_mangle)]
2503    pub extern "C" fn slint_grid_layout_info(
2504        organized_data: &GridLayoutOrganizedData,
2505        constraints: Slice<LayoutItemInfo>,
2506        repeater_indices: Slice<u32>,
2507        repeater_steps: Slice<u32>,
2508        spacing: Coord,
2509        padding: &Padding,
2510        orientation: Orientation,
2511    ) -> LayoutInfo {
2512        super::grid_layout_info(
2513            organized_data.clone(),
2514            constraints,
2515            repeater_indices,
2516            repeater_steps,
2517            spacing,
2518            padding,
2519            orientation,
2520        )
2521    }
2522
2523    #[unsafe(no_mangle)]
2524    pub extern "C" fn slint_solve_box_layout(
2525        data: &BoxLayoutData,
2526        repeater_indices: Slice<u32>,
2527        result: &mut SharedVector<Coord>,
2528    ) {
2529        *result = super::solve_box_layout(data, repeater_indices)
2530    }
2531
2532    #[unsafe(no_mangle)]
2533    pub extern "C" fn slint_solve_box_layout_ortho(
2534        data: &BoxLayoutOrthoData,
2535        repeater_indices: Slice<u32>,
2536        result: &mut SharedVector<Coord>,
2537    ) {
2538        *result = super::solve_box_layout_ortho(data, repeater_indices)
2539    }
2540
2541    #[unsafe(no_mangle)]
2542    /// Return the LayoutInfo for a BoxLayout with the given cells.
2543    pub extern "C" fn slint_box_layout_info(
2544        cells: Slice<LayoutItemInfo>,
2545        spacing: Coord,
2546        padding: &Padding,
2547        alignment: LayoutAlignment,
2548    ) -> LayoutInfo {
2549        super::box_layout_info(cells, spacing, padding, alignment)
2550    }
2551
2552    #[unsafe(no_mangle)]
2553    /// Return the LayoutInfo for a BoxLayout with the given cells.
2554    pub extern "C" fn slint_box_layout_info_ortho(
2555        cells: Slice<LayoutItemInfo>,
2556        padding: &Padding,
2557    ) -> LayoutInfo {
2558        super::box_layout_info_ortho(cells, padding)
2559    }
2560
2561    /// The measure callback for C FFI. Returns (width, height) via out pointers.
2562    /// A dimension taffy has not determined (`known_* == false`) is pre-resolved
2563    /// to the cell's preferred size.
2564    /// A null function pointer means no measure callback.
2565    pub type FlexboxMeasureFnC = unsafe extern "C" fn(
2566        user_data: *mut core::ffi::c_void,
2567        child_index: usize,
2568        width: Coord,
2569        height: Coord,
2570        known_width: bool,
2571        known_height: bool,
2572        out_width: *mut Coord,
2573        out_height: *mut Coord,
2574    );
2575
2576    /// Turn a C measure callback (nullable fn pointer + user data) into the
2577    /// closure form used internally.
2578    ///
2579    /// # Safety
2580    /// `measure_fn`, when non-null, must be a valid `FlexboxMeasureFnC`
2581    /// function pointer, passed as `*const c_void` because cbindgen can't
2582    /// represent `Option<fn pointer>` in C++.
2583    unsafe fn measure_closure_from_c(
2584        measure_fn: *const core::ffi::c_void,
2585        measure_user_data: *mut core::ffi::c_void,
2586    ) -> Option<impl FnMut(usize, Coord, Coord, bool, bool) -> (Coord, Coord)> {
2587        const {
2588            assert!(
2589                core::mem::size_of::<*const core::ffi::c_void>()
2590                    == core::mem::size_of::<FlexboxMeasureFnC>()
2591            );
2592        }
2593        if measure_fn.is_null() {
2594            return None;
2595        }
2596        let c_measure = unsafe {
2597            core::mem::transmute::<*const core::ffi::c_void, FlexboxMeasureFnC>(measure_fn)
2598        };
2599        Some(move |child_index: usize, w: Coord, h: Coord, known_w: bool, known_h: bool| {
2600            let mut out_w: Coord = 0 as _;
2601            let mut out_h: Coord = 0 as _;
2602            // Safety: c_measure is a valid function pointer provided by the caller,
2603            // and out_w/out_h are valid mutable pointers.
2604            unsafe {
2605                c_measure(
2606                    measure_user_data,
2607                    child_index,
2608                    w,
2609                    h,
2610                    known_w,
2611                    known_h,
2612                    &mut out_w,
2613                    &mut out_h,
2614                );
2615            }
2616            (out_w, out_h)
2617        })
2618    }
2619
2620    #[unsafe(no_mangle)]
2621    pub extern "C" fn slint_solve_flexbox_layout(
2622        data: &FlexboxLayoutData,
2623        repeater_indices: Slice<u32>,
2624        result: &mut SharedVector<Coord>,
2625        measure_fn: *const core::ffi::c_void,
2626        measure_user_data: *mut core::ffi::c_void,
2627    ) {
2628        // Safety: the caller guarantees `measure_fn` is a valid `FlexboxMeasureFnC`
2629        // when non-null (see `measure_closure_from_c`).
2630        let measure = unsafe { measure_closure_from_c(measure_fn, measure_user_data) };
2631        if let Some(mut measure) = measure {
2632            *result = super::solve_flexbox_layout_with_measure(
2633                data,
2634                repeater_indices,
2635                Some(&mut measure),
2636            );
2637        } else {
2638            *result = super::solve_flexbox_layout(data, repeater_indices);
2639        }
2640    }
2641
2642    #[unsafe(no_mangle)]
2643    /// Return main-axis LayoutInfo for a FlexboxLayout (single-axis, no cross-axis dependency).
2644    pub extern "C" fn slint_flexbox_layout_info_main_axis(
2645        cells: Slice<LayoutItemInfo>,
2646        spacing: Coord,
2647        padding: &Padding,
2648        flex_wrap: FlexboxLayoutWrap,
2649    ) -> LayoutInfo {
2650        super::flexbox_layout_info_main_axis(cells, spacing, padding, flex_wrap)
2651    }
2652
2653    #[unsafe(no_mangle)]
2654    /// Return the flex's natural single-line (no-wrap) main-axis size.
2655    pub extern "C" fn slint_flexbox_layout_unwrapped_main(
2656        cells: Slice<LayoutItemInfo>,
2657        spacing: Coord,
2658        padding: &Padding,
2659    ) -> Coord {
2660        super::flexbox_layout_unwrapped_main(cells, spacing, padding)
2661    }
2662
2663    #[unsafe(no_mangle)]
2664    /// Return cross-axis LayoutInfo for a FlexboxLayout.
2665    pub extern "C" fn slint_flexbox_layout_info_cross_axis(
2666        cells_h: Slice<LayoutItemInfo>,
2667        cells_v: Slice<LayoutItemInfo>,
2668        flex_props: Slice<FlexItemProps>,
2669        spacing_h: Coord,
2670        spacing_v: Coord,
2671        padding_h: &Padding,
2672        padding_v: &Padding,
2673        direction: FlexboxLayoutDirection,
2674        alignment: LayoutAlignment,
2675        flex_wrap: FlexboxLayoutWrap,
2676        constraint_size: Coord,
2677    ) -> LayoutInfo {
2678        super::flexbox_layout_info_cross_axis(
2679            cells_h,
2680            cells_v,
2681            flex_props,
2682            spacing_h,
2683            spacing_v,
2684            padding_h,
2685            padding_v,
2686            direction,
2687            alignment,
2688            flex_wrap,
2689            constraint_size,
2690        )
2691    }
2692
2693    #[unsafe(no_mangle)]
2694    /// Like `slint_flexbox_layout_info_cross_axis`, with a measure callback so
2695    /// height-for-width cells are re-measured at the size taffy assigns them.
2696    pub extern "C" fn slint_flexbox_layout_info_cross_axis_with_measure(
2697        cells_h: Slice<LayoutItemInfo>,
2698        cells_v: Slice<LayoutItemInfo>,
2699        flex_props: Slice<FlexItemProps>,
2700        spacing_h: Coord,
2701        spacing_v: Coord,
2702        padding_h: &Padding,
2703        padding_v: &Padding,
2704        direction: FlexboxLayoutDirection,
2705        alignment: LayoutAlignment,
2706        flex_wrap: FlexboxLayoutWrap,
2707        constraint_size: Coord,
2708        measure_fn: *const core::ffi::c_void,
2709        measure_user_data: *mut core::ffi::c_void,
2710    ) -> LayoutInfo {
2711        // Safety: the caller guarantees `measure_fn` is a valid `FlexboxMeasureFnC`
2712        // when non-null (see `measure_closure_from_c`).
2713        let mut measure = unsafe { measure_closure_from_c(measure_fn, measure_user_data) };
2714        super::flexbox_layout_info_cross_axis_with_measure(
2715            cells_h,
2716            cells_v,
2717            flex_props,
2718            spacing_h,
2719            spacing_v,
2720            padding_h,
2721            padding_v,
2722            direction,
2723            alignment,
2724            flex_wrap,
2725            constraint_size,
2726            measure.as_mut().map(|m| m as _),
2727        )
2728    }
2729}
2730
2731#[cfg(test)]
2732mod tests {
2733    use super::*;
2734
2735    fn collect_from_organized_data(
2736        organized_data: &GridLayoutOrganizedData,
2737        num_cells: usize,
2738        repeater_indices: Slice<u32>,
2739        repeater_steps: Slice<u32>,
2740    ) -> Vec<(u16, u16, u16, u16)> {
2741        let mut result = Vec::new();
2742        for i in 0..num_cells {
2743            let col_and_span = organized_data.col_or_row_and_span(
2744                i,
2745                Orientation::Horizontal,
2746                &repeater_indices,
2747                &repeater_steps,
2748            );
2749            let row_and_span = organized_data.col_or_row_and_span(
2750                i,
2751                Orientation::Vertical,
2752                &repeater_indices,
2753                &repeater_steps,
2754            );
2755            result.push((col_and_span.0, col_and_span.1, row_and_span.0, row_and_span.1));
2756        }
2757        result
2758    }
2759
2760    #[test]
2761    fn test_organized_data_generator_2_fixed_cells() {
2762        // 2 fixed cells
2763        let mut result = GridLayoutOrganizedData::default();
2764        let num_cells = 2;
2765        let mut generator = OrganizedDataGenerator::new(&[], &[], num_cells, 0, 0, &mut result);
2766        generator.add(0, 1, 0, 1);
2767        generator.add(1, 2, 0, 3);
2768        assert_eq!(result.as_slice(), &[0, 1, 0, 1, 1, 2, 0, 3]);
2769
2770        let repeater_indices = Slice::from_slice(&[]);
2771        let empty_steps = Slice::from_slice(&[]);
2772        let collected_data =
2773            collect_from_organized_data(&result, num_cells, repeater_indices, empty_steps);
2774        assert_eq!(collected_data.as_slice(), &[(0, 1, 0, 1), (1, 2, 0, 3)]);
2775
2776        assert_eq!(
2777            result.max_value(num_cells, Orientation::Horizontal, &repeater_indices, &empty_steps),
2778            3
2779        );
2780        assert_eq!(
2781            result.max_value(num_cells, Orientation::Vertical, &repeater_indices, &empty_steps),
2782            3
2783        );
2784    }
2785
2786    #[test]
2787    fn test_organized_data_generator_1_fixed_cell_1_repeater() {
2788        // 4 cells: 1 fixed cell, 1 repeater with 3 repeated cells
2789        let mut result = GridLayoutOrganizedData::default();
2790        let num_cells = 4;
2791        let repeater_indices = &[1u32, 3u32];
2792        let mut generator =
2793            OrganizedDataGenerator::new(repeater_indices, &[], 1, 1, 3, &mut result);
2794        generator.add(0, 1, 0, 2); // fixed
2795        generator.add(1, 2, 1, 3); // repeated
2796        generator.add(1, 1, 2, 4);
2797        generator.add(2, 2, 3, 5);
2798        assert_eq!(
2799            result.as_slice(),
2800            &[
2801                0, 1, 0, 2, // fixed cell
2802                8, 4, 0, 0, // jump cell: data_base=8, stride=4 (step=1, epi=4)
2803                1, 2, 1, 3, // repeated cell 1
2804                1, 1, 2, 4, // repeated cell 2
2805                2, 2, 3, 5, // repeated cell 3
2806            ]
2807        );
2808        let repeater_indices = Slice::from_slice(repeater_indices);
2809        let empty_steps = Slice::from_slice(&[]);
2810        let collected_data =
2811            collect_from_organized_data(&result, num_cells, repeater_indices, empty_steps);
2812        assert_eq!(
2813            collected_data.as_slice(),
2814            &[(0, 1, 0, 2), (1, 2, 1, 3), (1, 1, 2, 4), (2, 2, 3, 5)]
2815        );
2816
2817        assert_eq!(
2818            result.max_value(num_cells, Orientation::Horizontal, &repeater_indices, &empty_steps),
2819            4
2820        );
2821        assert_eq!(
2822            result.max_value(num_cells, Orientation::Vertical, &repeater_indices, &empty_steps),
2823            8
2824        );
2825    }
2826
2827    #[test]
2828
2829    fn test_organize_data_with_auto_and_spans() {
2830        let auto = i_slint_common::ROW_COL_AUTO;
2831        let input = std::vec![
2832            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 2., rowspan: -1. },
2833            GridLayoutInputData { new_row: false, col: auto, row: auto, colspan: 1., rowspan: 2. },
2834            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 2., rowspan: 1. },
2835            GridLayoutInputData { new_row: true, col: -2., row: 80000., colspan: 2., rowspan: 1. },
2836        ];
2837        let repeater_indices = Slice::from_slice(&[]);
2838        let (organized_data, errors) = organize_grid_layout_impl(
2839            Slice::from_slice(&input),
2840            repeater_indices,
2841            Slice::from_slice(&[]),
2842        );
2843        assert_eq!(
2844            organized_data.as_slice(),
2845            &[
2846                0, 2, 0, 0, // row 0, col 0, rowspan 0 (see below)
2847                2, 1, 0, 2, // row 0, col 2 (due to colspan of first cell)
2848                0, 2, 1, 1, // row 1, col 0
2849                0, 2, 65535, 1, // row 65535, col 0
2850            ]
2851        );
2852        assert_eq!(errors.len(), 3);
2853        // Note that a rowspan of 0 is valid, it means the cell doesn't occupy any row
2854        assert_eq!(errors[0], "cell rowspan -1 is negative, clamping to 0");
2855        assert_eq!(errors[1], "cell row 80000 is too large, clamping to 65535");
2856        assert_eq!(errors[2], "cell col -2 is negative, clamping to 0");
2857        let empty_steps = Slice::from_slice(&[]);
2858        let collected_data = collect_from_organized_data(
2859            &organized_data,
2860            input.len(),
2861            repeater_indices,
2862            empty_steps,
2863        );
2864        assert_eq!(
2865            collected_data.as_slice(),
2866            &[(0, 2, 0, 0), (2, 1, 0, 2), (0, 2, 1, 1), (0, 2, 65535, 1)]
2867        );
2868        assert_eq!(
2869            organized_data.max_value(3, Orientation::Horizontal, &repeater_indices, &empty_steps),
2870            3
2871        );
2872        assert_eq!(
2873            organized_data.max_value(3, Orientation::Vertical, &repeater_indices, &empty_steps),
2874            2
2875        );
2876    }
2877
2878    #[test]
2879    fn test_organize_data_1_empty_repeater() {
2880        // Row { Text {}    if false: Text {} }, this test shows why we need i32 for cell_nr_adj
2881        let auto = i_slint_common::ROW_COL_AUTO;
2882        let cell =
2883            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 1., rowspan: 1. };
2884        let input = std::vec![cell];
2885        let repeater_indices = Slice::from_slice(&[1u32, 0u32]);
2886        let (organized_data, errors) = organize_grid_layout_impl(
2887            Slice::from_slice(&input),
2888            repeater_indices,
2889            Slice::from_slice(&[]),
2890        );
2891        assert_eq!(
2892            organized_data.as_slice(),
2893            &[
2894                0, 1, 0, 1, // fixed
2895                0, 0, 0, 0
2896            ] // jump to repeater data (not used)
2897        );
2898        assert_eq!(errors.len(), 0);
2899        let empty_steps = Slice::from_slice(&[]);
2900        let collected_data = collect_from_organized_data(
2901            &organized_data,
2902            input.len(),
2903            repeater_indices,
2904            empty_steps,
2905        );
2906        assert_eq!(collected_data.as_slice(), &[(0, 1, 0, 1)]);
2907        assert_eq!(
2908            organized_data.max_value(1, Orientation::Horizontal, &repeater_indices, &empty_steps),
2909            1
2910        );
2911    }
2912
2913    #[test]
2914    fn test_organize_data_4_repeaters() {
2915        let auto = i_slint_common::ROW_COL_AUTO;
2916        let mut cell =
2917            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 1., rowspan: 1. };
2918        let mut input = std::vec![cell.clone()];
2919        for _ in 0..8 {
2920            cell.new_row = false;
2921            input.push(cell.clone());
2922        }
2923        let repeater_indices = Slice::from_slice(&[0u32, 0u32, 1u32, 4u32, 6u32, 2u32, 8u32, 0u32]);
2924        let (organized_data, errors) = organize_grid_layout_impl(
2925            Slice::from_slice(&input),
2926            repeater_indices,
2927            Slice::from_slice(&[]),
2928        );
2929        assert_eq!(
2930            organized_data.as_slice(),
2931            &[
2932                28, 4, 0, 0, // rep0 jump: data at 28, stride=4 (empty)
2933                0, 1, 0, 1, // fixed cell (col=0)
2934                28, 4, 0, 0, // rep1 jump: data at 28, stride=4 (4 rows)
2935                5, 1, 0, 1, // fixed cell (col=5)
2936                44, 4, 0, 0, // rep2 jump: data at 44, stride=4 (2 rows)
2937                52, 4, 0, 0, // rep3 jump: data at 52, stride=4 (empty)
2938                8, 1, 0, 1, // fixed cell (col=8)
2939                1, 1, 0, 1, // rep1 row 0
2940                2, 1, 0, 1, // rep1 row 1
2941                3, 1, 0, 1, // rep1 row 2
2942                4, 1, 0, 1, // rep1 row 3
2943                6, 1, 0, 1, // rep2 row 0
2944                7, 1, 0, 1, // rep2 row 1
2945            ]
2946        );
2947        assert_eq!(errors.len(), 0);
2948        let empty_steps = Slice::from_slice(&[]);
2949        let collected_data = collect_from_organized_data(
2950            &organized_data,
2951            input.len(),
2952            repeater_indices,
2953            empty_steps,
2954        );
2955        assert_eq!(
2956            collected_data.as_slice(),
2957            &[
2958                (0, 1, 0, 1),
2959                (1, 1, 0, 1),
2960                (2, 1, 0, 1),
2961                (3, 1, 0, 1),
2962                (4, 1, 0, 1),
2963                (5, 1, 0, 1),
2964                (6, 1, 0, 1),
2965                (7, 1, 0, 1),
2966                (8, 1, 0, 1),
2967            ]
2968        );
2969        let empty_steps = Slice::from_slice(&[]);
2970        assert_eq!(
2971            organized_data.max_value(
2972                input.len(),
2973                Orientation::Horizontal,
2974                &repeater_indices,
2975                &empty_steps
2976            ),
2977            9
2978        );
2979    }
2980
2981    #[test]
2982    fn test_organize_data_repeated_rows() {
2983        let auto = i_slint_common::ROW_COL_AUTO;
2984        let mut input = Vec::new();
2985        let num_rows: u32 = 3;
2986        let num_columns: u32 = 2;
2987        // 3 rows of 2 columns each
2988        for _ in 0..num_rows {
2989            let mut cell = GridLayoutInputData {
2990                new_row: true,
2991                col: auto,
2992                row: auto,
2993                colspan: 1.,
2994                rowspan: 1.,
2995            };
2996            input.push(cell.clone());
2997            cell.new_row = false;
2998            input.push(cell.clone());
2999        }
3000        // Repeater 0: starts at index 0, has 3 instances of 2 elements
3001        let repeater_indices_arr = [0_u32, num_rows];
3002        let repeater_steps_arr = [num_columns];
3003        let repeater_steps = Slice::from_slice(&repeater_steps_arr);
3004        let repeater_indices = Slice::from_slice(&repeater_indices_arr);
3005        let (organized_data, errors) =
3006            organize_grid_layout_impl(Slice::from_slice(&input), repeater_indices, repeater_steps);
3007        assert_eq!(
3008            organized_data.as_slice(),
3009            &[
3010                4, 8, 0, 0, // jump cell: data at u16 idx 4, stride=8 (=step*4=2*4)
3011                0, 1, 0, 1, 1, 1, 0, 1, // row 0: col 0, col 1
3012                0, 1, 1, 1, 1, 1, 1, 1, // row 1: col 0, col 1
3013                0, 1, 2, 1, 1, 1, 2, 1, // row 2: col 0, col 1
3014            ]
3015        );
3016        assert_eq!(errors.len(), 0);
3017        let collected_data = collect_from_organized_data(
3018            &organized_data,
3019            input.len(),
3020            repeater_indices,
3021            repeater_steps,
3022        );
3023        assert_eq!(
3024            collected_data.as_slice(),
3025            // (col, colspan, row, rowspan) for each cell in input order
3026            &[(0, 1, 0, 1), (1, 1, 0, 1), (0, 1, 1, 1), (1, 1, 1, 1), (0, 1, 2, 1), (1, 1, 2, 1),]
3027        );
3028        assert_eq!(
3029            organized_data.max_value(
3030                input.len(),
3031                Orientation::Horizontal,
3032                &repeater_indices,
3033                &repeater_steps
3034            ),
3035            2
3036        );
3037        assert_eq!(
3038            organized_data.max_value(
3039                input.len(),
3040                Orientation::Vertical,
3041                &repeater_indices,
3042                &repeater_steps
3043            ),
3044            3
3045        );
3046
3047        // Now test GridLayoutCacheGenerator
3048        let mut layout_cache_v = SharedVector::<Coord>::default();
3049        let mut generator = GridLayoutCacheGenerator::new(
3050            repeater_indices.as_slice(),
3051            repeater_steps.as_slice(),
3052            0, // static_cells
3053            1, // num_repeaters
3054            6, // total_repeated_cells (3 rows * 2 columns)
3055            &mut layout_cache_v,
3056        );
3057        // Row 0
3058        generator.add(0., 50.);
3059        generator.add(0., 50.);
3060        // Row 1
3061        generator.add(50., 50.);
3062        generator.add(50., 50.);
3063        // Row 2
3064        generator.add(100., 50.);
3065        generator.add(100., 50.);
3066        assert_eq!(
3067            layout_cache_v.as_slice(),
3068            &[
3069                2., 4., // jump cell: data at pos 2, stride=4 (=step*2=2*2)
3070                0., 50., 0., 50., // row 0
3071                50., 50., 50., 50., // row 1
3072                100., 50., 100., 50., // row 2
3073            ]
3074        );
3075
3076        // GridRepeaterCacheAccess: cache[cache[jump_index] + ri * stride + child_offset]
3077        let layout_cache_v_access = |jump_index: usize,
3078                                     repeater_index: usize,
3079                                     stride: usize,
3080                                     child_offset: usize|
3081         -> Coord {
3082            let base = layout_cache_v[jump_index] as usize;
3083            let data_idx = base + repeater_index * stride + child_offset;
3084            layout_cache_v[data_idx]
3085        };
3086        // stride=4 (step=2, entries_per_item=2)
3087        // Y pos for child 0 (child_offset=0)
3088        assert_eq!(layout_cache_v_access(0, 0, 4, 0), 0.);
3089        assert_eq!(layout_cache_v_access(0, 1, 4, 0), 50.);
3090        assert_eq!(layout_cache_v_access(0, 2, 4, 0), 100.);
3091        // Y pos for child 1 (child_offset=2)
3092        assert_eq!(layout_cache_v_access(0, 0, 4, 2), 0.);
3093        assert_eq!(layout_cache_v_access(0, 1, 4, 2), 50.);
3094        assert_eq!(layout_cache_v_access(0, 2, 4, 2), 100.);
3095    }
3096
3097    #[test]
3098    fn test_organize_data_repeated_rows_multiple_repeaters() {
3099        let auto = i_slint_common::ROW_COL_AUTO;
3100        let mut input = Vec::new();
3101        let num_rows: u32 = 5;
3102        let mut cell =
3103            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 1., rowspan: 1. };
3104        // 3 rows of 2 columns each
3105        for _ in 0..3 {
3106            cell.new_row = true;
3107            input.push(cell.clone());
3108            cell.new_row = false;
3109            input.push(cell.clone());
3110        }
3111        // 2 rows of 3 columns each
3112        for _ in 0..2 {
3113            cell.new_row = true;
3114            input.push(cell.clone());
3115            cell.new_row = false;
3116            input.push(cell.clone());
3117            cell.new_row = false;
3118            input.push(cell.clone());
3119        }
3120        // Repeater 0: starts at index 0, has 3 instances of 2 elements
3121        // Repeater 1: starts at index 6 (after repeater 0's 3*2=6 cells), has 2 instances of 3 elements
3122        let repeater_indices_arr = [0_u32, 3, 6, 2];
3123        let repeater_steps_arr = [2, 3];
3124        let repeater_steps = Slice::from_slice(&repeater_steps_arr);
3125        let repeater_indices = Slice::from_slice(&repeater_indices_arr);
3126        let (organized_data, errors) =
3127            organize_grid_layout_impl(Slice::from_slice(&input), repeater_indices, repeater_steps);
3128        assert_eq!(
3129            organized_data.as_slice(),
3130            &[
3131                8, 8, 0, 0, // repeater 0 jump: data at 8, stride=8 (=step*4=2*4)
3132                32, 12, 0, 0, // repeater 1 jump: data at 32, stride=12 (=step*4=3*4)
3133                // Repeater 0 data
3134                0, 1, 0, 1, 1, 1, 0, 1, // row 0: col 0, col 1
3135                0, 1, 1, 1, 1, 1, 1, 1, // row 1: col 0, col 1
3136                0, 1, 2, 1, 1, 1, 2, 1, // row 2: col 0, col 1
3137                // Repeater 1 data
3138                0, 1, 3, 1, 1, 1, 3, 1, 2, 1, 3, 1, // row 0: col 0, col 1, col 2
3139                0, 1, 4, 1, 1, 1, 4, 1, 2, 1, 4, 1, // row 1: col 0, col 1, col 2
3140            ]
3141        );
3142        assert_eq!(errors.len(), 0);
3143        let collected_data = collect_from_organized_data(
3144            &organized_data,
3145            input.len(),
3146            repeater_indices,
3147            repeater_steps,
3148        );
3149        assert_eq!(
3150            collected_data.as_slice(),
3151            // (col, colspan, row, rowspan) for each cell in input order
3152            &[
3153                (0, 1, 0, 1),
3154                (1, 1, 0, 1),
3155                (0, 1, 1, 1),
3156                (1, 1, 1, 1),
3157                (0, 1, 2, 1),
3158                (1, 1, 2, 1),
3159                (0, 1, 3, 1),
3160                (1, 1, 3, 1),
3161                (2, 1, 3, 1),
3162                (0, 1, 4, 1),
3163                (1, 1, 4, 1),
3164                (2, 1, 4, 1)
3165            ]
3166        );
3167        assert_eq!(
3168            organized_data.max_value(
3169                input.len(),
3170                Orientation::Horizontal,
3171                &repeater_indices,
3172                &repeater_steps
3173            ),
3174            3 // max col (2) + colspan (1) = 3
3175        );
3176        assert_eq!(
3177            organized_data.max_value(
3178                input.len(),
3179                Orientation::Vertical,
3180                &repeater_indices,
3181                &repeater_steps
3182            ),
3183            num_rows as usize // max row (4) + rowspan (1) = 5
3184        );
3185
3186        // Now test GridLayoutCacheGenerator
3187        let mut layout_cache_v = SharedVector::<Coord>::default();
3188        let mut generator = GridLayoutCacheGenerator::new(
3189            repeater_indices.as_slice(),
3190            repeater_steps.as_slice(),
3191            0,  // static_cells
3192            2,  // num_repeaters
3193            12, // total_repeated_cells (3*2 + 2*3)
3194            &mut layout_cache_v,
3195        );
3196        // Row 0
3197        generator.add(0., 50.);
3198        generator.add(0., 50.);
3199        // Row 1
3200        generator.add(50., 50.);
3201        generator.add(50., 50.);
3202        // Row 2
3203        generator.add(100., 50.);
3204        generator.add(100., 50.);
3205        // Row 3
3206        generator.add(150., 50.);
3207        generator.add(150., 50.);
3208        generator.add(150., 50.);
3209        // Row 4
3210        generator.add(200., 50.);
3211        generator.add(200., 50.);
3212        generator.add(200., 50.);
3213        assert_eq!(
3214            layout_cache_v.as_slice(),
3215            &[
3216                4., 4., // repeater 0 jump: data at pos 4, stride=4 (=step*2=2*2)
3217                16., 6., // repeater 1 jump: data at pos 16, stride=6 (=step*2=3*2)
3218                0., 50., 0., 50., // repeater 0 row 0 data
3219                50., 50., 50., 50., // repeater 0 row 1 data
3220                100., 50., 100., 50., // repeater 0 row 2 data
3221                150., 50., 150., 50., 150., 50., // repeater 1 row 3 data
3222                200., 50., 200., 50., 200., 50., // repeater 1 row 4 data
3223            ]
3224        );
3225
3226        // GridRepeaterCacheAccess: cache[cache[jump_index] + ri * stride + child_offset]
3227        let layout_cache_v_access = |jump_index: usize,
3228                                     repeater_index: usize,
3229                                     stride: usize,
3230                                     child_offset: usize|
3231         -> Coord {
3232            let base = layout_cache_v[jump_index] as usize;
3233            let data_idx = base + repeater_index * stride + child_offset;
3234            layout_cache_v[data_idx]
3235        };
3236        // Repeater 0: Y pos for child 0 (child_offset=0), stride=4
3237        assert_eq!(layout_cache_v_access(0, 0, 4, 0), 0.);
3238        assert_eq!(layout_cache_v_access(0, 1, 4, 0), 50.);
3239        assert_eq!(layout_cache_v_access(0, 2, 4, 0), 100.);
3240        // Repeater 0: Y pos for child 1 (child_offset=2), stride=4
3241        assert_eq!(layout_cache_v_access(0, 0, 4, 2), 0.);
3242        assert_eq!(layout_cache_v_access(0, 1, 4, 2), 50.);
3243        assert_eq!(layout_cache_v_access(0, 2, 4, 2), 100.);
3244        // Repeater 1: Y pos for child 0 (child_offset=0), jump at index 2, stride=6
3245        assert_eq!(layout_cache_v_access(2, 0, 6, 0), 150.);
3246        assert_eq!(layout_cache_v_access(2, 1, 6, 0), 200.);
3247        // Repeater 1: Y pos for child 2 (child_offset=4), jump at index 2, stride=6
3248        assert_eq!(layout_cache_v_access(2, 0, 6, 4), 150.);
3249        assert_eq!(layout_cache_v_access(2, 1, 6, 4), 200.);
3250    }
3251
3252    #[test]
3253    fn test_layout_cache_generator_2_fixed_cells() {
3254        // 2 fixed cells
3255        let mut result = SharedVector::<Coord>::default();
3256        result.resize(2 * 2, 0 as _);
3257        let mut generator = LayoutCacheGenerator::new(&[], &mut result);
3258        generator.add(0., 50.); // fixed
3259        generator.add(80., 50.); // fixed
3260        assert_eq!(result.as_slice(), &[0., 50., 80., 50.]);
3261    }
3262
3263    #[test]
3264    fn test_layout_cache_generator_1_fixed_cell_1_repeater() {
3265        // 4 cells: 1 fixed cell, 1 repeater with 3 repeated cells
3266        let mut result = SharedVector::<Coord>::default();
3267        let repeater_indices = &[1, 3];
3268        result.resize(4 * 2 + repeater_indices.len(), 0 as _);
3269        let mut generator = LayoutCacheGenerator::new(repeater_indices, &mut result);
3270        generator.add(0., 50.); // fixed
3271        generator.add(80., 50.); // repeated
3272        generator.add(160., 50.);
3273        generator.add(240., 50.);
3274        assert_eq!(
3275            result.as_slice(),
3276            &[
3277                0., 50., // fixed
3278                4., 5., // jump to repeater data
3279                80., 50., 160., 50., 240., 50. // repeater data
3280            ]
3281        );
3282    }
3283
3284    #[test]
3285    fn test_layout_cache_generator_4_repeaters() {
3286        // 8 cells: 1 fixed cell, 1 empty repeater, 1 repeater with 4 repeated cells, 1 fixed cell, 1 repeater with 2 repeated cells, 1 empty repeater
3287        let mut result = SharedVector::<Coord>::default();
3288        let repeater_indices = &[1, 0, 1, 4, 6, 2, 8, 0];
3289        result.resize(8 * 2 + repeater_indices.len(), 0 as _);
3290        let mut generator = LayoutCacheGenerator::new(repeater_indices, &mut result);
3291        generator.add(0., 50.); // fixed
3292        generator.add(80., 10.); // repeated
3293        generator.add(160., 10.);
3294        generator.add(240., 10.);
3295        generator.add(320., 10.); // end of second repeater
3296        generator.add(400., 80.); // fixed
3297        generator.add(500., 20.); // repeated
3298        generator.add(600., 20.); // end of third repeater
3299        assert_eq!(
3300            result.as_slice(),
3301            &[
3302                0., 50., // fixed
3303                12., 13., // jump to first (empty) repeater (not used)
3304                12., 13., // jump to second repeater data
3305                400., 80., // fixed
3306                20., 21., // jump to third repeater data
3307                0., 0., // slot for jumping to fourth repeater (currently empty)
3308                80., 10., 160., 10., 240., 10., 320., 10., // first repeater data
3309                500., 20., 600., 20. // second repeater data
3310            ]
3311        );
3312    }
3313
3314    /// `flexbox_layout_unwrapped_main()` computes the container's max-content main
3315    /// size by hand, because the main-axis layout-info path may not read the
3316    /// cross-axis cells (that would be a binding loop). taffy computes the same
3317    /// thing from the same cells, so this pins the two together and fails the day
3318    /// the hand-rolled version drifts.
3319    ///
3320    /// Only meaningful for `no-wrap`: at max-content taffy never wraps (it reports a
3321    /// single line), while the wrapping branch of `flexbox_layout_info_main_axis()`
3322    /// deliberately reports a roughly square arrangement instead.
3323    mod max_content_matches_taffy {
3324        use super::*;
3325
3326        fn cell(preferred: Coord, stretch: f32) -> FlexboxLayoutItemInfo {
3327            FlexboxLayoutItemInfo {
3328                constraint: LayoutInfo { preferred, stretch, ..Default::default() },
3329                ..Default::default()
3330            }
3331        }
3332
3333        /// The constraint half of the bundled test cells, as the parallel
3334        /// array the runtime takes.
3335        fn constraints(cells: &[FlexboxLayoutItemInfo]) -> Vec<LayoutItemInfo> {
3336            cells
3337                .iter()
3338                .map(|c| LayoutItemInfo { constraint: c.constraint.clone(), ..Default::default() })
3339                .collect()
3340        }
3341
3342        /// Split the bundled test cells into the parallel (constraint, flex-props)
3343        /// arrays the runtime takes.
3344        fn split(cells: &[FlexboxLayoutItemInfo]) -> (Vec<LayoutItemInfo>, Vec<FlexItemProps>) {
3345            (constraints(cells), cells.iter().map(|c| c.props).collect())
3346        }
3347
3348        /// What taffy makes of the same cells, asked for its max-content main size.
3349        fn taffy_max_content_main(cells: &[FlexboxLayoutItemInfo]) -> Coord {
3350            let (main, flex) = split(cells);
3351            // The cross axis is deliberately left unconstrained: the main-axis result
3352            // must not depend on it, which is what makes the loop-free path sound.
3353            let cross: Vec<LayoutItemInfo> = cells
3354                .iter()
3355                .map(|_| LayoutItemInfo {
3356                    constraint: LayoutInfo { max: Coord::MAX, ..Default::default() },
3357                    ..Default::default()
3358                })
3359                .collect();
3360            let cells_h = Slice::from_slice(&main);
3361            let cells_v = Slice::from_slice(&cross);
3362            let flex_props = Slice::from_slice(&flex);
3363            let pad = Padding::default();
3364            let mut builder =
3365                flexbox_taffy::FlexboxTaffyBuilder::new(flexbox_taffy::FlexboxLayoutParams {
3366                    cells_h: &cells_h,
3367                    cells_v: &cells_v,
3368                    flex_props: &flex_props,
3369                    spacing_h: 0 as Coord,
3370                    spacing_v: 0 as Coord,
3371                    padding_h: &pad,
3372                    padding_v: &pad,
3373                    // Stretch: the strongest growing mode, to show the stretch
3374                    // factors cancel out of the max-content size.
3375                    alignment: LayoutAlignment::Stretch,
3376                    cross_axis_line_alignment: LayoutAlignment::Stretch,
3377                    cross_axis_alignment: CrossAxisAlignment::Stretch,
3378                    flex_wrap: FlexboxLayoutWrap::NoWrap,
3379                    flex_shrink: 1.,
3380                    flex_direction: flexbox_taffy::TaffyFlexDirection::Row,
3381                    container_width: None,
3382                    container_height: None,
3383                    cross_axis_sizing: flexbox_taffy::CrossAxisSizing::Preferred,
3384                });
3385            // A row item gets `size.width: auto`, so taffy asks the measure callback
3386            // for its content size. That callback is how Slint reports an item's
3387            // natural size, so the comparison is only fair if it answers here too.
3388            let mut measure = |idx: usize, known_w: Option<Coord>, known_h: Option<Coord>| {
3389                (
3390                    known_w.unwrap_or_else(|| cells[idx].constraint.preferred_bounded()),
3391                    known_h.unwrap_or(0 as Coord),
3392                )
3393            };
3394            builder.compute_layout(Coord::MAX, Coord::MAX, &mut measure);
3395            builder.container_size().0
3396        }
3397
3398        fn ours(cells: &[FlexboxLayoutItemInfo]) -> Coord {
3399            let main = constraints(cells);
3400            flexbox_layout_unwrapped_main(Slice::from_slice(&main), 0 as Coord, &Padding::default())
3401        }
3402
3403        #[track_caller]
3404        fn assert_agrees(name: &str, cells: &[FlexboxLayoutItemInfo]) {
3405            let (ours, theirs) = (ours(cells), taffy_max_content_main(cells));
3406            assert!((ours - theirs).abs() <= 1 as Coord, "{name}: ours={ours} taffy={theirs}");
3407        }
3408
3409        /// The grow factor (derived from the stretch factor) cancels out of
3410        /// taffy's max-content flex fraction, so an item contributes its
3411        /// content size and we must land on the same number.
3412        #[test]
3413        fn agrees() {
3414            assert_agrees("plain", &[cell(50., 0.), cell(250., 0.)]);
3415            assert_agrees("equal stretch", &[cell(50., 1.), cell(250., 1.)]);
3416            assert_agrees("uneven stretch", &[cell(60., 1.), cell(60., 3.)]);
3417            assert_agrees("fractional stretch", &[cell(60., 0.5), cell(40., 1.5)]);
3418            assert_agrees("mixed stretch and not", &[cell(50., 1.), cell(100., 0.)]);
3419            // `preferred-width: 0` with stretch collapses the max-content size
3420            // to nothing in taffy too, as it does in a HorizontalLayout.
3421            assert_agrees("zero preferred, stretchy", &[cell(0., 1.), cell(0., 1.)]);
3422        }
3423
3424        /// The same cells asked of a *column* container, where the main-axis size
3425        /// comes from the basis (the preferred height), not the measure callback.
3426        fn taffy_column_main(cells: &[FlexboxLayoutItemInfo]) -> Coord {
3427            let (main, flex) = split(cells);
3428            // For a column the main axis is vertical: main-axis sizes live in cells_v,
3429            // while the cross-axis (width) constraint is left unbounded.
3430            let h: Vec<LayoutItemInfo> = cells
3431                .iter()
3432                .map(|_| LayoutItemInfo {
3433                    constraint: LayoutInfo { max: Coord::MAX, ..Default::default() },
3434                    ..Default::default()
3435                })
3436                .collect();
3437            let cells_h = Slice::from_slice(&h);
3438            let cells_v = Slice::from_slice(&main);
3439            let flex_props = Slice::from_slice(&flex);
3440            let pad = Padding::default();
3441            let mut builder =
3442                flexbox_taffy::FlexboxTaffyBuilder::new(flexbox_taffy::FlexboxLayoutParams {
3443                    cells_h: &cells_h,
3444                    cells_v: &cells_v,
3445                    flex_props: &flex_props,
3446                    spacing_h: 0 as Coord,
3447                    spacing_v: 0 as Coord,
3448                    padding_h: &pad,
3449                    padding_v: &pad,
3450                    alignment: LayoutAlignment::Stretch,
3451                    cross_axis_line_alignment: LayoutAlignment::Stretch,
3452                    cross_axis_alignment: CrossAxisAlignment::Stretch,
3453                    flex_wrap: FlexboxLayoutWrap::NoWrap,
3454                    flex_shrink: 1.,
3455                    flex_direction: flexbox_taffy::TaffyFlexDirection::Column,
3456                    container_width: None,
3457                    container_height: None,
3458                    cross_axis_sizing: flexbox_taffy::CrossAxisSizing::Preferred,
3459                });
3460            let mut measure = |idx: usize, known_w: Option<Coord>, known_h: Option<Coord>| {
3461                (
3462                    known_w.unwrap_or(0 as Coord),
3463                    known_h.unwrap_or_else(|| cells[idx].constraint.preferred_bounded()),
3464                )
3465            };
3466            builder.compute_layout(Coord::MAX, Coord::MAX, &mut measure);
3467            builder.container_size().1
3468        }
3469
3470        #[track_caller]
3471        fn assert_agrees_column(name: &str, cells: &[FlexboxLayoutItemInfo]) {
3472            let (o, t) = (ours(cells), taffy_column_main(cells));
3473            assert!((o - t).abs() <= 1 as Coord, "{name}: ours={o} taffy={t}");
3474        }
3475
3476        /// Both axes report the sum of the preferred sizes, so the column case must
3477        /// agree just like the row case, even though taffy computes it from the
3478        /// basis there instead of the measure callback.
3479        #[test]
3480        fn agrees_for_a_column() {
3481            assert_agrees_column("plain", &[cell(50., 0.), cell(250., 0.)]);
3482            assert_agrees_column("equal stretch", &[cell(50., 1.), cell(250., 1.)]);
3483            assert_agrees_column("zero preferred, stretchy", &[cell(0., 1.), cell(0., 1.)]);
3484        }
3485    }
3486
3487    /// The taffy grow/shrink factors are derived from the container's
3488    /// `alignment` and the cells' main-axis stretch factors (see
3489    /// `FlexboxTaffyBuilder::new`), mirroring how a box layout distributes
3490    /// free space.
3491    mod stretch_driven_grow {
3492        use super::*;
3493
3494        fn info(preferred: Coord, stretch: f32) -> LayoutInfo {
3495            LayoutInfo { preferred, stretch, ..Default::default() }
3496        }
3497
3498        /// Solve a row of `constraints` in a 400px-wide container and return
3499        /// the resulting widths.
3500        fn solve_row(alignment: LayoutAlignment, constraints: &[LayoutInfo]) -> Vec<Coord> {
3501            let cells_h: Vec<LayoutItemInfo> = constraints
3502                .iter()
3503                .map(|c| LayoutItemInfo { constraint: c.clone(), ..Default::default() })
3504                .collect();
3505            let cells_v: Vec<LayoutItemInfo> =
3506                constraints.iter().map(|_| LayoutItemInfo::default()).collect();
3507            let flex_props: Vec<FlexItemProps> =
3508                constraints.iter().map(|_| FlexItemProps::default()).collect();
3509            let pad = Padding::default();
3510            let mut builder =
3511                flexbox_taffy::FlexboxTaffyBuilder::new(flexbox_taffy::FlexboxLayoutParams {
3512                    cells_h: &Slice::from_slice(&cells_h),
3513                    cells_v: &Slice::from_slice(&cells_v),
3514                    flex_props: &Slice::from_slice(&flex_props),
3515                    spacing_h: 0 as Coord,
3516                    spacing_v: 0 as Coord,
3517                    padding_h: &pad,
3518                    padding_v: &pad,
3519                    alignment,
3520                    cross_axis_line_alignment: LayoutAlignment::Stretch,
3521                    cross_axis_alignment: CrossAxisAlignment::Stretch,
3522                    flex_wrap: FlexboxLayoutWrap::Wrap,
3523                    flex_shrink: 1.,
3524                    flex_direction: flexbox_taffy::TaffyFlexDirection::Row,
3525                    container_width: Some(400 as Coord),
3526                    container_height: None,
3527                    cross_axis_sizing: flexbox_taffy::CrossAxisSizing::Preferred,
3528                });
3529            builder.compute_layout(400 as Coord, Coord::MAX, &mut zero_measure);
3530            (0..constraints.len()).map(|i| builder.child_geometry(i).2).collect()
3531        }
3532
3533        #[test]
3534        fn grows_by_stretch_only_under_alignment_stretch() {
3535            // 200 free pixels split by the stretch weights 3:1.
3536            let cells = [info(100., 3.), info(100., 1.)];
3537            assert_eq!(solve_row(LayoutAlignment::Stretch, &cells), [250., 150.]);
3538            // A zero factor next to a non-zero one stays at its preferred size.
3539            let cells = [info(100., 1.), info(100., 0.)];
3540            assert_eq!(solve_row(LayoutAlignment::Stretch, &cells), [300., 100.]);
3541            // All-zero factors split the free space evenly, like a HorizontalLayout.
3542            let cells = [info(100., 0.), info(100., 0.)];
3543            assert_eq!(solve_row(LayoutAlignment::Stretch, &cells), [200., 200.]);
3544            // Growing requires `alignment: stretch` on the container.
3545            let cells = [info(100., 3.), info(100., 1.)];
3546            assert_eq!(solve_row(LayoutAlignment::Start, &cells), [100., 100.]);
3547        }
3548
3549        /// An item alone on its line and larger than the container shrinks to
3550        /// fit whatever its stretch factor says (the constant taffy shrink
3551        /// factor of 1); only its `min` refuses shrinking.
3552        #[test]
3553        fn lone_oversized_item_shrinks_regardless_of_stretch() {
3554            let squeezable =
3555                LayoutInfo { preferred: 500., min: 200., stretch: 0., ..Default::default() };
3556            assert_eq!(solve_row(LayoutAlignment::Start, &[squeezable]), [400.]);
3557            let rigid =
3558                LayoutInfo { preferred: 500., min: 450., stretch: 0., ..Default::default() };
3559            assert_eq!(solve_row(LayoutAlignment::Start, &[rigid]), [450.]);
3560        }
3561    }
3562
3563    /// A percentage constraint must not be resolved against the `Coord::MAX`
3564    /// "unbounded" sentinel that the info path passes for a no-wrap main axis.
3565    /// `min_percent * MAX / 100` overflows the i32 build (debug panic) and gives
3566    /// infinity in the f32 build, which taffy then bakes into the item's size.
3567    /// When the container axis is unbounded a percentage has no basis, so the
3568    /// item keeps its natural (here zero) size instead.
3569    ///
3570    /// Checked at the builder because the containing info function reports only
3571    /// the *cross* size, while the overflow lands on the item's *main*-axis size;
3572    /// the f32 build hides it at the container level but not at the item.
3573    #[test]
3574    fn percentage_against_unbounded_container_leaves_item_finite() {
3575        // A single row cell with `width: 50%` (min_percent == max_percent == 50).
3576        let cells_h = [LayoutItemInfo {
3577            constraint: LayoutInfo {
3578                min_percent: 50 as Coord,
3579                max_percent: 50 as Coord,
3580                max: Coord::MAX,
3581                ..Default::default()
3582            },
3583            ..Default::default()
3584        }];
3585        let cells_v = [LayoutItemInfo {
3586            constraint: LayoutInfo {
3587                preferred: 30 as Coord,
3588                max: Coord::MAX,
3589                ..Default::default()
3590            },
3591            ..Default::default()
3592        }];
3593        let flex_props = [FlexItemProps::default()];
3594        let pad = Padding::default();
3595        let mut builder =
3596            flexbox_taffy::FlexboxTaffyBuilder::new(flexbox_taffy::FlexboxLayoutParams {
3597                cells_h: &Slice::from_slice(&cells_h),
3598                cells_v: &Slice::from_slice(&cells_v),
3599                flex_props: &Slice::from_slice(&flex_props),
3600                spacing_h: 0 as Coord,
3601                spacing_v: 0 as Coord,
3602                padding_h: &pad,
3603                padding_v: &pad,
3604                alignment: LayoutAlignment::Start,
3605                cross_axis_line_alignment: LayoutAlignment::Stretch,
3606                cross_axis_alignment: CrossAxisAlignment::Stretch,
3607                flex_wrap: FlexboxLayoutWrap::NoWrap,
3608                flex_shrink: 1.,
3609                flex_direction: flexbox_taffy::TaffyFlexDirection::Row,
3610                // The unbounded main axis: the value the info path feeds here.
3611                container_width: Some(Coord::MAX),
3612                container_height: None,
3613                cross_axis_sizing: flexbox_taffy::CrossAxisSizing::Preferred,
3614            });
3615        builder.compute_layout(Coord::MAX, Coord::MAX, &mut zero_measure);
3616        let (_x, _y, w, _h) = builder.child_geometry(0);
3617        // Without the fix the dropped `50% * MAX` makes this the f32 sentinel
3618        // (or panics under i32); with it the item just takes its natural size.
3619        assert!(w.is_finite() && w < Coord::MAX, "item main-axis size was {w}");
3620    }
3621
3622    /// Runs `grid_internal::to_layout_data` for a single row made of one
3623    /// non-spanning cell per constraint, and returns that row's combined
3624    /// LayoutData (min/max/pref), for testing the row/col aggregation in
3625    /// isolation from the rest of GridLayout.
3626    fn row_layout_data(constraints: &[LayoutInfo]) -> grid_internal::LayoutData {
3627        let mut organized_data = GridLayoutOrganizedData::default();
3628        let mut generator =
3629            OrganizedDataGenerator::new(&[], &[], constraints.len(), 0, 0, &mut organized_data);
3630        for col in 0..constraints.len() {
3631            generator.add(col as u16, 1, 0, 1);
3632        }
3633        let items: Vec<LayoutItemInfo> = constraints
3634            .iter()
3635            .map(|constraint| LayoutItemInfo { constraint: *constraint, ..Default::default() })
3636            .collect();
3637        let mut layout_data = grid_internal::to_layout_data(
3638            &organized_data,
3639            Slice::from_slice(&items),
3640            Orientation::Vertical,
3641            Slice::from_slice(&[]),
3642            Slice::from_slice(&[]),
3643            0 as _,
3644            None,
3645        );
3646        assert_eq!(layout_data.len(), 1);
3647        layout_data.remove(0)
3648    }
3649
3650    #[test]
3651    fn test_grid_row_collapsed_cell_raises_max_and_pulls_pref_down_with_it() {
3652        // Row with a cell collapsed to a fixed zero size (the `height: cond ?
3653        // x : 0px` idiom, #9724) next to a cell with an explicit min/preferred
3654        // and no max (e.g. `min-height: 5px; preferred-height: 50px;`).
3655        let row = row_layout_data(&[
3656            LayoutInfo { min: 0 as _, max: 0 as _, preferred: 0 as _, ..Default::default() },
3657            LayoutInfo { min: 5 as _, preferred: 50 as _, ..Default::default() },
3658        ]);
3659        // The collapsed cell's fixed zero pulls the row's max down to 0 while
3660        // its sibling's min pulls the row's min up to 5: a real conflict, so
3661        // max must be raised to meet min, and the sibling's now out-of-range
3662        // preferred (50) must be pulled down with it, not summed into the
3663        // row's LayoutInfo as-is.
3664        assert_eq!(row.min, 5 as Coord);
3665        assert_eq!(row.max, 5 as Coord);
3666        assert_eq!(row.pref, 5 as Coord);
3667    }
3668
3669    #[test]
3670    fn test_grid_row_without_conflicting_constraints_keeps_its_own_pref() {
3671        // No cell forces max below min here (max 10 >= min 0): not a #9724
3672        // conflict, so the fix must leave this row untouched. The second
3673        // cell's preferred (50) legitimately exceeds the row's own max (10)
3674        // already on master; to_layout_data's output also drives
3675        // solve_grid_layout, so clamping pref here would silently shrink
3676        // rows that never had a min/max conflict in the first place.
3677        let row = row_layout_data(&[
3678            LayoutInfo { min: 0 as _, max: 10 as _, preferred: 5 as _, ..Default::default() },
3679            LayoutInfo { min: 0 as _, preferred: 50 as _, ..Default::default() },
3680        ]);
3681        assert_eq!(row.min, 0 as Coord);
3682        assert_eq!(row.max, 10 as Coord);
3683        assert_eq!(row.pref, 50 as Coord);
3684    }
3685}