Skip to main content

i_slint_core/
layout.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! Runtime support for layouts.
5
6// cspell:ignore coord
7
8use crate::items::{
9    CrossAxisAlignment, DialogButtonRole, FlexboxLayoutAlignContent, 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        );
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 = &mut layout_data
385                        [(col_or_row as usize)..(col_or_row as usize + 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    ) -> usize {
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            // Widen to usize: a cell with an out-of-range row/col is clamped to u16::MAX, so adding
572            // the span here in u16 would overflow and under-size the layout vector.
573            max = max.max(col_or_row as usize + span.max(1) as usize);
574        }
575        max
576    }
577}
578
579/// Two-level indirection organized data generator for grid layouts with repeaters.
580/// Uses 2-level indirection: cache[cache[jump_pos] + ri * stride + col * 4]
581/// Each jump cell stores [data_base, stride, 0, 0] where stride = step * 4.
582///
583/// Layout: [static_cells (4 u16 each)] [jump_cells (4 u16 each, 1 per repeater)]
584///         [row_data (rep_count * step * 4 u16)] ... (repeated for each repeater)
585struct OrganizedDataGenerator<'a> {
586    // Input
587    repeater_indices: &'a [u32],
588    repeater_steps: &'a [u32],
589    // An always increasing counter, the index of the cell being added
590    counter: usize,
591    // The u16 position in result for the next repeater's data section
592    repeat_u16_offset: usize,
593    // The index/2 in repeater_indices (i.e. which repeater we're looking at next)
594    next_rep: usize,
595    // The cell index in result for the next non-repeated item (each cell = 4 u16)
596    current_offset: usize,
597    // Output
598    result: &'a mut GridLayoutOrganizedData,
599}
600
601impl<'a> OrganizedDataGenerator<'a> {
602    fn new(
603        repeater_indices: &'a [u32],
604        repeater_steps: &'a [u32],
605        static_cells: usize,
606        num_repeaters: usize,
607        total_repeated_cells_count: usize,
608        result: &'a mut GridLayoutOrganizedData,
609    ) -> Self {
610        result.resize((static_cells + num_repeaters + total_repeated_cells_count) * 4, 0 as _);
611        let repeat_u16_offset = (static_cells + num_repeaters) * 4;
612        Self {
613            repeater_indices,
614            repeater_steps,
615            counter: 0,
616            repeat_u16_offset,
617            next_rep: 0,
618            current_offset: 0,
619            result,
620        }
621    }
622    fn add(&mut self, col: u16, colspan: u16, row: u16, rowspan: u16) {
623        let res = self.result.make_mut_slice();
624        loop {
625            if let Some(nr) = self.repeater_indices.get(self.next_rep * 2) {
626                let nr = *nr as usize;
627                let step = self.repeater_steps.get(self.next_rep).copied().unwrap_or(1) as usize;
628                let rep_count = self.repeater_indices[self.next_rep * 2 + 1] as usize;
629
630                if nr == self.counter {
631                    // First cell of this repeater
632                    let data_u16_start = self.repeat_u16_offset;
633                    let stride = step * 4;
634
635                    // Write jump cell: [data_base, stride, 0, 0]
636                    res[self.current_offset * 4] = data_u16_start as _;
637                    res[self.current_offset * 4 + 1] = stride as _;
638                    self.current_offset += 1;
639                }
640                if self.counter >= nr {
641                    let cells_in_repeater = rep_count * step;
642                    if self.counter - nr == cells_in_repeater {
643                        // Past the end of this repeater — advance past data
644                        self.repeat_u16_offset += cells_in_repeater * 4;
645                        self.next_rep += 1;
646                        continue;
647                    }
648                    // Write data at the position determined by row/col within repeater
649                    let cell_in_rep = self.counter - nr;
650                    let row_in_rep = cell_in_rep / step;
651                    let col_in_rep = cell_in_rep % step;
652                    let data_u16_start = self.repeat_u16_offset;
653                    let u16_pos = data_u16_start + row_in_rep * step * 4 + col_in_rep * 4;
654                    res[u16_pos] = col;
655                    res[u16_pos + 1] = colspan;
656                    res[u16_pos + 2] = row;
657                    res[u16_pos + 3] = rowspan;
658                    self.counter += 1;
659                    return;
660                }
661            }
662            // Non-repeated cell
663            res[self.current_offset * 4] = col;
664            res[self.current_offset * 4 + 1] = colspan;
665            res[self.current_offset * 4 + 2] = row;
666            res[self.current_offset * 4 + 3] = rowspan;
667            self.current_offset += 1;
668            self.counter += 1;
669            return;
670        }
671    }
672}
673
674/// Given the cells of a layout of a Dialog, re-order the buttons according to the platform
675/// This function assume that the `roles` contains the roles of the button which are the first cells in `input_data`
676pub fn organize_dialog_button_layout(
677    input_data: Slice<GridLayoutInputData>,
678    dialog_button_roles: Slice<DialogButtonRole>,
679) -> GridLayoutOrganizedData {
680    let mut organized_data = GridLayoutOrganizedData::default();
681    organized_data.reserve(input_data.len() * 4);
682
683    #[cfg(feature = "std")]
684    fn is_kde() -> bool {
685        // assume some Unix, check if XDG_CURRENT_DESKTOP starts with K
686        std::env::var("XDG_CURRENT_DESKTOP")
687            .ok()
688            .and_then(|v| v.as_bytes().first().copied())
689            .is_some_and(|x| x.eq_ignore_ascii_case(&b'K'))
690    }
691    #[cfg(not(feature = "std"))]
692    let is_kde = || true;
693
694    let expected_order: &[DialogButtonRole] = match crate::detect_operating_system() {
695        crate::items::OperatingSystemType::Windows => {
696            &[
697                DialogButtonRole::Reset,
698                DialogButtonRole::None, // spacer
699                DialogButtonRole::Accept,
700                DialogButtonRole::Action,
701                DialogButtonRole::Reject,
702                DialogButtonRole::Apply,
703                DialogButtonRole::Help,
704            ]
705        }
706        crate::items::OperatingSystemType::Macos | crate::items::OperatingSystemType::Ios => {
707            &[
708                DialogButtonRole::Help,
709                DialogButtonRole::Reset,
710                DialogButtonRole::Apply,
711                DialogButtonRole::Action,
712                DialogButtonRole::None, // spacer
713                DialogButtonRole::Reject,
714                DialogButtonRole::Accept,
715            ]
716        }
717        _ if is_kde() => {
718            // KDE variant
719            &[
720                DialogButtonRole::Help,
721                DialogButtonRole::Reset,
722                DialogButtonRole::None, // spacer
723                DialogButtonRole::Action,
724                DialogButtonRole::Accept,
725                DialogButtonRole::Apply,
726                DialogButtonRole::Reject,
727            ]
728        }
729        _ => {
730            // GNOME variant and fallback for WASM build
731            &[
732                DialogButtonRole::Help,
733                DialogButtonRole::Reset,
734                DialogButtonRole::None, // spacer
735                DialogButtonRole::Action,
736                DialogButtonRole::Accept,
737                DialogButtonRole::Apply,
738                DialogButtonRole::Reject,
739            ]
740        }
741    };
742
743    // Reorder the actual buttons according to expected_order
744    let mut column_for_input: Vec<usize> = Vec::with_capacity(dialog_button_roles.len());
745    for role in expected_order.iter() {
746        if role == &DialogButtonRole::None {
747            column_for_input.push(usize::MAX); // empty column, ensure nothing will match
748            continue;
749        }
750        for (idx, r) in dialog_button_roles.as_slice().iter().enumerate() {
751            if *r == *role {
752                column_for_input.push(idx);
753            }
754        }
755    }
756
757    for (input_index, cell) in input_data.as_slice().iter().enumerate() {
758        let col = column_for_input.iter().position(|&x| x == input_index);
759        if let Some(col) = col {
760            organized_data.push_cell(col as _, cell.colspan as _, cell.row as _, cell.rowspan as _);
761        } else {
762            // This is used for the main window (which is the only cell which isn't a button)
763            // Given lower_dialog_layout(), this will always be a single cell at 0,0 with a colspan of number_of_buttons
764            organized_data.push_cell(
765                cell.col as _,
766                cell.colspan as _,
767                cell.row as _,
768                cell.rowspan as _,
769            );
770        }
771    }
772    organized_data
773}
774
775// GridLayout-specific
776fn total_repeated_cells<'a>(repeater_indices: &'a [u32], repeater_steps: &'a [u32]) -> usize {
777    repeater_indices
778        .chunks(2)
779        .enumerate()
780        .map(|(i, chunk)| {
781            let count = chunk.get(1).copied().unwrap_or(0) as usize;
782            let step = repeater_steps.get(i).copied().unwrap_or(1) as usize;
783            count * step
784        })
785        .sum()
786}
787
788type Errors = Vec<String>;
789
790pub fn organize_grid_layout(
791    input_data: Slice<GridLayoutInputData>,
792    repeater_indices: Slice<u32>,
793    repeater_steps: Slice<u32>,
794) -> GridLayoutOrganizedData {
795    let (organized_data, errors) =
796        organize_grid_layout_impl(input_data, repeater_indices, repeater_steps);
797    for error in errors {
798        crate::debug_log!("Slint layout error: {}", error);
799    }
800    organized_data
801}
802
803// Implement "auto" behavior for row/col numbers (unless specified in the slint file).
804fn organize_grid_layout_impl(
805    input_data: Slice<GridLayoutInputData>,
806    repeater_indices: Slice<u32>,
807    repeater_steps: Slice<u32>,
808) -> (GridLayoutOrganizedData, Errors) {
809    let mut organized_data = GridLayoutOrganizedData::default();
810    // Cache size: static_cells * 4 + num_repeaters * 4 (jump cells)
811    //              + per repeater: rep_count * step * 4 (data)
812    let num_repeaters = repeater_indices.len() / 2;
813    let total_repeated_cells =
814        total_repeated_cells(repeater_indices.as_slice(), repeater_steps.as_slice());
815    let static_cells = input_data.len() - total_repeated_cells;
816    let mut generator = OrganizedDataGenerator::new(
817        repeater_indices.as_slice(),
818        repeater_steps.as_slice(),
819        static_cells,
820        num_repeaters,
821        total_repeated_cells,
822        &mut organized_data,
823    );
824    let mut errors = Vec::new();
825
826    fn clamp_to_u16(value: f32, field_name: &str, errors: &mut Vec<String>) -> u16 {
827        if value < 0.0 {
828            errors.push(format!("cell {field_name} {value} is negative, clamping to 0"));
829            0
830        } else if value > u16::MAX as f32 {
831            errors
832                .push(format!("cell {field_name} {value} is too large, clamping to {}", u16::MAX));
833            u16::MAX
834        } else {
835            value as u16
836        }
837    }
838
839    let mut row = 0;
840    let mut col = 0;
841    let mut first = true;
842    for cell in input_data.as_slice().iter() {
843        if cell.new_row && !first {
844            row += 1;
845            col = 0;
846        }
847        first = false;
848
849        if cell.row != i_slint_common::ROW_COL_AUTO {
850            let cell_row = clamp_to_u16(cell.row, "row", &mut errors);
851            if row != cell_row {
852                row = cell_row;
853                col = 0;
854            }
855        }
856        if cell.col != i_slint_common::ROW_COL_AUTO {
857            col = clamp_to_u16(cell.col, "col", &mut errors);
858        }
859
860        let colspan = clamp_to_u16(cell.colspan, "colspan", &mut errors);
861        let rowspan = clamp_to_u16(cell.rowspan, "rowspan", &mut errors);
862        col = col.min(u16::MAX - colspan); // ensure col + colspan doesn't overflow
863        generator.add(col, colspan, row, rowspan);
864        col += colspan;
865    }
866    (organized_data, errors)
867}
868
869/// Layout cache generator for box layouts.
870/// The layout cache generator inserts the pos and size into the result array (which becomes the layout cache property),
871/// including the indirections for repeated items (so that the x,y,width,height properties for repeated items
872/// can point to indices known at compile time, those that contain the indirections)
873/// Example: for repeater_indices=[1,4] (meaning that item at index 1 is repeated 4 times),
874/// 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]
875///  i.e. pos1, width1, jump to idx 4, jump to idx 5, pos2, width2, pos3, width3, pos4, width4, pos5, width5
876struct LayoutCacheGenerator<'a> {
877    // Input
878    repeater_indices: &'a [u32],
879    // An always increasing counter, the index of the cell being added
880    counter: usize,
881    // The index/2 in result in which we should add the next repeated item
882    repeat_offset: usize,
883    // The index/2 in repeater_indices
884    next_rep: usize,
885    // The index/2 in result in which we should add the next non-repeated item
886    current_offset: usize,
887    // Output
888    result: &'a mut SharedVector<Coord>,
889}
890
891impl<'a> LayoutCacheGenerator<'a> {
892    fn new(repeater_indices: &'a [u32], result: &'a mut SharedVector<Coord>) -> Self {
893        let total_repeated_cells: usize = repeater_indices
894            .chunks(2)
895            .map(|chunk| chunk.get(1).copied().unwrap_or(0) as usize)
896            .sum();
897        assert!(result.len() >= total_repeated_cells * 2);
898        let repeat_offset = result.len() / 2 - total_repeated_cells;
899        Self { repeater_indices, counter: 0, repeat_offset, next_rep: 0, current_offset: 0, result }
900    }
901    fn add(&mut self, pos: Coord, size: Coord) {
902        let res = self.result.make_mut_slice();
903        let o = loop {
904            if let Some(nr) = self.repeater_indices.get(self.next_rep * 2) {
905                let nr = *nr as usize;
906                if nr == self.counter {
907                    // Write jump entry
908                    for o in 0..2 {
909                        res[self.current_offset * 2 + o] = (self.repeat_offset * 2 + o) as _;
910                    }
911                    self.current_offset += 1;
912                }
913                if self.counter >= nr {
914                    let rep_count = self.repeater_indices[self.next_rep * 2 + 1] as usize;
915                    if self.counter - nr == rep_count {
916                        self.repeat_offset += rep_count;
917                        self.next_rep += 1;
918                        continue;
919                    }
920                    let offset = self.repeat_offset + (self.counter - nr);
921                    break offset;
922                }
923            }
924            self.current_offset += 1;
925            break self.current_offset - 1;
926        };
927        res[o * 2] = pos;
928        res[o * 2 + 1] = size;
929        self.counter += 1;
930    }
931}
932
933/// Two-level indirection layout cache generator for grid layouts with repeaters.
934/// Uses 2-level indirection: cache[cache[jump] + ri * stride + child_offset]
935/// Each jump cell stores [data_base, stride] where stride = step * 2.
936struct GridLayoutCacheGenerator<'a> {
937    // Input
938    repeater_indices: &'a [u32],
939    repeater_steps: &'a [u32],
940    // An always increasing counter, the index of the cell being added
941    counter: usize,
942    // The f32 position in result for the next repeater's dynamic data section
943    repeat_f32_offset: usize,
944    // The index/2 in repeater_indices
945    next_rep: usize,
946    // The cell index (index/2) in result for the next non-repeated item
947    current_offset: usize,
948    // Output
949    result: &'a mut SharedVector<Coord>,
950}
951
952impl<'a> GridLayoutCacheGenerator<'a> {
953    fn new(
954        repeater_indices: &'a [u32],
955        repeater_steps: &'a [u32],
956        static_cells: usize,
957        num_repeaters: usize,
958        total_repeated_cells_count: usize,
959        result: &'a mut SharedVector<Coord>,
960    ) -> Self {
961        result.resize((static_cells + num_repeaters + total_repeated_cells_count) * 2, 0 as _);
962        let repeat_f32_offset = (static_cells + num_repeaters) * 2;
963        Self {
964            repeater_indices,
965            repeater_steps,
966            counter: 0,
967            repeat_f32_offset,
968            next_rep: 0,
969            current_offset: 0,
970            result,
971        }
972    }
973    fn add(&mut self, pos: Coord, size: Coord) {
974        let res = self.result.make_mut_slice();
975        loop {
976            if let Some(nr) = self.repeater_indices.get(self.next_rep * 2) {
977                let nr = *nr as usize;
978                let step = self.repeater_steps.get(self.next_rep).copied().unwrap_or(1) as usize;
979                let rep_count = self.repeater_indices[self.next_rep * 2 + 1] as usize;
980
981                if nr == self.counter {
982                    // First cell of this repeater
983                    let data_f32_start = self.repeat_f32_offset;
984                    let stride = step * 2;
985
986                    // Write 1 jump cell (2 f32): [data_base, stride]
987                    res[self.current_offset * 2] = data_f32_start as _;
988                    res[self.current_offset * 2 + 1] = stride as _;
989                    self.current_offset += 1;
990                }
991                if self.counter >= nr {
992                    let cells_in_repeater = rep_count * step;
993                    if self.counter - nr == cells_in_repeater {
994                        // Past the end of this repeater — advance past data
995                        self.repeat_f32_offset += cells_in_repeater * 2;
996                        self.next_rep += 1;
997                        continue;
998                    }
999                    // Write data at the position determined by row/col within repeater
1000                    let cell_in_rep = self.counter - nr;
1001                    let row_in_rep = cell_in_rep / step;
1002                    let col_in_rep = cell_in_rep % step;
1003                    let data_f32_start = self.repeat_f32_offset;
1004                    let f32_pos = data_f32_start + row_in_rep * step * 2 + col_in_rep * 2;
1005                    res[f32_pos] = pos;
1006                    res[f32_pos + 1] = size;
1007                    self.counter += 1;
1008                    return;
1009                }
1010            }
1011            // Non-repeated cell
1012            res[self.current_offset * 2] = pos;
1013            res[self.current_offset * 2 + 1] = size;
1014            self.current_offset += 1;
1015            self.counter += 1;
1016            return;
1017        }
1018    }
1019}
1020
1021/// return, an array which is of size `data.cells.len() * 2` which for each cell stores:
1022/// pos (x or y), size (width or height)
1023pub fn solve_grid_layout(
1024    data: &GridLayoutData,
1025    constraints: Slice<LayoutItemInfo>,
1026    orientation: Orientation,
1027    repeater_indices: Slice<u32>,
1028    repeater_steps: Slice<u32>,
1029) -> SharedVector<Coord> {
1030    let mut layout_data = grid_internal::to_layout_data(
1031        &data.organized_data,
1032        constraints,
1033        orientation,
1034        repeater_indices,
1035        repeater_steps,
1036        data.spacing,
1037        Some(data.size),
1038    );
1039
1040    if layout_data.is_empty() {
1041        return Default::default();
1042    }
1043
1044    grid_internal::layout_items(
1045        &mut layout_data,
1046        data.padding.begin,
1047        data.size - (data.padding.begin + data.padding.end),
1048        data.spacing,
1049    );
1050
1051    let mut result = SharedVector::<Coord>::default();
1052    let num_repeaters = repeater_indices.len() / 2;
1053    let total_repeated_cells =
1054        total_repeated_cells(repeater_indices.as_slice(), repeater_steps.as_slice());
1055    let static_cells = constraints.len() - total_repeated_cells;
1056    let mut generator = GridLayoutCacheGenerator::new(
1057        repeater_indices.as_slice(),
1058        repeater_steps.as_slice(),
1059        static_cells,
1060        num_repeaters,
1061        total_repeated_cells,
1062        &mut result,
1063    );
1064
1065    for idx in 0..constraints.len() {
1066        let (col_or_row, span) = data.organized_data.col_or_row_and_span(
1067            idx,
1068            orientation,
1069            &repeater_indices,
1070            &repeater_steps,
1071        );
1072        let cdata = &layout_data[col_or_row as usize];
1073        let size = if span > 0 {
1074            let last_cell = &layout_data[col_or_row as usize + span as usize - 1];
1075            last_cell.pos + last_cell.size - cdata.pos
1076        } else {
1077            0 as Coord
1078        };
1079        generator.add(cdata.pos, size);
1080    }
1081    result
1082}
1083
1084pub fn grid_layout_info(
1085    organized_data: GridLayoutOrganizedData, // not & because the code generator doesn't support it in ExtraBuiltinFunctionCall
1086    constraints: Slice<LayoutItemInfo>,
1087    repeater_indices: Slice<u32>,
1088    repeater_steps: Slice<u32>,
1089    spacing: Coord,
1090    padding: &Padding,
1091    orientation: Orientation,
1092) -> LayoutInfo {
1093    let layout_data = grid_internal::to_layout_data(
1094        &organized_data,
1095        constraints,
1096        orientation,
1097        repeater_indices,
1098        repeater_steps,
1099        spacing,
1100        None,
1101    );
1102    if layout_data.is_empty() {
1103        let mut info = LayoutInfo::default();
1104        info.min = padding.begin + padding.end;
1105        info.preferred = info.min;
1106        info.max = info.min;
1107        return info;
1108    }
1109    let spacing_w = spacing * (layout_data.len() - 1) as Coord + padding.begin + padding.end;
1110    let min = layout_data.iter().map(|data| data.min).sum::<Coord>() + spacing_w;
1111    let max = layout_data.iter().map(|data| data.max).fold(spacing_w, Saturating::add);
1112    let preferred = layout_data.iter().map(|data| data.pref).sum::<Coord>() + spacing_w;
1113    let stretch = layout_data.iter().map(|data| data.stretch).sum::<f32>();
1114    LayoutInfo { min, max, min_percent: 0 as _, max_percent: 100 as _, preferred, stretch }
1115}
1116
1117#[repr(C)]
1118#[derive(Debug)]
1119/// The BoxLayoutData is used to represent both a Horizontal and Vertical layout.
1120/// The width/height x/y correspond to that of a horizontal layout.
1121/// For vertical layout, they are inverted
1122pub struct BoxLayoutData<'a> {
1123    pub size: Coord,
1124    pub spacing: Coord,
1125    pub padding: Padding,
1126    pub alignment: LayoutAlignment,
1127    pub cells: Slice<'a, LayoutItemInfo>,
1128}
1129
1130/// Input for `solve_box_layout_ortho`.
1131#[repr(C)]
1132#[derive(Debug)]
1133pub struct BoxLayoutOrthoData<'a> {
1134    pub size: Coord,
1135    pub padding: Padding,
1136    pub cross_axis_alignment: CrossAxisAlignment,
1137    pub cells: Slice<'a, LayoutItemInfo>,
1138}
1139
1140#[repr(C)]
1141#[derive(Debug)]
1142/// The FlexboxLayoutData is used for a flex layout.
1143pub struct FlexboxLayoutData<'a> {
1144    pub width: Coord,
1145    pub height: Coord,
1146    pub spacing_h: Coord,
1147    pub spacing_v: Coord,
1148    pub padding_h: Padding,
1149    pub padding_v: Padding,
1150    pub alignment: LayoutAlignment,
1151    pub direction: FlexboxLayoutDirection,
1152    pub align_content: FlexboxLayoutAlignContent,
1153    pub cross_axis_alignment: CrossAxisAlignment,
1154    pub flex_wrap: FlexboxLayoutWrap,
1155    /// Horizontal constraints (width) for each cell
1156    pub cells_h: Slice<'a, FlexboxLayoutItemInfo>,
1157    /// Vertical constraints (height) for each cell
1158    pub cells_v: Slice<'a, FlexboxLayoutItemInfo>,
1159}
1160
1161#[repr(C)]
1162#[derive(Debug, Clone, Default)]
1163/// The information about a single item in a box or grid layout
1164pub struct LayoutItemInfo {
1165    pub constraint: LayoutInfo,
1166}
1167
1168#[repr(C)]
1169#[derive(Debug, Clone)]
1170/// The information about a single item in a flexbox layout
1171pub struct FlexboxLayoutItemInfo {
1172    pub constraint: LayoutInfo,
1173    /// Flex grow factor (0 = don't grow, default)
1174    pub flex_grow: f32,
1175    /// Flex shrink factor (0 = don't shrink, default)
1176    pub flex_shrink: f32,
1177    /// Flex basis in logical pixels (-1 = auto, meaning use preferred size; default)
1178    pub flex_basis: Coord,
1179    /// Per-item cross-axis alignment override (Auto = use container's cross-axis-alignment)
1180    pub flex_align_self: FlexboxLayoutAlignSelf,
1181    /// Visual ordering of flex items (lower values appear first, default 0)
1182    pub flex_order: i32,
1183}
1184
1185impl Default for FlexboxLayoutItemInfo {
1186    fn default() -> Self {
1187        Self {
1188            constraint: LayoutInfo::default(),
1189            flex_grow: 0.0,
1190            flex_shrink: 1.0,
1191            flex_basis: -1 as _,
1192            flex_align_self: FlexboxLayoutAlignSelf::Auto,
1193            flex_order: 0,
1194        }
1195    }
1196}
1197
1198impl From<LayoutItemInfo> for FlexboxLayoutItemInfo {
1199    fn from(info: LayoutItemInfo) -> Self {
1200        Self { constraint: info.constraint, ..Default::default() }
1201    }
1202}
1203
1204/// Solve a BoxLayout
1205pub fn solve_box_layout(data: &BoxLayoutData, repeater_indices: Slice<u32>) -> SharedVector<Coord> {
1206    let mut result = SharedVector::<Coord>::default();
1207    // One element results into two coordinates in the result vector. 1. Position, 2. Size
1208    result.resize(data.cells.len() * 2 + repeater_indices.len(), 0 as _);
1209
1210    if data.cells.is_empty() {
1211        return result;
1212    }
1213
1214    let size_without_padding = data.size - data.padding.begin - data.padding.end;
1215    let num_spacings = (data.cells.len() - 1) as Coord;
1216    let spacings = data.spacing * num_spacings;
1217    let content_size = size_without_padding - spacings; // The size the cells can occupy without going outside of the layout
1218    let mut layout_data: Vec<_> = data
1219        .cells
1220        .iter()
1221        .map(|c| {
1222            let min = c.constraint.min.max(c.constraint.min_percent * content_size / 100 as Coord);
1223            let max = c.constraint.max.min(c.constraint.max_percent * content_size / 100 as Coord);
1224            grid_internal::LayoutData {
1225                min,
1226                max,
1227                pref: c.constraint.preferred.min(max).max(min),
1228                stretch: c.constraint.stretch,
1229                ..Default::default()
1230            }
1231        })
1232        .collect();
1233
1234    let pref_size: Coord = layout_data.iter().map(|it| it.pref).sum();
1235
1236    let align = match data.alignment {
1237        LayoutAlignment::Stretch => {
1238            grid_internal::layout_items(
1239                &mut layout_data,
1240                data.padding.begin,
1241                size_without_padding,
1242                data.spacing,
1243            );
1244            None
1245        }
1246        _ if size_without_padding <= pref_size + spacings => {
1247            grid_internal::layout_items(
1248                &mut layout_data,
1249                data.padding.begin,
1250                size_without_padding,
1251                data.spacing,
1252            );
1253            None
1254        }
1255        LayoutAlignment::Center => Some((
1256            data.padding.begin + (size_without_padding - pref_size - spacings) / 2 as Coord,
1257            data.spacing,
1258        )),
1259        LayoutAlignment::Start => Some((data.padding.begin, data.spacing)),
1260        LayoutAlignment::End => {
1261            Some((data.padding.begin + (size_without_padding - pref_size - spacings), data.spacing))
1262        }
1263        LayoutAlignment::SpaceBetween => {
1264            Some((data.padding.begin, (size_without_padding - pref_size) / num_spacings))
1265        }
1266        LayoutAlignment::SpaceAround => {
1267            let spacing = (size_without_padding - pref_size) / (num_spacings + 1 as Coord);
1268            Some((data.padding.begin + spacing / 2 as Coord, spacing))
1269        }
1270        LayoutAlignment::SpaceEvenly => {
1271            let spacing = (size_without_padding - pref_size) / (num_spacings + 2 as Coord);
1272            Some((data.padding.begin + spacing, spacing))
1273        }
1274    };
1275    if let Some((mut pos, spacing)) = align {
1276        for it in &mut layout_data {
1277            it.pos = pos;
1278            it.size = it.pref;
1279            pos += spacing + it.size;
1280        }
1281    }
1282
1283    let mut generator = LayoutCacheGenerator::new(&repeater_indices, &mut result);
1284    for layout in layout_data.iter() {
1285        generator.add(layout.pos, layout.size);
1286    }
1287    result
1288}
1289
1290/// Cross-axis solve: returns (position, size) per cell, like [`solve_box_layout`].
1291pub fn solve_box_layout_ortho(
1292    data: &BoxLayoutOrthoData,
1293    repeater_indices: Slice<u32>,
1294) -> SharedVector<Coord> {
1295    let mut result = SharedVector::<Coord>::default();
1296    result.resize(data.cells.len() * 2 + repeater_indices.len(), 0 as _);
1297    if data.cells.is_empty() {
1298        return result;
1299    }
1300    let size_without_padding = data.size - data.padding.begin - data.padding.end;
1301    let mut generator = LayoutCacheGenerator::new(&repeater_indices, &mut result);
1302    for c in data.cells.iter() {
1303        let min =
1304            c.constraint.min.max(c.constraint.min_percent * size_without_padding / 100 as Coord);
1305        let max =
1306            c.constraint.max.min(c.constraint.max_percent * size_without_padding / 100 as Coord);
1307        let size = match data.cross_axis_alignment {
1308            CrossAxisAlignment::Stretch => size_without_padding,
1309            _ => c.constraint.preferred,
1310        }
1311        .min(max)
1312        .max(min);
1313        let pos = match data.cross_axis_alignment {
1314            CrossAxisAlignment::Stretch | CrossAxisAlignment::Start => data.padding.begin,
1315            CrossAxisAlignment::End => data.padding.begin + size_without_padding - size,
1316            CrossAxisAlignment::Center => {
1317                data.padding.begin + (size_without_padding - size) / 2 as Coord
1318            }
1319        };
1320        generator.add(pos, size);
1321    }
1322    result
1323}
1324
1325/// Return the LayoutInfo for a BoxLayout with the given cells.
1326pub fn box_layout_info(
1327    cells: Slice<LayoutItemInfo>,
1328    spacing: Coord,
1329    padding: &Padding,
1330    alignment: LayoutAlignment,
1331) -> LayoutInfo {
1332    let count = cells.len();
1333    let is_stretch = alignment == LayoutAlignment::Stretch;
1334    if count < 1 {
1335        let mut info = LayoutInfo::default();
1336        info.min = padding.begin + padding.end;
1337        info.preferred = info.min;
1338        if is_stretch {
1339            info.max = info.min;
1340        }
1341        return info;
1342    };
1343    let extra_w = padding.begin + padding.end + spacing * (count - 1) as Coord;
1344    let min = cells.iter().map(|c| c.constraint.min).sum::<Coord>() + extra_w; // Minimum size of the complete layout
1345    let max = if is_stretch {
1346        (cells.iter().map(|c| c.constraint.max).fold(extra_w, Saturating::add)).max(min)
1347    } else {
1348        Coord::MAX
1349    }; // Maximum size of the complete layout
1350    let preferred = cells.iter().map(|c| c.constraint.preferred_bounded()).sum::<Coord>() + extra_w;
1351    let stretch = cells.iter().map(|c| c.constraint.stretch).sum::<f32>();
1352    LayoutInfo { min, max, min_percent: 0 as _, max_percent: 100 as _, preferred, stretch }
1353}
1354
1355pub fn box_layout_info_ortho(cells: Slice<LayoutItemInfo>, padding: &Padding) -> LayoutInfo {
1356    let extra_w = padding.begin + padding.end;
1357    let mut fold =
1358        cells.iter().fold(LayoutInfo { stretch: f32::MAX, ..Default::default() }, |a, b| {
1359            a.merge(&b.constraint)
1360        });
1361    fold.max = fold.max.max(fold.min);
1362    fold.preferred = fold.preferred.clamp(fold.min, fold.max);
1363    fold.min += extra_w;
1364    fold.max = Saturating::add(fold.max, extra_w);
1365    fold.preferred += extra_w;
1366    // Don't propagate children's percentage constraints to the parent.
1367    // Percentages are relative to the layout's own size, not the grandparent's.
1368    fold.min_percent = 0 as _;
1369    fold.max_percent = 100 as _;
1370    fold
1371}
1372
1373/// Helper module for taffy-based flexbox layout
1374mod flexbox_taffy {
1375    use super::{
1376        Coord, CrossAxisAlignment, FlexboxLayoutAlignContent, FlexboxLayoutAlignSelf,
1377        FlexboxLayoutItemInfo, FlexboxLayoutWrap as SlintFlexboxLayoutWrap, LayoutAlignment,
1378        Padding, Slice,
1379    };
1380    use alloc::vec::Vec;
1381    pub use taffy::prelude::FlexDirection as TaffyFlexDirection;
1382    use taffy::prelude::*;
1383
1384    /// Parameters for FlexboxTaffyBuilder::new
1385    pub struct FlexboxLayoutParams<'a> {
1386        pub cells_h: &'a Slice<'a, FlexboxLayoutItemInfo>,
1387        pub cells_v: &'a Slice<'a, FlexboxLayoutItemInfo>,
1388        pub spacing_h: Coord,
1389        pub spacing_v: Coord,
1390        pub padding_h: &'a Padding,
1391        pub padding_v: &'a Padding,
1392        pub alignment: LayoutAlignment,
1393        pub align_content: FlexboxLayoutAlignContent,
1394        pub cross_axis_alignment: CrossAxisAlignment,
1395        pub flex_wrap: SlintFlexboxLayoutWrap,
1396        pub flex_direction: TaffyFlexDirection,
1397        pub container_width: Option<Coord>,
1398        pub container_height: Option<Coord>,
1399        /// When true, set the cross-axis dimension to `auto` for all items
1400        /// so that the measure callback can compute it dynamically (height-for-width).
1401        pub use_measure_for_cross_axis: bool,
1402    }
1403
1404    /// Build a taffy tree from Slint layout constraints.
1405    /// The NodeContext (usize) stores the original child index for measure callbacks.
1406    pub struct FlexboxTaffyBuilder {
1407        pub taffy: TaffyTree<usize>,
1408        pub children: Vec<NodeId>,
1409        pub container: NodeId,
1410        /// Maps taffy child position -> original cell index (empty if no reordering needed)
1411        pub order_map: Vec<usize>,
1412    }
1413
1414    impl FlexboxTaffyBuilder {
1415        /// Create a new flexbox layout tree from item constraints
1416        pub fn new(params: FlexboxLayoutParams) -> Self {
1417            let mut taffy = TaffyTree::<usize>::new();
1418
1419            // Cross-axis (width) upper bound for a column item: never wider than
1420            // the container's content box, so height-for-width items (e.g. wrapped
1421            // Text) measure against a real width.
1422            let column_cross_cap = match params.flex_direction {
1423                TaffyFlexDirection::Column | TaffyFlexDirection::ColumnReverse => params
1424                    .container_width
1425                    .map(|cw| (cw - params.padding_h.begin - params.padding_h.end).max(0 as Coord)),
1426                _ => None,
1427            };
1428
1429            // Create child nodes from Slint constraints
1430            let mut children: Vec<NodeId> = params
1431                .cells_h
1432                .iter()
1433                .enumerate()
1434                .map(|(idx, cell_h)| {
1435                    let cell_v = params.cells_v.get(idx);
1436                    let h_constraint = &cell_h.constraint;
1437                    let v_constraint = cell_v.map(|c| &c.constraint);
1438
1439                    // Use preferred_bounded() which clamps preferred to min/max bounds
1440                    let preferred_width = h_constraint.preferred_bounded();
1441                    let preferred_height =
1442                        v_constraint.map(|vc| vc.preferred_bounded()).unwrap_or(0 as Coord);
1443
1444                    // flex_basis: use explicit value if set (>= 0), otherwise use preferred size
1445                    let flex_basis = if cell_h.flex_basis >= 0 as Coord {
1446                        Dimension::length(cell_h.flex_basis as _)
1447                    } else {
1448                        match params.flex_direction {
1449                            TaffyFlexDirection::Row | TaffyFlexDirection::RowReverse => {
1450                                Dimension::length(preferred_width as _)
1451                            }
1452                            TaffyFlexDirection::Column | TaffyFlexDirection::ColumnReverse => {
1453                                Dimension::length(preferred_height as _)
1454                            }
1455                        }
1456                    };
1457
1458                    let max_width = h_constraint.max.min(column_cross_cap.unwrap_or(Coord::MAX));
1459                    let max_width_dim = if max_width < Coord::MAX {
1460                        Dimension::length(max_width as _)
1461                    } else {
1462                        Dimension::auto()
1463                    };
1464
1465                    taffy
1466                        .new_leaf_with_context(
1467                            Style {
1468                                flex_basis,
1469                                size: Size {
1470                                    width: match params.flex_direction {
1471                                        TaffyFlexDirection::Column
1472                                        | TaffyFlexDirection::ColumnReverse => {
1473                                            // Stretching items get `auto` width so they
1474                                            // size to their flex *line's* cross size (the
1475                                            // per-column width when wrapped), not the whole
1476                                            // container. A per-item `flex-align-self`
1477                                            // overrides the container's alignment.
1478                                            let stretches = match cell_h.flex_align_self {
1479                                                FlexboxLayoutAlignSelf::Auto => {
1480                                                    params.cross_axis_alignment
1481                                                        == CrossAxisAlignment::Stretch
1482                                                }
1483                                                FlexboxLayoutAlignSelf::Stretch => true,
1484                                                _ => false,
1485                                            };
1486                                            if stretches {
1487                                                Dimension::auto()
1488                                            } else if preferred_width > 0 as Coord {
1489                                                Dimension::length(preferred_width as _)
1490                                            } else {
1491                                                Dimension::auto()
1492                                            }
1493                                        }
1494                                        _ => Dimension::auto(),
1495                                    },
1496                                    height: match params.flex_direction {
1497                                        TaffyFlexDirection::Row
1498                                        | TaffyFlexDirection::RowReverse => {
1499                                            // Cross-axis for row: use auto when measure
1500                                            // callback will compute it dynamically
1501                                            if params.use_measure_for_cross_axis {
1502                                                Dimension::auto()
1503                                            } else if preferred_height > 0 as Coord {
1504                                                Dimension::length(preferred_height as _)
1505                                            } else {
1506                                                Dimension::auto()
1507                                            }
1508                                        }
1509                                        _ => Dimension::auto(),
1510                                    },
1511                                },
1512                                min_size: Size {
1513                                    width: Dimension::length(h_constraint.min as _),
1514                                    height: Dimension::length(
1515                                        v_constraint.map(|vc| vc.min as f32).unwrap_or(0.0),
1516                                    ),
1517                                },
1518                                max_size: Size {
1519                                    width: max_width_dim,
1520                                    height: if let Some(vc) = v_constraint {
1521                                        if vc.max < Coord::MAX {
1522                                            Dimension::length(vc.max as _)
1523                                        } else {
1524                                            Dimension::auto()
1525                                        }
1526                                    } else {
1527                                        Dimension::auto()
1528                                    },
1529                                },
1530                                flex_grow: cell_h.flex_grow,
1531                                flex_shrink: cell_h.flex_shrink,
1532                                align_self: match cell_h.flex_align_self {
1533                                    FlexboxLayoutAlignSelf::Auto => None,
1534                                    FlexboxLayoutAlignSelf::Stretch => Some(AlignSelf::Stretch),
1535                                    FlexboxLayoutAlignSelf::Start => Some(AlignSelf::FlexStart),
1536                                    FlexboxLayoutAlignSelf::End => Some(AlignSelf::FlexEnd),
1537                                    FlexboxLayoutAlignSelf::Center => Some(AlignSelf::Center),
1538                                },
1539                                ..Default::default()
1540                            },
1541                            idx,
1542                        )
1543                        .unwrap() // cannot fail
1544                })
1545                .collect();
1546
1547            // Sort children by CSS `order` property if any item has a non-zero order.
1548            // Build a mapping from sorted position -> original index.
1549            let has_order = params.cells_h.iter().any(|c| c.flex_order != 0);
1550            let order_map: Vec<usize> = if has_order {
1551                let mut indices: Vec<usize> = (0..children.len()).collect();
1552                // sort_by_key is a stable sort, as required by CSS
1553                indices.sort_by_key(|&i| params.cells_h.get(i).map_or(0, |c| c.flex_order));
1554                let sorted_children: Vec<NodeId> = indices.iter().map(|&i| children[i]).collect();
1555                children = sorted_children;
1556                indices
1557            } else {
1558                Vec::new()
1559            };
1560
1561            // Create container node
1562            let container = taffy
1563                .new_with_children(
1564                    Style {
1565                        display: Display::Flex,
1566                        flex_direction: params.flex_direction,
1567                        flex_wrap: match params.flex_wrap {
1568                            SlintFlexboxLayoutWrap::Wrap => FlexWrap::Wrap,
1569                            SlintFlexboxLayoutWrap::NoWrap => FlexWrap::NoWrap,
1570                            SlintFlexboxLayoutWrap::WrapReverse => FlexWrap::WrapReverse,
1571                        },
1572                        justify_content: Some(match params.alignment {
1573                            // Start/End map to FlexStart/FlexEnd to respect flex direction (including reverse)
1574                            // AlignContent::Start/End would ignore direction and always use writing mode
1575                            LayoutAlignment::Start => AlignContent::FlexStart,
1576                            LayoutAlignment::End => AlignContent::FlexEnd,
1577                            LayoutAlignment::Center => AlignContent::Center,
1578                            LayoutAlignment::Stretch => AlignContent::Stretch,
1579                            LayoutAlignment::SpaceBetween => AlignContent::SpaceBetween,
1580                            LayoutAlignment::SpaceAround => AlignContent::SpaceAround,
1581                            LayoutAlignment::SpaceEvenly => AlignContent::SpaceEvenly,
1582                        }),
1583                        align_items: Some(match params.cross_axis_alignment {
1584                            CrossAxisAlignment::Stretch => AlignItems::Stretch,
1585                            CrossAxisAlignment::Start => AlignItems::FlexStart,
1586                            CrossAxisAlignment::End => AlignItems::FlexEnd,
1587                            CrossAxisAlignment::Center => AlignItems::Center,
1588                        }),
1589                        align_content: Some(match params.align_content {
1590                            FlexboxLayoutAlignContent::Stretch => AlignContent::Stretch,
1591                            FlexboxLayoutAlignContent::Start => AlignContent::FlexStart,
1592                            FlexboxLayoutAlignContent::End => AlignContent::FlexEnd,
1593                            FlexboxLayoutAlignContent::Center => AlignContent::Center,
1594                            FlexboxLayoutAlignContent::SpaceBetween => AlignContent::SpaceBetween,
1595                            FlexboxLayoutAlignContent::SpaceAround => AlignContent::SpaceAround,
1596                            FlexboxLayoutAlignContent::SpaceEvenly => AlignContent::SpaceEvenly,
1597                        }),
1598                        gap: Size {
1599                            width: LengthPercentage::length(params.spacing_h as _),
1600                            height: LengthPercentage::length(params.spacing_v as _),
1601                        },
1602                        padding: Rect {
1603                            left: LengthPercentage::length(params.padding_h.begin as _),
1604                            right: LengthPercentage::length(params.padding_h.end as _),
1605                            top: LengthPercentage::length(params.padding_v.begin as _),
1606                            bottom: LengthPercentage::length(params.padding_v.end as _),
1607                        },
1608                        size: Size {
1609                            width: params
1610                                .container_width
1611                                .map(|w| Dimension::length(w as _))
1612                                .unwrap_or(Dimension::auto()),
1613                            height: params
1614                                .container_height
1615                                .map(|h| Dimension::length(h as _))
1616                                .unwrap_or(Dimension::auto()),
1617                        },
1618                        ..Default::default()
1619                    },
1620                    &children,
1621                )
1622                .unwrap(); // cannot fail
1623
1624            Self { taffy, children, container, order_map }
1625        }
1626
1627        /// Compute the layout with the given available space.
1628        ///
1629        /// The optional `measure` callback is called by taffy for leaf nodes
1630        /// that need dynamic height-for-width (or width-for-height) measurement.
1631        /// It receives `(child_index, known_width, known_height)` where `known_width`
1632        /// / `known_height` are `Some` if taffy has already determined that dimension,
1633        /// and returns `(width, height)`.
1634        pub fn compute_layout(
1635            &mut self,
1636            available_width: Coord,
1637            available_height: Coord,
1638            mut measure: Option<
1639                &mut dyn FnMut(usize, Option<Coord>, Option<Coord>) -> (Coord, Coord),
1640            >,
1641        ) {
1642            let available_space = taffy::prelude::Size {
1643                width: if available_width < Coord::MAX {
1644                    AvailableSpace::Definite(available_width as _)
1645                } else {
1646                    AvailableSpace::MaxContent
1647                },
1648                height: if available_height < Coord::MAX {
1649                    AvailableSpace::Definite(available_height as _)
1650                } else {
1651                    AvailableSpace::MaxContent
1652                },
1653            };
1654            self.taffy
1655                .compute_layout_with_measure(
1656                    self.container,
1657                    available_space,
1658                    |known_dimensions, _available_space, _node_id, node_context, _style| {
1659                        if let (Some(measure), Some(&mut child_index)) =
1660                            (measure.as_deref_mut(), node_context)
1661                        {
1662                            let known_w = known_dimensions.width.map(|w| w as Coord);
1663                            let known_h = known_dimensions.height.map(|h| h as Coord);
1664                            let (w, h) = measure(child_index, known_w, known_h);
1665                            taffy::prelude::Size { width: w as f32, height: h as f32 }
1666                        } else {
1667                            taffy::prelude::Size::ZERO
1668                        }
1669                    },
1670                )
1671                .unwrap_or_else(|e| {
1672                    crate::debug_log!("FlexboxLayout computation error: {}", e);
1673                });
1674        }
1675
1676        /// Get the computed container size
1677        pub fn container_size(&self) -> (Coord, Coord) {
1678            let layout = self.taffy.layout(self.container).unwrap();
1679            (layout.size.width as Coord, layout.size.height as Coord)
1680        }
1681
1682        /// Get the geometry for a specific child
1683        pub fn child_geometry(&self, idx: usize) -> (Coord, Coord, Coord, Coord) {
1684            let layout = self.taffy.layout(self.children[idx]).unwrap();
1685            (
1686                layout.location.x as Coord,
1687                layout.location.y as Coord,
1688                layout.size.width as Coord,
1689                layout.size.height as Coord,
1690            )
1691        }
1692
1693        /// Map a taffy child index to the original cell index (accounting for `order` sorting).
1694        pub fn original_index(&self, taffy_idx: usize) -> usize {
1695            if self.order_map.is_empty() { taffy_idx } else { self.order_map[taffy_idx] }
1696        }
1697    }
1698}
1699
1700/// A cache generator for FlexboxLayout that handles 4 values per item (x, y, width, height)
1701struct FlexboxLayoutCacheGenerator<'a> {
1702    // Input
1703    repeater_indices: &'a [u32],
1704    // An always increasing counter, the index of the cell being added
1705    counter: usize,
1706    // The index/4 in result in which we should add the next repeated item
1707    repeat_offset: usize,
1708    // The index/4 in repeater_indices
1709    next_rep: usize,
1710    // The index/4 in result in which we should add the next non-repeated item
1711    current_offset: usize,
1712    // Output
1713    result: &'a mut SharedVector<Coord>,
1714}
1715
1716impl<'a> FlexboxLayoutCacheGenerator<'a> {
1717    fn new(repeater_indices: &'a [u32], result: &'a mut SharedVector<Coord>) -> Self {
1718        // Calculate total repeated cells (count for each repeater)
1719        let total_repeated_cells: usize = repeater_indices
1720            .chunks(2)
1721            .map(|chunk| chunk.get(1).copied().unwrap_or(0) as usize)
1722            .sum();
1723        assert!(result.len() >= total_repeated_cells * 4);
1724        let repeat_offset = result.len() / 4 - total_repeated_cells;
1725        Self { repeater_indices, counter: 0, repeat_offset, next_rep: 0, current_offset: 0, result }
1726    }
1727
1728    fn add(&mut self, x: Coord, y: Coord, w: Coord, h: Coord) {
1729        let res = self.result.make_mut_slice();
1730        let o = loop {
1731            if let Some(nr) = self.repeater_indices.get(self.next_rep * 2) {
1732                let nr = *nr as usize;
1733                if nr == self.counter {
1734                    // Write jump entries for repeater start
1735                    // Store the base offset (index into the repeated data region)
1736                    res[self.current_offset * 4] = (self.repeat_offset * 4) as Coord;
1737                    res[self.current_offset * 4 + 1] = (self.repeat_offset * 4 + 1) as Coord;
1738                    res[self.current_offset * 4 + 2] = (self.repeat_offset * 4 + 2) as Coord;
1739                    res[self.current_offset * 4 + 3] = (self.repeat_offset * 4 + 3) as Coord;
1740                    self.current_offset += 1;
1741                }
1742                if self.counter >= nr {
1743                    let rep_count = self.repeater_indices[self.next_rep * 2 + 1] as usize;
1744                    if self.counter - nr == rep_count {
1745                        // Advance repeat_offset past this repeater's data before moving to next
1746                        self.repeat_offset += rep_count;
1747                        self.next_rep += 1;
1748                        continue;
1749                    }
1750                    // Calculate offset into repeated data
1751                    let cell_in_rep = self.counter - nr;
1752                    let offset = self.repeat_offset + cell_in_rep;
1753                    break offset;
1754                }
1755            }
1756            self.current_offset += 1;
1757            break self.current_offset - 1;
1758        };
1759        res[o * 4] = x;
1760        res[o * 4 + 1] = y;
1761        res[o * 4 + 2] = w;
1762        res[o * 4 + 3] = h;
1763        self.counter += 1;
1764    }
1765}
1766
1767/// Measure callback for height-for-width items in a FlexboxLayout.
1768///
1769/// Called by taffy during the flex solve for items that need dynamic sizing.
1770/// Receives `(child_index, known_width, known_height)` where `known_width`/`known_height`
1771/// are `Some` if taffy has already determined that dimension.
1772/// Returns `(width, height)`.
1773pub type FlexboxMeasureFn<'a> =
1774    Option<&'a mut dyn FnMut(usize, Option<Coord>, Option<Coord>) -> (Coord, Coord)>;
1775
1776pub fn solve_flexbox_layout(
1777    data: &FlexboxLayoutData,
1778    repeater_indices: Slice<u32>,
1779) -> SharedVector<Coord> {
1780    // Build a simple measure callback from the pre-computed cells data.
1781    // This enables height-for-width: taffy calls back with the actual
1782    // assigned width, and we return the pre-computed height from cells_v.
1783    // The cells_v heights were computed with the horizontal preferred size
1784    // as constraint, which is a good approximation.
1785    let mut measure = |child_index: usize,
1786                       known_w: Option<Coord>,
1787                       known_h: Option<Coord>|
1788     -> (Coord, Coord) {
1789        let w = known_w.unwrap_or_else(|| {
1790            data.cells_h.get(child_index).map_or(0 as Coord, |c| c.constraint.preferred_bounded())
1791        });
1792        let h = known_h.unwrap_or_else(|| {
1793            data.cells_v.get(child_index).map_or(0 as Coord, |c| c.constraint.preferred_bounded())
1794        });
1795        (w, h)
1796    };
1797    solve_flexbox_layout_with_measure(data, repeater_indices, Some(&mut measure))
1798}
1799
1800/// Solve a FlexboxLayout using Taffy
1801/// Returns: [x1, y1, w1, h1, x2, y2, w2, h2, ...] for each item
1802pub fn solve_flexbox_layout_with_measure(
1803    data: &FlexboxLayoutData,
1804    repeater_indices: Slice<u32>,
1805    measure: FlexboxMeasureFn<'_>,
1806) -> SharedVector<Coord> {
1807    // 4 values per item: x, y, width, height
1808    let mut result = SharedVector::<Coord>::default();
1809    result.resize(data.cells_h.len() * 4 + repeater_indices.len() * 2, 0 as _);
1810
1811    if data.cells_h.is_empty() {
1812        return result;
1813    }
1814
1815    let taffy_direction = match data.direction {
1816        FlexboxLayoutDirection::Row => flexbox_taffy::TaffyFlexDirection::Row,
1817        FlexboxLayoutDirection::RowReverse => flexbox_taffy::TaffyFlexDirection::RowReverse,
1818        FlexboxLayoutDirection::Column => flexbox_taffy::TaffyFlexDirection::Column,
1819        FlexboxLayoutDirection::ColumnReverse => flexbox_taffy::TaffyFlexDirection::ColumnReverse,
1820    };
1821
1822    let (container_width, container_height) = (
1823        if data.width > 0 as Coord { Some(data.width) } else { None },
1824        if data.height > 0 as Coord { Some(data.height) } else { None },
1825    );
1826
1827    let use_measure = measure.is_some();
1828    let mut builder = flexbox_taffy::FlexboxTaffyBuilder::new(flexbox_taffy::FlexboxLayoutParams {
1829        cells_h: &data.cells_h,
1830        cells_v: &data.cells_v,
1831        spacing_h: data.spacing_h,
1832        spacing_v: data.spacing_v,
1833        padding_h: &data.padding_h,
1834        padding_v: &data.padding_v,
1835        alignment: data.alignment,
1836        align_content: data.align_content,
1837        cross_axis_alignment: data.cross_axis_alignment,
1838        flex_wrap: data.flex_wrap,
1839        flex_direction: taffy_direction,
1840        container_width,
1841        container_height,
1842        use_measure_for_cross_axis: use_measure,
1843    });
1844
1845    let (available_width, available_height) = match data.direction {
1846        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => {
1847            (data.width, Coord::MAX)
1848        }
1849        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => {
1850            (Coord::MAX, data.height)
1851        }
1852    };
1853
1854    builder.compute_layout(available_width, available_height, measure);
1855
1856    // Extract results using the cache generator to handle repeaters.
1857    // If `order` sorting was applied, we need to collect results by original index first,
1858    // because the cache generator expects items in their original declaration order.
1859    if builder.order_map.is_empty() {
1860        let mut generator = FlexboxLayoutCacheGenerator::new(&repeater_indices, &mut result);
1861        for idx in 0..data.cells_h.len() {
1862            let (x, y, w, h) = builder.child_geometry(idx);
1863            generator.add(x, y, w, h);
1864        }
1865    } else {
1866        let count = data.cells_h.len();
1867        let mut geom = alloc::vec![(0 as Coord, 0 as Coord, 0 as Coord, 0 as Coord); count];
1868        for taffy_idx in 0..count {
1869            let orig_idx = builder.original_index(taffy_idx);
1870            geom[orig_idx] = builder.child_geometry(taffy_idx);
1871        }
1872        let mut generator = FlexboxLayoutCacheGenerator::new(&repeater_indices, &mut result);
1873        for (x, y, w, h) in geom {
1874            generator.add(x, y, w, h);
1875        }
1876    }
1877
1878    result
1879}
1880
1881/// Return main-axis LayoutInfo for a FlexboxLayout.
1882/// Only needs the same-axis cells, avoiding a cross-axis binding loop.
1883pub fn flexbox_layout_info_main_axis(
1884    cells: Slice<FlexboxLayoutItemInfo>,
1885    spacing: Coord,
1886    padding: &Padding,
1887    flex_wrap: FlexboxLayoutWrap,
1888) -> LayoutInfo {
1889    let extra_pad = padding.begin + padding.end;
1890    if cells.is_empty() {
1891        return LayoutInfo {
1892            min: extra_pad,
1893            preferred: extra_pad,
1894            max: extra_pad,
1895            ..Default::default()
1896        };
1897    }
1898    let num_spacings = cells.len().saturating_sub(1) as Coord;
1899    let min = if matches!(flex_wrap, FlexboxLayoutWrap::NoWrap) {
1900        cells.iter().map(|c| c.constraint.min).sum::<Coord>() + spacing * num_spacings + extra_pad
1901    } else {
1902        // Wrapping: the widest single item must fit
1903        cells.iter().map(|c| c.constraint.min).fold(0.0 as Coord, |a, b| a.max(b)) + extra_pad
1904    };
1905    let preferred = if matches!(flex_wrap, FlexboxLayoutWrap::NoWrap) {
1906        // No wrapping: all items on one line
1907        cells.iter().map(|c| c.constraint.preferred_bounded()).sum::<Coord>()
1908            + spacing * num_spacings
1909            + extra_pad
1910    } else {
1911        // Wrapping: estimate a roughly square layout using only main-axis sizes.
1912        // Approximate total area assuming each item is square (width == height),
1913        // then take sqrt to get a reasonable main-axis extent.
1914        let total_area: Coord = cells
1915            .iter()
1916            .map(|c| {
1917                let w = c.constraint.preferred_bounded();
1918                w * w
1919            })
1920            .sum();
1921        let count = cells.len();
1922        Float::sqrt(total_area as f32) as Coord + spacing * (count - 1) as Coord + extra_pad
1923    };
1924    let stretch = cells.iter().map(|c| c.constraint.stretch).sum::<f32>();
1925    LayoutInfo {
1926        min,
1927        max: Coord::MAX,
1928        min_percent: 0 as _,
1929        max_percent: 100 as _,
1930        preferred,
1931        stretch,
1932    }
1933}
1934
1935/// Return cross-axis LayoutInfo for a FlexboxLayout.
1936///
1937/// `constraint_size` is the main-axis container dimension (width for row,
1938/// height for column). When valid (> 0 and < MAX), it's used as the taffy
1939/// constraint for accurate wrapping. When invalid (e.g. 0, negative, or
1940/// MAX — which can happen due to circular dependencies in nested
1941/// perpendicular flexboxes), falls back to a heuristic based on
1942/// `flexbox_layout_info_main_axis`.
1943pub fn flexbox_layout_info_cross_axis(
1944    cells_h: Slice<FlexboxLayoutItemInfo>,
1945    cells_v: Slice<FlexboxLayoutItemInfo>,
1946    spacing_h: Coord,
1947    spacing_v: Coord,
1948    padding_h: &Padding,
1949    padding_v: &Padding,
1950    direction: FlexboxLayoutDirection,
1951    flex_wrap: FlexboxLayoutWrap,
1952    constraint_size: Coord,
1953) -> LayoutInfo {
1954    if cells_h.is_empty() {
1955        assert!(cells_v.is_empty());
1956        let orientation = match direction {
1957            FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => {
1958                Orientation::Vertical
1959            }
1960            FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => {
1961                Orientation::Horizontal
1962            }
1963        };
1964        let padding = match orientation {
1965            Orientation::Horizontal => padding_h,
1966            Orientation::Vertical => padding_v,
1967        };
1968        let pad = padding.begin + padding.end;
1969        return LayoutInfo { min: pad, preferred: pad, max: pad, ..Default::default() };
1970    }
1971
1972    // Determine which axis is cross
1973    let cross_cells = match direction {
1974        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => &cells_v,
1975        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => &cells_h,
1976    };
1977
1978    // Compute the main-axis preferred size to use as the constraint for taffy,
1979    // using the same heuristic as flexbox_layout_info_main_axis.
1980    let (main_cells, main_spacing, main_padding) = match direction {
1981        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => {
1982            (&cells_h, spacing_h, padding_h)
1983        }
1984        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => {
1985            (&cells_v, spacing_v, padding_v)
1986        }
1987    };
1988    let main_extra_pad = main_padding.begin + main_padding.end;
1989    let main_axis_constraint = if constraint_size > 0 as Coord && constraint_size < Coord::MAX {
1990        // Use the actual container main-axis dimension (accurate)
1991        constraint_size
1992    } else if matches!(flex_wrap, FlexboxLayoutWrap::NoWrap) || constraint_size >= Coord::MAX {
1993        // No-wrap mode, or caller signalled "unconstrained" via MAX
1994        // (used when no real main-axis dimension is in scope, e.g.
1995        // a nested perpendicular flex queried via vtable): treat the
1996        // main axis as unbounded so items don't wrap. This gives the
1997        // natural max-cell-cross-axis result rather than the
1998        // sqrt(item-areas) heuristic.
1999        Coord::MAX
2000    } else {
2001        // Use actual item areas (main * cross) for the heuristic, since both
2002        // axes' cells are available here (unlike flexbox_layout_info_main_axis).
2003        let total_area: Coord = main_cells
2004            .iter()
2005            .zip(cross_cells.iter())
2006            .map(|(m, c)| m.constraint.preferred_bounded() * c.constraint.preferred_bounded())
2007            .sum();
2008        let count = main_cells.len();
2009        Float::sqrt(total_area as f32) as Coord
2010            + main_spacing * (count - 1) as Coord
2011            + main_extra_pad
2012    };
2013
2014    let taffy_direction = match direction {
2015        FlexboxLayoutDirection::Row => flexbox_taffy::TaffyFlexDirection::Row,
2016        FlexboxLayoutDirection::RowReverse => flexbox_taffy::TaffyFlexDirection::RowReverse,
2017        FlexboxLayoutDirection::Column => flexbox_taffy::TaffyFlexDirection::Column,
2018        FlexboxLayoutDirection::ColumnReverse => flexbox_taffy::TaffyFlexDirection::ColumnReverse,
2019    };
2020
2021    let (container_width, container_height) = match direction {
2022        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => {
2023            (Some(main_axis_constraint), None)
2024        }
2025        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => {
2026            (None, Some(main_axis_constraint))
2027        }
2028    };
2029
2030    let mut builder = flexbox_taffy::FlexboxTaffyBuilder::new(flexbox_taffy::FlexboxLayoutParams {
2031        cells_h: &cells_h,
2032        cells_v: &cells_v,
2033        spacing_h,
2034        spacing_v,
2035        padding_h,
2036        padding_v,
2037        alignment: LayoutAlignment::Start,
2038        align_content: FlexboxLayoutAlignContent::Stretch,
2039        cross_axis_alignment: CrossAxisAlignment::Stretch,
2040        flex_wrap,
2041        flex_direction: taffy_direction,
2042        container_width,
2043        container_height,
2044        use_measure_for_cross_axis: false,
2045    });
2046
2047    let (available_width, available_height) = match direction {
2048        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => {
2049            (main_axis_constraint, Coord::MAX)
2050        }
2051        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => {
2052            (Coord::MAX, main_axis_constraint)
2053        }
2054    };
2055
2056    builder.compute_layout(available_width, available_height, None);
2057
2058    let (total_width, total_height) = builder.container_size();
2059    let cross_size = match direction {
2060        FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => total_height,
2061        FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => total_width,
2062    };
2063
2064    LayoutInfo {
2065        min: cross_size,
2066        max: Coord::MAX,
2067        min_percent: 0 as _,
2068        max_percent: 100 as _,
2069        preferred: cross_size,
2070        stretch: 0.0,
2071    }
2072}
2073
2074#[cfg(feature = "ffi")]
2075pub(crate) mod ffi {
2076    #![allow(unsafe_code)]
2077
2078    use super::*;
2079
2080    #[unsafe(no_mangle)]
2081    pub extern "C" fn slint_organize_grid_layout(
2082        input_data: Slice<GridLayoutInputData>,
2083        repeater_indices: Slice<u32>,
2084        repeater_steps: Slice<u32>,
2085        result: &mut GridLayoutOrganizedData,
2086    ) {
2087        *result = super::organize_grid_layout(input_data, repeater_indices, repeater_steps);
2088    }
2089
2090    #[unsafe(no_mangle)]
2091    pub extern "C" fn slint_organize_dialog_button_layout(
2092        input_data: Slice<GridLayoutInputData>,
2093        dialog_button_roles: Slice<DialogButtonRole>,
2094        result: &mut GridLayoutOrganizedData,
2095    ) {
2096        *result = super::organize_dialog_button_layout(input_data, dialog_button_roles);
2097    }
2098
2099    #[unsafe(no_mangle)]
2100    pub extern "C" fn slint_solve_grid_layout(
2101        data: &GridLayoutData,
2102        constraints: Slice<LayoutItemInfo>,
2103        orientation: Orientation,
2104        repeater_indices: Slice<u32>,
2105        repeater_steps: Slice<u32>,
2106        result: &mut SharedVector<Coord>,
2107    ) {
2108        *result = super::solve_grid_layout(
2109            data,
2110            constraints,
2111            orientation,
2112            repeater_indices,
2113            repeater_steps,
2114        )
2115    }
2116
2117    #[unsafe(no_mangle)]
2118    pub extern "C" fn slint_grid_layout_info(
2119        organized_data: &GridLayoutOrganizedData,
2120        constraints: Slice<LayoutItemInfo>,
2121        repeater_indices: Slice<u32>,
2122        repeater_steps: Slice<u32>,
2123        spacing: Coord,
2124        padding: &Padding,
2125        orientation: Orientation,
2126    ) -> LayoutInfo {
2127        super::grid_layout_info(
2128            organized_data.clone(),
2129            constraints,
2130            repeater_indices,
2131            repeater_steps,
2132            spacing,
2133            padding,
2134            orientation,
2135        )
2136    }
2137
2138    #[unsafe(no_mangle)]
2139    pub extern "C" fn slint_solve_box_layout(
2140        data: &BoxLayoutData,
2141        repeater_indices: Slice<u32>,
2142        result: &mut SharedVector<Coord>,
2143    ) {
2144        *result = super::solve_box_layout(data, repeater_indices)
2145    }
2146
2147    #[unsafe(no_mangle)]
2148    pub extern "C" fn slint_solve_box_layout_ortho(
2149        data: &BoxLayoutOrthoData,
2150        repeater_indices: Slice<u32>,
2151        result: &mut SharedVector<Coord>,
2152    ) {
2153        *result = super::solve_box_layout_ortho(data, repeater_indices)
2154    }
2155
2156    #[unsafe(no_mangle)]
2157    /// Return the LayoutInfo for a BoxLayout with the given cells.
2158    pub extern "C" fn slint_box_layout_info(
2159        cells: Slice<LayoutItemInfo>,
2160        spacing: Coord,
2161        padding: &Padding,
2162        alignment: LayoutAlignment,
2163    ) -> LayoutInfo {
2164        super::box_layout_info(cells, spacing, padding, alignment)
2165    }
2166
2167    #[unsafe(no_mangle)]
2168    /// Return the LayoutInfo for a BoxLayout with the given cells.
2169    pub extern "C" fn slint_box_layout_info_ortho(
2170        cells: Slice<LayoutItemInfo>,
2171        padding: &Padding,
2172    ) -> LayoutInfo {
2173        super::box_layout_info_ortho(cells, padding)
2174    }
2175
2176    /// The measure callback for C FFI. Returns (width, height) via out pointers.
2177    /// `known_width`/`known_height` are negative if not determined yet.
2178    /// A null function pointer means no measure callback.
2179    pub type FlexboxMeasureFnC = unsafe extern "C" fn(
2180        user_data: *mut core::ffi::c_void,
2181        child_index: usize,
2182        known_width: Coord,
2183        known_height: Coord,
2184        out_width: *mut Coord,
2185        out_height: *mut Coord,
2186    );
2187
2188    #[unsafe(no_mangle)]
2189    #[allow(unsafe_code)]
2190    pub extern "C" fn slint_solve_flexbox_layout(
2191        data: &FlexboxLayoutData,
2192        repeater_indices: Slice<u32>,
2193        result: &mut SharedVector<Coord>,
2194        measure_fn: *const core::ffi::c_void,
2195        measure_user_data: *mut core::ffi::c_void,
2196    ) {
2197        // Safety: measure_fn, when non-null, is a valid FlexboxMeasureFnC function pointer
2198        // passed as *const c_void because cbindgen can't represent Option<fn pointer> in C++.
2199        const {
2200            assert!(
2201                core::mem::size_of::<*const core::ffi::c_void>()
2202                    == core::mem::size_of::<FlexboxMeasureFnC>()
2203            );
2204        }
2205        let measure_fn: Option<FlexboxMeasureFnC> = if measure_fn.is_null() {
2206            None
2207        } else {
2208            Some(unsafe {
2209                core::mem::transmute::<*const core::ffi::c_void, FlexboxMeasureFnC>(measure_fn)
2210            })
2211        };
2212        if let Some(c_measure) = measure_fn {
2213            let mut measure = |child_index: usize,
2214                               known_w: Option<Coord>,
2215                               known_h: Option<Coord>|
2216             -> (Coord, Coord) {
2217                let mut out_w: Coord = 0 as _;
2218                let mut out_h: Coord = 0 as _;
2219                // Safety: c_measure is a valid function pointer provided by the caller,
2220                // and out_w/out_h are valid mutable pointers.
2221                unsafe {
2222                    c_measure(
2223                        measure_user_data,
2224                        child_index,
2225                        known_w.unwrap_or(-1 as _),
2226                        known_h.unwrap_or(-1 as _),
2227                        &mut out_w,
2228                        &mut out_h,
2229                    );
2230                }
2231                (out_w, out_h)
2232            };
2233            *result = super::solve_flexbox_layout_with_measure(
2234                data,
2235                repeater_indices,
2236                Some(&mut measure),
2237            );
2238        } else {
2239            *result = super::solve_flexbox_layout(data, repeater_indices);
2240        }
2241    }
2242
2243    #[unsafe(no_mangle)]
2244    /// Return main-axis LayoutInfo for a FlexboxLayout (single-axis, no cross-axis dependency).
2245    pub extern "C" fn slint_flexbox_layout_info_main_axis(
2246        cells: Slice<FlexboxLayoutItemInfo>,
2247        spacing: Coord,
2248        padding: &Padding,
2249        flex_wrap: FlexboxLayoutWrap,
2250    ) -> LayoutInfo {
2251        super::flexbox_layout_info_main_axis(cells, spacing, padding, flex_wrap)
2252    }
2253
2254    #[unsafe(no_mangle)]
2255    /// Return cross-axis LayoutInfo for a FlexboxLayout.
2256    pub extern "C" fn slint_flexbox_layout_info_cross_axis(
2257        cells_h: Slice<FlexboxLayoutItemInfo>,
2258        cells_v: Slice<FlexboxLayoutItemInfo>,
2259        spacing_h: Coord,
2260        spacing_v: Coord,
2261        padding_h: &Padding,
2262        padding_v: &Padding,
2263        direction: FlexboxLayoutDirection,
2264        flex_wrap: FlexboxLayoutWrap,
2265        constraint_size: Coord,
2266    ) -> LayoutInfo {
2267        super::flexbox_layout_info_cross_axis(
2268            cells_h,
2269            cells_v,
2270            spacing_h,
2271            spacing_v,
2272            padding_h,
2273            padding_v,
2274            direction,
2275            flex_wrap,
2276            constraint_size,
2277        )
2278    }
2279}
2280
2281#[cfg(test)]
2282mod tests {
2283    use super::*;
2284
2285    fn collect_from_organized_data(
2286        organized_data: &GridLayoutOrganizedData,
2287        num_cells: usize,
2288        repeater_indices: Slice<u32>,
2289        repeater_steps: Slice<u32>,
2290    ) -> Vec<(u16, u16, u16, u16)> {
2291        let mut result = Vec::new();
2292        for i in 0..num_cells {
2293            let col_and_span = organized_data.col_or_row_and_span(
2294                i,
2295                Orientation::Horizontal,
2296                &repeater_indices,
2297                &repeater_steps,
2298            );
2299            let row_and_span = organized_data.col_or_row_and_span(
2300                i,
2301                Orientation::Vertical,
2302                &repeater_indices,
2303                &repeater_steps,
2304            );
2305            result.push((col_and_span.0, col_and_span.1, row_and_span.0, row_and_span.1));
2306        }
2307        result
2308    }
2309
2310    #[test]
2311    fn test_organized_data_generator_2_fixed_cells() {
2312        // 2 fixed cells
2313        let mut result = GridLayoutOrganizedData::default();
2314        let num_cells = 2;
2315        let mut generator = OrganizedDataGenerator::new(&[], &[], num_cells, 0, 0, &mut result);
2316        generator.add(0, 1, 0, 1);
2317        generator.add(1, 2, 0, 3);
2318        assert_eq!(result.as_slice(), &[0, 1, 0, 1, 1, 2, 0, 3]);
2319
2320        let repeater_indices = Slice::from_slice(&[]);
2321        let empty_steps = Slice::from_slice(&[]);
2322        let collected_data =
2323            collect_from_organized_data(&result, num_cells, repeater_indices, empty_steps);
2324        assert_eq!(collected_data.as_slice(), &[(0, 1, 0, 1), (1, 2, 0, 3)]);
2325
2326        assert_eq!(
2327            result.max_value(num_cells, Orientation::Horizontal, &repeater_indices, &empty_steps),
2328            3
2329        );
2330        assert_eq!(
2331            result.max_value(num_cells, Orientation::Vertical, &repeater_indices, &empty_steps),
2332            3
2333        );
2334    }
2335
2336    #[test]
2337    fn test_organized_data_generator_1_fixed_cell_1_repeater() {
2338        // 4 cells: 1 fixed cell, 1 repeater with 3 repeated cells
2339        let mut result = GridLayoutOrganizedData::default();
2340        let num_cells = 4;
2341        let repeater_indices = &[1u32, 3u32];
2342        let mut generator =
2343            OrganizedDataGenerator::new(repeater_indices, &[], 1, 1, 3, &mut result);
2344        generator.add(0, 1, 0, 2); // fixed
2345        generator.add(1, 2, 1, 3); // repeated
2346        generator.add(1, 1, 2, 4);
2347        generator.add(2, 2, 3, 5);
2348        assert_eq!(
2349            result.as_slice(),
2350            &[
2351                0, 1, 0, 2, // fixed cell
2352                8, 4, 0, 0, // jump cell: data_base=8, stride=4 (step=1, epi=4)
2353                1, 2, 1, 3, // repeated cell 1
2354                1, 1, 2, 4, // repeated cell 2
2355                2, 2, 3, 5, // repeated cell 3
2356            ]
2357        );
2358        let repeater_indices = Slice::from_slice(repeater_indices);
2359        let empty_steps = Slice::from_slice(&[]);
2360        let collected_data =
2361            collect_from_organized_data(&result, num_cells, repeater_indices, empty_steps);
2362        assert_eq!(
2363            collected_data.as_slice(),
2364            &[(0, 1, 0, 2), (1, 2, 1, 3), (1, 1, 2, 4), (2, 2, 3, 5)]
2365        );
2366
2367        assert_eq!(
2368            result.max_value(num_cells, Orientation::Horizontal, &repeater_indices, &empty_steps),
2369            4
2370        );
2371        assert_eq!(
2372            result.max_value(num_cells, Orientation::Vertical, &repeater_indices, &empty_steps),
2373            8
2374        );
2375    }
2376
2377    #[test]
2378
2379    fn test_organize_data_with_auto_and_spans() {
2380        let auto = i_slint_common::ROW_COL_AUTO;
2381        let input = std::vec![
2382            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 2., rowspan: -1. },
2383            GridLayoutInputData { new_row: false, col: auto, row: auto, colspan: 1., rowspan: 2. },
2384            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 2., rowspan: 1. },
2385            GridLayoutInputData { new_row: true, col: -2., row: 80000., colspan: 2., rowspan: 1. },
2386        ];
2387        let repeater_indices = Slice::from_slice(&[]);
2388        let (organized_data, errors) = organize_grid_layout_impl(
2389            Slice::from_slice(&input),
2390            repeater_indices,
2391            Slice::from_slice(&[]),
2392        );
2393        assert_eq!(
2394            organized_data.as_slice(),
2395            &[
2396                0, 2, 0, 0, // row 0, col 0, rowspan 0 (see below)
2397                2, 1, 0, 2, // row 0, col 2 (due to colspan of first cell)
2398                0, 2, 1, 1, // row 1, col 0
2399                0, 2, 65535, 1, // row 65535, col 0
2400            ]
2401        );
2402        assert_eq!(errors.len(), 3);
2403        // Note that a rowspan of 0 is valid, it means the cell doesn't occupy any row
2404        assert_eq!(errors[0], "cell rowspan -1 is negative, clamping to 0");
2405        assert_eq!(errors[1], "cell row 80000 is too large, clamping to 65535");
2406        assert_eq!(errors[2], "cell col -2 is negative, clamping to 0");
2407        let empty_steps = Slice::from_slice(&[]);
2408        let collected_data = collect_from_organized_data(
2409            &organized_data,
2410            input.len(),
2411            repeater_indices,
2412            empty_steps,
2413        );
2414        assert_eq!(
2415            collected_data.as_slice(),
2416            &[(0, 2, 0, 0), (2, 1, 0, 2), (0, 2, 1, 1), (0, 2, 65535, 1)]
2417        );
2418        assert_eq!(
2419            organized_data.max_value(3, Orientation::Horizontal, &repeater_indices, &empty_steps),
2420            3
2421        );
2422        assert_eq!(
2423            organized_data.max_value(3, Orientation::Vertical, &repeater_indices, &empty_steps),
2424            2
2425        );
2426    }
2427
2428    #[test]
2429    fn test_organize_data_1_empty_repeater() {
2430        // Row { Text {}    if false: Text {} }, this test shows why we need i32 for cell_nr_adj
2431        let auto = i_slint_common::ROW_COL_AUTO;
2432        let cell =
2433            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 1., rowspan: 1. };
2434        let input = std::vec![cell];
2435        let repeater_indices = Slice::from_slice(&[1u32, 0u32]);
2436        let (organized_data, errors) = organize_grid_layout_impl(
2437            Slice::from_slice(&input),
2438            repeater_indices,
2439            Slice::from_slice(&[]),
2440        );
2441        assert_eq!(
2442            organized_data.as_slice(),
2443            &[
2444                0, 1, 0, 1, // fixed
2445                0, 0, 0, 0
2446            ] // jump to repeater data (not used)
2447        );
2448        assert_eq!(errors.len(), 0);
2449        let empty_steps = Slice::from_slice(&[]);
2450        let collected_data = collect_from_organized_data(
2451            &organized_data,
2452            input.len(),
2453            repeater_indices,
2454            empty_steps,
2455        );
2456        assert_eq!(collected_data.as_slice(), &[(0, 1, 0, 1)]);
2457        assert_eq!(
2458            organized_data.max_value(1, Orientation::Horizontal, &repeater_indices, &empty_steps),
2459            1
2460        );
2461    }
2462
2463    #[test]
2464    fn test_organize_data_4_repeaters() {
2465        let auto = i_slint_common::ROW_COL_AUTO;
2466        let mut cell =
2467            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 1., rowspan: 1. };
2468        let mut input = std::vec![cell.clone()];
2469        for _ in 0..8 {
2470            cell.new_row = false;
2471            input.push(cell.clone());
2472        }
2473        let repeater_indices = Slice::from_slice(&[0u32, 0u32, 1u32, 4u32, 6u32, 2u32, 8u32, 0u32]);
2474        let (organized_data, errors) = organize_grid_layout_impl(
2475            Slice::from_slice(&input),
2476            repeater_indices,
2477            Slice::from_slice(&[]),
2478        );
2479        assert_eq!(
2480            organized_data.as_slice(),
2481            &[
2482                28, 4, 0, 0, // rep0 jump: data at 28, stride=4 (empty)
2483                0, 1, 0, 1, // fixed cell (col=0)
2484                28, 4, 0, 0, // rep1 jump: data at 28, stride=4 (4 rows)
2485                5, 1, 0, 1, // fixed cell (col=5)
2486                44, 4, 0, 0, // rep2 jump: data at 44, stride=4 (2 rows)
2487                52, 4, 0, 0, // rep3 jump: data at 52, stride=4 (empty)
2488                8, 1, 0, 1, // fixed cell (col=8)
2489                1, 1, 0, 1, // rep1 row 0
2490                2, 1, 0, 1, // rep1 row 1
2491                3, 1, 0, 1, // rep1 row 2
2492                4, 1, 0, 1, // rep1 row 3
2493                6, 1, 0, 1, // rep2 row 0
2494                7, 1, 0, 1, // rep2 row 1
2495            ]
2496        );
2497        assert_eq!(errors.len(), 0);
2498        let empty_steps = Slice::from_slice(&[]);
2499        let collected_data = collect_from_organized_data(
2500            &organized_data,
2501            input.len(),
2502            repeater_indices,
2503            empty_steps,
2504        );
2505        assert_eq!(
2506            collected_data.as_slice(),
2507            &[
2508                (0, 1, 0, 1),
2509                (1, 1, 0, 1),
2510                (2, 1, 0, 1),
2511                (3, 1, 0, 1),
2512                (4, 1, 0, 1),
2513                (5, 1, 0, 1),
2514                (6, 1, 0, 1),
2515                (7, 1, 0, 1),
2516                (8, 1, 0, 1),
2517            ]
2518        );
2519        let empty_steps = Slice::from_slice(&[]);
2520        assert_eq!(
2521            organized_data.max_value(
2522                input.len(),
2523                Orientation::Horizontal,
2524                &repeater_indices,
2525                &empty_steps
2526            ),
2527            9
2528        );
2529    }
2530
2531    #[test]
2532    fn test_organize_data_repeated_rows() {
2533        let auto = i_slint_common::ROW_COL_AUTO;
2534        let mut input = Vec::new();
2535        let num_rows: u32 = 3;
2536        let num_columns: u32 = 2;
2537        // 3 rows of 2 columns each
2538        for _ in 0..num_rows {
2539            let mut cell = GridLayoutInputData {
2540                new_row: true,
2541                col: auto,
2542                row: auto,
2543                colspan: 1.,
2544                rowspan: 1.,
2545            };
2546            input.push(cell.clone());
2547            cell.new_row = false;
2548            input.push(cell.clone());
2549        }
2550        // Repeater 0: starts at index 0, has 3 instances of 2 elements
2551        let repeater_indices_arr = [0_u32, num_rows];
2552        let repeater_steps_arr = [num_columns];
2553        let repeater_steps = Slice::from_slice(&repeater_steps_arr);
2554        let repeater_indices = Slice::from_slice(&repeater_indices_arr);
2555        let (organized_data, errors) =
2556            organize_grid_layout_impl(Slice::from_slice(&input), repeater_indices, repeater_steps);
2557        assert_eq!(
2558            organized_data.as_slice(),
2559            &[
2560                4, 8, 0, 0, // jump cell: data at u16 idx 4, stride=8 (=step*4=2*4)
2561                0, 1, 0, 1, 1, 1, 0, 1, // row 0: col 0, col 1
2562                0, 1, 1, 1, 1, 1, 1, 1, // row 1: col 0, col 1
2563                0, 1, 2, 1, 1, 1, 2, 1, // row 2: col 0, col 1
2564            ]
2565        );
2566        assert_eq!(errors.len(), 0);
2567        let collected_data = collect_from_organized_data(
2568            &organized_data,
2569            input.len(),
2570            repeater_indices,
2571            repeater_steps,
2572        );
2573        assert_eq!(
2574            collected_data.as_slice(),
2575            // (col, colspan, row, rowspan) for each cell in input order
2576            &[(0, 1, 0, 1), (1, 1, 0, 1), (0, 1, 1, 1), (1, 1, 1, 1), (0, 1, 2, 1), (1, 1, 2, 1),]
2577        );
2578        assert_eq!(
2579            organized_data.max_value(
2580                input.len(),
2581                Orientation::Horizontal,
2582                &repeater_indices,
2583                &repeater_steps
2584            ),
2585            2
2586        );
2587        assert_eq!(
2588            organized_data.max_value(
2589                input.len(),
2590                Orientation::Vertical,
2591                &repeater_indices,
2592                &repeater_steps
2593            ),
2594            3
2595        );
2596
2597        // Now test GridLayoutCacheGenerator
2598        let mut layout_cache_v = SharedVector::<Coord>::default();
2599        let mut generator = GridLayoutCacheGenerator::new(
2600            repeater_indices.as_slice(),
2601            repeater_steps.as_slice(),
2602            0, // static_cells
2603            1, // num_repeaters
2604            6, // total_repeated_cells (3 rows * 2 columns)
2605            &mut layout_cache_v,
2606        );
2607        // Row 0
2608        generator.add(0., 50.);
2609        generator.add(0., 50.);
2610        // Row 1
2611        generator.add(50., 50.);
2612        generator.add(50., 50.);
2613        // Row 2
2614        generator.add(100., 50.);
2615        generator.add(100., 50.);
2616        assert_eq!(
2617            layout_cache_v.as_slice(),
2618            &[
2619                2., 4., // jump cell: data at pos 2, stride=4 (=step*2=2*2)
2620                0., 50., 0., 50., // row 0
2621                50., 50., 50., 50., // row 1
2622                100., 50., 100., 50., // row 2
2623            ]
2624        );
2625
2626        // GridRepeaterCacheAccess: cache[cache[jump_index] + ri * stride + child_offset]
2627        let layout_cache_v_access = |jump_index: usize,
2628                                     repeater_index: usize,
2629                                     stride: usize,
2630                                     child_offset: usize|
2631         -> Coord {
2632            let base = layout_cache_v[jump_index] as usize;
2633            let data_idx = base + repeater_index * stride + child_offset;
2634            layout_cache_v[data_idx]
2635        };
2636        // stride=4 (step=2, entries_per_item=2)
2637        // Y pos for child 0 (child_offset=0)
2638        assert_eq!(layout_cache_v_access(0, 0, 4, 0), 0.);
2639        assert_eq!(layout_cache_v_access(0, 1, 4, 0), 50.);
2640        assert_eq!(layout_cache_v_access(0, 2, 4, 0), 100.);
2641        // Y pos for child 1 (child_offset=2)
2642        assert_eq!(layout_cache_v_access(0, 0, 4, 2), 0.);
2643        assert_eq!(layout_cache_v_access(0, 1, 4, 2), 50.);
2644        assert_eq!(layout_cache_v_access(0, 2, 4, 2), 100.);
2645    }
2646
2647    #[test]
2648    fn test_organize_data_repeated_rows_multiple_repeaters() {
2649        let auto = i_slint_common::ROW_COL_AUTO;
2650        let mut input = Vec::new();
2651        let num_rows: u32 = 5;
2652        let mut cell =
2653            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 1., rowspan: 1. };
2654        // 3 rows of 2 columns each
2655        for _ in 0..3 {
2656            cell.new_row = true;
2657            input.push(cell.clone());
2658            cell.new_row = false;
2659            input.push(cell.clone());
2660        }
2661        // 2 rows of 3 columns each
2662        for _ in 0..2 {
2663            cell.new_row = true;
2664            input.push(cell.clone());
2665            cell.new_row = false;
2666            input.push(cell.clone());
2667            cell.new_row = false;
2668            input.push(cell.clone());
2669        }
2670        // Repeater 0: starts at index 0, has 3 instances of 2 elements
2671        // Repeater 1: starts at index 6 (after repeater 0's 3*2=6 cells), has 2 instances of 3 elements
2672        let repeater_indices_arr = [0_u32, 3, 6, 2];
2673        let repeater_steps_arr = [2, 3];
2674        let repeater_steps = Slice::from_slice(&repeater_steps_arr);
2675        let repeater_indices = Slice::from_slice(&repeater_indices_arr);
2676        let (organized_data, errors) =
2677            organize_grid_layout_impl(Slice::from_slice(&input), repeater_indices, repeater_steps);
2678        assert_eq!(
2679            organized_data.as_slice(),
2680            &[
2681                8, 8, 0, 0, // repeater 0 jump: data at 8, stride=8 (=step*4=2*4)
2682                32, 12, 0, 0, // repeater 1 jump: data at 32, stride=12 (=step*4=3*4)
2683                // Repeater 0 data
2684                0, 1, 0, 1, 1, 1, 0, 1, // row 0: col 0, col 1
2685                0, 1, 1, 1, 1, 1, 1, 1, // row 1: col 0, col 1
2686                0, 1, 2, 1, 1, 1, 2, 1, // row 2: col 0, col 1
2687                // Repeater 1 data
2688                0, 1, 3, 1, 1, 1, 3, 1, 2, 1, 3, 1, // row 0: col 0, col 1, col 2
2689                0, 1, 4, 1, 1, 1, 4, 1, 2, 1, 4, 1, // row 1: col 0, col 1, col 2
2690            ]
2691        );
2692        assert_eq!(errors.len(), 0);
2693        let collected_data = collect_from_organized_data(
2694            &organized_data,
2695            input.len(),
2696            repeater_indices,
2697            repeater_steps,
2698        );
2699        assert_eq!(
2700            collected_data.as_slice(),
2701            // (col, colspan, row, rowspan) for each cell in input order
2702            &[
2703                (0, 1, 0, 1),
2704                (1, 1, 0, 1),
2705                (0, 1, 1, 1),
2706                (1, 1, 1, 1),
2707                (0, 1, 2, 1),
2708                (1, 1, 2, 1),
2709                (0, 1, 3, 1),
2710                (1, 1, 3, 1),
2711                (2, 1, 3, 1),
2712                (0, 1, 4, 1),
2713                (1, 1, 4, 1),
2714                (2, 1, 4, 1)
2715            ]
2716        );
2717        assert_eq!(
2718            organized_data.max_value(
2719                input.len(),
2720                Orientation::Horizontal,
2721                &repeater_indices,
2722                &repeater_steps
2723            ),
2724            3 // max col (2) + colspan (1) = 3
2725        );
2726        assert_eq!(
2727            organized_data.max_value(
2728                input.len(),
2729                Orientation::Vertical,
2730                &repeater_indices,
2731                &repeater_steps
2732            ),
2733            num_rows as usize // max row (4) + rowspan (1) = 5
2734        );
2735
2736        // Now test GridLayoutCacheGenerator
2737        let mut layout_cache_v = SharedVector::<Coord>::default();
2738        let mut generator = GridLayoutCacheGenerator::new(
2739            repeater_indices.as_slice(),
2740            repeater_steps.as_slice(),
2741            0,  // static_cells
2742            2,  // num_repeaters
2743            12, // total_repeated_cells (3*2 + 2*3)
2744            &mut layout_cache_v,
2745        );
2746        // Row 0
2747        generator.add(0., 50.);
2748        generator.add(0., 50.);
2749        // Row 1
2750        generator.add(50., 50.);
2751        generator.add(50., 50.);
2752        // Row 2
2753        generator.add(100., 50.);
2754        generator.add(100., 50.);
2755        // Row 3
2756        generator.add(150., 50.);
2757        generator.add(150., 50.);
2758        generator.add(150., 50.);
2759        // Row 4
2760        generator.add(200., 50.);
2761        generator.add(200., 50.);
2762        generator.add(200., 50.);
2763        assert_eq!(
2764            layout_cache_v.as_slice(),
2765            &[
2766                4., 4., // repeater 0 jump: data at pos 4, stride=4 (=step*2=2*2)
2767                16., 6., // repeater 1 jump: data at pos 16, stride=6 (=step*2=3*2)
2768                0., 50., 0., 50., // repeater 0 row 0 data
2769                50., 50., 50., 50., // repeater 0 row 1 data
2770                100., 50., 100., 50., // repeater 0 row 2 data
2771                150., 50., 150., 50., 150., 50., // repeater 1 row 3 data
2772                200., 50., 200., 50., 200., 50., // repeater 1 row 4 data
2773            ]
2774        );
2775
2776        // GridRepeaterCacheAccess: cache[cache[jump_index] + ri * stride + child_offset]
2777        let layout_cache_v_access = |jump_index: usize,
2778                                     repeater_index: usize,
2779                                     stride: usize,
2780                                     child_offset: usize|
2781         -> Coord {
2782            let base = layout_cache_v[jump_index] as usize;
2783            let data_idx = base + repeater_index * stride + child_offset;
2784            layout_cache_v[data_idx]
2785        };
2786        // Repeater 0: Y pos for child 0 (child_offset=0), stride=4
2787        assert_eq!(layout_cache_v_access(0, 0, 4, 0), 0.);
2788        assert_eq!(layout_cache_v_access(0, 1, 4, 0), 50.);
2789        assert_eq!(layout_cache_v_access(0, 2, 4, 0), 100.);
2790        // Repeater 0: Y pos for child 1 (child_offset=2), stride=4
2791        assert_eq!(layout_cache_v_access(0, 0, 4, 2), 0.);
2792        assert_eq!(layout_cache_v_access(0, 1, 4, 2), 50.);
2793        assert_eq!(layout_cache_v_access(0, 2, 4, 2), 100.);
2794        // Repeater 1: Y pos for child 0 (child_offset=0), jump at index 2, stride=6
2795        assert_eq!(layout_cache_v_access(2, 0, 6, 0), 150.);
2796        assert_eq!(layout_cache_v_access(2, 1, 6, 0), 200.);
2797        // Repeater 1: Y pos for child 2 (child_offset=4), jump at index 2, stride=6
2798        assert_eq!(layout_cache_v_access(2, 0, 6, 4), 150.);
2799        assert_eq!(layout_cache_v_access(2, 1, 6, 4), 200.);
2800    }
2801
2802    #[test]
2803    fn test_layout_cache_generator_2_fixed_cells() {
2804        // 2 fixed cells
2805        let mut result = SharedVector::<Coord>::default();
2806        result.resize(2 * 2, 0 as _);
2807        let mut generator = LayoutCacheGenerator::new(&[], &mut result);
2808        generator.add(0., 50.); // fixed
2809        generator.add(80., 50.); // fixed
2810        assert_eq!(result.as_slice(), &[0., 50., 80., 50.]);
2811    }
2812
2813    #[test]
2814    fn test_layout_cache_generator_1_fixed_cell_1_repeater() {
2815        // 4 cells: 1 fixed cell, 1 repeater with 3 repeated cells
2816        let mut result = SharedVector::<Coord>::default();
2817        let repeater_indices = &[1, 3];
2818        result.resize(4 * 2 + repeater_indices.len(), 0 as _);
2819        let mut generator = LayoutCacheGenerator::new(repeater_indices, &mut result);
2820        generator.add(0., 50.); // fixed
2821        generator.add(80., 50.); // repeated
2822        generator.add(160., 50.);
2823        generator.add(240., 50.);
2824        assert_eq!(
2825            result.as_slice(),
2826            &[
2827                0., 50., // fixed
2828                4., 5., // jump to repeater data
2829                80., 50., 160., 50., 240., 50. // repeater data
2830            ]
2831        );
2832    }
2833
2834    #[test]
2835    fn test_layout_cache_generator_4_repeaters() {
2836        // 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
2837        let mut result = SharedVector::<Coord>::default();
2838        let repeater_indices = &[1, 0, 1, 4, 6, 2, 8, 0];
2839        result.resize(8 * 2 + repeater_indices.len(), 0 as _);
2840        let mut generator = LayoutCacheGenerator::new(repeater_indices, &mut result);
2841        generator.add(0., 50.); // fixed
2842        generator.add(80., 10.); // repeated
2843        generator.add(160., 10.);
2844        generator.add(240., 10.);
2845        generator.add(320., 10.); // end of second repeater
2846        generator.add(400., 80.); // fixed
2847        generator.add(500., 20.); // repeated
2848        generator.add(600., 20.); // end of third repeater
2849        assert_eq!(
2850            result.as_slice(),
2851            &[
2852                0., 50., // fixed
2853                12., 13., // jump to first (empty) repeater (not used)
2854                12., 13., // jump to second repeater data
2855                400., 80., // fixed
2856                20., 21., // jump to third repeater data
2857                0., 0., // slot for jumping to fourth repeater (currently empty)
2858                80., 10., 160., 10., 240., 10., 320., 10., // first repeater data
2859                500., 20., 600., 20. // second repeater data
2860            ]
2861        );
2862    }
2863}