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::{DialogButtonRole, LayoutAlignment};
9use crate::{Coord, SharedVector, slice::Slice};
10use alloc::format;
11use alloc::string::String;
12use alloc::vec::Vec;
13
14pub use crate::items::Orientation;
15
16/// The constraint that applies to an item
17// Also, the field needs to be in alphabetical order because how the generated code sort fields for struct
18#[repr(C)]
19#[derive(Clone, Copy, Debug, PartialEq)]
20pub struct LayoutInfo {
21    /// The maximum size for the item.
22    pub max: Coord,
23    /// The maximum size in percentage of the parent (value between 0 and 100).
24    pub max_percent: Coord,
25    /// The minimum size for this item.
26    pub min: Coord,
27    /// The minimum size in percentage of the parent (value between 0 and 100).
28    pub min_percent: Coord,
29    /// the preferred size
30    pub preferred: Coord,
31    /// the  stretch factor
32    pub stretch: f32,
33}
34
35impl Default for LayoutInfo {
36    fn default() -> Self {
37        LayoutInfo {
38            min: 0 as _,
39            max: Coord::MAX,
40            min_percent: 0 as _,
41            max_percent: 100 as _,
42            preferred: 0 as _,
43            stretch: 0 as _,
44        }
45    }
46}
47
48impl LayoutInfo {
49    // Note: This "logic" is duplicated in the cpp generator's generated code for merging layout infos.
50    #[must_use]
51    pub fn merge(&self, other: &LayoutInfo) -> Self {
52        Self {
53            min: self.min.max(other.min),
54            max: self.max.min(other.max),
55            min_percent: self.min_percent.max(other.min_percent),
56            max_percent: self.max_percent.min(other.max_percent),
57            preferred: self.preferred.max(other.preferred),
58            stretch: self.stretch.min(other.stretch),
59        }
60    }
61
62    /// Helper function to return a preferred size which is within the min/max constraints
63    #[must_use]
64    pub fn preferred_bounded(&self) -> Coord {
65        self.preferred.min(self.max).max(self.min)
66    }
67}
68
69impl core::ops::Add for LayoutInfo {
70    type Output = Self;
71
72    fn add(self, rhs: Self) -> Self::Output {
73        self.merge(&rhs)
74    }
75}
76
77/// Returns the logical min and max sizes given the provided layout constraints.
78pub fn min_max_size_for_layout_constraints(
79    constraints_horizontal: LayoutInfo,
80    constraints_vertical: LayoutInfo,
81) -> (Option<crate::api::LogicalSize>, Option<crate::api::LogicalSize>) {
82    let min_width = constraints_horizontal.min.min(constraints_horizontal.max) as f32;
83    let min_height = constraints_vertical.min.min(constraints_vertical.max) as f32;
84    let max_width = constraints_horizontal.max.max(constraints_horizontal.min) as f32;
85    let max_height = constraints_vertical.max.max(constraints_vertical.min) as f32;
86
87    //cfg!(target_arch = "wasm32") is there because wasm32 winit don't like when max size is None:
88    // panicked at 'Property is read only: JsValue(NoModificationAllowedError: CSSStyleDeclaration.removeProperty: Can't remove property 'max-width' from computed style
89
90    let min_size = if min_width > 0. || min_height > 0. || cfg!(target_arch = "wasm32") {
91        Some(crate::api::LogicalSize::new(min_width, min_height))
92    } else {
93        None
94    };
95
96    let max_size = if (max_width > 0.
97        && max_height > 0.
98        && (max_width < i32::MAX as f32 || max_height < i32::MAX as f32))
99        || cfg!(target_arch = "wasm32")
100    {
101        // maximum widget size for Qt and a workaround for the winit api not allowing partial constraints
102        let window_size_max = 16_777_215.;
103        Some(crate::api::LogicalSize::new(
104            max_width.min(window_size_max),
105            max_height.min(window_size_max),
106        ))
107    } else {
108        None
109    };
110
111    (min_size, max_size)
112}
113
114/// Implement a saturating_add version for both possible value of Coord.
115/// So that adding the max value does not overflow
116trait Saturating {
117    fn add(_: Self, _: Self) -> Self;
118}
119impl Saturating for i32 {
120    #[inline]
121    fn add(a: Self, b: Self) -> Self {
122        a.saturating_add(b)
123    }
124}
125impl Saturating for f32 {
126    #[inline]
127    fn add(a: Self, b: Self) -> Self {
128        a + b
129    }
130}
131
132mod grid_internal {
133    use super::*;
134
135    fn order_coord<T: PartialOrd>(a: &T, b: &T) -> core::cmp::Ordering {
136        a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)
137    }
138
139    #[derive(Debug, Clone)]
140    pub struct LayoutData {
141        // inputs
142        pub min: Coord,
143        pub max: Coord,
144        pub pref: Coord,
145        pub stretch: f32,
146
147        // outputs
148        pub pos: Coord,
149        pub size: Coord,
150    }
151
152    impl Default for LayoutData {
153        fn default() -> Self {
154            LayoutData {
155                min: 0 as _,
156                max: Coord::MAX,
157                pref: 0 as _,
158                stretch: f32::MAX,
159                pos: 0 as _,
160                size: 0 as _,
161            }
162        }
163    }
164
165    trait Adjust {
166        fn can_grow(_: &LayoutData) -> Coord;
167        fn to_distribute(expected_size: Coord, current_size: Coord) -> Coord;
168        fn distribute(_: &mut LayoutData, val: Coord);
169    }
170
171    struct Grow;
172    impl Adjust for Grow {
173        fn can_grow(it: &LayoutData) -> Coord {
174            it.max - it.size
175        }
176
177        fn to_distribute(expected_size: Coord, current_size: Coord) -> Coord {
178            expected_size - current_size
179        }
180
181        fn distribute(it: &mut LayoutData, val: Coord) {
182            it.size += val;
183        }
184    }
185
186    struct Shrink;
187    impl Adjust for Shrink {
188        fn can_grow(it: &LayoutData) -> Coord {
189            it.size - it.min
190        }
191
192        fn to_distribute(expected_size: Coord, current_size: Coord) -> Coord {
193            current_size - expected_size
194        }
195
196        fn distribute(it: &mut LayoutData, val: Coord) {
197            it.size -= val;
198        }
199    }
200
201    #[allow(clippy::unnecessary_cast)] // Coord
202    fn adjust_items<A: Adjust>(data: &mut [LayoutData], size_without_spacing: Coord) -> Option<()> {
203        loop {
204            let size_cannot_grow: Coord = data
205                .iter()
206                .filter(|it| A::can_grow(it) <= 0 as _)
207                .map(|it| it.size)
208                .fold(0 as Coord, Saturating::add);
209
210            let total_stretch: f32 =
211                data.iter().filter(|it| A::can_grow(it) > 0 as _).map(|it| it.stretch).sum();
212
213            let actual_stretch = |s: f32| if total_stretch <= 0. { 1. } else { s };
214
215            let max_grow = data
216                .iter()
217                .filter(|it| A::can_grow(it) > 0 as _)
218                .map(|it| A::can_grow(it) as f32 / actual_stretch(it.stretch))
219                .min_by(order_coord)?;
220
221            let current_size: Coord = data
222                .iter()
223                .filter(|it| A::can_grow(it) > 0 as _)
224                .map(|it| it.size)
225                .fold(0 as _, Saturating::add);
226
227            //let to_distribute = size_without_spacing - (size_cannot_grow + current_size);
228            let to_distribute =
229                A::to_distribute(size_without_spacing, size_cannot_grow + current_size) as f32;
230            if to_distribute <= 0. || max_grow <= 0. {
231                return Some(());
232            }
233
234            let grow = if total_stretch <= 0. {
235                to_distribute
236                    / (data.iter().filter(|it| A::can_grow(it) > 0 as _).count() as Coord) as f32
237            } else {
238                to_distribute / total_stretch
239            }
240            .min(max_grow);
241
242            let mut distributed = 0 as Coord;
243            for it in data.iter_mut().filter(|it| A::can_grow(it) > 0 as Coord) {
244                let val = (grow * actual_stretch(it.stretch)) as Coord;
245                A::distribute(it, val);
246                distributed += val;
247            }
248
249            if distributed <= 0 as Coord {
250                // This can happen when Coord is integer and there is less then a pixel to add to each elements
251                // just give the pixel to the one with the bigger stretch
252                if let Some(it) = data
253                    .iter_mut()
254                    .filter(|it| A::can_grow(it) > 0 as _)
255                    .max_by(|a, b| actual_stretch(a.stretch).total_cmp(&b.stretch))
256                {
257                    A::distribute(it, to_distribute as Coord);
258                }
259                return Some(());
260            }
261        }
262    }
263
264    pub fn layout_items(data: &mut [LayoutData], start_pos: Coord, size: Coord, spacing: Coord) {
265        let size_without_spacing = size - spacing * (data.len() - 1) as Coord;
266
267        let mut pref = 0 as Coord;
268        for it in data.iter_mut() {
269            it.size = it.pref;
270            pref += it.pref;
271        }
272        if size_without_spacing >= pref {
273            adjust_items::<Grow>(data, size_without_spacing);
274        } else if size_without_spacing < pref {
275            adjust_items::<Shrink>(data, size_without_spacing);
276        }
277
278        let mut pos = start_pos;
279        for it in data.iter_mut() {
280            it.pos = pos;
281            pos = Saturating::add(pos, Saturating::add(it.size, spacing));
282        }
283    }
284
285    #[test]
286    #[allow(clippy::float_cmp)] // We want bit-wise equality here
287    fn test_layout_items() {
288        let my_items = &mut [
289            LayoutData { min: 100., max: 200., pref: 100., stretch: 1., ..Default::default() },
290            LayoutData { min: 50., max: 300., pref: 100., stretch: 1., ..Default::default() },
291            LayoutData { min: 50., max: 150., pref: 100., stretch: 1., ..Default::default() },
292        ];
293
294        layout_items(my_items, 100., 650., 0.);
295        assert_eq!(my_items[0].size, 200.);
296        assert_eq!(my_items[1].size, 300.);
297        assert_eq!(my_items[2].size, 150.);
298
299        layout_items(my_items, 100., 200., 0.);
300        assert_eq!(my_items[0].size, 100.);
301        assert_eq!(my_items[1].size, 50.);
302        assert_eq!(my_items[2].size, 50.);
303
304        layout_items(my_items, 100., 300., 0.);
305        assert_eq!(my_items[0].size, 100.);
306        assert_eq!(my_items[1].size, 100.);
307        assert_eq!(my_items[2].size, 100.);
308    }
309
310    /// Create a vector of LayoutData (e.g. one per row if Vertical) based on the constraints and organized data
311    /// Used by both solve_grid_layout() and grid_layout_info()
312    pub fn to_layout_data(
313        organized_data: &GridLayoutOrganizedData,
314        constraints: Slice<LayoutItemInfo>,
315        orientation: Orientation,
316        repeater_indices: Slice<u32>,
317        repeater_steps: Slice<u32>,
318        spacing: Coord,
319        size: Option<Coord>,
320    ) -> Vec<LayoutData> {
321        assert!(organized_data.len().is_multiple_of(4));
322        let num = organized_data.max_value(
323            constraints.len(),
324            orientation,
325            &repeater_indices,
326            &repeater_steps,
327        ) as usize;
328        if num < 1 {
329            return Default::default();
330        }
331        let mut layout_data =
332            alloc::vec![grid_internal::LayoutData { stretch: 1., ..Default::default() }; num];
333        let mut has_spans = false;
334        for (idx, cell_data) in constraints.iter().enumerate() {
335            let constraint = &cell_data.constraint;
336            let mut max = constraint.max;
337            if let Some(size) = size {
338                max = max.min(size * constraint.max_percent / 100 as Coord);
339            }
340            let (col_or_row, span) = organized_data.col_or_row_and_span(
341                idx,
342                orientation,
343                &repeater_indices,
344                &repeater_steps,
345            );
346            for c in 0..(span as usize) {
347                let cdata = &mut layout_data[col_or_row as usize + c];
348                cdata.max = cdata.max.min(max);
349            }
350            if span == 1 {
351                let mut min = constraint.min;
352                if let Some(size) = size {
353                    min = min.max(size * constraint.min_percent / 100 as Coord);
354                }
355                let pref = constraint.preferred.min(max).max(min);
356                let cdata = &mut layout_data[col_or_row as usize];
357                cdata.min = cdata.min.max(min);
358                cdata.pref = cdata.pref.max(pref);
359                cdata.stretch = cdata.stretch.min(constraint.stretch);
360            } else {
361                has_spans = true;
362            }
363        }
364        if has_spans {
365            for (idx, cell_data) in constraints.iter().enumerate() {
366                let constraint = &cell_data.constraint;
367                let (col_or_row, span) = organized_data.col_or_row_and_span(
368                    idx,
369                    orientation,
370                    &repeater_indices,
371                    &repeater_steps,
372                );
373                if span > 1 {
374                    let span_data =
375                        &mut layout_data[(col_or_row as usize)..(col_or_row + span) as usize];
376
377                    // Adjust minimum sizes
378                    let mut min = constraint.min;
379                    if let Some(size) = size {
380                        min = min.max(size * constraint.min_percent / 100 as Coord);
381                    }
382                    grid_internal::layout_items(span_data, 0 as _, min, spacing);
383                    for cdata in span_data.iter_mut() {
384                        if cdata.min < cdata.size {
385                            cdata.min = cdata.size;
386                        }
387                    }
388
389                    // Adjust maximum sizes
390                    let mut max = constraint.max;
391                    if let Some(size) = size {
392                        max = max.min(size * constraint.max_percent / 100 as Coord);
393                    }
394                    grid_internal::layout_items(span_data, 0 as _, max, spacing);
395                    for cdata in span_data.iter_mut() {
396                        if cdata.max > cdata.size {
397                            cdata.max = cdata.size;
398                        }
399                    }
400
401                    // Adjust preferred sizes
402                    grid_internal::layout_items(span_data, 0 as _, constraint.preferred, spacing);
403                    for cdata in span_data.iter_mut() {
404                        cdata.pref = cdata.pref.max(cdata.size).min(cdata.max).max(cdata.min);
405                    }
406
407                    // Adjust stretches
408                    let total_stretch: f32 = span_data.iter().map(|c| c.stretch).sum();
409                    if total_stretch > constraint.stretch {
410                        for cdata in span_data.iter_mut() {
411                            cdata.stretch *= constraint.stretch / total_stretch;
412                        }
413                    }
414                }
415            }
416        }
417        layout_data
418    }
419}
420
421#[repr(C)]
422pub struct Constraint {
423    pub min: Coord,
424    pub max: Coord,
425}
426
427impl Default for Constraint {
428    fn default() -> Self {
429        Constraint { min: 0 as Coord, max: Coord::MAX }
430    }
431}
432
433#[repr(C)]
434#[derive(Copy, Clone, Debug, Default)]
435pub struct Padding {
436    pub begin: Coord,
437    pub end: Coord,
438}
439
440#[repr(C)]
441#[derive(Debug)]
442/// The horizontal or vertical data for all cells of a GridLayout, used as input to solve_grid_layout()
443pub struct GridLayoutData {
444    pub size: Coord,
445    pub spacing: Coord,
446    pub padding: Padding,
447    pub organized_data: GridLayoutOrganizedData,
448}
449
450/// The input data for a cell of a GridLayout, before row/col determination and before H/V split
451/// Used as input to organize_grid_layout()
452#[repr(C)]
453#[derive(Default, Debug, Clone)]
454pub struct GridLayoutInputData {
455    /// whether this cell is the first one in a Row element
456    pub new_row: bool,
457    /// col and row number.
458    /// Only ROW_COL_AUTO and the u16 range are valid, values outside of
459    /// that will be clamped with a warning at runtime
460    pub col: f32,
461    pub row: f32,
462    /// colspan and rowspan
463    /// Only the u16 range is valid, values outside of that will be clamped with a warning at runtime
464    pub colspan: f32,
465    pub rowspan: f32,
466}
467
468/// The organized layout data for a GridLayout, after row/col determination:
469/// For each cell, stores col, colspan, row, rowspan
470pub type GridLayoutOrganizedData = SharedVector<u16>;
471
472impl GridLayoutOrganizedData {
473    fn push_cell(&mut self, col: u16, colspan: u16, row: u16, rowspan: u16) {
474        self.push(col);
475        self.push(colspan);
476        self.push(row);
477        self.push(rowspan);
478    }
479
480    fn col_or_row_and_span(
481        &self,
482        cell_number: usize,
483        orientation: Orientation,
484        repeater_indices: &Slice<u32>,
485        repeater_steps: &Slice<u32>,
486    ) -> (u16, u16) {
487        // For every cell, we have 4 entries, each at their own index
488        // But we also need to take into account indirections for repeated items
489        let mut final_idx = 0;
490        let mut cell_nr_adj = 0i32; // needs to be signed in case we start with an empty repeater
491        let cell_number = cell_number as i32;
492        // repeater_indices is a list of (start_cell, count) pairs
493        for rep_idx in 0..(repeater_indices.len() / 2) {
494            let ri_start_cell = repeater_indices[rep_idx * 2] as i32;
495            if cell_number < ri_start_cell {
496                break;
497            }
498            let ri_cell_count = repeater_indices[rep_idx * 2 + 1] as i32;
499            let step = repeater_steps.get(rep_idx).copied().unwrap_or(1) as i32;
500            let cells_in_repeater = ri_cell_count * step;
501            if cells_in_repeater > 0
502                && cell_number >= ri_start_cell
503                && cell_number < ri_start_cell + cells_in_repeater
504            {
505                let cell_in_rep = cell_number - ri_start_cell;
506                let jump_pos = (ri_start_cell - cell_nr_adj) as usize * 4;
507                final_idx = self[jump_pos] as usize + (cell_in_rep * 4) as usize;
508                break;
509            }
510            // -1 is correct for an empty repeater (e.g. if false), which takes one position, for 0 real cells
511            // With step > 1, we have 'step' jump entries but cells_in_repeater actual cells
512            cell_nr_adj += cells_in_repeater - step;
513        }
514        if final_idx == 0 {
515            final_idx = ((cell_number - cell_nr_adj) * 4) as usize;
516        }
517        let offset = if orientation == Orientation::Horizontal { 0 } else { 2 };
518        (self[final_idx + offset], self[final_idx + offset + 1])
519    }
520
521    fn max_value(
522        &self,
523        num_cells: usize,
524        orientation: Orientation,
525        repeater_indices: &Slice<u32>,
526        repeater_steps: &Slice<u32>,
527    ) -> u16 {
528        let mut max = 0;
529        // This could be rewritten more efficiently to avoid a loop calling a loop, by keeping track of the repeaters we saw until now
530        // Not sure it's worth the complexity though
531        for idx in 0..num_cells {
532            let (col_or_row, span) =
533                self.col_or_row_and_span(idx, orientation, repeater_indices, repeater_steps);
534            max = max.max(col_or_row + span.max(1));
535        }
536        max
537    }
538}
539
540struct OrganizedDataGenerator<'a> {
541    // Input
542    repeater_indices: &'a [u32],
543    repeater_steps: &'a [u32],
544    // An always increasing counter, the index of the cell being added
545    counter: usize,
546    // The index/4 in result in which we should add the next repeated item
547    repeat_offset: usize,
548    // The index/4 in repeater_indices
549    next_rep: usize,
550    // The index/4 in result in which we should add the next non-repeated item
551    current_offset: usize,
552    // Output
553    result: &'a mut GridLayoutOrganizedData,
554}
555
556impl<'a> OrganizedDataGenerator<'a> {
557    fn new(
558        repeater_indices: &'a [u32],
559        repeater_steps: &'a [u32],
560        result: &'a mut GridLayoutOrganizedData,
561    ) -> Self {
562        // Calculate total repeated cells (count * step for each repeater)
563        let total_repeated_cells: usize = repeater_indices
564            .chunks(2)
565            .enumerate()
566            .map(|(i, chunk)| {
567                let count = chunk.get(1).copied().unwrap_or(0) as usize;
568                let step = repeater_steps.get(i).copied().unwrap_or(1) as usize;
569                count * step
570            })
571            .sum();
572        assert!(result.len() >= total_repeated_cells * 4);
573        let repeat_offset = result.len() / 4 - total_repeated_cells;
574        Self {
575            repeater_indices,
576            repeater_steps,
577            counter: 0,
578            repeat_offset,
579            next_rep: 0,
580            current_offset: 0,
581            result,
582        }
583    }
584    fn add(&mut self, col: u16, colspan: u16, row: u16, rowspan: u16) {
585        let res = self.result.make_mut_slice();
586        let o = loop {
587            if let Some(nr) = self.repeater_indices.get(self.next_rep * 2) {
588                let nr = *nr as usize;
589                let step = self.repeater_steps.get(self.next_rep).copied().unwrap_or(1) as usize;
590                if nr == self.counter {
591                    // Write jump entries for each element in the step
592                    for s in 0..step {
593                        for o in 0..4 {
594                            res[(self.current_offset + s) * 4 + o] =
595                                ((self.repeat_offset + s) * 4 + o) as _;
596                        }
597                    }
598                    self.current_offset += step;
599                }
600                if self.counter >= nr {
601                    let rep_count = self.repeater_indices[self.next_rep * 2 + 1] as usize;
602                    let cells_in_repeater = rep_count * step;
603                    if self.counter - nr == cells_in_repeater {
604                        // Advance repeat_offset past this repeater's data before moving to next
605                        self.repeat_offset += cells_in_repeater;
606                        self.next_rep += 1;
607                        continue;
608                    }
609                    // Calculate offset using row-major ordering: step entries per row
610                    let cell_in_rep = self.counter - nr;
611                    let row_in_rep = cell_in_rep / step;
612                    let col_in_rep = cell_in_rep % step;
613                    let offset = self.repeat_offset + col_in_rep + row_in_rep * step;
614                    break offset;
615                }
616            }
617            self.current_offset += 1;
618            break self.current_offset - 1;
619        };
620        res[o * 4] = col;
621        res[o * 4 + 1] = colspan;
622        res[o * 4 + 2] = row;
623        res[o * 4 + 3] = rowspan;
624        self.counter += 1;
625    }
626}
627
628/// Given the cells of a layout of a Dialog, re-order the buttons according to the platform
629/// This function assume that the `roles` contains the roles of the button which are the first cells in `input_data`
630pub fn organize_dialog_button_layout(
631    input_data: Slice<GridLayoutInputData>,
632    dialog_button_roles: Slice<DialogButtonRole>,
633) -> GridLayoutOrganizedData {
634    let mut organized_data = GridLayoutOrganizedData::default();
635    organized_data.reserve(input_data.len() * 4);
636
637    #[cfg(feature = "std")]
638    fn is_kde() -> bool {
639        // assume some Unix, check if XDG_CURRENT_DESKTOP starts with K
640        std::env::var("XDG_CURRENT_DESKTOP")
641            .ok()
642            .and_then(|v| v.as_bytes().first().copied())
643            .is_some_and(|x| x.eq_ignore_ascii_case(&b'K'))
644    }
645    #[cfg(not(feature = "std"))]
646    let is_kde = || true;
647
648    let expected_order: &[DialogButtonRole] = match crate::detect_operating_system() {
649        crate::items::OperatingSystemType::Windows => {
650            &[
651                DialogButtonRole::Reset,
652                DialogButtonRole::None, // spacer
653                DialogButtonRole::Accept,
654                DialogButtonRole::Action,
655                DialogButtonRole::Reject,
656                DialogButtonRole::Apply,
657                DialogButtonRole::Help,
658            ]
659        }
660        crate::items::OperatingSystemType::Macos | crate::items::OperatingSystemType::Ios => {
661            &[
662                DialogButtonRole::Help,
663                DialogButtonRole::Reset,
664                DialogButtonRole::Apply,
665                DialogButtonRole::Action,
666                DialogButtonRole::None, // spacer
667                DialogButtonRole::Reject,
668                DialogButtonRole::Accept,
669            ]
670        }
671        _ if is_kde() => {
672            // KDE variant
673            &[
674                DialogButtonRole::Help,
675                DialogButtonRole::Reset,
676                DialogButtonRole::None, // spacer
677                DialogButtonRole::Action,
678                DialogButtonRole::Accept,
679                DialogButtonRole::Apply,
680                DialogButtonRole::Reject,
681            ]
682        }
683        _ => {
684            // GNOME variant and fallback for WASM build
685            &[
686                DialogButtonRole::Help,
687                DialogButtonRole::Reset,
688                DialogButtonRole::None, // spacer
689                DialogButtonRole::Action,
690                DialogButtonRole::Accept,
691                DialogButtonRole::Apply,
692                DialogButtonRole::Reject,
693            ]
694        }
695    };
696
697    // Reorder the actual buttons according to expected_order
698    let mut column_for_input: Vec<usize> = Vec::with_capacity(dialog_button_roles.len());
699    for role in expected_order.iter() {
700        if role == &DialogButtonRole::None {
701            column_for_input.push(usize::MAX); // empty column, ensure nothing will match
702            continue;
703        }
704        for (idx, r) in dialog_button_roles.as_slice().iter().enumerate() {
705            if *r == *role {
706                column_for_input.push(idx);
707            }
708        }
709    }
710
711    for (input_index, cell) in input_data.as_slice().iter().enumerate() {
712        let col = column_for_input.iter().position(|&x| x == input_index);
713        if let Some(col) = col {
714            organized_data.push_cell(col as _, cell.colspan as _, cell.row as _, cell.rowspan as _);
715        } else {
716            // This is used for the main window (which is the only cell which isn't a button)
717            // Given lower_dialog_layout(), this will always be a single cell at 0,0 with a colspan of number_of_buttons
718            organized_data.push_cell(
719                cell.col as _,
720                cell.colspan as _,
721                cell.row as _,
722                cell.rowspan as _,
723            );
724        }
725    }
726    organized_data
727}
728
729type Errors = Vec<String>;
730
731pub fn organize_grid_layout(
732    input_data: Slice<GridLayoutInputData>,
733    repeater_indices: Slice<u32>,
734    repeater_steps: Slice<u32>,
735) -> GridLayoutOrganizedData {
736    let (organized_data, errors) =
737        organize_grid_layout_impl(input_data, repeater_indices, repeater_steps);
738    for error in errors {
739        crate::debug_log!("Slint layout error: {}", error);
740    }
741    organized_data
742}
743
744// Implement "auto" behavior for row/col numbers (unless specified in the slint file).
745fn organize_grid_layout_impl(
746    input_data: Slice<GridLayoutInputData>,
747    repeater_indices: Slice<u32>,
748    repeater_steps: Slice<u32>,
749) -> (GridLayoutOrganizedData, Errors) {
750    let mut organized_data = GridLayoutOrganizedData::default();
751    // Calculate extra space needed for jump entries when step > 1
752    // Each repeater with step > 1 needs (step - 1) extra entries for the additional jump slots
753    let extra_jump_entries: usize =
754        repeater_steps.iter().map(|&s| (s as usize).saturating_sub(1)).sum();
755    organized_data
756        .resize(input_data.len() * 4 + repeater_indices.len() * 2 + extra_jump_entries * 4, 0 as _);
757    let mut generator = OrganizedDataGenerator::new(
758        repeater_indices.as_slice(),
759        repeater_steps.as_slice(),
760        &mut organized_data,
761    );
762    let mut errors = Vec::new();
763
764    fn clamp_to_u16(value: f32, field_name: &str, errors: &mut Vec<String>) -> u16 {
765        if value < 0.0 {
766            errors.push(format!("cell {field_name} {value} is negative, clamping to 0"));
767            0
768        } else if value > u16::MAX as f32 {
769            errors
770                .push(format!("cell {field_name} {value} is too large, clamping to {}", u16::MAX));
771            u16::MAX
772        } else {
773            value as u16
774        }
775    }
776
777    let mut row = 0;
778    let mut col = 0;
779    let mut first = true;
780    for cell in input_data.as_slice().iter() {
781        if cell.new_row && !first {
782            row += 1;
783            col = 0;
784        }
785        first = false;
786
787        if cell.row != i_slint_common::ROW_COL_AUTO {
788            let cell_row = clamp_to_u16(cell.row, "row", &mut errors);
789            if row != cell_row {
790                row = cell_row;
791                col = 0;
792            }
793        }
794        if cell.col != i_slint_common::ROW_COL_AUTO {
795            col = clamp_to_u16(cell.col, "col", &mut errors);
796        }
797
798        let colspan = clamp_to_u16(cell.colspan, "colspan", &mut errors);
799        let rowspan = clamp_to_u16(cell.rowspan, "rowspan", &mut errors);
800        col = col.min(u16::MAX - colspan); // ensure col + colspan doesn't overflow
801        generator.add(col, colspan, row, rowspan);
802        col += colspan;
803    }
804    (organized_data, errors)
805}
806
807/// The layout cache generator inserts the pos and size into the result array (which becomes the layout cache property),
808/// including the indirections for repeated items (so that the x,y,width,height properties for repeated items
809/// can point to indices known at compile time, those that contain the indirections)
810/// Example: for repeater_indices=[1,4] (meaning that item at index 1 is repeated 4 times),
811/// 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]
812///  i.e. pos1, width1, jump to idx 4, jump to idx 5, pos2, width2, pos3, width3, pos4, width4, pos5, width5
813struct LayoutCacheGenerator<'a> {
814    // Input
815    repeater_indices: &'a [u32],
816    repeater_steps: &'a [u32],
817    // An always increasing counter, the index of the cell being added
818    counter: usize,
819    // The index/2 in result in which we should add the next repeated item
820    repeat_offset: usize,
821    // The index/2 in repeater_indices
822    next_rep: usize,
823    // The index/2 in result in which we should add the next non-repeated item
824    current_offset: usize,
825    // Output
826    result: &'a mut SharedVector<Coord>,
827}
828
829impl<'a> LayoutCacheGenerator<'a> {
830    fn new(
831        repeater_indices: &'a [u32],
832        repeater_steps: &'a [u32],
833        result: &'a mut SharedVector<Coord>,
834    ) -> Self {
835        // Calculate total repeated cells (count * step for each repeater)
836        let total_repeated_cells: usize = repeater_indices
837            .chunks(2)
838            .enumerate()
839            .map(|(i, chunk)| {
840                let count = chunk.get(1).copied().unwrap_or(0) as usize;
841                let step = repeater_steps.get(i).copied().unwrap_or(1) as usize;
842                count * step
843            })
844            .sum();
845        assert!(result.len() >= total_repeated_cells * 2);
846        let repeat_offset = result.len() / 2 - total_repeated_cells;
847        Self {
848            repeater_indices,
849            repeater_steps,
850            counter: 0,
851            repeat_offset,
852            next_rep: 0,
853            current_offset: 0,
854            result,
855        }
856    }
857    fn add(&mut self, pos: Coord, size: Coord) {
858        let res = self.result.make_mut_slice();
859        let o = loop {
860            if let Some(nr) = self.repeater_indices.get(self.next_rep * 2) {
861                let nr = *nr as usize;
862                let step = self.repeater_steps.get(self.next_rep).copied().unwrap_or(1) as usize;
863                if nr == self.counter {
864                    // Write jump entries for each element in the step
865                    for s in 0..step {
866                        for o in 0..2 {
867                            res[(self.current_offset + s) * 2 + o] =
868                                ((self.repeat_offset + s) * 2 + o) as _;
869                        }
870                    }
871                    self.current_offset += step;
872                }
873                if self.counter >= nr {
874                    let rep_count = self.repeater_indices[self.next_rep * 2 + 1] as usize;
875                    let cells_in_repeater = rep_count * step;
876                    if self.counter - nr == cells_in_repeater {
877                        // Advance repeat_offset past this repeater's data before moving to next
878                        self.repeat_offset += cells_in_repeater;
879                        self.next_rep += 1;
880                        continue;
881                    }
882                    // Calculate offset using row-major ordering: step entries per row
883                    let cell_in_rep = self.counter - nr;
884                    let row_in_rep = cell_in_rep / step;
885                    let col_in_rep = cell_in_rep % step;
886                    let offset = self.repeat_offset + col_in_rep + row_in_rep * step;
887                    break offset;
888                }
889            }
890            self.current_offset += 1;
891            break self.current_offset - 1;
892        };
893        res[o * 2] = pos;
894        res[o * 2 + 1] = size;
895        self.counter += 1;
896    }
897}
898
899/// return, an array which is of size `data.cells.len() * 2` which for each cell stores:
900/// pos (x or y), size (width or height)
901pub fn solve_grid_layout(
902    data: &GridLayoutData,
903    constraints: Slice<LayoutItemInfo>,
904    orientation: Orientation,
905    repeater_indices: Slice<u32>,
906    repeater_steps: Slice<u32>,
907) -> SharedVector<Coord> {
908    let mut layout_data = grid_internal::to_layout_data(
909        &data.organized_data,
910        constraints,
911        orientation,
912        repeater_indices,
913        repeater_steps,
914        data.spacing,
915        Some(data.size),
916    );
917
918    if layout_data.is_empty() {
919        return Default::default();
920    }
921
922    grid_internal::layout_items(
923        &mut layout_data,
924        data.padding.begin,
925        data.size - (data.padding.begin + data.padding.end),
926        data.spacing,
927    );
928
929    let mut result = SharedVector::<Coord>::default();
930    // Calculate extra space for jump entries when step > 1
931    // Each repeater with step > 1 needs (step - 1) extra entries for additional jump slots
932    let extra_jump_entries: usize =
933        repeater_steps.iter().map(|&s| (s as usize).saturating_sub(1)).sum();
934    result.resize(2 * constraints.len() + repeater_indices.len() + extra_jump_entries * 2, 0 as _);
935    let mut generator = LayoutCacheGenerator::new(&repeater_indices, &repeater_steps, &mut result);
936
937    for idx in 0..constraints.len() {
938        let (col_or_row, span) = data.organized_data.col_or_row_and_span(
939            idx,
940            orientation,
941            &repeater_indices,
942            &repeater_steps,
943        );
944        let cdata = &layout_data[col_or_row as usize];
945        let size = if span > 0 {
946            let last_cell = &layout_data[col_or_row as usize + span as usize - 1];
947            last_cell.pos + last_cell.size - cdata.pos
948        } else {
949            0 as Coord
950        };
951        generator.add(cdata.pos, size);
952    }
953    result
954}
955
956pub fn grid_layout_info(
957    organized_data: GridLayoutOrganizedData, // not & because the code generator doesn't support it in ExtraBuiltinFunctionCall
958    constraints: Slice<LayoutItemInfo>,
959    repeater_indices: Slice<u32>,
960    repeater_steps: Slice<u32>,
961    spacing: Coord,
962    padding: &Padding,
963    orientation: Orientation,
964) -> LayoutInfo {
965    let layout_data = grid_internal::to_layout_data(
966        &organized_data,
967        constraints,
968        orientation,
969        repeater_indices,
970        repeater_steps,
971        spacing,
972        None,
973    );
974    if layout_data.is_empty() {
975        return Default::default();
976    }
977    let spacing_w = spacing * (layout_data.len() - 1) as Coord + padding.begin + padding.end;
978    let min = layout_data.iter().map(|data| data.min).sum::<Coord>() + spacing_w;
979    let max = layout_data.iter().map(|data| data.max).fold(spacing_w, Saturating::add);
980    let preferred = layout_data.iter().map(|data| data.pref).sum::<Coord>() + spacing_w;
981    let stretch = layout_data.iter().map(|data| data.stretch).sum::<f32>();
982    LayoutInfo { min, max, min_percent: 0 as _, max_percent: 100 as _, preferred, stretch }
983}
984
985#[repr(C)]
986#[derive(Debug)]
987/// The BoxLayoutData is used to represent both a Horizontal and Vertical layout.
988/// The width/height x/y correspond to that of a horizontal layout.
989/// For vertical layout, they are inverted
990pub struct BoxLayoutData<'a> {
991    pub size: Coord,
992    pub spacing: Coord,
993    pub padding: Padding,
994    pub alignment: LayoutAlignment,
995    pub cells: Slice<'a, LayoutItemInfo>,
996}
997
998#[repr(C)]
999#[derive(Default, Debug, Clone)]
1000/// The information about a single item in a layout
1001/// For now this only contains the LayoutInfo constraints, but could be extended in the future
1002pub struct LayoutItemInfo {
1003    pub constraint: LayoutInfo,
1004}
1005
1006/// Solve a BoxLayout
1007pub fn solve_box_layout(data: &BoxLayoutData, repeater_indices: Slice<u32>) -> SharedVector<Coord> {
1008    let mut result = SharedVector::<Coord>::default();
1009    result.resize(data.cells.len() * 2 + repeater_indices.len(), 0 as _);
1010
1011    if data.cells.is_empty() {
1012        return result;
1013    }
1014
1015    let mut layout_data: Vec<_> = data
1016        .cells
1017        .iter()
1018        .map(|c| {
1019            let min = c.constraint.min.max(c.constraint.min_percent * data.size / 100 as Coord);
1020            let max = c.constraint.max.min(c.constraint.max_percent * data.size / 100 as Coord);
1021            grid_internal::LayoutData {
1022                min,
1023                max,
1024                pref: c.constraint.preferred.min(max).max(min),
1025                stretch: c.constraint.stretch,
1026                ..Default::default()
1027            }
1028        })
1029        .collect();
1030
1031    let size_without_padding = data.size - data.padding.begin - data.padding.end;
1032    let pref_size: Coord = layout_data.iter().map(|it| it.pref).sum();
1033    let num_spacings = (layout_data.len() - 1) as Coord;
1034    let spacings = data.spacing * num_spacings;
1035
1036    let align = match data.alignment {
1037        LayoutAlignment::Stretch => {
1038            grid_internal::layout_items(
1039                &mut layout_data,
1040                data.padding.begin,
1041                size_without_padding,
1042                data.spacing,
1043            );
1044            None
1045        }
1046        _ if size_without_padding <= pref_size + spacings => {
1047            grid_internal::layout_items(
1048                &mut layout_data,
1049                data.padding.begin,
1050                size_without_padding,
1051                data.spacing,
1052            );
1053            None
1054        }
1055        LayoutAlignment::Center => Some((
1056            data.padding.begin + (size_without_padding - pref_size - spacings) / 2 as Coord,
1057            data.spacing,
1058        )),
1059        LayoutAlignment::Start => Some((data.padding.begin, data.spacing)),
1060        LayoutAlignment::End => {
1061            Some((data.padding.begin + (size_without_padding - pref_size - spacings), data.spacing))
1062        }
1063        LayoutAlignment::SpaceBetween => {
1064            Some((data.padding.begin, (size_without_padding - pref_size) / num_spacings))
1065        }
1066        LayoutAlignment::SpaceAround => {
1067            let spacing = (size_without_padding - pref_size) / (num_spacings + 1 as Coord);
1068            Some((data.padding.begin + spacing / 2 as Coord, spacing))
1069        }
1070        LayoutAlignment::SpaceEvenly => {
1071            let spacing = (size_without_padding - pref_size) / (num_spacings + 2 as Coord);
1072            Some((data.padding.begin + spacing, spacing))
1073        }
1074    };
1075    if let Some((mut pos, spacing)) = align {
1076        for it in &mut layout_data {
1077            it.pos = pos;
1078            it.size = it.pref;
1079            pos += spacing + it.size;
1080        }
1081    }
1082
1083    let mut generator = LayoutCacheGenerator::new(&repeater_indices, &[], &mut result);
1084    for layout in layout_data.iter() {
1085        generator.add(layout.pos, layout.size);
1086    }
1087    result
1088}
1089
1090/// Return the LayoutInfo for a BoxLayout with the given cells.
1091pub fn box_layout_info(
1092    cells: Slice<LayoutItemInfo>,
1093    spacing: Coord,
1094    padding: &Padding,
1095    alignment: LayoutAlignment,
1096) -> LayoutInfo {
1097    let count = cells.len();
1098    let is_stretch = alignment == LayoutAlignment::Stretch;
1099    if count < 1 {
1100        let mut info = LayoutInfo::default();
1101        info.min = padding.begin + padding.end;
1102        info.preferred = info.min;
1103        if is_stretch {
1104            info.max = info.min;
1105        }
1106        return info;
1107    };
1108    let extra_w = padding.begin + padding.end + spacing * (count - 1) as Coord;
1109    let min = cells.iter().map(|c| c.constraint.min).sum::<Coord>() + extra_w;
1110    let max = if is_stretch {
1111        (cells.iter().map(|c| c.constraint.max).fold(extra_w, Saturating::add)).max(min)
1112    } else {
1113        Coord::MAX
1114    };
1115    let preferred = cells.iter().map(|c| c.constraint.preferred_bounded()).sum::<Coord>() + extra_w;
1116    let stretch = cells.iter().map(|c| c.constraint.stretch).sum::<f32>();
1117    LayoutInfo { min, max, min_percent: 0 as _, max_percent: 100 as _, preferred, stretch }
1118}
1119
1120pub fn box_layout_info_ortho(cells: Slice<LayoutItemInfo>, padding: &Padding) -> LayoutInfo {
1121    let extra_w = padding.begin + padding.end;
1122    let mut fold =
1123        cells.iter().fold(LayoutInfo { stretch: f32::MAX, ..Default::default() }, |a, b| {
1124            a.merge(&b.constraint)
1125        });
1126    fold.max = fold.max.max(fold.min);
1127    fold.preferred = fold.preferred.clamp(fold.min, fold.max);
1128    fold.min += extra_w;
1129    fold.max = Saturating::add(fold.max, extra_w);
1130    fold.preferred += extra_w;
1131    fold
1132}
1133
1134#[cfg(feature = "ffi")]
1135pub(crate) mod ffi {
1136    #![allow(unsafe_code)]
1137
1138    use super::*;
1139
1140    #[unsafe(no_mangle)]
1141    pub extern "C" fn slint_organize_grid_layout(
1142        input_data: Slice<GridLayoutInputData>,
1143        repeater_indices: Slice<u32>,
1144        repeater_steps: Slice<u32>,
1145        result: &mut GridLayoutOrganizedData,
1146    ) {
1147        *result = super::organize_grid_layout(input_data, repeater_indices, repeater_steps);
1148    }
1149
1150    #[unsafe(no_mangle)]
1151    pub extern "C" fn slint_organize_dialog_button_layout(
1152        input_data: Slice<GridLayoutInputData>,
1153        dialog_button_roles: Slice<DialogButtonRole>,
1154        result: &mut GridLayoutOrganizedData,
1155    ) {
1156        *result = super::organize_dialog_button_layout(input_data, dialog_button_roles);
1157    }
1158
1159    #[unsafe(no_mangle)]
1160    pub extern "C" fn slint_solve_grid_layout(
1161        data: &GridLayoutData,
1162        constraints: Slice<LayoutItemInfo>,
1163        orientation: Orientation,
1164        repeater_indices: Slice<u32>,
1165        repeater_steps: Slice<u32>,
1166        result: &mut SharedVector<Coord>,
1167    ) {
1168        *result = super::solve_grid_layout(
1169            data,
1170            constraints,
1171            orientation,
1172            repeater_indices,
1173            repeater_steps,
1174        )
1175    }
1176
1177    #[unsafe(no_mangle)]
1178    pub extern "C" fn slint_grid_layout_info(
1179        organized_data: &GridLayoutOrganizedData,
1180        constraints: Slice<LayoutItemInfo>,
1181        repeater_indices: Slice<u32>,
1182        repeater_steps: Slice<u32>,
1183        spacing: Coord,
1184        padding: &Padding,
1185        orientation: Orientation,
1186    ) -> LayoutInfo {
1187        super::grid_layout_info(
1188            organized_data.clone(),
1189            constraints,
1190            repeater_indices,
1191            repeater_steps,
1192            spacing,
1193            padding,
1194            orientation,
1195        )
1196    }
1197
1198    #[unsafe(no_mangle)]
1199    pub extern "C" fn slint_solve_box_layout(
1200        data: &BoxLayoutData,
1201        repeater_indices: Slice<u32>,
1202        result: &mut SharedVector<Coord>,
1203    ) {
1204        *result = super::solve_box_layout(data, repeater_indices)
1205    }
1206
1207    #[unsafe(no_mangle)]
1208    /// Return the LayoutInfo for a BoxLayout with the given cells.
1209    pub extern "C" fn slint_box_layout_info(
1210        cells: Slice<LayoutItemInfo>,
1211        spacing: Coord,
1212        padding: &Padding,
1213        alignment: LayoutAlignment,
1214    ) -> LayoutInfo {
1215        super::box_layout_info(cells, spacing, padding, alignment)
1216    }
1217
1218    #[unsafe(no_mangle)]
1219    /// Return the LayoutInfo for a BoxLayout with the given cells.
1220    pub extern "C" fn slint_box_layout_info_ortho(
1221        cells: Slice<LayoutItemInfo>,
1222        padding: &Padding,
1223    ) -> LayoutInfo {
1224        super::box_layout_info_ortho(cells, padding)
1225    }
1226}
1227
1228#[cfg(test)]
1229mod tests {
1230    use super::*;
1231
1232    fn collect_from_organized_data(
1233        organized_data: &GridLayoutOrganizedData,
1234        num_cells: usize,
1235        repeater_indices: Slice<u32>,
1236        repeater_steps: Slice<u32>,
1237    ) -> Vec<(u16, u16, u16, u16)> {
1238        let mut result = Vec::new();
1239        for i in 0..num_cells {
1240            let col_and_span = organized_data.col_or_row_and_span(
1241                i,
1242                Orientation::Horizontal,
1243                &repeater_indices,
1244                &repeater_steps,
1245            );
1246            let row_and_span = organized_data.col_or_row_and_span(
1247                i,
1248                Orientation::Vertical,
1249                &repeater_indices,
1250                &repeater_steps,
1251            );
1252            result.push((col_and_span.0, col_and_span.1, row_and_span.0, row_and_span.1));
1253        }
1254        result
1255    }
1256
1257    #[test]
1258    fn test_organized_data_generator_2_fixed_cells() {
1259        // 2 fixed cells
1260        let mut result = GridLayoutOrganizedData::default();
1261        let num_cells = 2;
1262        result.resize(num_cells * 4, 0 as _);
1263        let mut generator = OrganizedDataGenerator::new(&[], &[], &mut result);
1264        generator.add(0, 1, 0, 1);
1265        generator.add(1, 2, 0, 3);
1266        assert_eq!(result.as_slice(), &[0, 1, 0, 1, 1, 2, 0, 3]);
1267
1268        let repeater_indices = Slice::from_slice(&[]);
1269        let empty_steps = Slice::from_slice(&[]);
1270        let collected_data =
1271            collect_from_organized_data(&result, num_cells, repeater_indices, empty_steps);
1272        assert_eq!(collected_data.as_slice(), &[(0, 1, 0, 1), (1, 2, 0, 3)]);
1273
1274        assert_eq!(
1275            result.max_value(num_cells, Orientation::Horizontal, &repeater_indices, &empty_steps),
1276            3
1277        );
1278        assert_eq!(
1279            result.max_value(num_cells, Orientation::Vertical, &repeater_indices, &empty_steps),
1280            3
1281        );
1282    }
1283
1284    #[test]
1285    fn test_organized_data_generator_1_fixed_cell_1_repeater() {
1286        // 4 cells: 1 fixed cell, 1 repeater with 3 repeated cells
1287        let mut result = GridLayoutOrganizedData::default();
1288        let num_cells = 4;
1289        let repeater_indices = &[1u32, 3u32];
1290        result.resize(num_cells * 4 + 2 * repeater_indices.len(), 0 as _);
1291        let mut generator = OrganizedDataGenerator::new(repeater_indices, &[], &mut result);
1292        generator.add(0, 1, 0, 2); // fixed
1293        generator.add(1, 2, 1, 3); // repeated
1294        generator.add(1, 1, 2, 4);
1295        generator.add(2, 2, 3, 5);
1296        assert_eq!(
1297            result.as_slice(),
1298            &[
1299                0, 1, 0, 2, // fixed
1300                8, 9, 10, 11, // jump to repeater data
1301                1, 2, 1, 3, 1, 1, 2, 4, 2, 2, 3, 5 // repeater data
1302            ]
1303        );
1304        let repeater_indices = Slice::from_slice(repeater_indices);
1305        let empty_steps = Slice::from_slice(&[]);
1306        let collected_data =
1307            collect_from_organized_data(&result, num_cells, repeater_indices, empty_steps);
1308        assert_eq!(
1309            collected_data.as_slice(),
1310            &[(0, 1, 0, 2), (1, 2, 1, 3), (1, 1, 2, 4), (2, 2, 3, 5)]
1311        );
1312
1313        assert_eq!(
1314            result.max_value(num_cells, Orientation::Horizontal, &repeater_indices, &empty_steps),
1315            4
1316        );
1317        assert_eq!(
1318            result.max_value(num_cells, Orientation::Vertical, &repeater_indices, &empty_steps),
1319            8
1320        );
1321    }
1322
1323    #[test]
1324
1325    fn test_organize_data_with_auto_and_spans() {
1326        let auto = i_slint_common::ROW_COL_AUTO;
1327        let input = std::vec![
1328            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 2., rowspan: -1. },
1329            GridLayoutInputData { new_row: false, col: auto, row: auto, colspan: 1., rowspan: 2. },
1330            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 2., rowspan: 1. },
1331            GridLayoutInputData { new_row: true, col: -2., row: 80000., colspan: 2., rowspan: 1. },
1332        ];
1333        let repeater_indices = Slice::from_slice(&[]);
1334        let (organized_data, errors) = organize_grid_layout_impl(
1335            Slice::from_slice(&input),
1336            repeater_indices,
1337            Slice::from_slice(&[]),
1338        );
1339        assert_eq!(
1340            organized_data.as_slice(),
1341            &[
1342                0, 2, 0, 0, // row 0, col 0, rowspan 0 (see below)
1343                2, 1, 0, 2, // row 0, col 2 (due to colspan of first cell)
1344                0, 2, 1, 1, // row 1, col 0
1345                0, 2, 65535, 1, // row 65535, col 0
1346            ]
1347        );
1348        assert_eq!(errors.len(), 3);
1349        // Note that a rowspan of 0 is valid, it means the cell doesn't occupy any row
1350        assert_eq!(errors[0], "cell rowspan -1 is negative, clamping to 0");
1351        assert_eq!(errors[1], "cell row 80000 is too large, clamping to 65535");
1352        assert_eq!(errors[2], "cell col -2 is negative, clamping to 0");
1353        let empty_steps = Slice::from_slice(&[]);
1354        let collected_data = collect_from_organized_data(
1355            &organized_data,
1356            input.len(),
1357            repeater_indices,
1358            empty_steps,
1359        );
1360        assert_eq!(
1361            collected_data.as_slice(),
1362            &[(0, 2, 0, 0), (2, 1, 0, 2), (0, 2, 1, 1), (0, 2, 65535, 1)]
1363        );
1364        assert_eq!(
1365            organized_data.max_value(3, Orientation::Horizontal, &repeater_indices, &empty_steps),
1366            3
1367        );
1368        assert_eq!(
1369            organized_data.max_value(3, Orientation::Vertical, &repeater_indices, &empty_steps),
1370            2
1371        );
1372    }
1373
1374    #[test]
1375    fn test_organize_data_1_empty_repeater() {
1376        // Row { Text {}    if false: Text {} }, this test shows why we need i32 for cell_nr_adj
1377        let auto = i_slint_common::ROW_COL_AUTO;
1378        let cell =
1379            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 1., rowspan: 1. };
1380        let input = std::vec![cell];
1381        let repeater_indices = Slice::from_slice(&[1u32, 0u32]);
1382        let (organized_data, errors) = organize_grid_layout_impl(
1383            Slice::from_slice(&input),
1384            repeater_indices,
1385            Slice::from_slice(&[]),
1386        );
1387        assert_eq!(
1388            organized_data.as_slice(),
1389            &[
1390                0, 1, 0, 1, // fixed
1391                0, 0, 0, 0
1392            ] // jump to repeater data (not used)
1393        );
1394        assert_eq!(errors.len(), 0);
1395        let empty_steps = Slice::from_slice(&[]);
1396        let collected_data = collect_from_organized_data(
1397            &organized_data,
1398            input.len(),
1399            repeater_indices,
1400            empty_steps,
1401        );
1402        assert_eq!(collected_data.as_slice(), &[(0, 1, 0, 1)]);
1403        assert_eq!(
1404            organized_data.max_value(1, Orientation::Horizontal, &repeater_indices, &empty_steps),
1405            1
1406        );
1407    }
1408
1409    #[test]
1410    fn test_organize_data_4_repeaters() {
1411        let auto = i_slint_common::ROW_COL_AUTO;
1412        let mut cell =
1413            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 1., rowspan: 1. };
1414        let mut input = std::vec![cell.clone()];
1415        for _ in 0..8 {
1416            cell.new_row = false;
1417            input.push(cell.clone());
1418        }
1419        let repeater_indices = Slice::from_slice(&[0u32, 0u32, 1u32, 4u32, 6u32, 2u32, 8u32, 0u32]);
1420        let (organized_data, errors) = organize_grid_layout_impl(
1421            Slice::from_slice(&input),
1422            repeater_indices,
1423            Slice::from_slice(&[]),
1424        );
1425        assert_eq!(
1426            organized_data.as_slice(),
1427            &[
1428                28, 29, 30, 31, // jump to first (empty) repeater (not used)
1429                0, 1, 0, 1, // first row, first column
1430                28, 29, 30, 31, // jump to first repeater data
1431                5, 1, 0, 1, // fixed
1432                44, 45, 46, 47, // jump to second repeater data
1433                52, 53, 54, 55, // slot for jumping to 3rd repeater (out of bounds, not used)
1434                8, 1, 0, 1, // final fixed element
1435                1, 1, 0, 1, // first repeater data
1436                2, 1, 0, 1, 3, 1, 0, 1, 4, 1, 0, 1, // end of first repeater
1437                6, 1, 0, 1, // second repeater data
1438                7, 1, 0, 1 // end of second repeater
1439            ]
1440        );
1441        assert_eq!(errors.len(), 0);
1442        let empty_steps = Slice::from_slice(&[]);
1443        let collected_data = collect_from_organized_data(
1444            &organized_data,
1445            input.len(),
1446            repeater_indices,
1447            empty_steps,
1448        );
1449        assert_eq!(
1450            collected_data.as_slice(),
1451            &[
1452                (0, 1, 0, 1),
1453                (1, 1, 0, 1),
1454                (2, 1, 0, 1),
1455                (3, 1, 0, 1),
1456                (4, 1, 0, 1),
1457                (5, 1, 0, 1),
1458                (6, 1, 0, 1),
1459                (7, 1, 0, 1),
1460                (8, 1, 0, 1),
1461            ]
1462        );
1463        let empty_steps = Slice::from_slice(&[]);
1464        assert_eq!(
1465            organized_data.max_value(
1466                input.len(),
1467                Orientation::Horizontal,
1468                &repeater_indices,
1469                &empty_steps
1470            ),
1471            9
1472        );
1473    }
1474
1475    #[test]
1476    fn test_organize_data_repeated_rows() {
1477        let auto = i_slint_common::ROW_COL_AUTO;
1478        let mut input = Vec::new();
1479        let num_rows: u32 = 3;
1480        let num_columns: u32 = 2;
1481        // 3 rows of 2 columns each
1482        for _ in 0..num_rows {
1483            let mut cell = GridLayoutInputData {
1484                new_row: true,
1485                col: auto,
1486                row: auto,
1487                colspan: 1.,
1488                rowspan: 1.,
1489            };
1490            input.push(cell.clone());
1491            cell.new_row = false;
1492            input.push(cell.clone());
1493        }
1494        // Repeater 0: starts at index 0, has 3 instances of 2 elements
1495        let repeater_indices_arr = [0_u32, num_rows];
1496        let repeater_steps_arr = [num_columns];
1497        let repeater_steps = Slice::from_slice(&repeater_steps_arr);
1498        let repeater_indices = Slice::from_slice(&repeater_indices_arr);
1499        let (organized_data, errors) =
1500            organize_grid_layout_impl(Slice::from_slice(&input), repeater_indices, repeater_steps);
1501        assert_eq!(
1502            organized_data.as_slice(),
1503            &[
1504                8, 9, 10, 11, // jump to repeater data for column 0 (offset 2)
1505                12, 13, 14, 15, // jump to repeater data for column 1 (offset 3)
1506                0, 1, 0, 1, // row 0, col 0 (offset 2)
1507                1, 1, 0, 1, // row 0, col 1 (offset 3)
1508                0, 1, 1, 1, // row 1, col 0 (offset 4)
1509                1, 1, 1, 1, // row 1, col 1 (offset 5)
1510                0, 1, 2, 1, // row 2, col 0 (offset 6)
1511                1, 1, 2, 1, // row 2, col 1 (offset 7)
1512            ]
1513        );
1514        assert_eq!(errors.len(), 0);
1515        let collected_data = collect_from_organized_data(
1516            &organized_data,
1517            input.len(),
1518            repeater_indices,
1519            repeater_steps,
1520        );
1521        assert_eq!(
1522            collected_data.as_slice(),
1523            // (col, colspan, row, rowspan) for each cell in input order
1524            &[(0, 1, 0, 1), (1, 1, 0, 1), (0, 1, 1, 1), (1, 1, 1, 1), (0, 1, 2, 1), (1, 1, 2, 1),]
1525        );
1526        assert_eq!(
1527            organized_data.max_value(
1528                input.len(),
1529                Orientation::Horizontal,
1530                &repeater_indices,
1531                &repeater_steps
1532            ),
1533            2
1534        );
1535        assert_eq!(
1536            organized_data.max_value(
1537                input.len(),
1538                Orientation::Vertical,
1539                &repeater_indices,
1540                &repeater_steps
1541            ),
1542            3
1543        );
1544
1545        // Now test LayoutCacheGenerator
1546        let mut layout_cache_v = SharedVector::<Coord>::default();
1547        // 2 jump entries (one per column) + 6 data entries (3 rows × 2 columns)
1548        layout_cache_v.resize((num_columns * 2 + num_columns * num_rows * 2) as usize, 0 as _);
1549        let mut generator = LayoutCacheGenerator::new(
1550            repeater_indices.as_slice(),
1551            repeater_steps.as_slice(),
1552            &mut layout_cache_v,
1553        );
1554        // Row 0
1555        generator.add(0., 50.);
1556        generator.add(0., 50.);
1557        // Row 1
1558        generator.add(50., 50.);
1559        generator.add(50., 50.);
1560        // Row 2
1561        generator.add(100., 50.);
1562        generator.add(100., 50.);
1563        assert_eq!(
1564            layout_cache_v.as_slice(),
1565            &[
1566                4., 5., 6., 7., // jump to repeater data
1567                0., 50., 0., 50., // row 0
1568                50., 50., 50., 50., // row 1
1569                100., 50., 100., 50., // row 2
1570            ]
1571        );
1572
1573        let layout_cache_v_access = |index: usize, repeater_index: usize, step: usize| -> Coord {
1574            let entries_per_item = 2usize;
1575            let offset = repeater_index;
1576            // same as the code generated for LayoutCacheAccess in rust.rs
1577            *layout_cache_v
1578                .get((layout_cache_v[index] as usize) + offset as usize * entries_per_item * step)
1579                .unwrap()
1580        };
1581        // Y values for A
1582        assert_eq!(layout_cache_v_access(0, 0, 2), 0.);
1583        assert_eq!(layout_cache_v_access(0, 1, 2), 50.);
1584        assert_eq!(layout_cache_v_access(0, 2, 2), 100.);
1585        // Y values for B
1586        assert_eq!(layout_cache_v_access(1 * 2, 0, 2), 0.);
1587        assert_eq!(layout_cache_v_access(1 * 2, 1, 2), 50.);
1588        assert_eq!(layout_cache_v_access(1 * 2, 2, 2), 100.);
1589    }
1590
1591    #[test]
1592    fn test_organize_data_repeated_rows_multiple_repeaters() {
1593        let auto = i_slint_common::ROW_COL_AUTO;
1594        let mut input = Vec::new();
1595        let num_rows: u32 = 5;
1596        let mut cell =
1597            GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 1., rowspan: 1. };
1598        // 3 rows of 2 columns each
1599        for _ in 0..3 {
1600            cell.new_row = true;
1601            input.push(cell.clone());
1602            cell.new_row = false;
1603            input.push(cell.clone());
1604        }
1605        // 2 rows of 3 columns each
1606        for _ in 0..2 {
1607            cell.new_row = true;
1608            input.push(cell.clone());
1609            cell.new_row = false;
1610            input.push(cell.clone());
1611            cell.new_row = false;
1612            input.push(cell.clone());
1613        }
1614        // Repeater 0: starts at index 0, has 3 instances of 2 elements
1615        // Repeater 1: starts at index 6 (after repeater 0's 3*2=6 cells), has 2 instances of 3 elements
1616        let repeater_indices_arr = [0_u32, 3, 6, 2];
1617        let repeater_steps_arr = [2, 3];
1618        let repeater_steps = Slice::from_slice(&repeater_steps_arr);
1619        let repeater_indices = Slice::from_slice(&repeater_indices_arr);
1620        let (organized_data, errors) =
1621            organize_grid_layout_impl(Slice::from_slice(&input), repeater_indices, repeater_steps);
1622        assert_eq!(
1623            organized_data.as_slice(),
1624            &[
1625                20, 21, 22, 23, // repeater 0: jump to repeater data for column 0 (offset 5)
1626                24, 25, 26, 27, // repeater 0: jump to repeater data for column 1 (offset 6)
1627                44, 45, 46, 47, // repeater 1: jump to repeater data for column 0 (offset 11)
1628                48, 49, 50, 51, // repeater 1: jump to repeater data for column 1 (offset 12)
1629                52, 53, 54, 55, // repeater 1: jump to repeater data for column 2 (offset 13)
1630                // Repeater 0
1631                0, 1, 0, 1, // row 0, col 0 (offset 5)
1632                1, 1, 0, 1, // row 0, col 1 (offset 6)
1633                0, 1, 1, 1, // row 1, col 0 (offset 7)
1634                1, 1, 1, 1, // row 1, col 1 (offset 8)
1635                0, 1, 2, 1, // row 2, col 0 (offset 9)
1636                1, 1, 2, 1, // row 2, col 1 (offset 10)
1637                // Repeater 1
1638                0, 1, 3, 1, // row 3, col 0 (offset 11)
1639                1, 1, 3, 1, // row 3, col 1 (offset 12)
1640                2, 1, 3, 1, // row 3, col 2 (offset 13)
1641                0, 1, 4, 1, // row 4, col 0 (offset 14)
1642                1, 1, 4, 1, // row 4, col 1 (offset 15)
1643                2, 1, 4, 1, // row 4, col 2 (offset 16)
1644            ]
1645        );
1646        assert_eq!(errors.len(), 0);
1647        let collected_data = collect_from_organized_data(
1648            &organized_data,
1649            input.len(),
1650            repeater_indices,
1651            repeater_steps,
1652        );
1653        assert_eq!(
1654            collected_data.as_slice(),
1655            // (col, colspan, row, rowspan) for each cell in input order
1656            &[
1657                (0, 1, 0, 1),
1658                (1, 1, 0, 1),
1659                (0, 1, 1, 1),
1660                (1, 1, 1, 1),
1661                (0, 1, 2, 1),
1662                (1, 1, 2, 1),
1663                (0, 1, 3, 1),
1664                (1, 1, 3, 1),
1665                (2, 1, 3, 1),
1666                (0, 1, 4, 1),
1667                (1, 1, 4, 1),
1668                (2, 1, 4, 1)
1669            ]
1670        );
1671        assert_eq!(
1672            organized_data.max_value(
1673                input.len(),
1674                Orientation::Horizontal,
1675                &repeater_indices,
1676                &repeater_steps
1677            ),
1678            3 // max col (2) + colspan (1) = 3
1679        );
1680        assert_eq!(
1681            organized_data.max_value(
1682                input.len(),
1683                Orientation::Vertical,
1684                &repeater_indices,
1685                &repeater_steps
1686            ),
1687            num_rows as u16 // max row (4) + rowspan (1) = 5
1688        );
1689
1690        // Now test LayoutCacheGenerator
1691        let mut layout_cache_v = SharedVector::<Coord>::default();
1692        // 5 jump entries, just like above + 12 data entries (3*2+2*3) - where each entry has 2 values (pos, size)
1693        layout_cache_v.resize((5 * 2 + (3 * 2 + 2 * 3) * 2) as usize, 0 as _);
1694        let mut generator = LayoutCacheGenerator::new(
1695            repeater_indices.as_slice(),
1696            repeater_steps.as_slice(),
1697            &mut layout_cache_v,
1698        );
1699        // Row 0
1700        generator.add(0., 50.);
1701        generator.add(0., 50.);
1702        // Row 1
1703        generator.add(50., 50.);
1704        generator.add(50., 50.);
1705        // Row 2
1706        generator.add(100., 50.);
1707        generator.add(100., 50.);
1708        // Row 3
1709        generator.add(150., 50.);
1710        generator.add(150., 50.);
1711        generator.add(150., 50.);
1712        // Row 4
1713        generator.add(200., 50.);
1714        generator.add(200., 50.);
1715        generator.add(200., 50.);
1716        assert_eq!(
1717            layout_cache_v.as_slice(),
1718            &[
1719                10., 11., 12., 13., // repeater 0: jump to repeater data
1720                22., 23., 24., 25., 26., 27., // repeater 1: jump to repeater data
1721                0., 50., 0., 50., // row 0
1722                50., 50., 50., 50., // row 1
1723                100., 50., 100., 50., // row 2
1724                150., 50., 150., 50., 150., 50., // row 3
1725                200., 50., 200., 50., 200., 50., // row 4
1726            ]
1727        );
1728
1729        let layout_cache_v_access = |index: usize, repeater_index: usize, step: usize| -> Coord {
1730            let entries_per_item = 2usize;
1731            let offset = repeater_index;
1732            // same as the code generated for LayoutCacheAccess in rust.rs
1733            *layout_cache_v
1734                .get((layout_cache_v[index] as usize) + offset as usize * entries_per_item * step)
1735                .unwrap()
1736        };
1737        // Y values for A
1738        assert_eq!(layout_cache_v_access(0, 0, 2), 0.);
1739        assert_eq!(layout_cache_v_access(0, 1, 2), 50.);
1740        assert_eq!(layout_cache_v_access(0, 2, 2), 100.);
1741        // Y values for B
1742        assert_eq!(layout_cache_v_access(1 * 2, 0, 2), 0.);
1743        assert_eq!(layout_cache_v_access(1 * 2, 1, 2), 50.);
1744        assert_eq!(layout_cache_v_access(1 * 2, 2, 2), 100.);
1745    }
1746
1747    #[test]
1748    fn test_layout_cache_generator_2_fixed_cells() {
1749        // 2 fixed cells
1750        let mut result = SharedVector::<Coord>::default();
1751        result.resize(2 * 2, 0 as _);
1752        let mut generator = LayoutCacheGenerator::new(&[], &[], &mut result);
1753        generator.add(0., 50.); // fixed
1754        generator.add(80., 50.); // fixed
1755        assert_eq!(result.as_slice(), &[0., 50., 80., 50.]);
1756    }
1757
1758    #[test]
1759    fn test_layout_cache_generator_1_fixed_cell_1_repeater() {
1760        // 4 cells: 1 fixed cell, 1 repeater with 3 repeated cells
1761        let mut result = SharedVector::<Coord>::default();
1762        let repeater_indices = &[1, 3];
1763        result.resize(4 * 2 + repeater_indices.len(), 0 as _);
1764        let mut generator = LayoutCacheGenerator::new(repeater_indices, &[], &mut result);
1765        generator.add(0., 50.); // fixed
1766        generator.add(80., 50.); // repeated
1767        generator.add(160., 50.);
1768        generator.add(240., 50.);
1769        assert_eq!(
1770            result.as_slice(),
1771            &[
1772                0., 50., // fixed
1773                4., 5., // jump to repeater data
1774                80., 50., 160., 50., 240., 50. // repeater data
1775            ]
1776        );
1777    }
1778
1779    #[test]
1780    fn test_layout_cache_generator_4_repeaters() {
1781        // 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
1782        let mut result = SharedVector::<Coord>::default();
1783        let repeater_indices = &[1, 0, 1, 4, 6, 2, 8, 0];
1784        result.resize(8 * 2 + repeater_indices.len(), 0 as _);
1785        let mut generator = LayoutCacheGenerator::new(repeater_indices, &[], &mut result);
1786        generator.add(0., 50.); // fixed
1787        generator.add(80., 10.); // repeated
1788        generator.add(160., 10.);
1789        generator.add(240., 10.);
1790        generator.add(320., 10.); // end of second repeater
1791        generator.add(400., 80.); // fixed
1792        generator.add(500., 20.); // repeated
1793        generator.add(600., 20.); // end of third repeater
1794        assert_eq!(
1795            result.as_slice(),
1796            &[
1797                0., 50., // fixed
1798                12., 13., // jump to first (empty) repeater (not used)
1799                12., 13., // jump to second repeater data
1800                400., 80., // fixed
1801                20., 21., // jump to third repeater data
1802                0., 0., // slot for jumping to fourth repeater (currently empty)
1803                80., 10., 160., 10., 240., 10., 320., 10., // first repeater data
1804                500., 20., 600., 20. // second repeater data
1805            ]
1806        );
1807    }
1808}