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    DialogButtonRole, FlexboxLayoutAlignContent, FlexboxLayoutAlignItems, FlexboxLayoutAlignSelf,
10    FlexboxLayoutDirection, FlexboxLayoutWrap, 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 infos.
54    #[must_use]
55    pub fn merge(&self, other: &LayoutInfo) -> Self {
56        Self {
57            min: self.min.max(other.min),
58            max: self.max.min(other.max),
59            min_percent: self.min_percent.max(other.min_percent),
60            max_percent: self.max_percent.min(other.max_percent),
61            preferred: self.preferred.max(other.preferred),
62            stretch: self.stretch.min(other.stretch),
63        }
64    }
65
66    /// Helper function to return a preferred size which is within the min/max constraints
67    #[must_use]
68    pub fn preferred_bounded(&self) -> Coord {
69        self.preferred.min(self.max).max(self.min)
70    }
71}
72
73impl core::ops::Add for LayoutInfo {
74    type Output = Self;
75
76    fn add(self, rhs: Self) -> Self::Output {
77        self.merge(&rhs)
78    }
79}
80
81/// Returns the logical min and max sizes given the provided layout constraints.
82pub fn min_max_size_for_layout_constraints(
83    constraints_horizontal: LayoutInfo,
84    constraints_vertical: LayoutInfo,
85) -> (Option<crate::api::LogicalSize>, Option<crate::api::LogicalSize>) {
86    let min_width = constraints_horizontal.min.min(constraints_horizontal.max) as f32;
87    let min_height = constraints_vertical.min.min(constraints_vertical.max) as f32;
88    let max_width = constraints_horizontal.max.max(constraints_horizontal.min) as f32;
89    let max_height = constraints_vertical.max.max(constraints_vertical.min) as f32;
90
91    //cfg!(target_arch = "wasm32") is there because wasm32 winit don't like when max size is None:
92    // panicked at 'Property is read only: JsValue(NoModificationAllowedError: CSSStyleDeclaration.removeProperty: Can't remove property 'max-width' from computed style
93
94    let min_size = if min_width > 0. || min_height > 0. || cfg!(target_arch = "wasm32") {
95        Some(crate::api::LogicalSize::new(min_width, min_height))
96    } else {
97        None
98    };
99
100    let max_size = if (max_width > 0.
101        && max_height > 0.
102        && (max_width < i32::MAX as f32 || max_height < i32::MAX as f32))
103        || cfg!(target_arch = "wasm32")
104    {
105        // maximum widget size for Qt and a workaround for the winit api not allowing partial constraints
106        let window_size_max = 16_777_215.;
107        Some(crate::api::LogicalSize::new(
108            max_width.min(window_size_max),
109            max_height.min(window_size_max),
110        ))
111    } else {
112        None
113    };
114
115    (min_size, max_size)
116}
117
118/// Implement a saturating_add version for both possible value of Coord.
119/// So that adding the max value does not overflow
120trait Saturating {
121    fn add(_: Self, _: Self) -> Self;
122}
123impl Saturating for i32 {
124    #[inline]
125    fn add(a: Self, b: Self) -> Self {
126        a.saturating_add(b)
127    }
128}
129impl Saturating for f32 {
130    #[inline]
131    fn add(a: Self, b: Self) -> Self {
132        a + b
133    }
134}
135
136mod grid_internal {
137    use super::*;
138
139    fn order_coord<T: PartialOrd>(a: &T, b: &T) -> core::cmp::Ordering {
140        a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)
141    }
142
143    #[derive(Debug, Clone)]
144    pub struct LayoutData {
145        // inputs
146        pub min: Coord,
147        pub max: Coord,
148        pub pref: Coord,
149        pub stretch: f32,
150
151        // outputs
152        pub pos: Coord,
153        pub size: Coord,
154    }
155
156    impl Default for LayoutData {
157        fn default() -> Self {
158            LayoutData {
159                min: 0 as _,
160                max: Coord::MAX,
161                pref: 0 as _,
162                stretch: f32::MAX,
163                pos: 0 as _,
164                size: 0 as _,
165            }
166        }
167    }
168
169    trait Adjust {
170        fn can_grow(_: &LayoutData) -> Coord;
171        fn to_distribute(expected_size: Coord, current_size: Coord) -> Coord;
172        fn distribute(_: &mut LayoutData, val: Coord);
173    }
174
175    struct Grow;
176    impl Adjust for Grow {
177        fn can_grow(it: &LayoutData) -> Coord {
178            it.max - it.size
179        }
180
181        fn to_distribute(expected_size: Coord, current_size: Coord) -> Coord {
182            expected_size - current_size
183        }
184
185        fn distribute(it: &mut LayoutData, val: Coord) {
186            it.size += val;
187        }
188    }
189
190    struct Shrink;
191    impl Adjust for Shrink {
192        fn can_grow(it: &LayoutData) -> Coord {
193            it.size - it.min
194        }
195
196        fn to_distribute(expected_size: Coord, current_size: Coord) -> Coord {
197            current_size - expected_size
198        }
199
200        fn distribute(it: &mut LayoutData, val: Coord) {
201            it.size -= val;
202        }
203    }
204
205    #[allow(clippy::unnecessary_cast)] // Coord
206    fn adjust_items<A: Adjust>(data: &mut [LayoutData], size_without_spacing: Coord) -> Option<()> {
207        loop {
208            let size_cannot_grow: Coord = data
209                .iter()
210                .filter(|it| A::can_grow(it) <= 0 as _)
211                .map(|it| it.size)
212                .fold(0 as Coord, Saturating::add);
213
214            let total_stretch: f32 =
215                data.iter().filter(|it| A::can_grow(it) > 0 as _).map(|it| it.stretch).sum();
216
217            let actual_stretch = |s: f32| if total_stretch <= 0. { 1. } else { s };
218
219            let max_grow = data
220                .iter()
221                .filter(|it| A::can_grow(it) > 0 as _)
222                .map(|it| A::can_grow(it) as f32 / actual_stretch(it.stretch))
223                .min_by(order_coord)?;
224
225            let current_size: Coord = data
226                .iter()
227                .filter(|it| A::can_grow(it) > 0 as _)
228                .map(|it| it.size)
229                .fold(0 as _, Saturating::add);
230
231            //let to_distribute = size_without_spacing - (size_cannot_grow + current_size);
232            let to_distribute =
233                A::to_distribute(size_without_spacing, size_cannot_grow + current_size) as f32;
234            if to_distribute <= 0. || max_grow <= 0. {
235                return Some(());
236            }
237
238            let grow = if total_stretch <= 0. {
239                to_distribute
240                    / (data.iter().filter(|it| A::can_grow(it) > 0 as _).count() as Coord) as f32
241            } else {
242                to_distribute / total_stretch
243            }
244            .min(max_grow);
245
246            let mut distributed = 0 as Coord;
247            for it in data.iter_mut().filter(|it| A::can_grow(it) > 0 as Coord) {
248                let val = (grow * actual_stretch(it.stretch)) as Coord;
249                A::distribute(it, val);
250                distributed += val;
251            }
252
253            if distributed <= 0 as Coord {
254                // This can happen when Coord is integer and there is less then a pixel to add to each elements
255                // just give the pixel to the one with the bigger stretch
256                if let Some(it) = data
257                    .iter_mut()
258                    .filter(|it| A::can_grow(it) > 0 as _)
259                    .max_by(|a, b| actual_stretch(a.stretch).total_cmp(&b.stretch))
260                {
261                    A::distribute(it, to_distribute as Coord);
262                }
263                return Some(());
264            }
265        }
266    }
267
268    pub fn layout_items(data: &mut [LayoutData], start_pos: Coord, size: Coord, spacing: Coord) {
269        let size_without_spacing = size - spacing * (data.len() - 1) as Coord;
270
271        let mut pref = 0 as Coord;
272        for it in data.iter_mut() {
273            it.size = it.pref;
274            pref += it.pref;
275        }
276        if size_without_spacing >= pref {
277            adjust_items::<Grow>(data, size_without_spacing);
278        } else if size_without_spacing < pref {
279            adjust_items::<Shrink>(data, size_without_spacing);
280        }
281
282        let mut pos = start_pos;
283        for it in data.iter_mut() {
284            it.pos = pos;
285            pos = Saturating::add(pos, Saturating::add(it.size, spacing));
286        }
287    }
288
289    #[test]
290    #[allow(clippy::float_cmp)] // We want bit-wise equality here
291    fn test_layout_items() {
292        let my_items = &mut [
293            LayoutData { min: 100., max: 200., pref: 100., stretch: 1., ..Default::default() },
294            LayoutData { min: 50., max: 300., pref: 100., stretch: 1., ..Default::default() },
295            LayoutData { min: 50., max: 150., pref: 100., stretch: 1., ..Default::default() },
296        ];
297
298        layout_items(my_items, 100., 650., 0.);
299        assert_eq!(my_items[0].size, 200.);
300        assert_eq!(my_items[1].size, 300.);
301        assert_eq!(my_items[2].size, 150.);
302
303        layout_items(my_items, 100., 200., 0.);
304        assert_eq!(my_items[0].size, 100.);
305        assert_eq!(my_items[1].size, 50.);
306        assert_eq!(my_items[2].size, 50.);
307
308        layout_items(my_items, 100., 300., 0.);
309        assert_eq!(my_items[0].size, 100.);
310        assert_eq!(my_items[1].size, 100.);
311        assert_eq!(my_items[2].size, 100.);
312    }
313
314    /// Create a vector of LayoutData (e.g. one per row if Vertical) based on the constraints and organized data
315    /// Used by both solve_grid_layout() and grid_layout_info()
316    pub fn to_layout_data(
317        organized_data: &GridLayoutOrganizedData,
318        constraints: Slice<LayoutItemInfo>,
319        orientation: Orientation,
320        repeater_indices: Slice<u32>,
321        repeater_steps: Slice<u32>,
322        spacing: Coord,
323        size: Option<Coord>,
324    ) -> Vec<LayoutData> {
325        assert!(organized_data.len().is_multiple_of(4));
326        let num = organized_data.max_value(
327            constraints.len(),
328            orientation,
329            &repeater_indices,
330            &repeater_steps,
331        ) as usize;
332        if num < 1 {
333            return Default::default();
334        }
335        let marker_for_empty = -1.;
336        let mut layout_data = alloc::vec![grid_internal::LayoutData { max: 0 as Coord, stretch: marker_for_empty, ..Default::default() }; num];
337        let mut has_spans = false;
338        for (idx, cell_data) in constraints.iter().enumerate() {
339            let constraint = &cell_data.constraint;
340            let mut max = constraint.max;
341            if let Some(size) = size {
342                max = max.min(size * constraint.max_percent / 100 as Coord);
343            }
344            let (col_or_row, span) = organized_data.col_or_row_and_span(
345                idx,
346                orientation,
347                &repeater_indices,
348                &repeater_steps,
349            );
350            for c in 0..(span as usize) {
351                let cdata = &mut layout_data[col_or_row as usize + c];
352                // Initialize max/stretch to proper defaults on first item in this row/col
353                // so that empty rows/columns don't stretch.
354                if cdata.stretch == marker_for_empty {
355                    cdata.max = Coord::MAX;
356                    cdata.stretch = 1.;
357                }
358                cdata.max = cdata.max.min(max);
359            }
360            if span == 1 {
361                let mut min = constraint.min;
362                if let Some(size) = size {
363                    min = min.max(size * constraint.min_percent / 100 as Coord);
364                }
365                let pref = constraint.preferred.min(max).max(min);
366                let cdata = &mut layout_data[col_or_row as usize];
367                cdata.min = cdata.min.max(min);
368                cdata.pref = cdata.pref.max(pref);
369                cdata.stretch = cdata.stretch.min(constraint.stretch);
370            } else {
371                has_spans = true;
372            }
373        }
374        if has_spans {
375            for (idx, cell_data) in constraints.iter().enumerate() {
376                let constraint = &cell_data.constraint;
377                let (col_or_row, span) = organized_data.col_or_row_and_span(
378                    idx,
379                    orientation,
380                    &repeater_indices,
381                    &repeater_steps,
382                );
383                if span > 1 {
384                    let span_data =
385                        &mut layout_data[(col_or_row as usize)..(col_or_row + span) as usize];
386
387                    // Adjust minimum sizes
388                    let mut min = constraint.min;
389                    if let Some(size) = size {
390                        min = min.max(size * constraint.min_percent / 100 as Coord);
391                    }
392                    grid_internal::layout_items(span_data, 0 as _, min, spacing);
393                    for cdata in span_data.iter_mut() {
394                        if cdata.min < cdata.size {
395                            cdata.min = cdata.size;
396                        }
397                    }
398
399                    // Adjust maximum sizes
400                    let mut max = constraint.max;
401                    if let Some(size) = size {
402                        max = max.min(size * constraint.max_percent / 100 as Coord);
403                    }
404                    grid_internal::layout_items(span_data, 0 as _, max, spacing);
405                    for cdata in span_data.iter_mut() {
406                        if cdata.max > cdata.size {
407                            cdata.max = cdata.size;
408                        }
409                    }
410
411                    // Adjust preferred sizes
412                    grid_internal::layout_items(span_data, 0 as _, constraint.preferred, spacing);
413                    for cdata in span_data.iter_mut() {
414                        cdata.pref = cdata.pref.max(cdata.size).min(cdata.max).max(cdata.min);
415                    }
416
417                    // Adjust stretches
418                    let total_stretch: f32 = span_data.iter().map(|c| c.stretch).sum();
419                    if total_stretch > constraint.stretch {
420                        for cdata in span_data.iter_mut() {
421                            cdata.stretch *= constraint.stretch / total_stretch;
422                        }
423                    }
424                }
425            }
426        }
427        for cdata in layout_data.iter_mut() {
428            if cdata.stretch == marker_for_empty {
429                cdata.stretch = 0.;
430            }
431        }
432        layout_data
433    }
434}
435
436#[repr(C)]
437pub struct Constraint {
438    pub min: Coord,
439    pub max: Coord,
440}
441
442impl Default for Constraint {
443    fn default() -> Self {
444        Constraint { min: 0 as Coord, max: Coord::MAX }
445    }
446}
447
448#[repr(C)]
449#[derive(Copy, Clone, Debug, Default)]
450pub struct Padding {
451    pub begin: Coord,
452    pub end: Coord,
453}
454
455#[repr(C)]
456#[derive(Debug)]
457/// The horizontal or vertical data for all cells of a GridLayout, used as input to solve_grid_layout()
458pub struct GridLayoutData {
459    pub size: Coord,
460    pub spacing: Coord,
461    pub padding: Padding,
462    pub organized_data: GridLayoutOrganizedData,
463}
464
465/// The input data for a cell of a GridLayout, before row/col determination and before H/V split
466/// Used as input to organize_grid_layout()
467#[repr(C)]
468#[derive(Debug, Clone)]
469pub struct GridLayoutInputData {
470    /// whether this cell is the first one in a Row element
471    pub new_row: bool,
472    /// col and row number.
473    /// Only ROW_COL_AUTO and the u16 range are valid, values outside of
474    /// that will be clamped with a warning at runtime
475    pub col: f32,
476    pub row: f32,
477    /// colspan and rowspan
478    /// Only the u16 range is valid, values outside of that will be clamped with a warning at runtime
479    pub colspan: f32,
480    pub rowspan: f32,
481}
482
483impl Default for GridLayoutInputData {
484    fn default() -> Self {
485        Self {
486            new_row: false,
487            col: i_slint_common::ROW_COL_AUTO,
488            row: i_slint_common::ROW_COL_AUTO,
489            colspan: 1.0,
490            rowspan: 1.0,
491        }
492    }
493}
494
495/// The organized layout data for a GridLayout, after row/col determination:
496/// For each cell, stores col, colspan, row, rowspan
497pub type GridLayoutOrganizedData = SharedVector<u16>;
498
499impl GridLayoutOrganizedData {
500    fn push_cell(&mut self, col: u16, colspan: u16, row: u16, rowspan: u16) {
501        self.push(col);
502        self.push(colspan);
503        self.push(row);
504        self.push(rowspan);
505    }
506
507    fn col_or_row_and_span(
508        &self,
509        cell_number: usize,
510        orientation: Orientation,
511        repeater_indices: &Slice<u32>,
512        repeater_steps: &Slice<u32>,
513    ) -> (u16, u16) {
514        // For every cell, we have 4 entries, each at their own index
515        // But we also need to take into account indirections for repeated items
516
517        // Two-level indirection for repeated items:
518        //   jump_pos = (ri_start_cell - cell_nr_adj) * 4
519        //   data_base = self[jump_pos]        (base of this repeater's data)
520        //   stride    = self[jump_pos + 1]    (u16 entries per row = step * 4)
521        //   data_idx = data_base + row_in_rep * stride + col_in_rep * 4
522        let mut final_idx = 0;
523        let mut cell_nr_adj = 0i32; // needs to be signed in case we start with an empty repeater
524        let cell_number = cell_number as i32;
525        // repeater_indices is a list of (start_cell, count) pairs
526        for rep_idx in 0..(repeater_indices.len() / 2) {
527            let ri_start_cell = repeater_indices[rep_idx * 2] as i32;
528            if cell_number < ri_start_cell {
529                break;
530            }
531            let ri_cell_count = repeater_indices[rep_idx * 2 + 1] as i32;
532            let step = repeater_steps.get(rep_idx).copied().unwrap_or(1) as i32;
533            let cells_in_repeater = ri_cell_count * step;
534            if cells_in_repeater > 0
535                && cell_number >= ri_start_cell
536                && cell_number < ri_start_cell + cells_in_repeater
537            {
538                let cell_in_rep = cell_number - ri_start_cell;
539                let row_in_rep = cell_in_rep / step;
540                let col_in_rep = cell_in_rep % step;
541                let jump_pos = (ri_start_cell - cell_nr_adj) as usize * 4;
542                let data_base = self[jump_pos] as usize;
543                let stride = self[jump_pos + 1] as usize;
544                final_idx = data_base + row_in_rep as usize * stride + col_in_rep as usize * 4;
545                break;
546            }
547            // Each repeater occupies 1 jump cell in the static area but cells_in_repeater cells logically
548            // Note: -1 is correct for an empty repeater (e.g. if false), which occupies 1 jump cell, for 0 real cells
549            cell_nr_adj += cells_in_repeater - 1;
550        }
551        if final_idx == 0 {
552            final_idx = ((cell_number - cell_nr_adj) * 4) as usize;
553        }
554        let offset = if orientation == Orientation::Horizontal { 0 } else { 2 };
555        (self[final_idx + offset], self[final_idx + offset + 1])
556    }
557
558    fn max_value(
559        &self,
560        num_cells: usize,
561        orientation: Orientation,
562        repeater_indices: &Slice<u32>,
563        repeater_steps: &Slice<u32>,
564    ) -> u16 {
565        let mut max = 0;
566        // This could be rewritten more efficiently to avoid a loop calling a loop, by keeping track of the repeaters we saw until now
567        // Not sure it's worth the complexity though
568        for idx in 0..num_cells {
569            let (col_or_row, span) =
570                self.col_or_row_and_span(idx, orientation, repeater_indices, repeater_steps);
571            max = max.max(col_or_row + span.max(1));
572        }
573        max
574    }
575}
576
577/// Two-level indirection organized data generator for grid layouts with repeaters.
578/// Uses 2-level indirection: cache[cache[jump_pos] + ri * stride + col * 4]
579/// Each jump cell stores [data_base, stride, 0, 0] where stride = step * 4.
580///
581/// Layout: [static_cells (4 u16 each)] [jump_cells (4 u16 each, 1 per repeater)]
582///         [row_data (rep_count * step * 4 u16)] ... (repeated for each repeater)
583struct OrganizedDataGenerator<'a> {
584    // Input
585    repeater_indices: &'a [u32],
586    repeater_steps: &'a [u32],
587    // An always increasing counter, the index of the cell being added
588    counter: usize,
589    // The u16 position in result for the next repeater's data section
590    repeat_u16_offset: usize,
591    // The index/2 in repeater_indices (i.e. which repeater we're looking at next)
592    next_rep: usize,
593    // The cell index in result for the next non-repeated item (each cell = 4 u16)
594    current_offset: usize,
595    // Output
596    result: &'a mut GridLayoutOrganizedData,
597}
598
599impl<'a> OrganizedDataGenerator<'a> {
600    fn new(
601        repeater_indices: &'a [u32],
602        repeater_steps: &'a [u32],
603        static_cells: usize,
604        num_repeaters: usize,
605        total_repeated_cells_count: usize,
606        result: &'a mut GridLayoutOrganizedData,
607    ) -> Self {
608        result.resize((static_cells + num_repeaters + total_repeated_cells_count) * 4, 0 as _);
609        let repeat_u16_offset = (static_cells + num_repeaters) * 4;
610        Self {
611            repeater_indices,
612            repeater_steps,
613            counter: 0,
614            repeat_u16_offset,
615            next_rep: 0,
616            current_offset: 0,
617            result,
618        }
619    }
620    fn add(&mut self, col: u16, colspan: u16, row: u16, rowspan: u16) {
621        let res = self.result.make_mut_slice();
622        loop {
623            if let Some(nr) = self.repeater_indices.get(self.next_rep * 2) {
624                let nr = *nr as usize;
625                let step = self.repeater_steps.get(self.next_rep).copied().unwrap_or(1) as usize;
626                let rep_count = self.repeater_indices[self.next_rep * 2 + 1] as usize;
627
628                if nr == self.counter {
629                    // First cell of this repeater
630                    let data_u16_start = self.repeat_u16_offset;
631                    let stride = step * 4;
632
633                    // Write jump cell: [data_base, stride, 0, 0]
634                    res[self.current_offset * 4] = data_u16_start as _;
635                    res[self.current_offset * 4 + 1] = stride as _;
636                    self.current_offset += 1;
637                }
638                if self.counter >= nr {
639                    let cells_in_repeater = rep_count * step;
640                    if self.counter - nr == cells_in_repeater {
641                        // Past the end of this repeater — advance past data
642                        self.repeat_u16_offset += cells_in_repeater * 4;
643                        self.next_rep += 1;
644                        continue;
645                    }
646                    // Write data at the position determined by row/col within repeater
647                    let cell_in_rep = self.counter - nr;
648                    let row_in_rep = cell_in_rep / step;
649                    let col_in_rep = cell_in_rep % step;
650                    let data_u16_start = self.repeat_u16_offset;
651                    let u16_pos = data_u16_start + row_in_rep * step * 4 + col_in_rep * 4;
652                    res[u16_pos] = col;
653                    res[u16_pos + 1] = colspan;
654                    res[u16_pos + 2] = row;
655                    res[u16_pos + 3] = rowspan;
656                    self.counter += 1;
657                    return;
658                }
659            }
660            // Non-repeated cell
661            res[self.current_offset * 4] = col;
662            res[self.current_offset * 4 + 1] = colspan;
663            res[self.current_offset * 4 + 2] = row;
664            res[self.current_offset * 4 + 3] = rowspan;
665            self.current_offset += 1;
666            self.counter += 1;
667            return;
668        }
669    }
670}
671
672/// Given the cells of a layout of a Dialog, re-order the buttons according to the platform
673/// This function assume that the `roles` contains the roles of the button which are the first cells in `input_data`
674pub fn organize_dialog_button_layout(
675    input_data: Slice<GridLayoutInputData>,
676    dialog_button_roles: Slice<DialogButtonRole>,
677) -> GridLayoutOrganizedData {
678    let mut organized_data = GridLayoutOrganizedData::default();
679    organized_data.reserve(input_data.len() * 4);
680
681    #[cfg(feature = "std")]
682    fn is_kde() -> bool {
683        // assume some Unix, check if XDG_CURRENT_DESKTOP starts with K
684        std::env::var("XDG_CURRENT_DESKTOP")
685            .ok()
686            .and_then(|v| v.as_bytes().first().copied())
687            .is_some_and(|x| x.eq_ignore_ascii_case(&b'K'))
688    }
689    #[cfg(not(feature = "std"))]
690    let is_kde = || true;
691
692    let expected_order: &[DialogButtonRole] = match crate::detect_operating_system() {
693        crate::items::OperatingSystemType::Windows => {
694            &[
695                DialogButtonRole::Reset,
696                DialogButtonRole::None, // spacer
697                DialogButtonRole::Accept,
698                DialogButtonRole::Action,
699                DialogButtonRole::Reject,
700                DialogButtonRole::Apply,
701                DialogButtonRole::Help,
702            ]
703        }
704        crate::items::OperatingSystemType::Macos | crate::items::OperatingSystemType::Ios => {
705            &[
706                DialogButtonRole::Help,
707                DialogButtonRole::Reset,
708                DialogButtonRole::Apply,
709                DialogButtonRole::Action,
710                DialogButtonRole::None, // spacer
711                DialogButtonRole::Reject,
712                DialogButtonRole::Accept,
713            ]
714        }
715        _ if is_kde() => {
716            // KDE variant
717            &[
718                DialogButtonRole::Help,
719                DialogButtonRole::Reset,
720                DialogButtonRole::None, // spacer
721                DialogButtonRole::Action,
722                DialogButtonRole::Accept,
723                DialogButtonRole::Apply,
724                DialogButtonRole::Reject,
725            ]
726        }
727        _ => {
728            // GNOME variant and fallback for WASM build
729            &[
730                DialogButtonRole::Help,
731                DialogButtonRole::Reset,
732                DialogButtonRole::None, // spacer
733                DialogButtonRole::Action,
734                DialogButtonRole::Accept,
735                DialogButtonRole::Apply,
736                DialogButtonRole::Reject,
737            ]
738        }
739    };
740
741    // Reorder the actual buttons according to expected_order
742    let mut column_for_input: Vec<usize> = Vec::with_capacity(dialog_button_roles.len());
743    for role in expected_order.iter() {
744        if role == &DialogButtonRole::None {
745            column_for_input.push(usize::MAX); // empty column, ensure nothing will match
746            continue;
747        }
748        for (idx, r) in dialog_button_roles.as_slice().iter().enumerate() {
749            if *r == *role {
750                column_for_input.push(idx);
751            }
752        }
753    }
754
755    for (input_index, cell) in input_data.as_slice().iter().enumerate() {
756        let col = column_for_input.iter().position(|&x| x == input_index);
757        if let Some(col) = col {
758            organized_data.push_cell(col as _, cell.colspan as _, cell.row as _, cell.rowspan as _);
759        } else {
760            // This is used for the main window (which is the only cell which isn't a button)
761            // Given lower_dialog_layout(), this will always be a single cell at 0,0 with a colspan of number_of_buttons
762            organized_data.push_cell(
763                cell.col as _,
764                cell.colspan as _,
765                cell.row as _,
766                cell.rowspan as _,
767            );
768        }
769    }
770    organized_data
771}
772
773// GridLayout-specific
774fn total_repeated_cells<'a>(repeater_indices: &'a [u32], repeater_steps: &'a [u32]) -> usize {
775    repeater_indices
776        .chunks(2)
777        .enumerate()
778        .map(|(i, chunk)| {
779            let count = chunk.get(1).copied().unwrap_or(0) as usize;
780            let step = repeater_steps.get(i).copied().unwrap_or(1) as usize;
781            count * step
782        })
783        .sum()
784}
785
786type Errors = Vec<String>;
787
788pub fn organize_grid_layout(
789    input_data: Slice<GridLayoutInputData>,
790    repeater_indices: Slice<u32>,
791    repeater_steps: Slice<u32>,
792) -> GridLayoutOrganizedData {
793    let (organized_data, errors) =
794        organize_grid_layout_impl(input_data, repeater_indices, repeater_steps);
795    for error in errors {
796        crate::debug_log!("Slint layout error: {}", error);
797    }
798    organized_data
799}
800
801// Implement "auto" behavior for row/col numbers (unless specified in the slint file).
802fn organize_grid_layout_impl(
803    input_data: Slice<GridLayoutInputData>,
804    repeater_indices: Slice<u32>,
805    repeater_steps: Slice<u32>,
806) -> (GridLayoutOrganizedData, Errors) {
807    let mut organized_data = GridLayoutOrganizedData::default();
808    // Cache size: static_cells * 4 + num_repeaters * 4 (jump cells)
809    //              + per repeater: rep_count * step * 4 (data)
810    let num_repeaters = repeater_indices.len() / 2;
811    let total_repeated_cells =
812        total_repeated_cells(repeater_indices.as_slice(), repeater_steps.as_slice());
813    let static_cells = input_data.len() - total_repeated_cells;
814    let mut generator = OrganizedDataGenerator::new(
815        repeater_indices.as_slice(),
816        repeater_steps.as_slice(),
817        static_cells,
818        num_repeaters,
819        total_repeated_cells,
820        &mut organized_data,
821    );
822    let mut errors = Vec::new();
823
824    fn clamp_to_u16(value: f32, field_name: &str, errors: &mut Vec<String>) -> u16 {
825        if value < 0.0 {
826            errors.push(format!("cell {field_name} {value} is negative, clamping to 0"));
827            0
828        } else if value > u16::MAX as f32 {
829            errors
830                .push(format!("cell {field_name} {value} is too large, clamping to {}", u16::MAX));
831            u16::MAX
832        } else {
833            value as u16
834        }
835    }
836
837    let mut row = 0;
838    let mut col = 0;
839    let mut first = true;
840    for cell in input_data.as_slice().iter() {
841        if cell.new_row && !first {
842            row += 1;
843            col = 0;
844        }
845        first = false;
846
847        if cell.row != i_slint_common::ROW_COL_AUTO {
848            let cell_row = clamp_to_u16(cell.row, "row", &mut errors);
849            if row != cell_row {
850                row = cell_row;
851                col = 0;
852            }
853        }
854        if cell.col != i_slint_common::ROW_COL_AUTO {
855            col = clamp_to_u16(cell.col, "col", &mut errors);
856        }
857
858        let colspan = clamp_to_u16(cell.colspan, "colspan", &mut errors);
859        let rowspan = clamp_to_u16(cell.rowspan, "rowspan", &mut errors);
860        col = col.min(u16::MAX - colspan); // ensure col + colspan doesn't overflow
861        generator.add(col, colspan, row, rowspan);
862        col += colspan;
863    }
864    (organized_data, errors)
865}
866
867/// Layout cache generator for box layouts.
868/// The layout cache generator inserts the pos and size into the result array (which becomes the layout cache property),
869/// including the indirections for repeated items (so that the x,y,width,height properties for repeated items
870/// can point to indices known at compile time, those that contain the indirections)
871/// Example: for repeater_indices=[1,4] (meaning that item at index 1 is repeated 4 times),
872/// 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]
873///  i.e. pos1, width1, jump to idx 4, jump to idx 5, pos2, width2, pos3, width3, pos4, width4, pos5, width5
874struct LayoutCacheGenerator<'a> {
875    // Input
876    repeater_indices: &'a [u32],
877    // An always increasing counter, the index of the cell being added
878    counter: usize,
879    // The index/2 in result in which we should add the next repeated item
880    repeat_offset: usize,
881    // The index/2 in repeater_indices
882    next_rep: usize,
883    // The index/2 in result in which we should add the next non-repeated item
884    current_offset: usize,
885    // Output
886    result: &'a mut SharedVector<Coord>,
887}
888
889impl<'a> LayoutCacheGenerator<'a> {
890    fn new(repeater_indices: &'a [u32], result: &'a mut SharedVector<Coord>) -> Self {
891        let total_repeated_cells: usize = repeater_indices
892            .chunks(2)
893            .map(|chunk| chunk.get(1).copied().unwrap_or(0) as usize)
894            .sum();
895        assert!(result.len() >= total_repeated_cells * 2);
896        let repeat_offset = result.len() / 2 - total_repeated_cells;
897        Self { repeater_indices, counter: 0, repeat_offset, next_rep: 0, current_offset: 0, result }
898    }
899    fn add(&mut self, pos: Coord, size: Coord) {
900        let res = self.result.make_mut_slice();
901        let o = loop {
902            if let Some(nr) = self.repeater_indices.get(self.next_rep * 2) {
903                let nr = *nr as usize;
904                if nr == self.counter {
905                    // Write jump entry
906                    for o in 0..2 {
907                        res[self.current_offset * 2 + o] = (self.repeat_offset * 2 + o) as _;
908                    }
909                    self.current_offset += 1;
910                }
911                if self.counter >= nr {
912                    let rep_count = self.repeater_indices[self.next_rep * 2 + 1] as usize;
913                    if self.counter - nr == rep_count {
914                        self.repeat_offset += rep_count;
915                        self.next_rep += 1;
916                        continue;
917                    }
918                    let offset = self.repeat_offset + (self.counter - nr);
919                    break offset;
920                }
921            }
922            self.current_offset += 1;
923            break self.current_offset - 1;
924        };
925        res[o * 2] = pos;
926        res[o * 2 + 1] = size;
927        self.counter += 1;
928    }
929}
930
931/// Two-level indirection layout cache generator for grid layouts with repeaters.
932/// Uses 2-level indirection: cache[cache[jump] + ri * stride + child_offset]
933/// Each jump cell stores [data_base, stride] where stride = step * 2.
934struct GridLayoutCacheGenerator<'a> {
935    // Input
936    repeater_indices: &'a [u32],
937    repeater_steps: &'a [u32],
938    // An always increasing counter, the index of the cell being added
939    counter: usize,
940    // The f32 position in result for the next repeater's dynamic data section
941    repeat_f32_offset: usize,
942    // The index/2 in repeater_indices
943    next_rep: usize,
944    // The cell index (index/2) in result for the next non-repeated item
945    current_offset: usize,
946    // Output
947    result: &'a mut SharedVector<Coord>,
948}
949
950impl<'a> GridLayoutCacheGenerator<'a> {
951    fn new(
952        repeater_indices: &'a [u32],
953        repeater_steps: &'a [u32],
954        static_cells: usize,
955        num_repeaters: usize,
956        total_repeated_cells_count: usize,
957        result: &'a mut SharedVector<Coord>,
958    ) -> Self {
959        result.resize((static_cells + num_repeaters + total_repeated_cells_count) * 2, 0 as _);
960        let repeat_f32_offset = (static_cells + num_repeaters) * 2;
961        Self {
962            repeater_indices,
963            repeater_steps,
964            counter: 0,
965            repeat_f32_offset,
966            next_rep: 0,
967            current_offset: 0,
968            result,
969        }
970    }
971    fn add(&mut self, pos: Coord, size: Coord) {
972        let res = self.result.make_mut_slice();
973        loop {
974            if let Some(nr) = self.repeater_indices.get(self.next_rep * 2) {
975                let nr = *nr as usize;
976                let step = self.repeater_steps.get(self.next_rep).copied().unwrap_or(1) as usize;
977                let rep_count = self.repeater_indices[self.next_rep * 2 + 1] as usize;
978
979                if nr == self.counter {
980                    // First cell of this repeater
981                    let data_f32_start = self.repeat_f32_offset;
982                    let stride = step * 2;
983
984                    // Write 1 jump cell (2 f32): [data_base, stride]
985                    res[self.current_offset * 2] = data_f32_start as _;
986                    res[self.current_offset * 2 + 1] = stride as _;
987                    self.current_offset += 1;
988                }
989                if self.counter >= nr {
990                    let cells_in_repeater = rep_count * step;
991                    if self.counter - nr == cells_in_repeater {
992                        // Past the end of this repeater — advance past data
993                        self.repeat_f32_offset += cells_in_repeater * 2;
994                        self.next_rep += 1;
995                        continue;
996                    }
997                    // Write data at the position determined by row/col within repeater
998                    let cell_in_rep = self.counter - nr;
999                    let row_in_rep = cell_in_rep / step;
1000                    let col_in_rep = cell_in_rep % step;
1001                    let data_f32_start = self.repeat_f32_offset;
1002                    let f32_pos = data_f32_start + row_in_rep * step * 2 + col_in_rep * 2;
1003                    res[f32_pos] = pos;
1004                    res[f32_pos + 1] = size;
1005                    self.counter += 1;
1006                    return;
1007                }
1008            }
1009            // Non-repeated cell
1010            res[self.current_offset * 2] = pos;
1011            res[self.current_offset * 2 + 1] = size;
1012            self.current_offset += 1;
1013            self.counter += 1;
1014            return;
1015        }
1016    }
1017}
1018
1019/// return, an array which is of size `data.cells.len() * 2` which for each cell stores:
1020/// pos (x or y), size (width or height)
1021pub fn solve_grid_layout(
1022    data: &GridLayoutData,
1023    constraints: Slice<LayoutItemInfo>,
1024    orientation: Orientation,
1025    repeater_indices: Slice<u32>,
1026    repeater_steps: Slice<u32>,
1027) -> SharedVector<Coord> {
1028    let mut layout_data = grid_internal::to_layout_data(
1029        &data.organized_data,
1030        constraints,
1031        orientation,
1032        repeater_indices,
1033        repeater_steps,
1034        data.spacing,
1035        Some(data.size),
1036    );
1037
1038    if layout_data.is_empty() {
1039        return Default::default();
1040    }
1041
1042    grid_internal::layout_items(
1043        &mut layout_data,
1044        data.padding.begin,
1045        data.size - (data.padding.begin + data.padding.end),
1046        data.spacing,
1047    );
1048
1049    let mut result = SharedVector::<Coord>::default();
1050    let num_repeaters = repeater_indices.len() / 2;
1051    let total_repeated_cells =
1052        total_repeated_cells(repeater_indices.as_slice(), repeater_steps.as_slice());
1053    let static_cells = constraints.len() - total_repeated_cells;
1054    let mut generator = GridLayoutCacheGenerator::new(
1055        repeater_indices.as_slice(),
1056        repeater_steps.as_slice(),
1057        static_cells,
1058        num_repeaters,
1059        total_repeated_cells,
1060        &mut result,
1061    );
1062
1063    for idx in 0..constraints.len() {
1064        let (col_or_row, span) = data.organized_data.col_or_row_and_span(
1065            idx,
1066            orientation,
1067            &repeater_indices,
1068            &repeater_steps,
1069        );
1070        let cdata = &layout_data[col_or_row as usize];
1071        let size = if span > 0 {
1072            let last_cell = &layout_data[col_or_row as usize + span as usize - 1];
1073            last_cell.pos + last_cell.size - cdata.pos
1074        } else {
1075            0 as Coord
1076        };
1077        generator.add(cdata.pos, size);
1078    }
1079    result
1080}
1081
1082pub fn grid_layout_info(
1083    organized_data: GridLayoutOrganizedData, // not & because the code generator doesn't support it in ExtraBuiltinFunctionCall
1084    constraints: Slice<LayoutItemInfo>,
1085    repeater_indices: Slice<u32>,
1086    repeater_steps: Slice<u32>,
1087    spacing: Coord,
1088    padding: &Padding,
1089    orientation: Orientation,
1090) -> LayoutInfo {
1091    let layout_data = grid_internal::to_layout_data(
1092        &organized_data,
1093        constraints,
1094        orientation,
1095        repeater_indices,
1096        repeater_steps,
1097        spacing,
1098        None,
1099    );
1100    if layout_data.is_empty() {
1101        let mut info = LayoutInfo::default();
1102        info.min = padding.begin + padding.end;
1103        info.preferred = info.min;
1104        info.max = info.min;
1105        return info;
1106    }
1107    let spacing_w = spacing * (layout_data.len() - 1) as Coord + padding.begin + padding.end;
1108    let min = layout_data.iter().map(|data| data.min).sum::<Coord>() + spacing_w;
1109    let max = layout_data.iter().map(|data| data.max).fold(spacing_w, Saturating::add);
1110    let preferred = layout_data.iter().map(|data| data.pref).sum::<Coord>() + spacing_w;
1111    let stretch = layout_data.iter().map(|data| data.stretch).sum::<f32>();
1112    LayoutInfo { min, max, min_percent: 0 as _, max_percent: 100 as _, preferred, stretch }
1113}
1114
1115#[repr(C)]
1116#[derive(Debug)]
1117/// The BoxLayoutData is used to represent both a Horizontal and Vertical layout.
1118/// The width/height x/y correspond to that of a horizontal layout.
1119/// For vertical layout, they are inverted
1120pub struct BoxLayoutData<'a> {
1121    pub size: Coord,
1122    pub spacing: Coord,
1123    pub padding: Padding,
1124    pub alignment: LayoutAlignment,
1125    pub cells: Slice<'a, LayoutItemInfo>,
1126}
1127
1128#[repr(C)]
1129#[derive(Debug)]
1130/// The FlexboxLayoutData is used for a flex layout.
1131pub struct FlexboxLayoutData<'a> {
1132    pub width: Coord,
1133    pub height: Coord,
1134    pub spacing_h: Coord,
1135    pub spacing_v: Coord,
1136    pub padding_h: Padding,
1137    pub padding_v: Padding,
1138    pub alignment: LayoutAlignment,
1139    pub direction: FlexboxLayoutDirection,
1140    pub align_content: FlexboxLayoutAlignContent,
1141    pub align_items: FlexboxLayoutAlignItems,
1142    pub flex_wrap: FlexboxLayoutWrap,
1143    /// Horizontal constraints (width) for each cell
1144    pub cells_h: Slice<'a, FlexboxLayoutItemInfo>,
1145    /// Vertical constraints (height) for each cell
1146    pub cells_v: Slice<'a, FlexboxLayoutItemInfo>,
1147}
1148
1149#[repr(C)]
1150#[derive(Debug, Clone, Default)]
1151/// The information about a single item in a box or grid layout
1152pub struct LayoutItemInfo {
1153    pub constraint: LayoutInfo,
1154}
1155
1156#[repr(C)]
1157#[derive(Debug, Clone)]
1158/// The information about a single item in a flexbox layout
1159pub struct FlexboxLayoutItemInfo {
1160    pub constraint: LayoutInfo,
1161    /// Flex grow factor (0 = don't grow, default)
1162    pub flex_grow: f32,
1163    /// Flex shrink factor (0 = don't shrink, default)
1164    pub flex_shrink: f32,
1165    /// Flex basis in logical pixels (-1 = auto, meaning use preferred size; default)
1166    pub flex_basis: Coord,
1167    /// Per-item cross-axis alignment override (Auto = use container's align-items)
1168    pub flex_align_self: FlexboxLayoutAlignSelf,
1169    /// Visual ordering of flex items (lower values appear first, default 0)
1170    pub flex_order: i32,
1171}
1172
1173impl Default for FlexboxLayoutItemInfo {
1174    fn default() -> Self {
1175        Self {
1176            constraint: LayoutInfo::default(),
1177            flex_grow: 0.0,
1178            flex_shrink: 1.0,
1179            flex_basis: -1 as _,
1180            flex_align_self: FlexboxLayoutAlignSelf::Auto,
1181            flex_order: 0,
1182        }
1183    }
1184}
1185
1186impl From<LayoutItemInfo> for FlexboxLayoutItemInfo {
1187    fn from(info: LayoutItemInfo) -> Self {
1188        Self { constraint: info.constraint, ..Default::default() }
1189    }
1190}
1191
1192/// Solve a BoxLayout
1193pub fn solve_box_layout(data: &BoxLayoutData, repeater_indices: Slice<u32>) -> SharedVector<Coord> {
1194    let mut result = SharedVector::<Coord>::default();
1195    // One element results into two coordinates in the result vector. 1. Position, 2. Size
1196    result.resize(data.cells.len() * 2 + repeater_indices.len(), 0 as _);
1197
1198    if data.cells.is_empty() {
1199        return result;
1200    }
1201
1202    let size_without_padding = data.size - data.padding.begin - data.padding.end;
1203    let num_spacings = (data.cells.len() - 1) as Coord;
1204    let spacings = data.spacing * num_spacings;
1205    let content_size = size_without_padding - spacings; // The size the cells can occupy without going outside of the layout
1206    let mut layout_data: Vec<_> = data
1207        .cells
1208        .iter()
1209        .map(|c| {
1210            let min = c.constraint.min.max(c.constraint.min_percent * content_size / 100 as Coord);
1211            let max = c.constraint.max.min(c.constraint.max_percent * content_size / 100 as Coord);
1212            grid_internal::LayoutData {
1213                min,
1214                max,
1215                pref: c.constraint.preferred.min(max).max(min),
1216                stretch: c.constraint.stretch,
1217                ..Default::default()
1218            }
1219        })
1220        .collect();
1221
1222    let pref_size: Coord = layout_data.iter().map(|it| it.pref).sum();
1223
1224    let align = match data.alignment {
1225        LayoutAlignment::Stretch => {
1226            grid_internal::layout_items(
1227                &mut layout_data,
1228                data.padding.begin,
1229                size_without_padding,
1230                data.spacing,
1231            );
1232            None
1233        }
1234        _ if size_without_padding <= pref_size + spacings => {
1235            grid_internal::layout_items(
1236                &mut layout_data,
1237                data.padding.begin,
1238                size_without_padding,
1239                data.spacing,
1240            );
1241            None
1242        }
1243        LayoutAlignment::Center => Some((
1244            data.padding.begin + (size_without_padding - pref_size - spacings) / 2 as Coord,
1245            data.spacing,
1246        )),
1247        LayoutAlignment::Start => Some((data.padding.begin, data.spacing)),
1248        LayoutAlignment::End => {
1249            Some((data.padding.begin + (size_without_padding - pref_size - spacings), data.spacing))
1250        }
1251        LayoutAlignment::SpaceBetween => {
1252            Some((data.padding.begin, (size_without_padding - pref_size) / num_spacings))
1253        }
1254        LayoutAlignment::SpaceAround => {
1255            let spacing = (size_without_padding - pref_size) / (num_spacings + 1 as Coord);
1256            Some((data.padding.begin + spacing / 2 as Coord, spacing))
1257        }
1258        LayoutAlignment::SpaceEvenly => {
1259            let spacing = (size_without_padding - pref_size) / (num_spacings + 2 as Coord);
1260            Some((data.padding.begin + spacing, spacing))
1261        }
1262    };
1263    if let Some((mut pos, spacing)) = align {
1264        for it in &mut layout_data {
1265            it.pos = pos;
1266            it.size = it.pref;
1267            pos += spacing + it.size;
1268        }
1269    }
1270
1271    let mut generator = LayoutCacheGenerator::new(&repeater_indices, &mut result);
1272    for layout in layout_data.iter() {
1273        generator.add(layout.pos, layout.size);
1274    }
1275    result
1276}
1277
1278/// Return the LayoutInfo for a BoxLayout with the given cells.
1279pub fn box_layout_info(
1280    cells: Slice<LayoutItemInfo>,
1281    spacing: Coord,
1282    padding: &Padding,
1283    alignment: LayoutAlignment,
1284) -> LayoutInfo {
1285    let count = cells.len();
1286    let is_stretch = alignment == LayoutAlignment::Stretch;
1287    if count < 1 {
1288        let mut info = LayoutInfo::default();
1289        info.min = padding.begin + padding.end;
1290        info.preferred = info.min;
1291        if is_stretch {
1292            info.max = info.min;
1293        }
1294        return info;
1295    };
1296    let extra_w = padding.begin + padding.end + spacing * (count - 1) as Coord;
1297    let min = cells.iter().map(|c| c.constraint.min).sum::<Coord>() + extra_w; // Minimum size of the complete layout
1298    let max = if is_stretch {
1299        (cells.iter().map(|c| c.constraint.max).fold(extra_w, Saturating::add)).max(min)
1300    } else {
1301        Coord::MAX
1302    }; // Maximum size of the complete layout
1303    let preferred = cells.iter().map(|c| c.constraint.preferred_bounded()).sum::<Coord>() + extra_w;
1304    let stretch = cells.iter().map(|c| c.constraint.stretch).sum::<f32>();
1305    LayoutInfo { min, max, min_percent: 0 as _, max_percent: 100 as _, preferred, stretch }
1306}
1307
1308pub fn box_layout_info_ortho(cells: Slice<LayoutItemInfo>, padding: &Padding) -> LayoutInfo {
1309    let extra_w = padding.begin + padding.end;
1310    let mut fold =
1311        cells.iter().fold(LayoutInfo { stretch: f32::MAX, ..Default::default() }, |a, b| {
1312            a.merge(&b.constraint)
1313        });
1314    fold.max = fold.max.max(fold.min);
1315    fold.preferred = fold.preferred.clamp(fold.min, fold.max);
1316    fold.min += extra_w;
1317    fold.max = Saturating::add(fold.max, extra_w);
1318    fold.preferred += extra_w;
1319    fold
1320}
1321
1322/// Helper module for taffy-based flexbox layout
1323mod flexbox_taffy {
1324    use super::{
1325        Coord, FlexboxLayoutAlignContent, FlexboxLayoutAlignItems, FlexboxLayoutAlignSelf,
1326        FlexboxLayoutItemInfo, FlexboxLayoutWrap as SlintFlexboxLayoutWrap, LayoutAlignment,
1327        Padding, Slice,
1328    };
1329    use alloc::vec::Vec;
1330    pub use taffy::prelude::FlexDirection as TaffyFlexDirection;
1331    use taffy::prelude::*;
1332
1333    /// Parameters for FlexboxTaffyBuilder::new
1334    pub struct FlexboxLayoutParams<'a> {
1335        pub cells_h: &'a Slice<'a, FlexboxLayoutItemInfo>,
1336        pub cells_v: &'a Slice<'a, FlexboxLayoutItemInfo>,
1337        pub spacing_h: Coord,
1338        pub spacing_v: Coord,
1339        pub padding_h: &'a Padding,
1340        pub padding_v: &'a Padding,
1341        pub alignment: LayoutAlignment,
1342        pub align_content: FlexboxLayoutAlignContent,
1343        pub align_items: FlexboxLayoutAlignItems,
1344        pub flex_wrap: SlintFlexboxLayoutWrap,
1345        pub flex_direction: TaffyFlexDirection,
1346        pub container_width: Option<Coord>,
1347        pub container_height: Option<Coord>,
1348        /// When true, set the cross-axis dimension to `auto` for all items
1349        /// so that the measure callback can compute it dynamically (height-for-width).
1350        pub use_measure_for_cross_axis: bool,
1351    }
1352
1353    /// Build a taffy tree from Slint layout constraints.
1354    /// The NodeContext (usize) stores the original child index for measure callbacks.
1355    pub struct FlexboxTaffyBuilder {
1356        pub taffy: TaffyTree<usize>,
1357        pub children: Vec<NodeId>,
1358        pub container: NodeId,
1359        /// Maps taffy child position -> original cell index (empty if no reordering needed)
1360        pub order_map: Vec<usize>,
1361    }
1362
1363    impl FlexboxTaffyBuilder {
1364        /// Create a new flexbox layout tree from item constraints
1365        pub fn new(params: FlexboxLayoutParams) -> Self {
1366            let mut taffy = TaffyTree::<usize>::new();
1367
1368            // Create child nodes from Slint constraints
1369            let mut children: Vec<NodeId> = params
1370                .cells_h
1371                .iter()
1372                .enumerate()
1373                .map(|(idx, cell_h)| {
1374                    let cell_v = params.cells_v.get(idx);
1375                    let h_constraint = &cell_h.constraint;
1376                    let v_constraint = cell_v.map(|c| &c.constraint);
1377
1378                    // Use preferred_bounded() which clamps preferred to min/max bounds
1379                    let preferred_width = h_constraint.preferred_bounded();
1380                    let preferred_height =
1381                        v_constraint.map(|vc| vc.preferred_bounded()).unwrap_or(0 as Coord);
1382
1383                    // flex_basis: use explicit value if set (>= 0), otherwise use preferred size
1384                    let flex_basis = if cell_h.flex_basis >= 0 as Coord {
1385                        Dimension::length(cell_h.flex_basis as _)
1386                    } else {
1387                        match params.flex_direction {
1388                            TaffyFlexDirection::Row | TaffyFlexDirection::RowReverse => {
1389                                Dimension::length(preferred_width as _)
1390                            }
1391                            TaffyFlexDirection::Column | TaffyFlexDirection::ColumnReverse => {
1392                                Dimension::length(preferred_height as _)
1393                            }
1394                        }
1395                    };
1396
1397                    taffy
1398                        .new_leaf_with_context(
1399                            Style {
1400                                flex_basis,
1401                                size: Size {
1402                                    width: match params.flex_direction {
1403                                        TaffyFlexDirection::Column
1404                                        | TaffyFlexDirection::ColumnReverse => {
1405                                            if let Some(cw) = params.container_width {
1406                                                Dimension::length(cw as _)
1407                                            } else if preferred_width > 0 as Coord {
1408                                                Dimension::length(preferred_width as _)
1409                                            } else {
1410                                                Dimension::auto()
1411                                            }
1412                                        }
1413                                        _ => Dimension::auto(),
1414                                    },
1415                                    height: match params.flex_direction {
1416                                        TaffyFlexDirection::Row
1417                                        | TaffyFlexDirection::RowReverse => {
1418                                            // Cross-axis for row: use auto when measure
1419                                            // callback will compute it dynamically
1420                                            if params.use_measure_for_cross_axis {
1421                                                Dimension::auto()
1422                                            } else if preferred_height > 0 as Coord {
1423                                                Dimension::length(preferred_height as _)
1424                                            } else {
1425                                                Dimension::auto()
1426                                            }
1427                                        }
1428                                        _ => Dimension::auto(),
1429                                    },
1430                                },
1431                                min_size: Size {
1432                                    width: Dimension::length(h_constraint.min as _),
1433                                    height: Dimension::length(
1434                                        v_constraint.map(|vc| vc.min as f32).unwrap_or(0.0),
1435                                    ),
1436                                },
1437                                max_size: Size {
1438                                    width: if h_constraint.max < Coord::MAX {
1439                                        Dimension::length(h_constraint.max as _)
1440                                    } else {
1441                                        Dimension::auto()
1442                                    },
1443                                    height: if let Some(vc) = v_constraint {
1444                                        if vc.max < Coord::MAX {
1445                                            Dimension::length(vc.max as _)
1446                                        } else {
1447                                            Dimension::auto()
1448                                        }
1449                                    } else {
1450                                        Dimension::auto()
1451                                    },
1452                                },
1453                                flex_grow: cell_h.flex_grow,
1454                                flex_shrink: cell_h.flex_shrink,
1455                                align_self: match cell_h.flex_align_self {
1456                                    FlexboxLayoutAlignSelf::Auto => None,
1457                                    FlexboxLayoutAlignSelf::Stretch => Some(AlignSelf::Stretch),
1458                                    FlexboxLayoutAlignSelf::Start => Some(AlignSelf::FlexStart),
1459                                    FlexboxLayoutAlignSelf::End => Some(AlignSelf::FlexEnd),
1460                                    FlexboxLayoutAlignSelf::Center => Some(AlignSelf::Center),
1461                                },
1462                                ..Default::default()
1463                            },
1464                            idx,
1465                        )
1466                        .unwrap() // cannot fail
1467                })
1468                .collect();
1469
1470            // Sort children by CSS `order` property if any item has a non-zero order.
1471            // Build a mapping from sorted position -> original index.
1472            let has_order = params.cells_h.iter().any(|c| c.flex_order != 0);
1473            let order_map: Vec<usize> = if has_order {
1474                let mut indices: Vec<usize> = (0..children.len()).collect();
1475                // sort_by_key is a stable sort, as required by CSS
1476                indices.sort_by_key(|&i| params.cells_h.get(i).map_or(0, |c| c.flex_order));
1477                let sorted_children: Vec<NodeId> = indices.iter().map(|&i| children[i]).collect();
1478                children = sorted_children;
1479                indices
1480            } else {
1481                Vec::new()
1482            };
1483
1484            // Create container node
1485            let container = taffy
1486                .new_with_children(
1487                    Style {
1488                        display: Display::Flex,
1489                        flex_direction: params.flex_direction,
1490                        flex_wrap: match params.flex_wrap {
1491                            SlintFlexboxLayoutWrap::Wrap => FlexWrap::Wrap,
1492                            SlintFlexboxLayoutWrap::NoWrap => FlexWrap::NoWrap,
1493                            SlintFlexboxLayoutWrap::WrapReverse => FlexWrap::WrapReverse,
1494                        },
1495                        justify_content: Some(match params.alignment {
1496                            // Start/End map to FlexStart/FlexEnd to respect flex direction (including reverse)
1497                            // AlignContent::Start/End would ignore direction and always use writing mode
1498                            LayoutAlignment::Start => AlignContent::FlexStart,
1499                            LayoutAlignment::End => AlignContent::FlexEnd,
1500                            LayoutAlignment::Center => AlignContent::Center,
1501                            LayoutAlignment::Stretch => AlignContent::Stretch,
1502                            LayoutAlignment::SpaceBetween => AlignContent::SpaceBetween,
1503                            LayoutAlignment::SpaceAround => AlignContent::SpaceAround,
1504                            LayoutAlignment::SpaceEvenly => AlignContent::SpaceEvenly,
1505                        }),
1506                        align_items: Some(match params.align_items {
1507                            FlexboxLayoutAlignItems::Stretch => AlignItems::Stretch,
1508                            FlexboxLayoutAlignItems::Start => AlignItems::FlexStart,
1509                            FlexboxLayoutAlignItems::End => AlignItems::FlexEnd,
1510                            FlexboxLayoutAlignItems::Center => AlignItems::Center,
1511                        }),
1512                        align_content: Some(match params.align_content {
1513                            FlexboxLayoutAlignContent::Stretch => AlignContent::Stretch,
1514                            FlexboxLayoutAlignContent::Start => AlignContent::FlexStart,
1515                            FlexboxLayoutAlignContent::End => AlignContent::FlexEnd,
1516                            FlexboxLayoutAlignContent::Center => AlignContent::Center,
1517                            FlexboxLayoutAlignContent::SpaceBetween => AlignContent::SpaceBetween,
1518                            FlexboxLayoutAlignContent::SpaceAround => AlignContent::SpaceAround,
1519                            FlexboxLayoutAlignContent::SpaceEvenly => AlignContent::SpaceEvenly,
1520                        }),
1521                        gap: Size {
1522                            width: LengthPercentage::length(params.spacing_h as _),
1523                            height: LengthPercentage::length(params.spacing_v as _),
1524                        },
1525                        padding: Rect {
1526                            left: LengthPercentage::length(params.padding_h.begin as _),
1527                            right: LengthPercentage::length(params.padding_h.end as _),
1528                            top: LengthPercentage::length(params.padding_v.begin as _),
1529                            bottom: LengthPercentage::length(params.padding_v.end as _),
1530                        },
1531                        size: Size {
1532                            width: params
1533                                .container_width
1534                                .map(|w| Dimension::length(w as _))
1535                                .unwrap_or(Dimension::auto()),
1536                            height: params
1537                                .container_height
1538                                .map(|h| Dimension::length(h as _))
1539                                .unwrap_or(Dimension::auto()),
1540                        },
1541                        ..Default::default()
1542                    },
1543                    &children,
1544                )
1545                .unwrap(); // cannot fail
1546
1547            Self { taffy, children, container, order_map }
1548        }
1549
1550        /// Compute the layout with the given available space.
1551        ///
1552        /// The optional `measure` callback is called by taffy for leaf nodes
1553        /// that need dynamic height-for-width (or width-for-height) measurement.
1554        /// It receives `(child_index, known_width, known_height)` where `known_width`
1555        /// / `known_height` are `Some` if taffy has already determined that dimension,
1556        /// and returns `(width, height)`.
1557        pub fn compute_layout(
1558            &mut self,
1559            available_width: Coord,
1560            available_height: Coord,
1561            mut measure: Option<
1562                &mut dyn FnMut(usize, Option<Coord>, Option<Coord>) -> (Coord, Coord),
1563            >,
1564        ) {
1565            let available_space = taffy::prelude::Size {
1566                width: if available_width < Coord::MAX {
1567                    AvailableSpace::Definite(available_width as _)
1568                } else {
1569                    AvailableSpace::MaxContent
1570                },
1571                height: if available_height < Coord::MAX {
1572                    AvailableSpace::Definite(available_height as _)
1573                } else {
1574                    AvailableSpace::MaxContent
1575                },
1576            };
1577            self.taffy
1578                .compute_layout_with_measure(
1579                    self.container,
1580                    available_space,
1581                    |known_dimensions, _available_space, _node_id, node_context, _style| {
1582                        if let (Some(measure), Some(&mut child_index)) =
1583                            (measure.as_deref_mut(), node_context)
1584                        {
1585                            let known_w = known_dimensions.width.map(|w| w as Coord);
1586                            let known_h = known_dimensions.height.map(|h| h as Coord);
1587                            let (w, h) = measure(child_index, known_w, known_h);
1588                            taffy::prelude::Size { width: w as f32, height: h as f32 }
1589                        } else {
1590                            taffy::prelude::Size::ZERO
1591                        }
1592                    },
1593                )
1594                .unwrap_or_else(|e| {
1595                    crate::debug_log!("FlexboxLayout computation error: {}", e);
1596                });
1597        }
1598
1599        /// Get the computed container size
1600        pub fn container_size(&self) -> (Coord, Coord) {
1601            let layout = self.taffy.layout(self.container).unwrap();
1602            (layout.size.width as Coord, layout.size.height as Coord)
1603        }
1604
1605        /// Get the geometry for a specific child
1606        pub fn child_geometry(&self, idx: usize) -> (Coord, Coord, Coord, Coord) {
1607            let layout = self.taffy.layout(self.children[idx]).unwrap();
1608            (
1609                layout.location.x as Coord,
1610                layout.location.y as Coord,
1611                layout.size.width as Coord,
1612                layout.size.height as Coord,
1613            )
1614        }
1615
1616        /// Map a taffy child index to the original cell index (accounting for `order` sorting).
1617        pub fn original_index(&self, taffy_idx: usize) -> usize {
1618            if self.order_map.is_empty() { taffy_idx } else { self.order_map[taffy_idx] }
1619        }
1620    }
1621}
1622
1623/// A cache generator for FlexboxLayout that handles 4 values per item (x, y, width, height)
1624struct FlexboxLayoutCacheGenerator<'a> {
1625    // Input
1626    repeater_indices: &'a [u32],
1627    // An always increasing counter, the index of the cell being added
1628    counter: usize,
1629    // The index/4 in result in which we should add the next repeated item
1630    repeat_offset: usize,
1631    // The index/4 in repeater_indices
1632    next_rep: usize,
1633    // The index/4 in result in which we should add the next non-repeated item
1634    current_offset: usize,
1635    // Output
1636    result: &'a mut SharedVector<Coord>,
1637}
1638
1639impl<'a> FlexboxLayoutCacheGenerator<'a> {
1640    fn new(repeater_indices: &'a [u32], result: &'a mut SharedVector<Coord>) -> Self {
1641        // Calculate total repeated cells (count for each repeater)
1642        let total_repeated_cells: usize = repeater_indices
1643            .chunks(2)
1644            .map(|chunk| chunk.get(1).copied().unwrap_or(0) as usize)
1645            .sum();
1646        assert!(result.len() >= total_repeated_cells * 4);
1647        let repeat_offset = result.len() / 4 - total_repeated_cells;
1648        Self { repeater_indices, counter: 0, repeat_offset, next_rep: 0, current_offset: 0, result }
1649    }
1650
1651    fn add(&mut self, x: Coord, y: Coord, w: Coord, h: Coord) {
1652        let res = self.result.make_mut_slice();
1653        let o = loop {
1654            if let Some(nr) = self.repeater_indices.get(self.next_rep * 2) {
1655                let nr = *nr as usize;
1656                if nr == self.counter {
1657                    // Write jump entries for repeater start
1658                    // Store the base offset (index into the repeated data region)
1659                    res[self.current_offset * 4] = (self.repeat_offset * 4) as Coord;
1660                    res[self.current_offset * 4 + 1] = (self.repeat_offset * 4 + 1) as Coord;
1661                    res[self.current_offset * 4 + 2] = (self.repeat_offset * 4 + 2) as Coord;
1662                    res[self.current_offset * 4 + 3] = (self.repeat_offset * 4 + 3) as Coord;
1663                    self.current_offset += 1;
1664                }
1665                if self.counter >= nr {
1666                    let rep_count = self.repeater_indices[self.next_rep * 2 + 1] as usize;
1667                    if self.counter - nr == rep_count {
1668                        // Advance repeat_offset past this repeater's data before moving to next
1669                        self.repeat_offset += rep_count;
1670                        self.next_rep += 1;
1671                        continue;
1672                    }
1673                    // Calculate offset into repeated data
1674                    let cell_in_rep = self.counter - nr;
1675                    let offset = self.repeat_offset + cell_in_rep;
1676                    break offset;
1677                }
1678            }
1679            self.current_offset += 1;
1680            break self.current_offset - 1;
1681        };
1682        res[o * 4] = x;
1683        res[o * 4 + 1] = y;
1684        res[o * 4 + 2] = w;
1685        res[o * 4 + 3] = h;
1686        self.counter += 1;
1687    }
1688}
1689
1690/// Measure callback for height-for-width items in a FlexboxLayout.
1691///
1692/// Called by taffy during the flex solve for items that need dynamic sizing.
1693/// Receives `(child_index, known_width, known_height)` where `known_width`/`known_height`
1694/// are `Some` if taffy has already determined that dimension.
1695/// Returns `(width, height)`.
1696pub type FlexboxMeasureFn<'a> =
1697    Option<&'a mut dyn FnMut(usize, Option<Coord>, Option<Coord>) -> (Coord, Coord)>;
1698
1699pub fn solve_flexbox_layout(
1700    data: &FlexboxLayoutData,
1701    repeater_indices: Slice<u32>,
1702) -> SharedVector<Coord> {
1703    // Build a simple measure callback from the pre-computed cells data.
1704    // This enables height-for-width: taffy calls back with the actual
1705    // assigned width, and we return the pre-computed height from cells_v.
1706    // The cells_v heights were computed with the horizontal preferred size
1707    // as constraint, which is a good approximation.
1708    let mut measure = |child_index: usize,
1709                       known_w: Option<Coord>,
1710                       known_h: Option<Coord>|
1711     -> (Coord, Coord) {
1712        let w = known_w.unwrap_or_else(|| {
1713            data.cells_h.get(child_index).map_or(0 as Coord, |c| c.constraint.preferred_bounded())
1714        });
1715        let h = known_h.unwrap_or_else(|| {
1716            data.cells_v.get(child_index).map_or(0 as Coord, |c| c.constraint.preferred_bounded())
1717        });
1718        (w, h)
1719    };
1720    solve_flexbox_layout_with_measure(data, repeater_indices, Some(&mut measure))
1721}
1722
1723/// Solve a FlexboxLayout using Taffy
1724/// Returns: [x1, y1, w1, h1, x2, y2, w2, h2, ...] for each item
1725pub fn solve_flexbox_layout_with_measure(
1726    data: &FlexboxLayoutData,
1727    repeater_indices: Slice<u32>,
1728    measure: FlexboxMeasureFn<'_>,
1729) -> SharedVector<Coord> {
1730    // 4 values per item: x, y, width, height
1731    let mut result = SharedVector::<Coord>::default();
1732    result.resize(data.cells_h.len() * 4 + repeater_indices.len() * 2, 0 as _);
1733
1734    if data.cells_h.is_empty() {
1735        return result;
1736    }
1737
1738    let taffy_direction = match data.direction {
1739        FlexboxLayoutDirection::Row => flexbox_taffy::TaffyFlexDirection::Row,
1740        FlexboxLayoutDirection::RowReverse => flexbox_taffy::TaffyFlexDirection::RowReverse,
1741        FlexboxLayoutDirection::Column => flexbox_taffy::TaffyFlexDirection::Column,
1742        FlexboxLayoutDirection::ColumnReverse => flexbox_taffy::TaffyFlexDirection::ColumnReverse,
1743    };
1744
1745    let (container_width, container_height) = (
1746        if data.width > 0 as Coord { Some(data.width) } else { None },
1747        if data.height > 0 as Coord { Some(data.height) } else { None },
1748    );
1749
1750    let use_measure = measure.is_some();
1751    let mut builder = flexbox_taffy::FlexboxTaffyBuilder::new(flexbox_taffy::FlexboxLayoutParams {
1752        cells_h: &data.cells_h,
1753        cells_v: &data.cells_v,
1754        spacing_h: data.spacing_h,
1755        spacing_v: data.spacing_v,
1756        padding_h: &data.padding_h,
1757        padding_v: &data.padding_v,
1758        alignment: data.alignment,
1759        align_content: data.align_content,
1760        align_items: data.align_items,
1761        flex_wrap: data.flex_wrap,
1762        flex_direction: taffy_direction,
1763        container_width,
1764        container_height,
1765        use_measure_for_cross_axis: use_measure,
1766    });
1767
1768    let (available_width, available_height) = match data.direction {
1769        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => {
1770            (data.width, Coord::MAX)
1771        }
1772        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => {
1773            (Coord::MAX, data.height)
1774        }
1775    };
1776
1777    builder.compute_layout(available_width, available_height, measure);
1778
1779    // Extract results using the cache generator to handle repeaters.
1780    // If `order` sorting was applied, we need to collect results by original index first,
1781    // because the cache generator expects items in their original declaration order.
1782    if builder.order_map.is_empty() {
1783        let mut generator = FlexboxLayoutCacheGenerator::new(&repeater_indices, &mut result);
1784        for idx in 0..data.cells_h.len() {
1785            let (x, y, w, h) = builder.child_geometry(idx);
1786            generator.add(x, y, w, h);
1787        }
1788    } else {
1789        let count = data.cells_h.len();
1790        let mut geom = alloc::vec![(0 as Coord, 0 as Coord, 0 as Coord, 0 as Coord); count];
1791        for taffy_idx in 0..count {
1792            let orig_idx = builder.original_index(taffy_idx);
1793            geom[orig_idx] = builder.child_geometry(taffy_idx);
1794        }
1795        let mut generator = FlexboxLayoutCacheGenerator::new(&repeater_indices, &mut result);
1796        for (x, y, w, h) in geom {
1797            generator.add(x, y, w, h);
1798        }
1799    }
1800
1801    result
1802}
1803
1804/// Return main-axis LayoutInfo for a FlexboxLayout.
1805/// Only needs the same-axis cells, avoiding a cross-axis binding loop.
1806pub fn flexbox_layout_info_main_axis(
1807    cells: Slice<FlexboxLayoutItemInfo>,
1808    spacing: Coord,
1809    padding: &Padding,
1810    flex_wrap: FlexboxLayoutWrap,
1811) -> LayoutInfo {
1812    let extra_pad = padding.begin + padding.end;
1813    if cells.is_empty() {
1814        return LayoutInfo {
1815            min: extra_pad,
1816            preferred: extra_pad,
1817            max: extra_pad,
1818            ..Default::default()
1819        };
1820    }
1821    let num_spacings = cells.len().saturating_sub(1) as Coord;
1822    let min = if matches!(flex_wrap, FlexboxLayoutWrap::NoWrap) {
1823        cells.iter().map(|c| c.constraint.min).sum::<Coord>() + spacing * num_spacings + extra_pad
1824    } else {
1825        // Wrapping: the widest single item must fit
1826        cells.iter().map(|c| c.constraint.min).fold(0.0 as Coord, |a, b| a.max(b)) + extra_pad
1827    };
1828    let preferred = if matches!(flex_wrap, FlexboxLayoutWrap::NoWrap) {
1829        // No wrapping: all items on one line
1830        cells.iter().map(|c| c.constraint.preferred_bounded()).sum::<Coord>()
1831            + spacing * num_spacings
1832            + extra_pad
1833    } else {
1834        // Wrapping: estimate a roughly square layout using only main-axis sizes.
1835        // Approximate total area assuming each item is square (width == height),
1836        // then take sqrt to get a reasonable main-axis extent.
1837        let total_area: Coord = cells
1838            .iter()
1839            .map(|c| {
1840                let w = c.constraint.preferred_bounded();
1841                w * w
1842            })
1843            .sum();
1844        let count = cells.len();
1845        Float::sqrt(total_area as f32) as Coord + spacing * (count - 1) as Coord + extra_pad
1846    };
1847    let stretch = cells.iter().map(|c| c.constraint.stretch).sum::<f32>();
1848    LayoutInfo {
1849        min,
1850        max: Coord::MAX,
1851        min_percent: 0 as _,
1852        max_percent: 100 as _,
1853        preferred,
1854        stretch,
1855    }
1856}
1857
1858/// Return cross-axis LayoutInfo for a FlexboxLayout.
1859///
1860/// `constraint_size` is the main-axis container dimension (width for row,
1861/// height for column). When valid (> 0 and < MAX), it's used as the taffy
1862/// constraint for accurate wrapping. When invalid (e.g. 0, negative, or
1863/// MAX — which can happen due to circular dependencies in nested
1864/// perpendicular flexboxes), falls back to a heuristic based on
1865/// `flexbox_layout_info_main_axis`.
1866pub fn flexbox_layout_info_cross_axis(
1867    cells_h: Slice<FlexboxLayoutItemInfo>,
1868    cells_v: Slice<FlexboxLayoutItemInfo>,
1869    spacing_h: Coord,
1870    spacing_v: Coord,
1871    padding_h: &Padding,
1872    padding_v: &Padding,
1873    direction: FlexboxLayoutDirection,
1874    flex_wrap: FlexboxLayoutWrap,
1875    constraint_size: Coord,
1876) -> LayoutInfo {
1877    if cells_h.is_empty() {
1878        assert!(cells_v.is_empty());
1879        let orientation = match direction {
1880            FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => {
1881                Orientation::Vertical
1882            }
1883            FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => {
1884                Orientation::Horizontal
1885            }
1886        };
1887        let padding = match orientation {
1888            Orientation::Horizontal => padding_h,
1889            Orientation::Vertical => padding_v,
1890        };
1891        let pad = padding.begin + padding.end;
1892        return LayoutInfo { min: pad, preferred: pad, max: pad, ..Default::default() };
1893    }
1894
1895    // Determine which axis is cross
1896    let cross_cells = match direction {
1897        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => &cells_v,
1898        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => &cells_h,
1899    };
1900
1901    // Compute the main-axis preferred size to use as the constraint for taffy,
1902    // using the same heuristic as flexbox_layout_info_main_axis.
1903    let (main_cells, main_spacing, main_padding) = match direction {
1904        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => {
1905            (&cells_h, spacing_h, padding_h)
1906        }
1907        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => {
1908            (&cells_v, spacing_v, padding_v)
1909        }
1910    };
1911    let main_extra_pad = main_padding.begin + main_padding.end;
1912    let main_axis_constraint = if constraint_size > 0 as Coord && constraint_size < Coord::MAX {
1913        // Use the actual container main-axis dimension (accurate)
1914        constraint_size
1915    } else if matches!(flex_wrap, FlexboxLayoutWrap::NoWrap) {
1916        Coord::MAX
1917    } else {
1918        // Use actual item areas (main * cross) for the heuristic, since both
1919        // axes' cells are available here (unlike flexbox_layout_info_main_axis).
1920        let total_area: Coord = main_cells
1921            .iter()
1922            .zip(cross_cells.iter())
1923            .map(|(m, c)| m.constraint.preferred_bounded() * c.constraint.preferred_bounded())
1924            .sum();
1925        let count = main_cells.len();
1926        Float::sqrt(total_area as f32) as Coord
1927            + main_spacing * (count - 1) as Coord
1928            + main_extra_pad
1929    };
1930
1931    let taffy_direction = match direction {
1932        FlexboxLayoutDirection::Row => flexbox_taffy::TaffyFlexDirection::Row,
1933        FlexboxLayoutDirection::RowReverse => flexbox_taffy::TaffyFlexDirection::RowReverse,
1934        FlexboxLayoutDirection::Column => flexbox_taffy::TaffyFlexDirection::Column,
1935        FlexboxLayoutDirection::ColumnReverse => flexbox_taffy::TaffyFlexDirection::ColumnReverse,
1936    };
1937
1938    let (container_width, container_height) = match direction {
1939        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => {
1940            (Some(main_axis_constraint), None)
1941        }
1942        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => {
1943            (None, Some(main_axis_constraint))
1944        }
1945    };
1946
1947    let mut builder = flexbox_taffy::FlexboxTaffyBuilder::new(flexbox_taffy::FlexboxLayoutParams {
1948        cells_h: &cells_h,
1949        cells_v: &cells_v,
1950        spacing_h,
1951        spacing_v,
1952        padding_h,
1953        padding_v,
1954        alignment: LayoutAlignment::Start,
1955        align_content: FlexboxLayoutAlignContent::Stretch,
1956        align_items: FlexboxLayoutAlignItems::Stretch,
1957        flex_wrap,
1958        flex_direction: taffy_direction,
1959        container_width,
1960        container_height,
1961        use_measure_for_cross_axis: false,
1962    });
1963
1964    let (available_width, available_height) = match direction {
1965        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => {
1966            (main_axis_constraint, Coord::MAX)
1967        }
1968        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => {
1969            (Coord::MAX, main_axis_constraint)
1970        }
1971    };
1972
1973    builder.compute_layout(available_width, available_height, None);
1974
1975    let (total_width, total_height) = builder.container_size();
1976    let cross_size = match direction {
1977        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => total_height,
1978        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => total_width,
1979    };
1980
1981    LayoutInfo {
1982        min: cross_size,
1983        max: Coord::MAX,
1984        min_percent: 0 as _,
1985        max_percent: 100 as _,
1986        preferred: cross_size,
1987        stretch: 0.0,
1988    }
1989}
1990
1991#[cfg(feature = "ffi")]
1992pub(crate) mod ffi {
1993    #![allow(unsafe_code)]
1994
1995    use super::*;
1996
1997    #[unsafe(no_mangle)]
1998    pub extern "C" fn slint_organize_grid_layout(
1999        input_data: Slice<GridLayoutInputData>,
2000        repeater_indices: Slice<u32>,
2001        repeater_steps: Slice<u32>,
2002        result: &mut GridLayoutOrganizedData,
2003    ) {
2004        *result = super::organize_grid_layout(input_data, repeater_indices, repeater_steps);
2005    }
2006
2007    #[unsafe(no_mangle)]
2008    pub extern "C" fn slint_organize_dialog_button_layout(
2009        input_data: Slice<GridLayoutInputData>,
2010        dialog_button_roles: Slice<DialogButtonRole>,
2011        result: &mut GridLayoutOrganizedData,
2012    ) {
2013        *result = super::organize_dialog_button_layout(input_data, dialog_button_roles);
2014    }
2015
2016    #[unsafe(no_mangle)]
2017    pub extern "C" fn slint_solve_grid_layout(
2018        data: &GridLayoutData,
2019        constraints: Slice<LayoutItemInfo>,
2020        orientation: Orientation,
2021        repeater_indices: Slice<u32>,
2022        repeater_steps: Slice<u32>,
2023        result: &mut SharedVector<Coord>,
2024    ) {
2025        *result = super::solve_grid_layout(
2026            data,
2027            constraints,
2028            orientation,
2029            repeater_indices,
2030            repeater_steps,
2031        )
2032    }
2033
2034    #[unsafe(no_mangle)]
2035    pub extern "C" fn slint_grid_layout_info(
2036        organized_data: &GridLayoutOrganizedData,
2037        constraints: Slice<LayoutItemInfo>,
2038        repeater_indices: Slice<u32>,
2039        repeater_steps: Slice<u32>,
2040        spacing: Coord,
2041        padding: &Padding,
2042        orientation: Orientation,
2043    ) -> LayoutInfo {
2044        super::grid_layout_info(
2045            organized_data.clone(),
2046            constraints,
2047            repeater_indices,
2048            repeater_steps,
2049            spacing,
2050            padding,
2051            orientation,
2052        )
2053    }
2054
2055    #[unsafe(no_mangle)]
2056    pub extern "C" fn slint_solve_box_layout(
2057        data: &BoxLayoutData,
2058        repeater_indices: Slice<u32>,
2059        result: &mut SharedVector<Coord>,
2060    ) {
2061        *result = super::solve_box_layout(data, repeater_indices)
2062    }
2063
2064    #[unsafe(no_mangle)]
2065    /// Return the LayoutInfo for a BoxLayout with the given cells.
2066    pub extern "C" fn slint_box_layout_info(
2067        cells: Slice<LayoutItemInfo>,
2068        spacing: Coord,
2069        padding: &Padding,
2070        alignment: LayoutAlignment,
2071    ) -> LayoutInfo {
2072        super::box_layout_info(cells, spacing, padding, alignment)
2073    }
2074
2075    #[unsafe(no_mangle)]
2076    /// Return the LayoutInfo for a BoxLayout with the given cells.
2077    pub extern "C" fn slint_box_layout_info_ortho(
2078        cells: Slice<LayoutItemInfo>,
2079        padding: &Padding,
2080    ) -> LayoutInfo {
2081        super::box_layout_info_ortho(cells, padding)
2082    }
2083
2084    /// The measure callback for C FFI. Returns (width, height) via out pointers.
2085    /// `known_width`/`known_height` are negative if not determined yet.
2086    /// A null function pointer means no measure callback.
2087    pub type FlexboxMeasureFnC = unsafe extern "C" fn(
2088        user_data: *mut core::ffi::c_void,
2089        child_index: usize,
2090        known_width: Coord,
2091        known_height: Coord,
2092        out_width: *mut Coord,
2093        out_height: *mut Coord,
2094    );
2095
2096    #[unsafe(no_mangle)]
2097    #[allow(unsafe_code)]
2098    pub extern "C" fn slint_solve_flexbox_layout(
2099        data: &FlexboxLayoutData,
2100        repeater_indices: Slice<u32>,
2101        result: &mut SharedVector<Coord>,
2102        measure_fn: *const core::ffi::c_void,
2103        measure_user_data: *mut core::ffi::c_void,
2104    ) {
2105        // Safety: measure_fn, when non-null, is a valid FlexboxMeasureFnC function pointer
2106        // passed as *const c_void because cbindgen can't represent Option<fn pointer> in C++.
2107        const {
2108            assert!(
2109                core::mem::size_of::<*const core::ffi::c_void>()
2110                    == core::mem::size_of::<FlexboxMeasureFnC>()
2111            );
2112        }
2113        let measure_fn: Option<FlexboxMeasureFnC> = if measure_fn.is_null() {
2114            None
2115        } else {
2116            Some(unsafe {
2117                core::mem::transmute::<*const core::ffi::c_void, FlexboxMeasureFnC>(measure_fn)
2118            })
2119        };
2120        if let Some(c_measure) = measure_fn {
2121            let mut measure = |child_index: usize,
2122                               known_w: Option<Coord>,
2123                               known_h: Option<Coord>|
2124             -> (Coord, Coord) {
2125                let mut out_w: Coord = 0 as _;
2126                let mut out_h: Coord = 0 as _;
2127                // Safety: c_measure is a valid function pointer provided by the caller,
2128                // and out_w/out_h are valid mutable pointers.
2129                unsafe {
2130                    c_measure(
2131                        measure_user_data,
2132                        child_index,
2133                        known_w.unwrap_or(-1 as _),
2134                        known_h.unwrap_or(-1 as _),
2135                        &mut out_w,
2136                        &mut out_h,
2137                    );
2138                }
2139                (out_w, out_h)
2140            };
2141            *result = super::solve_flexbox_layout_with_measure(
2142                data,
2143                repeater_indices,
2144                Some(&mut measure),
2145            );
2146        } else {
2147            *result = super::solve_flexbox_layout(data, repeater_indices);
2148        }
2149    }
2150
2151    #[unsafe(no_mangle)]
2152    /// Return main-axis LayoutInfo for a FlexboxLayout (single-axis, no cross-axis dependency).
2153    pub extern "C" fn slint_flexbox_layout_info_main_axis(
2154        cells: Slice<FlexboxLayoutItemInfo>,
2155        spacing: Coord,
2156        padding: &Padding,
2157        flex_wrap: FlexboxLayoutWrap,
2158    ) -> LayoutInfo {
2159        super::flexbox_layout_info_main_axis(cells, spacing, padding, flex_wrap)
2160    }
2161
2162    #[unsafe(no_mangle)]
2163    /// Return cross-axis LayoutInfo for a FlexboxLayout.
2164    pub extern "C" fn slint_flexbox_layout_info_cross_axis(
2165        cells_h: Slice<FlexboxLayoutItemInfo>,
2166        cells_v: Slice<FlexboxLayoutItemInfo>,
2167        spacing_h: Coord,
2168        spacing_v: Coord,
2169        padding_h: &Padding,
2170        padding_v: &Padding,
2171        direction: FlexboxLayoutDirection,
2172        flex_wrap: FlexboxLayoutWrap,
2173        constraint_size: Coord,
2174    ) -> LayoutInfo {
2175        super::flexbox_layout_info_cross_axis(
2176            cells_h,
2177            cells_v,
2178            spacing_h,
2179            spacing_v,
2180            padding_h,
2181            padding_v,
2182            direction,
2183            flex_wrap,
2184            constraint_size,
2185        )
2186    }
2187}
2188
2189#[cfg(test)]
2190mod tests {
2191    use super::*;
2192
2193    fn collect_from_organized_data(
2194        organized_data: &GridLayoutOrganizedData,
2195        num_cells: usize,
2196        repeater_indices: Slice<u32>,
2197        repeater_steps: Slice<u32>,
2198    ) -> Vec<(u16, u16, u16, u16)> {
2199        let mut result = Vec::new();
2200        for i in 0..num_cells {
2201            let col_and_span = organized_data.col_or_row_and_span(
2202                i,
2203                Orientation::Horizontal,
2204                &repeater_indices,
2205                &repeater_steps,
2206            );
2207            let row_and_span = organized_data.col_or_row_and_span(
2208                i,
2209                Orientation::Vertical,
2210                &repeater_indices,
2211                &repeater_steps,
2212            );
2213            result.push((col_and_span.0, col_and_span.1, row_and_span.0, row_and_span.1));
2214        }
2215        result
2216    }
2217
2218    #[test]
2219    fn test_organized_data_generator_2_fixed_cells() {
2220        // 2 fixed cells
2221        let mut result = GridLayoutOrganizedData::default();
2222        let num_cells = 2;
2223        let mut generator = OrganizedDataGenerator::new(&[], &[], num_cells, 0, 0, &mut result);
2224        generator.add(0, 1, 0, 1);
2225        generator.add(1, 2, 0, 3);
2226        assert_eq!(result.as_slice(), &[0, 1, 0, 1, 1, 2, 0, 3]);
2227
2228        let repeater_indices = Slice::from_slice(&[]);
2229        let empty_steps = Slice::from_slice(&[]);
2230        let collected_data =
2231            collect_from_organized_data(&result, num_cells, repeater_indices, empty_steps);
2232        assert_eq!(collected_data.as_slice(), &[(0, 1, 0, 1), (1, 2, 0, 3)]);
2233
2234        assert_eq!(
2235            result.max_value(num_cells, Orientation::Horizontal, &repeater_indices, &empty_steps),
2236            3
2237        );
2238        assert_eq!(
2239            result.max_value(num_cells, Orientation::Vertical, &repeater_indices, &empty_steps),
2240            3
2241        );
2242    }
2243
2244    #[test]
2245    fn test_organized_data_generator_1_fixed_cell_1_repeater() {
2246        // 4 cells: 1 fixed cell, 1 repeater with 3 repeated cells
2247        let mut result = GridLayoutOrganizedData::default();
2248        let num_cells = 4;
2249        let repeater_indices = &[1u32, 3u32];
2250        let mut generator =
2251            OrganizedDataGenerator::new(repeater_indices, &[], 1, 1, 3, &mut result);
2252        generator.add(0, 1, 0, 2); // fixed
2253        generator.add(1, 2, 1, 3); // repeated
2254        generator.add(1, 1, 2, 4);
2255        generator.add(2, 2, 3, 5);
2256        assert_eq!(
2257            result.as_slice(),
2258            &[
2259                0, 1, 0, 2, // fixed cell
2260                8, 4, 0, 0, // jump cell: data_base=8, stride=4 (step=1, epi=4)
2261                1, 2, 1, 3, // repeated cell 1
2262                1, 1, 2, 4, // repeated cell 2
2263                2, 2, 3, 5, // repeated cell 3
2264            ]
2265        );
2266        let repeater_indices = Slice::from_slice(repeater_indices);
2267        let empty_steps = Slice::from_slice(&[]);
2268        let collected_data =
2269            collect_from_organized_data(&result, num_cells, repeater_indices, empty_steps);
2270        assert_eq!(
2271            collected_data.as_slice(),
2272            &[(0, 1, 0, 2), (1, 2, 1, 3), (1, 1, 2, 4), (2, 2, 3, 5)]
2273        );
2274
2275        assert_eq!(
2276            result.max_value(num_cells, Orientation::Horizontal, &repeater_indices, &empty_steps),
2277            4
2278        );
2279        assert_eq!(
2280            result.max_value(num_cells, Orientation::Vertical, &repeater_indices, &empty_steps),
2281            8
2282        );
2283    }
2284
2285    #[test]
2286
2287    fn test_organize_data_with_auto_and_spans() {
2288        let auto = i_slint_common::ROW_COL_AUTO;
2289        let input = std::vec![
2290            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 2., rowspan: -1. },
2291            GridLayoutInputData { new_row: false, col: auto, row: auto, colspan: 1., rowspan: 2. },
2292            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 2., rowspan: 1. },
2293            GridLayoutInputData { new_row: true, col: -2., row: 80000., colspan: 2., rowspan: 1. },
2294        ];
2295        let repeater_indices = Slice::from_slice(&[]);
2296        let (organized_data, errors) = organize_grid_layout_impl(
2297            Slice::from_slice(&input),
2298            repeater_indices,
2299            Slice::from_slice(&[]),
2300        );
2301        assert_eq!(
2302            organized_data.as_slice(),
2303            &[
2304                0, 2, 0, 0, // row 0, col 0, rowspan 0 (see below)
2305                2, 1, 0, 2, // row 0, col 2 (due to colspan of first cell)
2306                0, 2, 1, 1, // row 1, col 0
2307                0, 2, 65535, 1, // row 65535, col 0
2308            ]
2309        );
2310        assert_eq!(errors.len(), 3);
2311        // Note that a rowspan of 0 is valid, it means the cell doesn't occupy any row
2312        assert_eq!(errors[0], "cell rowspan -1 is negative, clamping to 0");
2313        assert_eq!(errors[1], "cell row 80000 is too large, clamping to 65535");
2314        assert_eq!(errors[2], "cell col -2 is negative, clamping to 0");
2315        let empty_steps = Slice::from_slice(&[]);
2316        let collected_data = collect_from_organized_data(
2317            &organized_data,
2318            input.len(),
2319            repeater_indices,
2320            empty_steps,
2321        );
2322        assert_eq!(
2323            collected_data.as_slice(),
2324            &[(0, 2, 0, 0), (2, 1, 0, 2), (0, 2, 1, 1), (0, 2, 65535, 1)]
2325        );
2326        assert_eq!(
2327            organized_data.max_value(3, Orientation::Horizontal, &repeater_indices, &empty_steps),
2328            3
2329        );
2330        assert_eq!(
2331            organized_data.max_value(3, Orientation::Vertical, &repeater_indices, &empty_steps),
2332            2
2333        );
2334    }
2335
2336    #[test]
2337    fn test_organize_data_1_empty_repeater() {
2338        // Row { Text {}    if false: Text {} }, this test shows why we need i32 for cell_nr_adj
2339        let auto = i_slint_common::ROW_COL_AUTO;
2340        let cell =
2341            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 1., rowspan: 1. };
2342        let input = std::vec![cell];
2343        let repeater_indices = Slice::from_slice(&[1u32, 0u32]);
2344        let (organized_data, errors) = organize_grid_layout_impl(
2345            Slice::from_slice(&input),
2346            repeater_indices,
2347            Slice::from_slice(&[]),
2348        );
2349        assert_eq!(
2350            organized_data.as_slice(),
2351            &[
2352                0, 1, 0, 1, // fixed
2353                0, 0, 0, 0
2354            ] // jump to repeater data (not used)
2355        );
2356        assert_eq!(errors.len(), 0);
2357        let empty_steps = Slice::from_slice(&[]);
2358        let collected_data = collect_from_organized_data(
2359            &organized_data,
2360            input.len(),
2361            repeater_indices,
2362            empty_steps,
2363        );
2364        assert_eq!(collected_data.as_slice(), &[(0, 1, 0, 1)]);
2365        assert_eq!(
2366            organized_data.max_value(1, Orientation::Horizontal, &repeater_indices, &empty_steps),
2367            1
2368        );
2369    }
2370
2371    #[test]
2372    fn test_organize_data_4_repeaters() {
2373        let auto = i_slint_common::ROW_COL_AUTO;
2374        let mut cell =
2375            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 1., rowspan: 1. };
2376        let mut input = std::vec![cell.clone()];
2377        for _ in 0..8 {
2378            cell.new_row = false;
2379            input.push(cell.clone());
2380        }
2381        let repeater_indices = Slice::from_slice(&[0u32, 0u32, 1u32, 4u32, 6u32, 2u32, 8u32, 0u32]);
2382        let (organized_data, errors) = organize_grid_layout_impl(
2383            Slice::from_slice(&input),
2384            repeater_indices,
2385            Slice::from_slice(&[]),
2386        );
2387        assert_eq!(
2388            organized_data.as_slice(),
2389            &[
2390                28, 4, 0, 0, // rep0 jump: data at 28, stride=4 (empty)
2391                0, 1, 0, 1, // fixed cell (col=0)
2392                28, 4, 0, 0, // rep1 jump: data at 28, stride=4 (4 rows)
2393                5, 1, 0, 1, // fixed cell (col=5)
2394                44, 4, 0, 0, // rep2 jump: data at 44, stride=4 (2 rows)
2395                52, 4, 0, 0, // rep3 jump: data at 52, stride=4 (empty)
2396                8, 1, 0, 1, // fixed cell (col=8)
2397                1, 1, 0, 1, // rep1 row 0
2398                2, 1, 0, 1, // rep1 row 1
2399                3, 1, 0, 1, // rep1 row 2
2400                4, 1, 0, 1, // rep1 row 3
2401                6, 1, 0, 1, // rep2 row 0
2402                7, 1, 0, 1, // rep2 row 1
2403            ]
2404        );
2405        assert_eq!(errors.len(), 0);
2406        let empty_steps = Slice::from_slice(&[]);
2407        let collected_data = collect_from_organized_data(
2408            &organized_data,
2409            input.len(),
2410            repeater_indices,
2411            empty_steps,
2412        );
2413        assert_eq!(
2414            collected_data.as_slice(),
2415            &[
2416                (0, 1, 0, 1),
2417                (1, 1, 0, 1),
2418                (2, 1, 0, 1),
2419                (3, 1, 0, 1),
2420                (4, 1, 0, 1),
2421                (5, 1, 0, 1),
2422                (6, 1, 0, 1),
2423                (7, 1, 0, 1),
2424                (8, 1, 0, 1),
2425            ]
2426        );
2427        let empty_steps = Slice::from_slice(&[]);
2428        assert_eq!(
2429            organized_data.max_value(
2430                input.len(),
2431                Orientation::Horizontal,
2432                &repeater_indices,
2433                &empty_steps
2434            ),
2435            9
2436        );
2437    }
2438
2439    #[test]
2440    fn test_organize_data_repeated_rows() {
2441        let auto = i_slint_common::ROW_COL_AUTO;
2442        let mut input = Vec::new();
2443        let num_rows: u32 = 3;
2444        let num_columns: u32 = 2;
2445        // 3 rows of 2 columns each
2446        for _ in 0..num_rows {
2447            let mut cell = GridLayoutInputData {
2448                new_row: true,
2449                col: auto,
2450                row: auto,
2451                colspan: 1.,
2452                rowspan: 1.,
2453            };
2454            input.push(cell.clone());
2455            cell.new_row = false;
2456            input.push(cell.clone());
2457        }
2458        // Repeater 0: starts at index 0, has 3 instances of 2 elements
2459        let repeater_indices_arr = [0_u32, num_rows];
2460        let repeater_steps_arr = [num_columns];
2461        let repeater_steps = Slice::from_slice(&repeater_steps_arr);
2462        let repeater_indices = Slice::from_slice(&repeater_indices_arr);
2463        let (organized_data, errors) =
2464            organize_grid_layout_impl(Slice::from_slice(&input), repeater_indices, repeater_steps);
2465        assert_eq!(
2466            organized_data.as_slice(),
2467            &[
2468                4, 8, 0, 0, // jump cell: data at u16 idx 4, stride=8 (=step*4=2*4)
2469                0, 1, 0, 1, 1, 1, 0, 1, // row 0: col 0, col 1
2470                0, 1, 1, 1, 1, 1, 1, 1, // row 1: col 0, col 1
2471                0, 1, 2, 1, 1, 1, 2, 1, // row 2: col 0, col 1
2472            ]
2473        );
2474        assert_eq!(errors.len(), 0);
2475        let collected_data = collect_from_organized_data(
2476            &organized_data,
2477            input.len(),
2478            repeater_indices,
2479            repeater_steps,
2480        );
2481        assert_eq!(
2482            collected_data.as_slice(),
2483            // (col, colspan, row, rowspan) for each cell in input order
2484            &[(0, 1, 0, 1), (1, 1, 0, 1), (0, 1, 1, 1), (1, 1, 1, 1), (0, 1, 2, 1), (1, 1, 2, 1),]
2485        );
2486        assert_eq!(
2487            organized_data.max_value(
2488                input.len(),
2489                Orientation::Horizontal,
2490                &repeater_indices,
2491                &repeater_steps
2492            ),
2493            2
2494        );
2495        assert_eq!(
2496            organized_data.max_value(
2497                input.len(),
2498                Orientation::Vertical,
2499                &repeater_indices,
2500                &repeater_steps
2501            ),
2502            3
2503        );
2504
2505        // Now test GridLayoutCacheGenerator
2506        let mut layout_cache_v = SharedVector::<Coord>::default();
2507        let mut generator = GridLayoutCacheGenerator::new(
2508            repeater_indices.as_slice(),
2509            repeater_steps.as_slice(),
2510            0, // static_cells
2511            1, // num_repeaters
2512            6, // total_repeated_cells (3 rows * 2 columns)
2513            &mut layout_cache_v,
2514        );
2515        // Row 0
2516        generator.add(0., 50.);
2517        generator.add(0., 50.);
2518        // Row 1
2519        generator.add(50., 50.);
2520        generator.add(50., 50.);
2521        // Row 2
2522        generator.add(100., 50.);
2523        generator.add(100., 50.);
2524        assert_eq!(
2525            layout_cache_v.as_slice(),
2526            &[
2527                2., 4., // jump cell: data at pos 2, stride=4 (=step*2=2*2)
2528                0., 50., 0., 50., // row 0
2529                50., 50., 50., 50., // row 1
2530                100., 50., 100., 50., // row 2
2531            ]
2532        );
2533
2534        // GridRepeaterCacheAccess: cache[cache[jump_index] + ri * stride + child_offset]
2535        let layout_cache_v_access = |jump_index: usize,
2536                                     repeater_index: usize,
2537                                     stride: usize,
2538                                     child_offset: usize|
2539         -> Coord {
2540            let base = layout_cache_v[jump_index] as usize;
2541            let data_idx = base + repeater_index * stride + child_offset;
2542            layout_cache_v[data_idx]
2543        };
2544        // stride=4 (step=2, entries_per_item=2)
2545        // Y pos for child 0 (child_offset=0)
2546        assert_eq!(layout_cache_v_access(0, 0, 4, 0), 0.);
2547        assert_eq!(layout_cache_v_access(0, 1, 4, 0), 50.);
2548        assert_eq!(layout_cache_v_access(0, 2, 4, 0), 100.);
2549        // Y pos for child 1 (child_offset=2)
2550        assert_eq!(layout_cache_v_access(0, 0, 4, 2), 0.);
2551        assert_eq!(layout_cache_v_access(0, 1, 4, 2), 50.);
2552        assert_eq!(layout_cache_v_access(0, 2, 4, 2), 100.);
2553    }
2554
2555    #[test]
2556    fn test_organize_data_repeated_rows_multiple_repeaters() {
2557        let auto = i_slint_common::ROW_COL_AUTO;
2558        let mut input = Vec::new();
2559        let num_rows: u32 = 5;
2560        let mut cell =
2561            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 1., rowspan: 1. };
2562        // 3 rows of 2 columns each
2563        for _ in 0..3 {
2564            cell.new_row = true;
2565            input.push(cell.clone());
2566            cell.new_row = false;
2567            input.push(cell.clone());
2568        }
2569        // 2 rows of 3 columns each
2570        for _ in 0..2 {
2571            cell.new_row = true;
2572            input.push(cell.clone());
2573            cell.new_row = false;
2574            input.push(cell.clone());
2575            cell.new_row = false;
2576            input.push(cell.clone());
2577        }
2578        // Repeater 0: starts at index 0, has 3 instances of 2 elements
2579        // Repeater 1: starts at index 6 (after repeater 0's 3*2=6 cells), has 2 instances of 3 elements
2580        let repeater_indices_arr = [0_u32, 3, 6, 2];
2581        let repeater_steps_arr = [2, 3];
2582        let repeater_steps = Slice::from_slice(&repeater_steps_arr);
2583        let repeater_indices = Slice::from_slice(&repeater_indices_arr);
2584        let (organized_data, errors) =
2585            organize_grid_layout_impl(Slice::from_slice(&input), repeater_indices, repeater_steps);
2586        assert_eq!(
2587            organized_data.as_slice(),
2588            &[
2589                8, 8, 0, 0, // repeater 0 jump: data at 8, stride=8 (=step*4=2*4)
2590                32, 12, 0, 0, // repeater 1 jump: data at 32, stride=12 (=step*4=3*4)
2591                // Repeater 0 data
2592                0, 1, 0, 1, 1, 1, 0, 1, // row 0: col 0, col 1
2593                0, 1, 1, 1, 1, 1, 1, 1, // row 1: col 0, col 1
2594                0, 1, 2, 1, 1, 1, 2, 1, // row 2: col 0, col 1
2595                // Repeater 1 data
2596                0, 1, 3, 1, 1, 1, 3, 1, 2, 1, 3, 1, // row 0: col 0, col 1, col 2
2597                0, 1, 4, 1, 1, 1, 4, 1, 2, 1, 4, 1, // row 1: col 0, col 1, col 2
2598            ]
2599        );
2600        assert_eq!(errors.len(), 0);
2601        let collected_data = collect_from_organized_data(
2602            &organized_data,
2603            input.len(),
2604            repeater_indices,
2605            repeater_steps,
2606        );
2607        assert_eq!(
2608            collected_data.as_slice(),
2609            // (col, colspan, row, rowspan) for each cell in input order
2610            &[
2611                (0, 1, 0, 1),
2612                (1, 1, 0, 1),
2613                (0, 1, 1, 1),
2614                (1, 1, 1, 1),
2615                (0, 1, 2, 1),
2616                (1, 1, 2, 1),
2617                (0, 1, 3, 1),
2618                (1, 1, 3, 1),
2619                (2, 1, 3, 1),
2620                (0, 1, 4, 1),
2621                (1, 1, 4, 1),
2622                (2, 1, 4, 1)
2623            ]
2624        );
2625        assert_eq!(
2626            organized_data.max_value(
2627                input.len(),
2628                Orientation::Horizontal,
2629                &repeater_indices,
2630                &repeater_steps
2631            ),
2632            3 // max col (2) + colspan (1) = 3
2633        );
2634        assert_eq!(
2635            organized_data.max_value(
2636                input.len(),
2637                Orientation::Vertical,
2638                &repeater_indices,
2639                &repeater_steps
2640            ),
2641            num_rows as u16 // max row (4) + rowspan (1) = 5
2642        );
2643
2644        // Now test GridLayoutCacheGenerator
2645        let mut layout_cache_v = SharedVector::<Coord>::default();
2646        let mut generator = GridLayoutCacheGenerator::new(
2647            repeater_indices.as_slice(),
2648            repeater_steps.as_slice(),
2649            0,  // static_cells
2650            2,  // num_repeaters
2651            12, // total_repeated_cells (3*2 + 2*3)
2652            &mut layout_cache_v,
2653        );
2654        // Row 0
2655        generator.add(0., 50.);
2656        generator.add(0., 50.);
2657        // Row 1
2658        generator.add(50., 50.);
2659        generator.add(50., 50.);
2660        // Row 2
2661        generator.add(100., 50.);
2662        generator.add(100., 50.);
2663        // Row 3
2664        generator.add(150., 50.);
2665        generator.add(150., 50.);
2666        generator.add(150., 50.);
2667        // Row 4
2668        generator.add(200., 50.);
2669        generator.add(200., 50.);
2670        generator.add(200., 50.);
2671        assert_eq!(
2672            layout_cache_v.as_slice(),
2673            &[
2674                4., 4., // repeater 0 jump: data at pos 4, stride=4 (=step*2=2*2)
2675                16., 6., // repeater 1 jump: data at pos 16, stride=6 (=step*2=3*2)
2676                0., 50., 0., 50., // repeater 0 row 0 data
2677                50., 50., 50., 50., // repeater 0 row 1 data
2678                100., 50., 100., 50., // repeater 0 row 2 data
2679                150., 50., 150., 50., 150., 50., // repeater 1 row 3 data
2680                200., 50., 200., 50., 200., 50., // repeater 1 row 4 data
2681            ]
2682        );
2683
2684        // GridRepeaterCacheAccess: cache[cache[jump_index] + ri * stride + child_offset]
2685        let layout_cache_v_access = |jump_index: usize,
2686                                     repeater_index: usize,
2687                                     stride: usize,
2688                                     child_offset: usize|
2689         -> Coord {
2690            let base = layout_cache_v[jump_index] as usize;
2691            let data_idx = base + repeater_index * stride + child_offset;
2692            layout_cache_v[data_idx]
2693        };
2694        // Repeater 0: Y pos for child 0 (child_offset=0), stride=4
2695        assert_eq!(layout_cache_v_access(0, 0, 4, 0), 0.);
2696        assert_eq!(layout_cache_v_access(0, 1, 4, 0), 50.);
2697        assert_eq!(layout_cache_v_access(0, 2, 4, 0), 100.);
2698        // Repeater 0: Y pos for child 1 (child_offset=2), stride=4
2699        assert_eq!(layout_cache_v_access(0, 0, 4, 2), 0.);
2700        assert_eq!(layout_cache_v_access(0, 1, 4, 2), 50.);
2701        assert_eq!(layout_cache_v_access(0, 2, 4, 2), 100.);
2702        // Repeater 1: Y pos for child 0 (child_offset=0), jump at index 2, stride=6
2703        assert_eq!(layout_cache_v_access(2, 0, 6, 0), 150.);
2704        assert_eq!(layout_cache_v_access(2, 1, 6, 0), 200.);
2705        // Repeater 1: Y pos for child 2 (child_offset=4), jump at index 2, stride=6
2706        assert_eq!(layout_cache_v_access(2, 0, 6, 4), 150.);
2707        assert_eq!(layout_cache_v_access(2, 1, 6, 4), 200.);
2708    }
2709
2710    #[test]
2711    fn test_layout_cache_generator_2_fixed_cells() {
2712        // 2 fixed cells
2713        let mut result = SharedVector::<Coord>::default();
2714        result.resize(2 * 2, 0 as _);
2715        let mut generator = LayoutCacheGenerator::new(&[], &mut result);
2716        generator.add(0., 50.); // fixed
2717        generator.add(80., 50.); // fixed
2718        assert_eq!(result.as_slice(), &[0., 50., 80., 50.]);
2719    }
2720
2721    #[test]
2722    fn test_layout_cache_generator_1_fixed_cell_1_repeater() {
2723        // 4 cells: 1 fixed cell, 1 repeater with 3 repeated cells
2724        let mut result = SharedVector::<Coord>::default();
2725        let repeater_indices = &[1, 3];
2726        result.resize(4 * 2 + repeater_indices.len(), 0 as _);
2727        let mut generator = LayoutCacheGenerator::new(repeater_indices, &mut result);
2728        generator.add(0., 50.); // fixed
2729        generator.add(80., 50.); // repeated
2730        generator.add(160., 50.);
2731        generator.add(240., 50.);
2732        assert_eq!(
2733            result.as_slice(),
2734            &[
2735                0., 50., // fixed
2736                4., 5., // jump to repeater data
2737                80., 50., 160., 50., 240., 50. // repeater data
2738            ]
2739        );
2740    }
2741
2742    #[test]
2743    fn test_layout_cache_generator_4_repeaters() {
2744        // 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
2745        let mut result = SharedVector::<Coord>::default();
2746        let repeater_indices = &[1, 0, 1, 4, 6, 2, 8, 0];
2747        result.resize(8 * 2 + repeater_indices.len(), 0 as _);
2748        let mut generator = LayoutCacheGenerator::new(repeater_indices, &mut result);
2749        generator.add(0., 50.); // fixed
2750        generator.add(80., 10.); // repeated
2751        generator.add(160., 10.);
2752        generator.add(240., 10.);
2753        generator.add(320., 10.); // end of second repeater
2754        generator.add(400., 80.); // fixed
2755        generator.add(500., 20.); // repeated
2756        generator.add(600., 20.); // end of third repeater
2757        assert_eq!(
2758            result.as_slice(),
2759            &[
2760                0., 50., // fixed
2761                12., 13., // jump to first (empty) repeater (not used)
2762                12., 13., // jump to second repeater data
2763                400., 80., // fixed
2764                20., 21., // jump to third repeater data
2765                0., 0., // slot for jumping to fourth repeater (currently empty)
2766                80., 10., 160., 10., 240., 10., 320., 10., // first repeater data
2767                500., 20., 600., 20. // second repeater data
2768            ]
2769        );
2770    }
2771}