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