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)` and returns `(width, height)`.
1925///
1926/// `resolve_measure_defaults` answers a query whose height taffy has already
1927/// settled, so the callback is only asked for a height taffy still needs.
1928/// `height` is therefore the cell's preferred height, or 0 past the last cell.
1929/// Hand it back for a cell whose height does not depend on its width; it is
1930/// not a constraint.
1931/// `width` is the one taffy assigned, or the cell's preferred width when it
1932/// has assigned none.
1933/// Return it unchanged.
1934/// Taffy can use a returned width when it has settled the height instead,
1935/// but `resolve_measure_defaults` answers that query without asking.
1936///
1937/// The result must be a self-consistent pair, each dimension measured at the
1938/// other: taffy caches it, and answers a later query for one dimension with
1939/// the half of the pair it kept.
1940///
1941/// `None` means the caller has no callback of its own, so a dimension taffy
1942/// has not assigned falls back to the cell's preferred size.
1943/// Taffy itself is always given a callback, so a forgotten one is a compile
1944/// error rather than a silently zero-sized item.
1945pub type FlexboxMeasureFn<'a> = Option<&'a mut dyn FnMut(usize, Coord, Coord) -> (Coord, Coord)>;
1946
1947/// The measure that reports nothing, so an item taffy sizes from its content
1948/// falls back to its min constraint.
1949/// It is taffy-facing: it does not go through
1950/// [`resolve_measure_defaults`], which would resolve the unknown dimensions to
1951/// the preferred size only to have them thrown away.
1952fn zero_measure(_: usize, _: Option<Coord>, _: Option<Coord>) -> (Coord, Coord) {
1953    (0 as Coord, 0 as Coord)
1954}
1955
1956/// Adapt a [`FlexboxMeasureFn`] to the taffy-facing closure: resolve the
1957/// dimensions taffy did not supply to the cell's preferred size, so the
1958/// callback always receives concrete ones.
1959///
1960/// It answers two kinds of query itself, returning the pre-resolved pair:
1961///
1962/// - one whose height taffy has already settled,
1963/// - any query at all when there is no callback.
1964fn resolve_measure_defaults<'a, 'm: 'a>(
1965    cells_h: &'a [LayoutItemInfo],
1966    cells_v: &'a [LayoutItemInfo],
1967    mut measure: FlexboxMeasureFn<'m>,
1968) -> impl FnMut(usize, Option<Coord>, Option<Coord>) -> (Coord, Coord) + 'a {
1969    move |index, known_w, known_h| {
1970        let w = known_w.unwrap_or_else(|| {
1971            cells_h.get(index).map_or(0 as Coord, |c| c.constraint.preferred_bounded())
1972        });
1973        let h = known_h.unwrap_or_else(|| {
1974            cells_v.get(index).map_or(0 as Coord, |c| c.constraint.preferred_bounded())
1975        });
1976        // Answering here rather than measuring saves the work.
1977        // Taffy keeps the height it settled and discards the one a callback
1978        // returns, so the measurement (a text shaping, for a wrapped `Text`)
1979        // would be thrown away.
1980        // The callback returns the width unchanged, so the pair is the same
1981        // either way.
1982        match (known_h, measure.as_mut()) {
1983            (None, Some(measure)) => measure(index, w, h),
1984            _ => (w, h),
1985        }
1986    }
1987}
1988
1989pub fn solve_flexbox_layout(
1990    data: &FlexboxLayoutData,
1991    repeater_indices: Slice<u32>,
1992) -> SharedVector<Coord> {
1993    solve_flexbox_layout_with_measure(data, repeater_indices, None)
1994}
1995
1996/// Solve a FlexboxLayout using Taffy
1997/// Returns: [x1, y1, w1, h1, x2, y2, w2, h2, ...] for each item
1998pub fn solve_flexbox_layout_with_measure(
1999    data: &FlexboxLayoutData,
2000    repeater_indices: Slice<u32>,
2001    measure: FlexboxMeasureFn<'_>,
2002) -> SharedVector<Coord> {
2003    // 4 values per item: x, y, width, height
2004    let mut result = SharedVector::<Coord>::default();
2005    result.resize(data.cells_h.len() * 4 + repeater_indices.len() * 2, 0 as _);
2006
2007    if data.cells_h.is_empty() {
2008        return result;
2009    }
2010
2011    let taffy_direction = match data.direction {
2012        FlexboxLayoutDirection::Row => flexbox_taffy::TaffyFlexDirection::Row,
2013        FlexboxLayoutDirection::RowReverse => flexbox_taffy::TaffyFlexDirection::RowReverse,
2014        FlexboxLayoutDirection::Column => flexbox_taffy::TaffyFlexDirection::Column,
2015        FlexboxLayoutDirection::ColumnReverse => flexbox_taffy::TaffyFlexDirection::ColumnReverse,
2016    };
2017
2018    let (container_width, container_height) = (
2019        if data.width > 0 as Coord { Some(data.width) } else { None },
2020        if data.height > 0 as Coord { Some(data.height) } else { None },
2021    );
2022
2023    let use_measure = measure.is_some();
2024    let build = |flex_wrap, flex_shrink| {
2025        flexbox_taffy::FlexboxTaffyBuilder::new(flexbox_taffy::FlexboxLayoutParams {
2026            cells_h: &data.cells_h,
2027            cells_v: &data.cells_v,
2028            flex_props: &data.flex_props,
2029            spacing_h: data.spacing_h,
2030            spacing_v: data.spacing_v,
2031            padding_h: &data.padding_h,
2032            padding_v: &data.padding_v,
2033            alignment: data.alignment,
2034            cross_axis_line_alignment: data.cross_axis_line_alignment,
2035            cross_axis_alignment: data.cross_axis_alignment,
2036            flex_wrap,
2037            flex_shrink,
2038            flex_direction: taffy_direction,
2039            container_width,
2040            container_height,
2041            cross_axis_sizing: if use_measure {
2042                flexbox_taffy::CrossAxisSizing::FromMeasure
2043            } else {
2044                flexbox_taffy::CrossAxisSizing::Preferred
2045            },
2046        })
2047    };
2048    let mut builder = build(data.flex_wrap, 1.);
2049
2050    let (available_width, available_height) = match data.direction {
2051        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => {
2052            (data.width, Coord::MAX)
2053        }
2054        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => {
2055            (Coord::MAX, data.height)
2056        }
2057    };
2058
2059    // A `None` callback is safe here: the compiler omits one only for layouts
2060    // without height-for-width cells, whose pre-computed height is correct for
2061    // whatever width taffy assigns.
2062    let mut measure = resolve_measure_defaults(&data.cells_h, &data.cells_v, measure);
2063    builder.compute_layout(available_width, available_height, &mut measure);
2064
2065    // A column flex wraps by height, so its columns can be wider than the
2066    // width it was given: one column's when its height is not settled (see
2067    // `flexbox_layout_info_cross_axis`). Never overflow sideways into a
2068    // sibling: solve without wrapping instead, and let the content overflow
2069    // downward, like a wrapped Text given too little height.
2070    //
2071    // `WrapReverse` is left alone. It anchors its lines at the cross end, which
2072    // `NoWrap` does not, so re-solving would move the content to the other side;
2073    // and the container's own height still drives the wrapping, so an unbounded
2074    // available height does not stop it either. Such a flex keeps wrapping past
2075    // its width.
2076    let is_column = matches!(
2077        data.direction,
2078        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse
2079    );
2080    if is_column && data.flex_wrap == FlexboxLayoutWrap::Wrap && data.width > 0 as Coord {
2081        // taffy computes in `f32`, so ignore an overflow below half a pixel.
2082        // Integer coordinates are exact and need no tolerance.
2083        #[cfg(not(slint_int_coord))]
2084        const OVERFLOW_TOLERANCE: Coord = 0.5;
2085        #[cfg(slint_int_coord)]
2086        const OVERFLOW_TOLERANCE: Coord = 0;
2087        let (left, right) = (data.padding_h.begin, data.width - data.padding_h.end);
2088        // Taffy indices, not original cell ones: `order` may have sorted them.
2089        // Asking whether *any* child overflows does not care about the order.
2090        let overflows = (0..data.cells_h.len()).any(|idx| {
2091            let (x, _, w, _) = builder.child_geometry(idx);
2092            x < left - OVERFLOW_TOLERANCE || x + w > right + OVERFLOW_TOLERANCE
2093        });
2094        if overflows {
2095            // A second full solve, deliberately, and a common one: an
2096            // unsettled-height column flex is given one column's width, so any
2097            // wrapping at all overflows it. `cross-axis-line-alignment`
2098            // places the lines, so a single line wider than the flex lands at a
2099            // negative `x` under `center` or `end`, and clamping that one line
2100            // back is not enough for the multi-line case this exists for:
2101            // taffy has to lay the items out again without wrapping.
2102            // `flexbox_column_wrap_line_alignment.slint` covers both.
2103            //
2104            // Shrink only where the first solve did: items that fit one column
2105            // keep the shrinking `wrap` gives them, while content that needed
2106            // more than one column is meant to overflow rather than be
2107            // compressed into the height that made it wrap
2108            // (`flexbox_column_wrap_shrink.slint`). A lone item never wrapped,
2109            // however far it overflows.
2110            // `cells_h` is the array the children were built from, so it is
2111            // the child count; `cells_v` is its main-axis twin, one entry per
2112            // cell. The sum uses the cells' preferred sizes, which is what
2113            // taffy wraps on too, though a height-for-width cell it measured
2114            // may end up a little taller: close enough to tell one column from
2115            // several, which is all this decides.
2116            let one_column = data.cells_h.len() < 2
2117                || flexbox_layout_unwrapped_main(
2118                    Slice::from_slice(data.cells_v.as_slice()),
2119                    data.spacing_v,
2120                    &data.padding_v,
2121                ) <= data.height;
2122            builder = build(FlexboxLayoutWrap::NoWrap, if one_column { 1. } else { 0. });
2123            builder.compute_layout(available_width, available_height, &mut measure);
2124        }
2125    }
2126
2127    // Extract results using the cache generator to handle repeaters.
2128    // If `order` sorting was applied, we need to collect results by original index first,
2129    // because the cache generator expects items in their original declaration order.
2130    if builder.order_map.is_empty() {
2131        let mut generator = FlexboxLayoutCacheGenerator::new(&repeater_indices, &mut result);
2132        for idx in 0..data.cells_h.len() {
2133            let (x, y, w, h) = builder.child_geometry(idx);
2134            generator.add(x, y, w, h);
2135        }
2136    } else {
2137        let count = data.cells_h.len();
2138        let mut geom = alloc::vec![(0 as Coord, 0 as Coord, 0 as Coord, 0 as Coord); count];
2139        for taffy_idx in 0..count {
2140            let orig_idx = builder.original_index(taffy_idx);
2141            geom[orig_idx] = builder.child_geometry(taffy_idx);
2142        }
2143        let mut generator = FlexboxLayoutCacheGenerator::new(&repeater_indices, &mut result);
2144        for (x, y, w, h) in geom {
2145            generator.add(x, y, w, h);
2146        }
2147    }
2148
2149    result
2150}
2151
2152/// The flex's natural single-line (no-wrap) main-axis size: the size it
2153/// occupies when all items sit on one line. A perpendicular parent uses this
2154/// to give a non-stretch wrapping-flex cell its natural size (and only wrap
2155/// when the available cross size is smaller), instead of the compact
2156/// `sqrt`-area "square" that [`flexbox_layout_info_main_axis`] reports as
2157/// `preferred`.
2158pub fn flexbox_layout_unwrapped_main(
2159    cells: Slice<LayoutItemInfo>,
2160    spacing: Coord,
2161    padding: &Padding,
2162) -> Coord {
2163    let extra_pad = padding.begin + padding.end;
2164    if cells.is_empty() {
2165        return extra_pad;
2166    }
2167    let num_spacings = cells.len().saturating_sub(1) as Coord;
2168    cells.iter().map(|c| c.constraint.preferred_bounded()).sum::<Coord>()
2169        + spacing * num_spacings
2170        + extra_pad
2171}
2172
2173/// Return main-axis LayoutInfo for a FlexboxLayout.
2174/// Only needs the same-axis cells, avoiding a cross-axis binding loop.
2175/// The reported `max` is always unbounded, even when every item is max-capped:
2176/// unlike `box_layout_info`, wrapping makes a sum-of-maxes cap ill-defined.
2177pub fn flexbox_layout_info_main_axis(
2178    cells: Slice<LayoutItemInfo>,
2179    spacing: Coord,
2180    padding: &Padding,
2181    flex_wrap: FlexboxLayoutWrap,
2182) -> LayoutInfo {
2183    let extra_pad = padding.begin + padding.end;
2184    if cells.is_empty() {
2185        return LayoutInfo {
2186            min: extra_pad,
2187            preferred: extra_pad,
2188            max: extra_pad,
2189            ..Default::default()
2190        };
2191    }
2192    let num_spacings = cells.len().saturating_sub(1) as Coord;
2193    let min = if matches!(flex_wrap, FlexboxLayoutWrap::NoWrap) {
2194        cells.iter().map(|c| c.constraint.min).sum::<Coord>() + spacing * num_spacings + extra_pad
2195    } else {
2196        // Wrapping: the widest single item must fit
2197        cells.iter().map(|c| c.constraint.min).fold(0.0 as Coord, |a, b| a.max(b)) + extra_pad
2198    };
2199    let preferred = if matches!(flex_wrap, FlexboxLayoutWrap::NoWrap) {
2200        // No wrapping: all items on one line
2201        flexbox_layout_unwrapped_main(cells, spacing, padding)
2202    } else {
2203        // Wrapping: aim for a roughly square (pixel-area) arrangement, using only
2204        // main-axis sizes so this stays independent of the cross axis. The square
2205        // side is the sqrt of the total area; each item is approximated as a
2206        // (size + spacing) square, so the gaps count toward the area and the grid
2207        // stays roughly square as spacing grows (otherwise a spacing-blind target
2208        // is reached with fewer items, skewing the grid taller). Snap that up to a
2209        // whole number of items, so a line holds a clean grid row instead of
2210        // wrapping mid-item (which would over-count columns: e.g. 3 equal items
2211        // want `A B / C`, not `A B C`).
2212        // Accumulate the area in f64: with the integer Coord build, Coord-typed
2213        // products (and their sum) would overflow for large items.
2214        let total_area: f64 = cells
2215            .iter()
2216            .map(|c| c.constraint.preferred_bounded() as f64 + spacing as f64)
2217            .map(|w| w * w)
2218            .sum();
2219        let target = Float::sqrt(total_area as f32) as Coord;
2220        let mut acc = 0 as Coord;
2221        let mut started = false;
2222        for c in cells.iter() {
2223            // taffy breaks the lines on the hypothetical size (the preferred size,
2224            // before any growing), so the line fitting has to measure the items
2225            // the same way.
2226            let size = c.constraint.preferred_bounded();
2227            acc += if started { spacing + size } else { size };
2228            started = true;
2229            // `acc` is the real row width (no trailing gap), but `target` budgets
2230            // a gap per item, so add one back before comparing — else a row grabs
2231            // one item too many near an integer sqrt.
2232            if acc + spacing >= target {
2233                break;
2234            }
2235        }
2236        acc + extra_pad
2237    };
2238    let stretch = cells.iter().map(|c| c.constraint.stretch).sum::<f32>();
2239    LayoutInfo {
2240        min,
2241        max: Coord::MAX,
2242        min_percent: 0 as _,
2243        max_percent: 100 as _,
2244        preferred,
2245        stretch,
2246    }
2247}
2248
2249/// Return cross-axis LayoutInfo for a FlexboxLayout.
2250///
2251/// The minimum and the preferred cross size are two different measurements of
2252/// the same items, so this runs the flex algorithm twice: once with every item
2253/// at its min constraint, once at its preferred size.
2254///
2255/// `constraint_size` is the main-axis container dimension (width for row,
2256/// height for column). When valid (> 0 and < MAX), it's used as the taffy
2257/// constraint for accurate wrapping. When invalid (e.g. 0, negative, or
2258/// MAX — which can happen due to circular dependencies in nested
2259/// perpendicular flexboxes), falls back to a heuristic based on
2260/// `flexbox_layout_info_main_axis`.
2261#[allow(clippy::too_many_arguments)]
2262pub fn flexbox_layout_info_cross_axis(
2263    cells_h: Slice<LayoutItemInfo>,
2264    cells_v: Slice<LayoutItemInfo>,
2265    flex_props: Slice<FlexItemProps>,
2266    spacing_h: Coord,
2267    spacing_v: Coord,
2268    padding_h: &Padding,
2269    padding_v: &Padding,
2270    direction: FlexboxLayoutDirection,
2271    alignment: LayoutAlignment,
2272    flex_wrap: FlexboxLayoutWrap,
2273    constraint_size: Coord,
2274) -> LayoutInfo {
2275    flexbox_layout_info_cross_axis_with_measure(
2276        cells_h,
2277        cells_v,
2278        flex_props,
2279        spacing_h,
2280        spacing_v,
2281        padding_h,
2282        padding_v,
2283        direction,
2284        alignment,
2285        flex_wrap,
2286        constraint_size,
2287        None,
2288    )
2289}
2290
2291/// Same as [`flexbox_layout_info_cross_axis`], with a measure callback so
2292/// height-for-width cells (e.g. a nested wrapping flexbox) are measured at the
2293/// main-axis size taffy actually assigns them, not at the pre-computed cell
2294/// size (which was measured at the container width).
2295///
2296/// `alignment` is the container's `alignment`: under `stretch` the solve grows
2297/// the cells along the main axis, which changes a height-for-width cell's
2298/// cross size, so this measurement must grow them the same way.
2299#[allow(clippy::too_many_arguments)]
2300pub fn flexbox_layout_info_cross_axis_with_measure(
2301    cells_h: Slice<LayoutItemInfo>,
2302    cells_v: Slice<LayoutItemInfo>,
2303    flex_props: Slice<FlexItemProps>,
2304    spacing_h: Coord,
2305    spacing_v: Coord,
2306    padding_h: &Padding,
2307    padding_v: &Padding,
2308    direction: FlexboxLayoutDirection,
2309    alignment: LayoutAlignment,
2310    flex_wrap: FlexboxLayoutWrap,
2311    constraint_size: Coord,
2312    measure: FlexboxMeasureFn<'_>,
2313) -> LayoutInfo {
2314    debug_assert_eq!(cells_h.len(), cells_v.len());
2315    debug_assert_eq!(cells_h.len(), flex_props.len());
2316    if cells_h.is_empty() {
2317        assert!(cells_v.is_empty());
2318        let orientation = match direction {
2319            FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => {
2320                Orientation::Vertical
2321            }
2322            FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => {
2323                Orientation::Horizontal
2324            }
2325        };
2326        let padding = match orientation {
2327            Orientation::Horizontal => padding_h,
2328            Orientation::Vertical => padding_v,
2329        };
2330        let pad = padding.begin + padding.end;
2331        return LayoutInfo { min: pad, preferred: pad, max: pad, ..Default::default() };
2332    }
2333
2334    // Determine which axis is cross
2335    let cross_cells = match direction {
2336        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => &cells_v,
2337        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => &cells_h,
2338    };
2339
2340    // Compute the main-axis preferred size to use as the constraint for taffy,
2341    // using the same heuristic as flexbox_layout_info_main_axis.
2342    let (main_cells, main_spacing, main_padding) = match direction {
2343        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => {
2344            (&cells_h, spacing_h, padding_h)
2345        }
2346        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => {
2347            (&cells_v, spacing_v, padding_v)
2348        }
2349    };
2350    let main_extra_pad = main_padding.begin + main_padding.end;
2351    let main_axis_constraint = if constraint_size > 0 as Coord && constraint_size < Coord::MAX {
2352        // Use the actual container main-axis dimension (accurate)
2353        constraint_size
2354    } else if matches!(flex_wrap, FlexboxLayoutWrap::NoWrap) || constraint_size >= Coord::MAX {
2355        // No-wrap mode, or caller signalled "unconstrained" via MAX
2356        // (used when no real main-axis dimension is in scope, e.g.
2357        // a nested perpendicular flex queried via vtable): treat the
2358        // main axis as unbounded so items don't wrap. This gives the
2359        // natural max-cell-cross-axis result rather than the
2360        // sqrt(item-areas) heuristic.
2361        Coord::MAX
2362    } else {
2363        // Use actual item areas (main * cross) for the heuristic, since both
2364        // axes' cells are available here (unlike flexbox_layout_info_main_axis).
2365        // Accumulate in f64: with the integer Coord build, Coord-typed products
2366        // (and their sum) would overflow for large items.
2367        let total_area: f64 = main_cells
2368            .iter()
2369            .zip(cross_cells.iter())
2370            .map(|(m, c)| {
2371                m.constraint.preferred_bounded() as f64 * c.constraint.preferred_bounded() as f64
2372            })
2373            .sum();
2374        let count = main_cells.len();
2375        Float::sqrt(total_area as f32) as Coord
2376            + main_spacing * (count - 1) as Coord
2377            + main_extra_pad
2378    };
2379
2380    let taffy_direction = match direction {
2381        FlexboxLayoutDirection::Row => flexbox_taffy::TaffyFlexDirection::Row,
2382        FlexboxLayoutDirection::RowReverse => flexbox_taffy::TaffyFlexDirection::RowReverse,
2383        FlexboxLayoutDirection::Column => flexbox_taffy::TaffyFlexDirection::Column,
2384        FlexboxLayoutDirection::ColumnReverse => flexbox_taffy::TaffyFlexDirection::ColumnReverse,
2385    };
2386
2387    let (container_width, container_height) = match direction {
2388        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => {
2389            (Some(main_axis_constraint), None)
2390        }
2391        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => {
2392            (None, Some(main_axis_constraint))
2393        }
2394    };
2395
2396    let params = |cross_axis_sizing| flexbox_taffy::FlexboxLayoutParams {
2397        cells_h: &cells_h,
2398        cells_v: &cells_v,
2399        flex_props: &flex_props,
2400        spacing_h,
2401        spacing_v,
2402        padding_h,
2403        padding_v,
2404        alignment,
2405        cross_axis_line_alignment: LayoutAlignment::Stretch,
2406        cross_axis_alignment: CrossAxisAlignment::Stretch,
2407        flex_wrap,
2408        flex_shrink: 1.,
2409        flex_direction: taffy_direction,
2410        container_width,
2411        container_height,
2412        cross_axis_sizing,
2413    };
2414
2415    let (available_width, available_height) = match direction {
2416        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => {
2417            (main_axis_constraint, Coord::MAX)
2418        }
2419        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => {
2420            (Coord::MAX, main_axis_constraint)
2421        }
2422    };
2423
2424    let cross_of = |(width, height): (Coord, Coord)| match direction {
2425        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => height,
2426        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => width,
2427    };
2428
2429    let mut builder =
2430        flexbox_taffy::FlexboxTaffyBuilder::new(params(flexbox_taffy::CrossAxisSizing::Minimum));
2431    // Report nothing, so every item falls back to its min constraint.
2432    let mut zero = zero_measure;
2433    builder.compute_layout(available_width, available_height, &mut zero);
2434    let cross_size = cross_of(builder.container_size());
2435    // The pass above resolved every `auto` cross size to the item's minimum.
2436    // Measure again, which resolves `auto` to the item's preferred size
2437    // instead, and sums the flex lines when the items wrap.
2438    let preferred = {
2439        let mut builder = flexbox_taffy::FlexboxTaffyBuilder::new(params(
2440            flexbox_taffy::CrossAxisSizing::FromMeasure,
2441        ));
2442        let mut resolved = resolve_measure_defaults(&cells_h, &cells_v, measure);
2443        builder.compute_layout(available_width, available_height, &mut resolved);
2444        cross_of(builder.container_size())
2445    };
2446
2447    LayoutInfo {
2448        min: cross_size,
2449        max: Coord::MAX,
2450        min_percent: 0 as _,
2451        max_percent: 100 as _,
2452        preferred,
2453        stretch: 0.0,
2454    }
2455}
2456
2457#[cfg(feature = "ffi")]
2458pub(crate) mod ffi {
2459    #![allow(unsafe_code)]
2460
2461    use super::*;
2462
2463    #[unsafe(no_mangle)]
2464    pub extern "C" fn slint_organize_grid_layout(
2465        input_data: Slice<GridLayoutInputData>,
2466        repeater_indices: Slice<u32>,
2467        repeater_steps: Slice<u32>,
2468        result: &mut GridLayoutOrganizedData,
2469    ) {
2470        *result = super::organize_grid_layout(input_data, repeater_indices, repeater_steps);
2471    }
2472
2473    #[unsafe(no_mangle)]
2474    pub extern "C" fn slint_organize_dialog_button_layout(
2475        input_data: Slice<GridLayoutInputData>,
2476        dialog_button_roles: Slice<DialogButtonRole>,
2477        result: &mut GridLayoutOrganizedData,
2478    ) {
2479        *result = super::organize_dialog_button_layout(input_data, dialog_button_roles);
2480    }
2481
2482    #[unsafe(no_mangle)]
2483    pub extern "C" fn slint_solve_grid_layout(
2484        data: &GridLayoutData,
2485        constraints: Slice<LayoutItemInfo>,
2486        orientation: Orientation,
2487        repeater_indices: Slice<u32>,
2488        repeater_steps: Slice<u32>,
2489        result: &mut SharedVector<Coord>,
2490    ) {
2491        *result = super::solve_grid_layout(
2492            data,
2493            constraints,
2494            orientation,
2495            repeater_indices,
2496            repeater_steps,
2497        )
2498    }
2499
2500    #[unsafe(no_mangle)]
2501    pub extern "C" fn slint_grid_layout_info(
2502        organized_data: &GridLayoutOrganizedData,
2503        constraints: Slice<LayoutItemInfo>,
2504        repeater_indices: Slice<u32>,
2505        repeater_steps: Slice<u32>,
2506        spacing: Coord,
2507        padding: &Padding,
2508        orientation: Orientation,
2509    ) -> LayoutInfo {
2510        super::grid_layout_info(
2511            organized_data.clone(),
2512            constraints,
2513            repeater_indices,
2514            repeater_steps,
2515            spacing,
2516            padding,
2517            orientation,
2518        )
2519    }
2520
2521    #[unsafe(no_mangle)]
2522    pub extern "C" fn slint_solve_box_layout(
2523        data: &BoxLayoutData,
2524        repeater_indices: Slice<u32>,
2525        result: &mut SharedVector<Coord>,
2526    ) {
2527        *result = super::solve_box_layout(data, repeater_indices)
2528    }
2529
2530    #[unsafe(no_mangle)]
2531    pub extern "C" fn slint_solve_box_layout_ortho(
2532        data: &BoxLayoutOrthoData,
2533        repeater_indices: Slice<u32>,
2534        result: &mut SharedVector<Coord>,
2535    ) {
2536        *result = super::solve_box_layout_ortho(data, repeater_indices)
2537    }
2538
2539    #[unsafe(no_mangle)]
2540    /// Return the LayoutInfo for a BoxLayout with the given cells.
2541    pub extern "C" fn slint_box_layout_info(
2542        cells: Slice<LayoutItemInfo>,
2543        spacing: Coord,
2544        padding: &Padding,
2545        alignment: LayoutAlignment,
2546    ) -> LayoutInfo {
2547        super::box_layout_info(cells, spacing, padding, alignment)
2548    }
2549
2550    #[unsafe(no_mangle)]
2551    /// Return the LayoutInfo for a BoxLayout with the given cells.
2552    pub extern "C" fn slint_box_layout_info_ortho(
2553        cells: Slice<LayoutItemInfo>,
2554        padding: &Padding,
2555    ) -> LayoutInfo {
2556        super::box_layout_info_ortho(cells, padding)
2557    }
2558
2559    /// The measure callback for C FFI. Returns (width, height) via out pointers.
2560    /// A dimension taffy has not determined arrives pre-resolved to the cell's
2561    /// preferred size, so both are concrete.
2562    /// A null function pointer means no measure callback.
2563    pub type FlexboxMeasureFnC = unsafe extern "C" fn(
2564        user_data: *mut core::ffi::c_void,
2565        child_index: usize,
2566        width: Coord,
2567        height: Coord,
2568        out_width: *mut Coord,
2569        out_height: *mut Coord,
2570    );
2571
2572    /// Turn a C measure callback (nullable fn pointer + user data) into the
2573    /// closure form used internally.
2574    ///
2575    /// # Safety
2576    /// `measure_fn`, when non-null, must be a valid `FlexboxMeasureFnC`
2577    /// function pointer, passed as `*const c_void` because cbindgen can't
2578    /// represent `Option<fn pointer>` in C++.
2579    unsafe fn measure_closure_from_c(
2580        measure_fn: *const core::ffi::c_void,
2581        measure_user_data: *mut core::ffi::c_void,
2582    ) -> Option<impl FnMut(usize, Coord, Coord) -> (Coord, Coord)> {
2583        const {
2584            assert!(
2585                core::mem::size_of::<*const core::ffi::c_void>()
2586                    == core::mem::size_of::<FlexboxMeasureFnC>()
2587            );
2588        }
2589        if measure_fn.is_null() {
2590            return None;
2591        }
2592        let c_measure = unsafe {
2593            core::mem::transmute::<*const core::ffi::c_void, FlexboxMeasureFnC>(measure_fn)
2594        };
2595        Some(move |child_index: usize, w: Coord, h: Coord| {
2596            let mut out_w: Coord = 0 as _;
2597            let mut out_h: Coord = 0 as _;
2598            // Safety: c_measure is a valid function pointer provided by the caller,
2599            // and out_w/out_h are valid mutable pointers.
2600            unsafe {
2601                c_measure(measure_user_data, child_index, w, h, &mut out_w, &mut out_h);
2602            }
2603            (out_w, out_h)
2604        })
2605    }
2606
2607    #[unsafe(no_mangle)]
2608    pub extern "C" fn slint_solve_flexbox_layout(
2609        data: &FlexboxLayoutData,
2610        repeater_indices: Slice<u32>,
2611        result: &mut SharedVector<Coord>,
2612        measure_fn: *const core::ffi::c_void,
2613        measure_user_data: *mut core::ffi::c_void,
2614    ) {
2615        // Safety: the caller guarantees `measure_fn` is a valid `FlexboxMeasureFnC`
2616        // when non-null (see `measure_closure_from_c`).
2617        let measure = unsafe { measure_closure_from_c(measure_fn, measure_user_data) };
2618        if let Some(mut measure) = measure {
2619            *result = super::solve_flexbox_layout_with_measure(
2620                data,
2621                repeater_indices,
2622                Some(&mut measure),
2623            );
2624        } else {
2625            *result = super::solve_flexbox_layout(data, repeater_indices);
2626        }
2627    }
2628
2629    #[unsafe(no_mangle)]
2630    /// Return main-axis LayoutInfo for a FlexboxLayout (single-axis, no cross-axis dependency).
2631    pub extern "C" fn slint_flexbox_layout_info_main_axis(
2632        cells: Slice<LayoutItemInfo>,
2633        spacing: Coord,
2634        padding: &Padding,
2635        flex_wrap: FlexboxLayoutWrap,
2636    ) -> LayoutInfo {
2637        super::flexbox_layout_info_main_axis(cells, spacing, padding, flex_wrap)
2638    }
2639
2640    #[unsafe(no_mangle)]
2641    /// Return the flex's natural single-line (no-wrap) main-axis size.
2642    pub extern "C" fn slint_flexbox_layout_unwrapped_main(
2643        cells: Slice<LayoutItemInfo>,
2644        spacing: Coord,
2645        padding: &Padding,
2646    ) -> Coord {
2647        super::flexbox_layout_unwrapped_main(cells, spacing, padding)
2648    }
2649
2650    #[unsafe(no_mangle)]
2651    /// Return cross-axis LayoutInfo for a FlexboxLayout.
2652    pub extern "C" fn slint_flexbox_layout_info_cross_axis(
2653        cells_h: Slice<LayoutItemInfo>,
2654        cells_v: Slice<LayoutItemInfo>,
2655        flex_props: Slice<FlexItemProps>,
2656        spacing_h: Coord,
2657        spacing_v: Coord,
2658        padding_h: &Padding,
2659        padding_v: &Padding,
2660        direction: FlexboxLayoutDirection,
2661        alignment: LayoutAlignment,
2662        flex_wrap: FlexboxLayoutWrap,
2663        constraint_size: Coord,
2664    ) -> LayoutInfo {
2665        super::flexbox_layout_info_cross_axis(
2666            cells_h,
2667            cells_v,
2668            flex_props,
2669            spacing_h,
2670            spacing_v,
2671            padding_h,
2672            padding_v,
2673            direction,
2674            alignment,
2675            flex_wrap,
2676            constraint_size,
2677        )
2678    }
2679
2680    #[unsafe(no_mangle)]
2681    /// Like `slint_flexbox_layout_info_cross_axis`, with a measure callback so
2682    /// height-for-width cells are re-measured at the size taffy assigns them.
2683    pub extern "C" fn slint_flexbox_layout_info_cross_axis_with_measure(
2684        cells_h: Slice<LayoutItemInfo>,
2685        cells_v: Slice<LayoutItemInfo>,
2686        flex_props: Slice<FlexItemProps>,
2687        spacing_h: Coord,
2688        spacing_v: Coord,
2689        padding_h: &Padding,
2690        padding_v: &Padding,
2691        direction: FlexboxLayoutDirection,
2692        alignment: LayoutAlignment,
2693        flex_wrap: FlexboxLayoutWrap,
2694        constraint_size: Coord,
2695        measure_fn: *const core::ffi::c_void,
2696        measure_user_data: *mut core::ffi::c_void,
2697    ) -> LayoutInfo {
2698        // Safety: the caller guarantees `measure_fn` is a valid `FlexboxMeasureFnC`
2699        // when non-null (see `measure_closure_from_c`).
2700        let mut measure = unsafe { measure_closure_from_c(measure_fn, measure_user_data) };
2701        super::flexbox_layout_info_cross_axis_with_measure(
2702            cells_h,
2703            cells_v,
2704            flex_props,
2705            spacing_h,
2706            spacing_v,
2707            padding_h,
2708            padding_v,
2709            direction,
2710            alignment,
2711            flex_wrap,
2712            constraint_size,
2713            measure.as_mut().map(|m| m as _),
2714        )
2715    }
2716}
2717
2718#[cfg(test)]
2719mod tests {
2720    use super::*;
2721
2722    fn collect_from_organized_data(
2723        organized_data: &GridLayoutOrganizedData,
2724        num_cells: usize,
2725        repeater_indices: Slice<u32>,
2726        repeater_steps: Slice<u32>,
2727    ) -> Vec<(u16, u16, u16, u16)> {
2728        let mut result = Vec::new();
2729        for i in 0..num_cells {
2730            let col_and_span = organized_data.col_or_row_and_span(
2731                i,
2732                Orientation::Horizontal,
2733                &repeater_indices,
2734                &repeater_steps,
2735            );
2736            let row_and_span = organized_data.col_or_row_and_span(
2737                i,
2738                Orientation::Vertical,
2739                &repeater_indices,
2740                &repeater_steps,
2741            );
2742            result.push((col_and_span.0, col_and_span.1, row_and_span.0, row_and_span.1));
2743        }
2744        result
2745    }
2746
2747    #[test]
2748    fn test_organized_data_generator_2_fixed_cells() {
2749        // 2 fixed cells
2750        let mut result = GridLayoutOrganizedData::default();
2751        let num_cells = 2;
2752        let mut generator = OrganizedDataGenerator::new(&[], &[], num_cells, 0, 0, &mut result);
2753        generator.add(0, 1, 0, 1);
2754        generator.add(1, 2, 0, 3);
2755        assert_eq!(result.as_slice(), &[0, 1, 0, 1, 1, 2, 0, 3]);
2756
2757        let repeater_indices = Slice::from_slice(&[]);
2758        let empty_steps = Slice::from_slice(&[]);
2759        let collected_data =
2760            collect_from_organized_data(&result, num_cells, repeater_indices, empty_steps);
2761        assert_eq!(collected_data.as_slice(), &[(0, 1, 0, 1), (1, 2, 0, 3)]);
2762
2763        assert_eq!(
2764            result.max_value(num_cells, Orientation::Horizontal, &repeater_indices, &empty_steps),
2765            3
2766        );
2767        assert_eq!(
2768            result.max_value(num_cells, Orientation::Vertical, &repeater_indices, &empty_steps),
2769            3
2770        );
2771    }
2772
2773    #[test]
2774    fn test_organized_data_generator_1_fixed_cell_1_repeater() {
2775        // 4 cells: 1 fixed cell, 1 repeater with 3 repeated cells
2776        let mut result = GridLayoutOrganizedData::default();
2777        let num_cells = 4;
2778        let repeater_indices = &[1u32, 3u32];
2779        let mut generator =
2780            OrganizedDataGenerator::new(repeater_indices, &[], 1, 1, 3, &mut result);
2781        generator.add(0, 1, 0, 2); // fixed
2782        generator.add(1, 2, 1, 3); // repeated
2783        generator.add(1, 1, 2, 4);
2784        generator.add(2, 2, 3, 5);
2785        assert_eq!(
2786            result.as_slice(),
2787            &[
2788                0, 1, 0, 2, // fixed cell
2789                8, 4, 0, 0, // jump cell: data_base=8, stride=4 (step=1, epi=4)
2790                1, 2, 1, 3, // repeated cell 1
2791                1, 1, 2, 4, // repeated cell 2
2792                2, 2, 3, 5, // repeated cell 3
2793            ]
2794        );
2795        let repeater_indices = Slice::from_slice(repeater_indices);
2796        let empty_steps = Slice::from_slice(&[]);
2797        let collected_data =
2798            collect_from_organized_data(&result, num_cells, repeater_indices, empty_steps);
2799        assert_eq!(
2800            collected_data.as_slice(),
2801            &[(0, 1, 0, 2), (1, 2, 1, 3), (1, 1, 2, 4), (2, 2, 3, 5)]
2802        );
2803
2804        assert_eq!(
2805            result.max_value(num_cells, Orientation::Horizontal, &repeater_indices, &empty_steps),
2806            4
2807        );
2808        assert_eq!(
2809            result.max_value(num_cells, Orientation::Vertical, &repeater_indices, &empty_steps),
2810            8
2811        );
2812    }
2813
2814    #[test]
2815
2816    fn test_organize_data_with_auto_and_spans() {
2817        let auto = i_slint_common::ROW_COL_AUTO;
2818        let input = std::vec![
2819            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 2., rowspan: -1. },
2820            GridLayoutInputData { new_row: false, col: auto, row: auto, colspan: 1., rowspan: 2. },
2821            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 2., rowspan: 1. },
2822            GridLayoutInputData { new_row: true, col: -2., row: 80000., colspan: 2., rowspan: 1. },
2823        ];
2824        let repeater_indices = Slice::from_slice(&[]);
2825        let (organized_data, errors) = organize_grid_layout_impl(
2826            Slice::from_slice(&input),
2827            repeater_indices,
2828            Slice::from_slice(&[]),
2829        );
2830        assert_eq!(
2831            organized_data.as_slice(),
2832            &[
2833                0, 2, 0, 0, // row 0, col 0, rowspan 0 (see below)
2834                2, 1, 0, 2, // row 0, col 2 (due to colspan of first cell)
2835                0, 2, 1, 1, // row 1, col 0
2836                0, 2, 65535, 1, // row 65535, col 0
2837            ]
2838        );
2839        assert_eq!(errors.len(), 3);
2840        // Note that a rowspan of 0 is valid, it means the cell doesn't occupy any row
2841        assert_eq!(errors[0], "cell rowspan -1 is negative, clamping to 0");
2842        assert_eq!(errors[1], "cell row 80000 is too large, clamping to 65535");
2843        assert_eq!(errors[2], "cell col -2 is negative, clamping to 0");
2844        let empty_steps = Slice::from_slice(&[]);
2845        let collected_data = collect_from_organized_data(
2846            &organized_data,
2847            input.len(),
2848            repeater_indices,
2849            empty_steps,
2850        );
2851        assert_eq!(
2852            collected_data.as_slice(),
2853            &[(0, 2, 0, 0), (2, 1, 0, 2), (0, 2, 1, 1), (0, 2, 65535, 1)]
2854        );
2855        assert_eq!(
2856            organized_data.max_value(3, Orientation::Horizontal, &repeater_indices, &empty_steps),
2857            3
2858        );
2859        assert_eq!(
2860            organized_data.max_value(3, Orientation::Vertical, &repeater_indices, &empty_steps),
2861            2
2862        );
2863    }
2864
2865    #[test]
2866    fn test_organize_data_1_empty_repeater() {
2867        // Row { Text {}    if false: Text {} }, this test shows why we need i32 for cell_nr_adj
2868        let auto = i_slint_common::ROW_COL_AUTO;
2869        let cell =
2870            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 1., rowspan: 1. };
2871        let input = std::vec![cell];
2872        let repeater_indices = Slice::from_slice(&[1u32, 0u32]);
2873        let (organized_data, errors) = organize_grid_layout_impl(
2874            Slice::from_slice(&input),
2875            repeater_indices,
2876            Slice::from_slice(&[]),
2877        );
2878        assert_eq!(
2879            organized_data.as_slice(),
2880            &[
2881                0, 1, 0, 1, // fixed
2882                0, 0, 0, 0
2883            ] // jump to repeater data (not used)
2884        );
2885        assert_eq!(errors.len(), 0);
2886        let empty_steps = Slice::from_slice(&[]);
2887        let collected_data = collect_from_organized_data(
2888            &organized_data,
2889            input.len(),
2890            repeater_indices,
2891            empty_steps,
2892        );
2893        assert_eq!(collected_data.as_slice(), &[(0, 1, 0, 1)]);
2894        assert_eq!(
2895            organized_data.max_value(1, Orientation::Horizontal, &repeater_indices, &empty_steps),
2896            1
2897        );
2898    }
2899
2900    #[test]
2901    fn test_organize_data_4_repeaters() {
2902        let auto = i_slint_common::ROW_COL_AUTO;
2903        let mut cell =
2904            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 1., rowspan: 1. };
2905        let mut input = std::vec![cell.clone()];
2906        for _ in 0..8 {
2907            cell.new_row = false;
2908            input.push(cell.clone());
2909        }
2910        let repeater_indices = Slice::from_slice(&[0u32, 0u32, 1u32, 4u32, 6u32, 2u32, 8u32, 0u32]);
2911        let (organized_data, errors) = organize_grid_layout_impl(
2912            Slice::from_slice(&input),
2913            repeater_indices,
2914            Slice::from_slice(&[]),
2915        );
2916        assert_eq!(
2917            organized_data.as_slice(),
2918            &[
2919                28, 4, 0, 0, // rep0 jump: data at 28, stride=4 (empty)
2920                0, 1, 0, 1, // fixed cell (col=0)
2921                28, 4, 0, 0, // rep1 jump: data at 28, stride=4 (4 rows)
2922                5, 1, 0, 1, // fixed cell (col=5)
2923                44, 4, 0, 0, // rep2 jump: data at 44, stride=4 (2 rows)
2924                52, 4, 0, 0, // rep3 jump: data at 52, stride=4 (empty)
2925                8, 1, 0, 1, // fixed cell (col=8)
2926                1, 1, 0, 1, // rep1 row 0
2927                2, 1, 0, 1, // rep1 row 1
2928                3, 1, 0, 1, // rep1 row 2
2929                4, 1, 0, 1, // rep1 row 3
2930                6, 1, 0, 1, // rep2 row 0
2931                7, 1, 0, 1, // rep2 row 1
2932            ]
2933        );
2934        assert_eq!(errors.len(), 0);
2935        let empty_steps = Slice::from_slice(&[]);
2936        let collected_data = collect_from_organized_data(
2937            &organized_data,
2938            input.len(),
2939            repeater_indices,
2940            empty_steps,
2941        );
2942        assert_eq!(
2943            collected_data.as_slice(),
2944            &[
2945                (0, 1, 0, 1),
2946                (1, 1, 0, 1),
2947                (2, 1, 0, 1),
2948                (3, 1, 0, 1),
2949                (4, 1, 0, 1),
2950                (5, 1, 0, 1),
2951                (6, 1, 0, 1),
2952                (7, 1, 0, 1),
2953                (8, 1, 0, 1),
2954            ]
2955        );
2956        let empty_steps = Slice::from_slice(&[]);
2957        assert_eq!(
2958            organized_data.max_value(
2959                input.len(),
2960                Orientation::Horizontal,
2961                &repeater_indices,
2962                &empty_steps
2963            ),
2964            9
2965        );
2966    }
2967
2968    #[test]
2969    fn test_organize_data_repeated_rows() {
2970        let auto = i_slint_common::ROW_COL_AUTO;
2971        let mut input = Vec::new();
2972        let num_rows: u32 = 3;
2973        let num_columns: u32 = 2;
2974        // 3 rows of 2 columns each
2975        for _ in 0..num_rows {
2976            let mut cell = GridLayoutInputData {
2977                new_row: true,
2978                col: auto,
2979                row: auto,
2980                colspan: 1.,
2981                rowspan: 1.,
2982            };
2983            input.push(cell.clone());
2984            cell.new_row = false;
2985            input.push(cell.clone());
2986        }
2987        // Repeater 0: starts at index 0, has 3 instances of 2 elements
2988        let repeater_indices_arr = [0_u32, num_rows];
2989        let repeater_steps_arr = [num_columns];
2990        let repeater_steps = Slice::from_slice(&repeater_steps_arr);
2991        let repeater_indices = Slice::from_slice(&repeater_indices_arr);
2992        let (organized_data, errors) =
2993            organize_grid_layout_impl(Slice::from_slice(&input), repeater_indices, repeater_steps);
2994        assert_eq!(
2995            organized_data.as_slice(),
2996            &[
2997                4, 8, 0, 0, // jump cell: data at u16 idx 4, stride=8 (=step*4=2*4)
2998                0, 1, 0, 1, 1, 1, 0, 1, // row 0: col 0, col 1
2999                0, 1, 1, 1, 1, 1, 1, 1, // row 1: col 0, col 1
3000                0, 1, 2, 1, 1, 1, 2, 1, // row 2: col 0, col 1
3001            ]
3002        );
3003        assert_eq!(errors.len(), 0);
3004        let collected_data = collect_from_organized_data(
3005            &organized_data,
3006            input.len(),
3007            repeater_indices,
3008            repeater_steps,
3009        );
3010        assert_eq!(
3011            collected_data.as_slice(),
3012            // (col, colspan, row, rowspan) for each cell in input order
3013            &[(0, 1, 0, 1), (1, 1, 0, 1), (0, 1, 1, 1), (1, 1, 1, 1), (0, 1, 2, 1), (1, 1, 2, 1),]
3014        );
3015        assert_eq!(
3016            organized_data.max_value(
3017                input.len(),
3018                Orientation::Horizontal,
3019                &repeater_indices,
3020                &repeater_steps
3021            ),
3022            2
3023        );
3024        assert_eq!(
3025            organized_data.max_value(
3026                input.len(),
3027                Orientation::Vertical,
3028                &repeater_indices,
3029                &repeater_steps
3030            ),
3031            3
3032        );
3033
3034        // Now test GridLayoutCacheGenerator
3035        let mut layout_cache_v = SharedVector::<Coord>::default();
3036        let mut generator = GridLayoutCacheGenerator::new(
3037            repeater_indices.as_slice(),
3038            repeater_steps.as_slice(),
3039            0, // static_cells
3040            1, // num_repeaters
3041            6, // total_repeated_cells (3 rows * 2 columns)
3042            &mut layout_cache_v,
3043        );
3044        // Row 0
3045        generator.add(0., 50.);
3046        generator.add(0., 50.);
3047        // Row 1
3048        generator.add(50., 50.);
3049        generator.add(50., 50.);
3050        // Row 2
3051        generator.add(100., 50.);
3052        generator.add(100., 50.);
3053        assert_eq!(
3054            layout_cache_v.as_slice(),
3055            &[
3056                2., 4., // jump cell: data at pos 2, stride=4 (=step*2=2*2)
3057                0., 50., 0., 50., // row 0
3058                50., 50., 50., 50., // row 1
3059                100., 50., 100., 50., // row 2
3060            ]
3061        );
3062
3063        // GridRepeaterCacheAccess: cache[cache[jump_index] + ri * stride + child_offset]
3064        let layout_cache_v_access = |jump_index: usize,
3065                                     repeater_index: usize,
3066                                     stride: usize,
3067                                     child_offset: usize|
3068         -> Coord {
3069            let base = layout_cache_v[jump_index] as usize;
3070            let data_idx = base + repeater_index * stride + child_offset;
3071            layout_cache_v[data_idx]
3072        };
3073        // stride=4 (step=2, entries_per_item=2)
3074        // Y pos for child 0 (child_offset=0)
3075        assert_eq!(layout_cache_v_access(0, 0, 4, 0), 0.);
3076        assert_eq!(layout_cache_v_access(0, 1, 4, 0), 50.);
3077        assert_eq!(layout_cache_v_access(0, 2, 4, 0), 100.);
3078        // Y pos for child 1 (child_offset=2)
3079        assert_eq!(layout_cache_v_access(0, 0, 4, 2), 0.);
3080        assert_eq!(layout_cache_v_access(0, 1, 4, 2), 50.);
3081        assert_eq!(layout_cache_v_access(0, 2, 4, 2), 100.);
3082    }
3083
3084    #[test]
3085    fn test_organize_data_repeated_rows_multiple_repeaters() {
3086        let auto = i_slint_common::ROW_COL_AUTO;
3087        let mut input = Vec::new();
3088        let num_rows: u32 = 5;
3089        let mut cell =
3090            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 1., rowspan: 1. };
3091        // 3 rows of 2 columns each
3092        for _ in 0..3 {
3093            cell.new_row = true;
3094            input.push(cell.clone());
3095            cell.new_row = false;
3096            input.push(cell.clone());
3097        }
3098        // 2 rows of 3 columns each
3099        for _ in 0..2 {
3100            cell.new_row = true;
3101            input.push(cell.clone());
3102            cell.new_row = false;
3103            input.push(cell.clone());
3104            cell.new_row = false;
3105            input.push(cell.clone());
3106        }
3107        // Repeater 0: starts at index 0, has 3 instances of 2 elements
3108        // Repeater 1: starts at index 6 (after repeater 0's 3*2=6 cells), has 2 instances of 3 elements
3109        let repeater_indices_arr = [0_u32, 3, 6, 2];
3110        let repeater_steps_arr = [2, 3];
3111        let repeater_steps = Slice::from_slice(&repeater_steps_arr);
3112        let repeater_indices = Slice::from_slice(&repeater_indices_arr);
3113        let (organized_data, errors) =
3114            organize_grid_layout_impl(Slice::from_slice(&input), repeater_indices, repeater_steps);
3115        assert_eq!(
3116            organized_data.as_slice(),
3117            &[
3118                8, 8, 0, 0, // repeater 0 jump: data at 8, stride=8 (=step*4=2*4)
3119                32, 12, 0, 0, // repeater 1 jump: data at 32, stride=12 (=step*4=3*4)
3120                // Repeater 0 data
3121                0, 1, 0, 1, 1, 1, 0, 1, // row 0: col 0, col 1
3122                0, 1, 1, 1, 1, 1, 1, 1, // row 1: col 0, col 1
3123                0, 1, 2, 1, 1, 1, 2, 1, // row 2: col 0, col 1
3124                // Repeater 1 data
3125                0, 1, 3, 1, 1, 1, 3, 1, 2, 1, 3, 1, // row 0: col 0, col 1, col 2
3126                0, 1, 4, 1, 1, 1, 4, 1, 2, 1, 4, 1, // row 1: col 0, col 1, col 2
3127            ]
3128        );
3129        assert_eq!(errors.len(), 0);
3130        let collected_data = collect_from_organized_data(
3131            &organized_data,
3132            input.len(),
3133            repeater_indices,
3134            repeater_steps,
3135        );
3136        assert_eq!(
3137            collected_data.as_slice(),
3138            // (col, colspan, row, rowspan) for each cell in input order
3139            &[
3140                (0, 1, 0, 1),
3141                (1, 1, 0, 1),
3142                (0, 1, 1, 1),
3143                (1, 1, 1, 1),
3144                (0, 1, 2, 1),
3145                (1, 1, 2, 1),
3146                (0, 1, 3, 1),
3147                (1, 1, 3, 1),
3148                (2, 1, 3, 1),
3149                (0, 1, 4, 1),
3150                (1, 1, 4, 1),
3151                (2, 1, 4, 1)
3152            ]
3153        );
3154        assert_eq!(
3155            organized_data.max_value(
3156                input.len(),
3157                Orientation::Horizontal,
3158                &repeater_indices,
3159                &repeater_steps
3160            ),
3161            3 // max col (2) + colspan (1) = 3
3162        );
3163        assert_eq!(
3164            organized_data.max_value(
3165                input.len(),
3166                Orientation::Vertical,
3167                &repeater_indices,
3168                &repeater_steps
3169            ),
3170            num_rows as usize // max row (4) + rowspan (1) = 5
3171        );
3172
3173        // Now test GridLayoutCacheGenerator
3174        let mut layout_cache_v = SharedVector::<Coord>::default();
3175        let mut generator = GridLayoutCacheGenerator::new(
3176            repeater_indices.as_slice(),
3177            repeater_steps.as_slice(),
3178            0,  // static_cells
3179            2,  // num_repeaters
3180            12, // total_repeated_cells (3*2 + 2*3)
3181            &mut layout_cache_v,
3182        );
3183        // Row 0
3184        generator.add(0., 50.);
3185        generator.add(0., 50.);
3186        // Row 1
3187        generator.add(50., 50.);
3188        generator.add(50., 50.);
3189        // Row 2
3190        generator.add(100., 50.);
3191        generator.add(100., 50.);
3192        // Row 3
3193        generator.add(150., 50.);
3194        generator.add(150., 50.);
3195        generator.add(150., 50.);
3196        // Row 4
3197        generator.add(200., 50.);
3198        generator.add(200., 50.);
3199        generator.add(200., 50.);
3200        assert_eq!(
3201            layout_cache_v.as_slice(),
3202            &[
3203                4., 4., // repeater 0 jump: data at pos 4, stride=4 (=step*2=2*2)
3204                16., 6., // repeater 1 jump: data at pos 16, stride=6 (=step*2=3*2)
3205                0., 50., 0., 50., // repeater 0 row 0 data
3206                50., 50., 50., 50., // repeater 0 row 1 data
3207                100., 50., 100., 50., // repeater 0 row 2 data
3208                150., 50., 150., 50., 150., 50., // repeater 1 row 3 data
3209                200., 50., 200., 50., 200., 50., // repeater 1 row 4 data
3210            ]
3211        );
3212
3213        // GridRepeaterCacheAccess: cache[cache[jump_index] + ri * stride + child_offset]
3214        let layout_cache_v_access = |jump_index: usize,
3215                                     repeater_index: usize,
3216                                     stride: usize,
3217                                     child_offset: usize|
3218         -> Coord {
3219            let base = layout_cache_v[jump_index] as usize;
3220            let data_idx = base + repeater_index * stride + child_offset;
3221            layout_cache_v[data_idx]
3222        };
3223        // Repeater 0: Y pos for child 0 (child_offset=0), stride=4
3224        assert_eq!(layout_cache_v_access(0, 0, 4, 0), 0.);
3225        assert_eq!(layout_cache_v_access(0, 1, 4, 0), 50.);
3226        assert_eq!(layout_cache_v_access(0, 2, 4, 0), 100.);
3227        // Repeater 0: Y pos for child 1 (child_offset=2), stride=4
3228        assert_eq!(layout_cache_v_access(0, 0, 4, 2), 0.);
3229        assert_eq!(layout_cache_v_access(0, 1, 4, 2), 50.);
3230        assert_eq!(layout_cache_v_access(0, 2, 4, 2), 100.);
3231        // Repeater 1: Y pos for child 0 (child_offset=0), jump at index 2, stride=6
3232        assert_eq!(layout_cache_v_access(2, 0, 6, 0), 150.);
3233        assert_eq!(layout_cache_v_access(2, 1, 6, 0), 200.);
3234        // Repeater 1: Y pos for child 2 (child_offset=4), jump at index 2, stride=6
3235        assert_eq!(layout_cache_v_access(2, 0, 6, 4), 150.);
3236        assert_eq!(layout_cache_v_access(2, 1, 6, 4), 200.);
3237    }
3238
3239    #[test]
3240    fn test_layout_cache_generator_2_fixed_cells() {
3241        // 2 fixed cells
3242        let mut result = SharedVector::<Coord>::default();
3243        result.resize(2 * 2, 0 as _);
3244        let mut generator = LayoutCacheGenerator::new(&[], &mut result);
3245        generator.add(0., 50.); // fixed
3246        generator.add(80., 50.); // fixed
3247        assert_eq!(result.as_slice(), &[0., 50., 80., 50.]);
3248    }
3249
3250    #[test]
3251    fn test_layout_cache_generator_1_fixed_cell_1_repeater() {
3252        // 4 cells: 1 fixed cell, 1 repeater with 3 repeated cells
3253        let mut result = SharedVector::<Coord>::default();
3254        let repeater_indices = &[1, 3];
3255        result.resize(4 * 2 + repeater_indices.len(), 0 as _);
3256        let mut generator = LayoutCacheGenerator::new(repeater_indices, &mut result);
3257        generator.add(0., 50.); // fixed
3258        generator.add(80., 50.); // repeated
3259        generator.add(160., 50.);
3260        generator.add(240., 50.);
3261        assert_eq!(
3262            result.as_slice(),
3263            &[
3264                0., 50., // fixed
3265                4., 5., // jump to repeater data
3266                80., 50., 160., 50., 240., 50. // repeater data
3267            ]
3268        );
3269    }
3270
3271    #[test]
3272    fn test_layout_cache_generator_4_repeaters() {
3273        // 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
3274        let mut result = SharedVector::<Coord>::default();
3275        let repeater_indices = &[1, 0, 1, 4, 6, 2, 8, 0];
3276        result.resize(8 * 2 + repeater_indices.len(), 0 as _);
3277        let mut generator = LayoutCacheGenerator::new(repeater_indices, &mut result);
3278        generator.add(0., 50.); // fixed
3279        generator.add(80., 10.); // repeated
3280        generator.add(160., 10.);
3281        generator.add(240., 10.);
3282        generator.add(320., 10.); // end of second repeater
3283        generator.add(400., 80.); // fixed
3284        generator.add(500., 20.); // repeated
3285        generator.add(600., 20.); // end of third repeater
3286        assert_eq!(
3287            result.as_slice(),
3288            &[
3289                0., 50., // fixed
3290                12., 13., // jump to first (empty) repeater (not used)
3291                12., 13., // jump to second repeater data
3292                400., 80., // fixed
3293                20., 21., // jump to third repeater data
3294                0., 0., // slot for jumping to fourth repeater (currently empty)
3295                80., 10., 160., 10., 240., 10., 320., 10., // first repeater data
3296                500., 20., 600., 20. // second repeater data
3297            ]
3298        );
3299    }
3300
3301    /// `flexbox_layout_unwrapped_main()` computes the container's max-content main
3302    /// size by hand, because the main-axis layout-info path may not read the
3303    /// cross-axis cells (that would be a binding loop). taffy computes the same
3304    /// thing from the same cells, so this pins the two together and fails the day
3305    /// the hand-rolled version drifts.
3306    ///
3307    /// Only meaningful for `no-wrap`: at max-content taffy never wraps (it reports a
3308    /// single line), while the wrapping branch of `flexbox_layout_info_main_axis()`
3309    /// deliberately reports a roughly square arrangement instead.
3310    mod max_content_matches_taffy {
3311        use super::*;
3312
3313        fn cell(preferred: Coord, stretch: f32) -> FlexboxLayoutItemInfo {
3314            FlexboxLayoutItemInfo {
3315                constraint: LayoutInfo { preferred, stretch, ..Default::default() },
3316                ..Default::default()
3317            }
3318        }
3319
3320        /// The constraint half of the bundled test cells, as the parallel
3321        /// array the runtime takes.
3322        fn constraints(cells: &[FlexboxLayoutItemInfo]) -> Vec<LayoutItemInfo> {
3323            cells
3324                .iter()
3325                .map(|c| LayoutItemInfo { constraint: c.constraint.clone(), ..Default::default() })
3326                .collect()
3327        }
3328
3329        /// Split the bundled test cells into the parallel (constraint, flex-props)
3330        /// arrays the runtime takes.
3331        fn split(cells: &[FlexboxLayoutItemInfo]) -> (Vec<LayoutItemInfo>, Vec<FlexItemProps>) {
3332            (constraints(cells), cells.iter().map(|c| c.props).collect())
3333        }
3334
3335        /// What taffy makes of the same cells, asked for its max-content main size.
3336        fn taffy_max_content_main(cells: &[FlexboxLayoutItemInfo]) -> Coord {
3337            let (main, flex) = split(cells);
3338            // The cross axis is deliberately left unconstrained: the main-axis result
3339            // must not depend on it, which is what makes the loop-free path sound.
3340            let cross: Vec<LayoutItemInfo> = cells
3341                .iter()
3342                .map(|_| LayoutItemInfo {
3343                    constraint: LayoutInfo { max: Coord::MAX, ..Default::default() },
3344                    ..Default::default()
3345                })
3346                .collect();
3347            let cells_h = Slice::from_slice(&main);
3348            let cells_v = Slice::from_slice(&cross);
3349            let flex_props = Slice::from_slice(&flex);
3350            let pad = Padding::default();
3351            let mut builder =
3352                flexbox_taffy::FlexboxTaffyBuilder::new(flexbox_taffy::FlexboxLayoutParams {
3353                    cells_h: &cells_h,
3354                    cells_v: &cells_v,
3355                    flex_props: &flex_props,
3356                    spacing_h: 0 as Coord,
3357                    spacing_v: 0 as Coord,
3358                    padding_h: &pad,
3359                    padding_v: &pad,
3360                    // Stretch: the strongest growing mode, to show the stretch
3361                    // factors cancel out of the max-content size.
3362                    alignment: LayoutAlignment::Stretch,
3363                    cross_axis_line_alignment: LayoutAlignment::Stretch,
3364                    cross_axis_alignment: CrossAxisAlignment::Stretch,
3365                    flex_wrap: FlexboxLayoutWrap::NoWrap,
3366                    flex_shrink: 1.,
3367                    flex_direction: flexbox_taffy::TaffyFlexDirection::Row,
3368                    container_width: None,
3369                    container_height: None,
3370                    cross_axis_sizing: flexbox_taffy::CrossAxisSizing::Preferred,
3371                });
3372            // A row item gets `size.width: auto`, so taffy asks the measure callback
3373            // for its content size. That callback is how Slint reports an item's
3374            // natural size, so the comparison is only fair if it answers here too.
3375            let mut measure = |idx: usize, known_w: Option<Coord>, known_h: Option<Coord>| {
3376                (
3377                    known_w.unwrap_or_else(|| cells[idx].constraint.preferred_bounded()),
3378                    known_h.unwrap_or(0 as Coord),
3379                )
3380            };
3381            builder.compute_layout(Coord::MAX, Coord::MAX, &mut measure);
3382            builder.container_size().0
3383        }
3384
3385        fn ours(cells: &[FlexboxLayoutItemInfo]) -> Coord {
3386            let main = constraints(cells);
3387            flexbox_layout_unwrapped_main(Slice::from_slice(&main), 0 as Coord, &Padding::default())
3388        }
3389
3390        #[track_caller]
3391        fn assert_agrees(name: &str, cells: &[FlexboxLayoutItemInfo]) {
3392            let (ours, theirs) = (ours(cells), taffy_max_content_main(cells));
3393            assert!((ours - theirs).abs() <= 1 as Coord, "{name}: ours={ours} taffy={theirs}");
3394        }
3395
3396        /// The grow factor (derived from the stretch factor) cancels out of
3397        /// taffy's max-content flex fraction, so an item contributes its
3398        /// content size and we must land on the same number.
3399        #[test]
3400        fn agrees() {
3401            assert_agrees("plain", &[cell(50., 0.), cell(250., 0.)]);
3402            assert_agrees("equal stretch", &[cell(50., 1.), cell(250., 1.)]);
3403            assert_agrees("uneven stretch", &[cell(60., 1.), cell(60., 3.)]);
3404            assert_agrees("fractional stretch", &[cell(60., 0.5), cell(40., 1.5)]);
3405            assert_agrees("mixed stretch and not", &[cell(50., 1.), cell(100., 0.)]);
3406            // `preferred-width: 0` with stretch collapses the max-content size
3407            // to nothing in taffy too, as it does in a HorizontalLayout.
3408            assert_agrees("zero preferred, stretchy", &[cell(0., 1.), cell(0., 1.)]);
3409        }
3410
3411        /// The same cells asked of a *column* container, where the main-axis size
3412        /// comes from the basis (the preferred height), not the measure callback.
3413        fn taffy_column_main(cells: &[FlexboxLayoutItemInfo]) -> Coord {
3414            let (main, flex) = split(cells);
3415            // For a column the main axis is vertical: main-axis sizes live in cells_v,
3416            // while the cross-axis (width) constraint is left unbounded.
3417            let h: Vec<LayoutItemInfo> = cells
3418                .iter()
3419                .map(|_| LayoutItemInfo {
3420                    constraint: LayoutInfo { max: Coord::MAX, ..Default::default() },
3421                    ..Default::default()
3422                })
3423                .collect();
3424            let cells_h = Slice::from_slice(&h);
3425            let cells_v = Slice::from_slice(&main);
3426            let flex_props = Slice::from_slice(&flex);
3427            let pad = Padding::default();
3428            let mut builder =
3429                flexbox_taffy::FlexboxTaffyBuilder::new(flexbox_taffy::FlexboxLayoutParams {
3430                    cells_h: &cells_h,
3431                    cells_v: &cells_v,
3432                    flex_props: &flex_props,
3433                    spacing_h: 0 as Coord,
3434                    spacing_v: 0 as Coord,
3435                    padding_h: &pad,
3436                    padding_v: &pad,
3437                    alignment: LayoutAlignment::Stretch,
3438                    cross_axis_line_alignment: LayoutAlignment::Stretch,
3439                    cross_axis_alignment: CrossAxisAlignment::Stretch,
3440                    flex_wrap: FlexboxLayoutWrap::NoWrap,
3441                    flex_shrink: 1.,
3442                    flex_direction: flexbox_taffy::TaffyFlexDirection::Column,
3443                    container_width: None,
3444                    container_height: None,
3445                    cross_axis_sizing: flexbox_taffy::CrossAxisSizing::Preferred,
3446                });
3447            let mut measure = |idx: usize, known_w: Option<Coord>, known_h: Option<Coord>| {
3448                (
3449                    known_w.unwrap_or(0 as Coord),
3450                    known_h.unwrap_or_else(|| cells[idx].constraint.preferred_bounded()),
3451                )
3452            };
3453            builder.compute_layout(Coord::MAX, Coord::MAX, &mut measure);
3454            builder.container_size().1
3455        }
3456
3457        #[track_caller]
3458        fn assert_agrees_column(name: &str, cells: &[FlexboxLayoutItemInfo]) {
3459            let (o, t) = (ours(cells), taffy_column_main(cells));
3460            assert!((o - t).abs() <= 1 as Coord, "{name}: ours={o} taffy={t}");
3461        }
3462
3463        /// Both axes report the sum of the preferred sizes, so the column case must
3464        /// agree just like the row case, even though taffy computes it from the
3465        /// basis there instead of the measure callback.
3466        #[test]
3467        fn agrees_for_a_column() {
3468            assert_agrees_column("plain", &[cell(50., 0.), cell(250., 0.)]);
3469            assert_agrees_column("equal stretch", &[cell(50., 1.), cell(250., 1.)]);
3470            assert_agrees_column("zero preferred, stretchy", &[cell(0., 1.), cell(0., 1.)]);
3471        }
3472    }
3473
3474    /// The taffy grow/shrink factors are derived from the container's
3475    /// `alignment` and the cells' main-axis stretch factors (see
3476    /// `FlexboxTaffyBuilder::new`), mirroring how a box layout distributes
3477    /// free space.
3478    mod stretch_driven_grow {
3479        use super::*;
3480
3481        fn info(preferred: Coord, stretch: f32) -> LayoutInfo {
3482            LayoutInfo { preferred, stretch, ..Default::default() }
3483        }
3484
3485        /// Solve a row of `constraints` in a 400px-wide container and return
3486        /// the resulting widths.
3487        fn solve_row(alignment: LayoutAlignment, constraints: &[LayoutInfo]) -> Vec<Coord> {
3488            let cells_h: Vec<LayoutItemInfo> = constraints
3489                .iter()
3490                .map(|c| LayoutItemInfo { constraint: c.clone(), ..Default::default() })
3491                .collect();
3492            let cells_v: Vec<LayoutItemInfo> =
3493                constraints.iter().map(|_| LayoutItemInfo::default()).collect();
3494            let flex_props: Vec<FlexItemProps> =
3495                constraints.iter().map(|_| FlexItemProps::default()).collect();
3496            let pad = Padding::default();
3497            let mut builder =
3498                flexbox_taffy::FlexboxTaffyBuilder::new(flexbox_taffy::FlexboxLayoutParams {
3499                    cells_h: &Slice::from_slice(&cells_h),
3500                    cells_v: &Slice::from_slice(&cells_v),
3501                    flex_props: &Slice::from_slice(&flex_props),
3502                    spacing_h: 0 as Coord,
3503                    spacing_v: 0 as Coord,
3504                    padding_h: &pad,
3505                    padding_v: &pad,
3506                    alignment,
3507                    cross_axis_line_alignment: LayoutAlignment::Stretch,
3508                    cross_axis_alignment: CrossAxisAlignment::Stretch,
3509                    flex_wrap: FlexboxLayoutWrap::Wrap,
3510                    flex_shrink: 1.,
3511                    flex_direction: flexbox_taffy::TaffyFlexDirection::Row,
3512                    container_width: Some(400 as Coord),
3513                    container_height: None,
3514                    cross_axis_sizing: flexbox_taffy::CrossAxisSizing::Preferred,
3515                });
3516            builder.compute_layout(400 as Coord, Coord::MAX, &mut zero_measure);
3517            (0..constraints.len()).map(|i| builder.child_geometry(i).2).collect()
3518        }
3519
3520        #[test]
3521        fn grows_by_stretch_only_under_alignment_stretch() {
3522            // 200 free pixels split by the stretch weights 3:1.
3523            let cells = [info(100., 3.), info(100., 1.)];
3524            assert_eq!(solve_row(LayoutAlignment::Stretch, &cells), [250., 150.]);
3525            // A zero factor next to a non-zero one stays at its preferred size.
3526            let cells = [info(100., 1.), info(100., 0.)];
3527            assert_eq!(solve_row(LayoutAlignment::Stretch, &cells), [300., 100.]);
3528            // All-zero factors split the free space evenly, like a HorizontalLayout.
3529            let cells = [info(100., 0.), info(100., 0.)];
3530            assert_eq!(solve_row(LayoutAlignment::Stretch, &cells), [200., 200.]);
3531            // Growing requires `alignment: stretch` on the container.
3532            let cells = [info(100., 3.), info(100., 1.)];
3533            assert_eq!(solve_row(LayoutAlignment::Start, &cells), [100., 100.]);
3534        }
3535
3536        /// An item alone on its line and larger than the container shrinks to
3537        /// fit whatever its stretch factor says (the constant taffy shrink
3538        /// factor of 1); only its `min` refuses shrinking.
3539        #[test]
3540        fn lone_oversized_item_shrinks_regardless_of_stretch() {
3541            let squeezable =
3542                LayoutInfo { preferred: 500., min: 200., stretch: 0., ..Default::default() };
3543            assert_eq!(solve_row(LayoutAlignment::Start, &[squeezable]), [400.]);
3544            let rigid =
3545                LayoutInfo { preferred: 500., min: 450., stretch: 0., ..Default::default() };
3546            assert_eq!(solve_row(LayoutAlignment::Start, &[rigid]), [450.]);
3547        }
3548    }
3549
3550    /// A percentage constraint must not be resolved against the `Coord::MAX`
3551    /// "unbounded" sentinel that the info path passes for a no-wrap main axis.
3552    /// `min_percent * MAX / 100` overflows the i32 build (debug panic) and gives
3553    /// infinity in the f32 build, which taffy then bakes into the item's size.
3554    /// When the container axis is unbounded a percentage has no basis, so the
3555    /// item keeps its natural (here zero) size instead.
3556    ///
3557    /// Checked at the builder because the containing info function reports only
3558    /// the *cross* size, while the overflow lands on the item's *main*-axis size;
3559    /// the f32 build hides it at the container level but not at the item.
3560    #[test]
3561    fn percentage_against_unbounded_container_leaves_item_finite() {
3562        // A single row cell with `width: 50%` (min_percent == max_percent == 50).
3563        let cells_h = [LayoutItemInfo {
3564            constraint: LayoutInfo {
3565                min_percent: 50 as Coord,
3566                max_percent: 50 as Coord,
3567                max: Coord::MAX,
3568                ..Default::default()
3569            },
3570            ..Default::default()
3571        }];
3572        let cells_v = [LayoutItemInfo {
3573            constraint: LayoutInfo {
3574                preferred: 30 as Coord,
3575                max: Coord::MAX,
3576                ..Default::default()
3577            },
3578            ..Default::default()
3579        }];
3580        let flex_props = [FlexItemProps::default()];
3581        let pad = Padding::default();
3582        let mut builder =
3583            flexbox_taffy::FlexboxTaffyBuilder::new(flexbox_taffy::FlexboxLayoutParams {
3584                cells_h: &Slice::from_slice(&cells_h),
3585                cells_v: &Slice::from_slice(&cells_v),
3586                flex_props: &Slice::from_slice(&flex_props),
3587                spacing_h: 0 as Coord,
3588                spacing_v: 0 as Coord,
3589                padding_h: &pad,
3590                padding_v: &pad,
3591                alignment: LayoutAlignment::Start,
3592                cross_axis_line_alignment: LayoutAlignment::Stretch,
3593                cross_axis_alignment: CrossAxisAlignment::Stretch,
3594                flex_wrap: FlexboxLayoutWrap::NoWrap,
3595                flex_shrink: 1.,
3596                flex_direction: flexbox_taffy::TaffyFlexDirection::Row,
3597                // The unbounded main axis: the value the info path feeds here.
3598                container_width: Some(Coord::MAX),
3599                container_height: None,
3600                cross_axis_sizing: flexbox_taffy::CrossAxisSizing::Preferred,
3601            });
3602        builder.compute_layout(Coord::MAX, Coord::MAX, &mut zero_measure);
3603        let (_x, _y, w, _h) = builder.child_geometry(0);
3604        // Without the fix the dropped `50% * MAX` makes this the f32 sentinel
3605        // (or panics under i32); with it the item just takes its natural size.
3606        assert!(w.is_finite() && w < Coord::MAX, "item main-axis size was {w}");
3607    }
3608
3609    /// Runs `grid_internal::to_layout_data` for a single row made of one
3610    /// non-spanning cell per constraint, and returns that row's combined
3611    /// LayoutData (min/max/pref), for testing the row/col aggregation in
3612    /// isolation from the rest of GridLayout.
3613    fn row_layout_data(constraints: &[LayoutInfo]) -> grid_internal::LayoutData {
3614        let mut organized_data = GridLayoutOrganizedData::default();
3615        let mut generator =
3616            OrganizedDataGenerator::new(&[], &[], constraints.len(), 0, 0, &mut organized_data);
3617        for col in 0..constraints.len() {
3618            generator.add(col as u16, 1, 0, 1);
3619        }
3620        let items: Vec<LayoutItemInfo> = constraints
3621            .iter()
3622            .map(|constraint| LayoutItemInfo { constraint: *constraint, ..Default::default() })
3623            .collect();
3624        let mut layout_data = grid_internal::to_layout_data(
3625            &organized_data,
3626            Slice::from_slice(&items),
3627            Orientation::Vertical,
3628            Slice::from_slice(&[]),
3629            Slice::from_slice(&[]),
3630            0 as _,
3631            None,
3632        );
3633        assert_eq!(layout_data.len(), 1);
3634        layout_data.remove(0)
3635    }
3636
3637    #[test]
3638    fn test_grid_row_collapsed_cell_raises_max_and_pulls_pref_down_with_it() {
3639        // Row with a cell collapsed to a fixed zero size (the `height: cond ?
3640        // x : 0px` idiom, #9724) next to a cell with an explicit min/preferred
3641        // and no max (e.g. `min-height: 5px; preferred-height: 50px;`).
3642        let row = row_layout_data(&[
3643            LayoutInfo { min: 0 as _, max: 0 as _, preferred: 0 as _, ..Default::default() },
3644            LayoutInfo { min: 5 as _, preferred: 50 as _, ..Default::default() },
3645        ]);
3646        // The collapsed cell's fixed zero pulls the row's max down to 0 while
3647        // its sibling's min pulls the row's min up to 5: a real conflict, so
3648        // max must be raised to meet min, and the sibling's now out-of-range
3649        // preferred (50) must be pulled down with it, not summed into the
3650        // row's LayoutInfo as-is.
3651        assert_eq!(row.min, 5 as Coord);
3652        assert_eq!(row.max, 5 as Coord);
3653        assert_eq!(row.pref, 5 as Coord);
3654    }
3655
3656    #[test]
3657    fn test_grid_row_without_conflicting_constraints_keeps_its_own_pref() {
3658        // No cell forces max below min here (max 10 >= min 0): not a #9724
3659        // conflict, so the fix must leave this row untouched. The second
3660        // cell's preferred (50) legitimately exceeds the row's own max (10)
3661        // already on master; to_layout_data's output also drives
3662        // solve_grid_layout, so clamping pref here would silently shrink
3663        // rows that never had a min/max conflict in the first place.
3664        let row = row_layout_data(&[
3665            LayoutInfo { min: 0 as _, max: 10 as _, preferred: 5 as _, ..Default::default() },
3666            LayoutInfo { min: 0 as _, preferred: 50 as _, ..Default::default() },
3667        ]);
3668        assert_eq!(row.min, 0 as Coord);
3669        assert_eq!(row.max, 10 as Coord);
3670        assert_eq!(row.pref, 50 as Coord);
3671    }
3672}