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::{slice::Slice, Coord, SharedVector};
10use alloc::vec::Vec;
11
12pub use crate::items::Orientation;
13
14/// The constraint that applies to an item
15// Also, the field needs to be in alphabetical order because how the generated code sort fields for struct
16#[repr(C)]
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct LayoutInfo {
19    /// The maximum size for the item.
20    pub max: Coord,
21    /// The maximum size in percentage of the parent (value between 0 and 100).
22    pub max_percent: Coord,
23    /// The minimum size for this item.
24    pub min: Coord,
25    /// The minimum size in percentage of the parent (value between 0 and 100).
26    pub min_percent: Coord,
27    /// the preferred size
28    pub preferred: Coord,
29    /// the  stretch factor
30    pub stretch: f32,
31}
32
33impl Default for LayoutInfo {
34    fn default() -> Self {
35        LayoutInfo {
36            min: 0 as _,
37            max: Coord::MAX,
38            min_percent: 0 as _,
39            max_percent: 100 as _,
40            preferred: 0 as _,
41            stretch: 0 as _,
42        }
43    }
44}
45
46impl LayoutInfo {
47    // Note: This "logic" is duplicated in the cpp generator's generated code for merging layout infos.
48    #[must_use]
49    pub fn merge(&self, other: &LayoutInfo) -> Self {
50        Self {
51            min: self.min.max(other.min),
52            max: self.max.min(other.max),
53            min_percent: self.min_percent.max(other.min_percent),
54            max_percent: self.max_percent.min(other.max_percent),
55            preferred: self.preferred.max(other.preferred),
56            stretch: self.stretch.min(other.stretch),
57        }
58    }
59
60    /// Helper function to return a preferred size which is within the min/max constraints
61    #[must_use]
62    pub fn preferred_bounded(&self) -> Coord {
63        self.preferred.min(self.max).max(self.min)
64    }
65}
66
67impl core::ops::Add for LayoutInfo {
68    type Output = Self;
69
70    fn add(self, rhs: Self) -> Self::Output {
71        self.merge(&rhs)
72    }
73}
74
75/// Returns the logical min and max sizes given the provided layout constraints.
76pub fn min_max_size_for_layout_constraints(
77    constraints_horizontal: LayoutInfo,
78    constraints_vertical: LayoutInfo,
79) -> (Option<crate::api::LogicalSize>, Option<crate::api::LogicalSize>) {
80    let min_width = constraints_horizontal.min.min(constraints_horizontal.max) as f32;
81    let min_height = constraints_vertical.min.min(constraints_vertical.max) as f32;
82    let max_width = constraints_horizontal.max.max(constraints_horizontal.min) as f32;
83    let max_height = constraints_vertical.max.max(constraints_vertical.min) as f32;
84
85    //cfg!(target_arch = "wasm32") is there because wasm32 winit don't like when max size is None:
86    // panicked at 'Property is read only: JsValue(NoModificationAllowedError: CSSStyleDeclaration.removeProperty: Can't remove property 'max-width' from computed style
87
88    let min_size = if min_width > 0. || min_height > 0. || cfg!(target_arch = "wasm32") {
89        Some(crate::api::LogicalSize::new(min_width, min_height))
90    } else {
91        None
92    };
93
94    let max_size = if (max_width > 0.
95        && max_height > 0.
96        && (max_width < i32::MAX as f32 || max_height < i32::MAX as f32))
97        || cfg!(target_arch = "wasm32")
98    {
99        // maximum widget size for Qt and a workaround for the winit api not allowing partial constraints
100        let window_size_max = 16_777_215.;
101        Some(crate::api::LogicalSize::new(
102            max_width.min(window_size_max),
103            max_height.min(window_size_max),
104        ))
105    } else {
106        None
107    };
108
109    (min_size, max_size)
110}
111
112/// Implement a saturating_add version for both possible value of Coord.
113/// So that adding the max value does not overflow
114trait Saturating {
115    fn add(_: Self, _: Self) -> Self;
116}
117impl Saturating for i32 {
118    #[inline]
119    fn add(a: Self, b: Self) -> Self {
120        a.saturating_add(b)
121    }
122}
123impl Saturating for f32 {
124    #[inline]
125    fn add(a: Self, b: Self) -> Self {
126        a + b
127    }
128}
129
130mod grid_internal {
131    use super::*;
132
133    fn order_coord<T: PartialOrd>(a: &T, b: &T) -> core::cmp::Ordering {
134        a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)
135    }
136
137    #[derive(Debug, Clone)]
138    pub struct LayoutData {
139        // inputs
140        pub min: Coord,
141        pub max: Coord,
142        pub pref: Coord,
143        pub stretch: f32,
144
145        // outputs
146        pub pos: Coord,
147        pub size: Coord,
148    }
149
150    impl Default for LayoutData {
151        fn default() -> Self {
152            LayoutData {
153                min: 0 as _,
154                max: Coord::MAX,
155                pref: 0 as _,
156                stretch: f32::MAX,
157                pos: 0 as _,
158                size: 0 as _,
159            }
160        }
161    }
162
163    trait Adjust {
164        fn can_grow(_: &LayoutData) -> Coord;
165        fn to_distribute(expected_size: Coord, current_size: Coord) -> Coord;
166        fn distribute(_: &mut LayoutData, val: Coord);
167    }
168
169    struct Grow;
170    impl Adjust for Grow {
171        fn can_grow(it: &LayoutData) -> Coord {
172            it.max - it.size
173        }
174
175        fn to_distribute(expected_size: Coord, current_size: Coord) -> Coord {
176            expected_size - current_size
177        }
178
179        fn distribute(it: &mut LayoutData, val: Coord) {
180            it.size += val;
181        }
182    }
183
184    struct Shrink;
185    impl Adjust for Shrink {
186        fn can_grow(it: &LayoutData) -> Coord {
187            it.size - it.min
188        }
189
190        fn to_distribute(expected_size: Coord, current_size: Coord) -> Coord {
191            current_size - expected_size
192        }
193
194        fn distribute(it: &mut LayoutData, val: Coord) {
195            it.size -= val;
196        }
197    }
198
199    #[allow(clippy::unnecessary_cast)] // Coord
200    fn adjust_items<A: Adjust>(data: &mut [LayoutData], size_without_spacing: Coord) -> Option<()> {
201        loop {
202            let size_cannot_grow: Coord = data
203                .iter()
204                .filter(|it| A::can_grow(it) <= 0 as _)
205                .map(|it| it.size)
206                .fold(0 as Coord, Saturating::add);
207
208            let total_stretch: f32 =
209                data.iter().filter(|it| A::can_grow(it) > 0 as _).map(|it| it.stretch).sum();
210
211            let actual_stretch = |s: f32| if total_stretch <= 0. { 1. } else { s };
212
213            let max_grow = data
214                .iter()
215                .filter(|it| A::can_grow(it) > 0 as _)
216                .map(|it| A::can_grow(it) as f32 / actual_stretch(it.stretch))
217                .min_by(order_coord)?;
218
219            let current_size: Coord = data
220                .iter()
221                .filter(|it| A::can_grow(it) > 0 as _)
222                .map(|it| it.size)
223                .fold(0 as _, Saturating::add);
224
225            //let to_distribute = size_without_spacing - (size_cannot_grow + current_size);
226            let to_distribute =
227                A::to_distribute(size_without_spacing, size_cannot_grow + current_size) as f32;
228            if to_distribute <= 0. || max_grow <= 0. {
229                return Some(());
230            }
231
232            let grow = if total_stretch <= 0. {
233                to_distribute
234                    / (data.iter().filter(|it| A::can_grow(it) > 0 as _).count() as Coord) as f32
235            } else {
236                to_distribute / total_stretch
237            }
238            .min(max_grow);
239
240            let mut distributed = 0 as Coord;
241            for it in data.iter_mut().filter(|it| A::can_grow(it) > 0 as Coord) {
242                let val = (grow * actual_stretch(it.stretch)) as Coord;
243                A::distribute(it, val);
244                distributed += val;
245            }
246
247            if distributed <= 0 as Coord {
248                // This can happen when Coord is integer and there is less then a pixel to add to each elements
249                // just give the pixel to the one with the bigger stretch
250                if let Some(it) = data
251                    .iter_mut()
252                    .filter(|it| A::can_grow(it) > 0 as _)
253                    .max_by(|a, b| actual_stretch(a.stretch).total_cmp(&b.stretch))
254                {
255                    A::distribute(it, to_distribute as Coord);
256                }
257                return Some(());
258            }
259        }
260    }
261
262    pub fn layout_items(data: &mut [LayoutData], start_pos: Coord, size: Coord, spacing: Coord) {
263        let size_without_spacing = size - spacing * (data.len() - 1) as Coord;
264
265        let mut pref = 0 as Coord;
266        for it in data.iter_mut() {
267            it.size = it.pref;
268            pref += it.pref;
269        }
270        if size_without_spacing >= pref {
271            adjust_items::<Grow>(data, size_without_spacing);
272        } else if size_without_spacing < pref {
273            adjust_items::<Shrink>(data, size_without_spacing);
274        }
275
276        let mut pos = start_pos;
277        for it in data.iter_mut() {
278            it.pos = pos;
279            pos = Saturating::add(pos, Saturating::add(it.size, spacing));
280        }
281    }
282
283    #[test]
284    #[allow(clippy::float_cmp)] // We want bit-wise equality here
285    fn test_layout_items() {
286        let my_items = &mut [
287            LayoutData { min: 100., max: 200., pref: 100., stretch: 1., ..Default::default() },
288            LayoutData { min: 50., max: 300., pref: 100., stretch: 1., ..Default::default() },
289            LayoutData { min: 50., max: 150., pref: 100., stretch: 1., ..Default::default() },
290        ];
291
292        layout_items(my_items, 100., 650., 0.);
293        assert_eq!(my_items[0].size, 200.);
294        assert_eq!(my_items[1].size, 300.);
295        assert_eq!(my_items[2].size, 150.);
296
297        layout_items(my_items, 100., 200., 0.);
298        assert_eq!(my_items[0].size, 100.);
299        assert_eq!(my_items[1].size, 50.);
300        assert_eq!(my_items[2].size, 50.);
301
302        layout_items(my_items, 100., 300., 0.);
303        assert_eq!(my_items[0].size, 100.);
304        assert_eq!(my_items[1].size, 100.);
305        assert_eq!(my_items[2].size, 100.);
306    }
307
308    /// Create a vector of LayoutData for an array of GridLayoutCellData
309    pub fn to_layout_data(
310        data: &[GridLayoutCellData],
311        spacing: Coord,
312        size: Option<Coord>,
313    ) -> Vec<LayoutData> {
314        let mut num = 0usize;
315        for cell in data {
316            num = num.max(cell.col_or_row as usize + cell.span.max(1) as usize);
317        }
318        if num < 1 {
319            return Default::default();
320        }
321        let mut layout_data =
322            alloc::vec![grid_internal::LayoutData { stretch: 1., ..Default::default() }; num];
323        let mut has_spans = false;
324        for cell in data {
325            let constraint = &cell.constraint;
326            let mut max = constraint.max;
327            if let Some(size) = size {
328                max = max.min(size * constraint.max_percent / 100 as Coord);
329            }
330            for c in 0..(cell.span as usize) {
331                let cdata = &mut layout_data[cell.col_or_row as usize + c];
332                cdata.max = cdata.max.min(max);
333            }
334            if cell.span == 1 {
335                let mut min = constraint.min;
336                if let Some(size) = size {
337                    min = min.max(size * constraint.min_percent / 100 as Coord);
338                }
339                let pref = constraint.preferred.min(max).max(min);
340                let cdata = &mut layout_data[cell.col_or_row as usize];
341                cdata.min = cdata.min.max(min);
342                cdata.pref = cdata.pref.max(pref);
343                cdata.stretch = cdata.stretch.min(constraint.stretch);
344            } else {
345                has_spans = true;
346            }
347        }
348        if has_spans {
349            // Adjust minimum sizes
350            for cell in data.iter().filter(|cell| cell.span > 1) {
351                let span_data = &mut layout_data
352                    [(cell.col_or_row as usize)..(cell.col_or_row + cell.span) as usize];
353                let mut min = cell.constraint.min;
354                if let Some(size) = size {
355                    min = min.max(size * cell.constraint.min_percent / 100 as Coord);
356                }
357                grid_internal::layout_items(span_data, 0 as _, min, spacing);
358                for cdata in span_data {
359                    if cdata.min < cdata.size {
360                        cdata.min = cdata.size;
361                    }
362                }
363            }
364            // Adjust maximum sizes
365            for cell in data.iter().filter(|cell| cell.span > 1) {
366                let span_data = &mut layout_data
367                    [(cell.col_or_row as usize)..(cell.col_or_row + cell.span) as usize];
368                let mut max = cell.constraint.max;
369                if let Some(size) = size {
370                    max = max.min(size * cell.constraint.max_percent / 100 as Coord);
371                }
372                grid_internal::layout_items(span_data, 0 as _, max, spacing);
373                for cdata in span_data {
374                    if cdata.max > cdata.size {
375                        cdata.max = cdata.size;
376                    }
377                }
378            }
379            // Adjust preferred sizes
380            for cell in data.iter().filter(|cell| cell.span > 1) {
381                let span_data = &mut layout_data
382                    [(cell.col_or_row as usize)..(cell.col_or_row + cell.span) as usize];
383                grid_internal::layout_items(span_data, 0 as _, cell.constraint.preferred, spacing);
384                for cdata in span_data {
385                    cdata.pref = cdata.pref.max(cdata.size).min(cdata.max).max(cdata.min);
386                }
387            }
388            // Adjust stretches
389            for cell in data.iter().filter(|cell| cell.span > 1) {
390                let span_data = &mut layout_data
391                    [(cell.col_or_row as usize)..(cell.col_or_row + cell.span) as usize];
392                let total_stretch: f32 = span_data.iter().map(|c| c.stretch).sum();
393                if total_stretch > cell.constraint.stretch {
394                    for cdata in span_data {
395                        cdata.stretch *= cell.constraint.stretch / total_stretch;
396                    }
397                }
398            }
399        }
400        layout_data
401    }
402}
403
404#[repr(C)]
405pub struct Constraint {
406    pub min: Coord,
407    pub max: Coord,
408}
409
410impl Default for Constraint {
411    fn default() -> Self {
412        Constraint { min: 0 as Coord, max: Coord::MAX }
413    }
414}
415
416#[repr(C)]
417#[derive(Copy, Clone, Debug, Default)]
418pub struct Padding {
419    pub begin: Coord,
420    pub end: Coord,
421}
422
423#[repr(C)]
424#[derive(Debug)]
425pub struct GridLayoutData<'a> {
426    pub size: Coord,
427    pub spacing: Coord,
428    pub padding: Padding,
429    pub cells: Slice<'a, GridLayoutCellData>,
430}
431
432#[repr(C)]
433#[derive(Default, Debug)]
434pub struct GridLayoutCellData {
435    /// col, or row.
436    pub col_or_row: u16,
437    /// colspan or rowspan
438    pub span: u16,
439    pub constraint: LayoutInfo,
440}
441
442/// return, an array which is of size `data.cells.len() * 2` which for each cell we give the pos, size
443pub fn solve_grid_layout(data: &GridLayoutData) -> SharedVector<Coord> {
444    let mut layout_data =
445        grid_internal::to_layout_data(data.cells.as_slice(), data.spacing, Some(data.size));
446
447    if layout_data.is_empty() {
448        return Default::default();
449    }
450
451    grid_internal::layout_items(
452        &mut layout_data,
453        data.padding.begin,
454        data.size - (data.padding.begin + data.padding.end),
455        data.spacing,
456    );
457
458    let mut result = SharedVector::with_capacity(4 * data.cells.len());
459    for cell in data.cells.iter() {
460        let cdata = &layout_data[cell.col_or_row as usize];
461        result.push(cdata.pos);
462        result.push(if cell.span > 0 {
463            let first_cell = &layout_data[cell.col_or_row as usize];
464            let last_cell = &layout_data[cell.col_or_row as usize + cell.span as usize - 1];
465            last_cell.pos + last_cell.size - first_cell.pos
466        } else {
467            0 as Coord
468        });
469    }
470    result
471}
472
473pub fn grid_layout_info(
474    cells: Slice<GridLayoutCellData>,
475    spacing: Coord,
476    padding: &Padding,
477) -> LayoutInfo {
478    let layout_data = grid_internal::to_layout_data(cells.as_slice(), spacing, None);
479    if layout_data.is_empty() {
480        return Default::default();
481    }
482    let spacing_w = spacing * (layout_data.len() - 1) as Coord + padding.begin + padding.end;
483    let min = layout_data.iter().map(|data| data.min).sum::<Coord>() + spacing_w;
484    let max = layout_data.iter().map(|data| data.max).fold(spacing_w, Saturating::add);
485    let preferred = layout_data.iter().map(|data| data.pref).sum::<Coord>() + spacing_w;
486    let stretch = layout_data.iter().map(|data| data.stretch).sum::<f32>();
487    LayoutInfo { min, max, min_percent: 0 as _, max_percent: 100 as _, preferred, stretch }
488}
489
490#[repr(C)]
491#[derive(Debug)]
492/// The BoxLayoutData is used to represent both a Horizontal and Vertical layout.
493/// The width/height x/y correspond to that of a horizontal layout.
494/// For vertical layout, they are inverted
495pub struct BoxLayoutData<'a> {
496    pub size: Coord,
497    pub spacing: Coord,
498    pub padding: Padding,
499    pub alignment: LayoutAlignment,
500    pub cells: Slice<'a, BoxLayoutCellData>,
501}
502
503#[repr(C)]
504#[derive(Default, Debug, Clone)]
505pub struct BoxLayoutCellData {
506    pub constraint: LayoutInfo,
507}
508
509/// Solve a BoxLayout
510pub fn solve_box_layout(data: &BoxLayoutData, repeater_indexes: Slice<u32>) -> SharedVector<Coord> {
511    let mut result = SharedVector::<Coord>::default();
512    result.resize(data.cells.len() * 2 + repeater_indexes.len(), 0 as _);
513
514    if data.cells.is_empty() {
515        return result;
516    }
517
518    let mut layout_data: Vec<_> = data
519        .cells
520        .iter()
521        .map(|c| {
522            let min = c.constraint.min.max(c.constraint.min_percent * data.size / 100 as Coord);
523            let max = c.constraint.max.min(c.constraint.max_percent * data.size / 100 as Coord);
524            grid_internal::LayoutData {
525                min,
526                max,
527                pref: c.constraint.preferred.min(max).max(min),
528                stretch: c.constraint.stretch,
529                ..Default::default()
530            }
531        })
532        .collect();
533
534    let size_without_padding = data.size - data.padding.begin - data.padding.end;
535    let pref_size: Coord = layout_data.iter().map(|it| it.pref).sum();
536    let num_spacings = (layout_data.len() - 1) as Coord;
537    let spacings = data.spacing * num_spacings;
538
539    let align = match data.alignment {
540        LayoutAlignment::Stretch => {
541            grid_internal::layout_items(
542                &mut layout_data,
543                data.padding.begin,
544                size_without_padding,
545                data.spacing,
546            );
547            None
548        }
549        _ if size_without_padding <= pref_size + spacings => {
550            grid_internal::layout_items(
551                &mut layout_data,
552                data.padding.begin,
553                size_without_padding,
554                data.spacing,
555            );
556            None
557        }
558        LayoutAlignment::Center => Some((
559            data.padding.begin + (size_without_padding - pref_size - spacings) / 2 as Coord,
560            data.spacing,
561        )),
562        LayoutAlignment::Start => Some((data.padding.begin, data.spacing)),
563        LayoutAlignment::End => {
564            Some((data.padding.begin + (size_without_padding - pref_size - spacings), data.spacing))
565        }
566        LayoutAlignment::SpaceBetween => {
567            Some((data.padding.begin, (size_without_padding - pref_size) / num_spacings))
568        }
569        LayoutAlignment::SpaceAround => {
570            let spacing = (size_without_padding - pref_size) / (num_spacings + 1 as Coord);
571            Some((data.padding.begin + spacing / 2 as Coord, spacing))
572        }
573        LayoutAlignment::SpaceEvenly => {
574            let spacing = (size_without_padding - pref_size) / (num_spacings + 2 as Coord);
575            Some((data.padding.begin + spacing, spacing))
576        }
577    };
578    if let Some((mut pos, spacing)) = align {
579        for it in &mut layout_data {
580            it.pos = pos;
581            it.size = it.pref;
582            pos += spacing + it.size;
583        }
584    }
585
586    let res = result.make_mut_slice();
587
588    // The index/2 in result in which we should add the next repeated item
589    let mut repeat_offset =
590        res.len() / 2 - repeater_indexes.iter().skip(1).step_by(2).sum::<u32>() as usize;
591    // The index/2  in repeater_indexes
592    let mut next_rep = 0;
593    // The index/2 in result in which we should add the next non-repeated item
594    let mut current_offset = 0;
595    for (idx, layout) in layout_data.iter().enumerate() {
596        let o = loop {
597            if let Some(nr) = repeater_indexes.get(next_rep * 2) {
598                let nr = *nr as usize;
599                if nr == idx {
600                    for o in 0..2 {
601                        res[current_offset * 2 + o] = (repeat_offset * 2 + o) as _;
602                    }
603                    current_offset += 1;
604                }
605                if idx >= nr {
606                    if idx - nr == repeater_indexes[next_rep * 2 + 1] as usize {
607                        next_rep += 1;
608                        continue;
609                    }
610                    repeat_offset += 1;
611                    break repeat_offset - 1;
612                }
613            }
614            current_offset += 1;
615            break current_offset - 1;
616        };
617        res[o * 2] = layout.pos;
618        res[o * 2 + 1] = layout.size;
619    }
620    result
621}
622
623/// Return the LayoutInfo for a BoxLayout with the given cells.
624pub fn box_layout_info(
625    cells: Slice<BoxLayoutCellData>,
626    spacing: Coord,
627    padding: &Padding,
628    alignment: LayoutAlignment,
629) -> LayoutInfo {
630    let count = cells.len();
631    let is_stretch = alignment == LayoutAlignment::Stretch;
632    if count < 1 {
633        let mut info = LayoutInfo::default();
634        info.min = padding.begin + padding.end;
635        info.preferred = info.min;
636        if is_stretch {
637            info.max = info.min;
638        }
639        return info;
640    };
641    let extra_w = padding.begin + padding.end + spacing * (count - 1) as Coord;
642    let min = cells.iter().map(|c| c.constraint.min).sum::<Coord>() + extra_w;
643    let max = if is_stretch {
644        (cells.iter().map(|c| c.constraint.max).fold(extra_w, Saturating::add)).max(min)
645    } else {
646        Coord::MAX
647    };
648    let preferred = cells.iter().map(|c| c.constraint.preferred_bounded()).sum::<Coord>() + extra_w;
649    let stretch = cells.iter().map(|c| c.constraint.stretch).sum::<f32>();
650    LayoutInfo { min, max, min_percent: 0 as _, max_percent: 100 as _, preferred, stretch }
651}
652
653pub fn box_layout_info_ortho(cells: Slice<BoxLayoutCellData>, padding: &Padding) -> LayoutInfo {
654    let extra_w = padding.begin + padding.end;
655    let mut fold =
656        cells.iter().fold(LayoutInfo { stretch: f32::MAX, ..Default::default() }, |a, b| {
657            a.merge(&b.constraint)
658        });
659    fold.max = fold.max.max(fold.min);
660    fold.preferred = fold.preferred.clamp(fold.min, fold.max);
661    fold.min += extra_w;
662    fold.max = Saturating::add(fold.max, extra_w);
663    fold.preferred += extra_w;
664    fold
665}
666
667/// Given the cells of a layout of a Dialog, re-order the button according to the platform
668///
669/// This function assume that the `roles` contains the roles of the button which are the first `cells`
670/// It will simply change the column field of the cell
671pub fn reorder_dialog_button_layout(cells: &mut [GridLayoutCellData], roles: &[DialogButtonRole]) {
672    fn add_buttons(
673        cells: &mut [GridLayoutCellData],
674        roles: &[DialogButtonRole],
675        idx: &mut u16,
676        role: DialogButtonRole,
677    ) {
678        for (cell, r) in cells.iter_mut().zip(roles.iter()) {
679            if *r == role {
680                cell.col_or_row = *idx;
681                *idx += 1;
682            }
683        }
684    }
685
686    #[cfg(feature = "std")]
687    fn is_kde() -> bool {
688        // assume some Unix, check if XDG_CURRENT_DESKTOP starts with K
689        std::env::var("XDG_CURRENT_DESKTOP")
690            .ok()
691            .and_then(|v| v.as_bytes().first().copied())
692            .is_some_and(|x| x.eq_ignore_ascii_case(&b'K'))
693    }
694    #[cfg(not(feature = "std"))]
695    let is_kde = || true;
696
697    let mut idx = 0;
698
699    if cfg!(windows) {
700        add_buttons(cells, roles, &mut idx, DialogButtonRole::Reset);
701        idx += 1;
702        add_buttons(cells, roles, &mut idx, DialogButtonRole::Accept);
703        add_buttons(cells, roles, &mut idx, DialogButtonRole::Action);
704        add_buttons(cells, roles, &mut idx, DialogButtonRole::Reject);
705        add_buttons(cells, roles, &mut idx, DialogButtonRole::Apply);
706        add_buttons(cells, roles, &mut idx, DialogButtonRole::Help);
707    } else if cfg!(target_os = "macos") {
708        add_buttons(cells, roles, &mut idx, DialogButtonRole::Help);
709        add_buttons(cells, roles, &mut idx, DialogButtonRole::Reset);
710        add_buttons(cells, roles, &mut idx, DialogButtonRole::Apply);
711        add_buttons(cells, roles, &mut idx, DialogButtonRole::Action);
712        idx += 1;
713        add_buttons(cells, roles, &mut idx, DialogButtonRole::Reject);
714        add_buttons(cells, roles, &mut idx, DialogButtonRole::Accept);
715    } else if is_kde() {
716        // KDE variant
717        add_buttons(cells, roles, &mut idx, DialogButtonRole::Help);
718        add_buttons(cells, roles, &mut idx, DialogButtonRole::Reset);
719        idx += 1;
720        add_buttons(cells, roles, &mut idx, DialogButtonRole::Action);
721        add_buttons(cells, roles, &mut idx, DialogButtonRole::Accept);
722        add_buttons(cells, roles, &mut idx, DialogButtonRole::Apply);
723        add_buttons(cells, roles, &mut idx, DialogButtonRole::Reject);
724    } else {
725        // GNOME variant and fallback for WASM build
726        add_buttons(cells, roles, &mut idx, DialogButtonRole::Help);
727        add_buttons(cells, roles, &mut idx, DialogButtonRole::Reset);
728        idx += 1;
729        add_buttons(cells, roles, &mut idx, DialogButtonRole::Action);
730        add_buttons(cells, roles, &mut idx, DialogButtonRole::Apply);
731        add_buttons(cells, roles, &mut idx, DialogButtonRole::Reject);
732        add_buttons(cells, roles, &mut idx, DialogButtonRole::Accept);
733    }
734}
735
736#[cfg(feature = "ffi")]
737pub(crate) mod ffi {
738    #![allow(unsafe_code)]
739
740    use super::*;
741
742    #[unsafe(no_mangle)]
743    pub extern "C" fn slint_solve_grid_layout(
744        data: &GridLayoutData,
745        result: &mut SharedVector<Coord>,
746    ) {
747        *result = super::solve_grid_layout(data)
748    }
749
750    #[unsafe(no_mangle)]
751    pub extern "C" fn slint_grid_layout_info(
752        cells: Slice<GridLayoutCellData>,
753        spacing: Coord,
754        padding: &Padding,
755    ) -> LayoutInfo {
756        super::grid_layout_info(cells, spacing, padding)
757    }
758
759    #[unsafe(no_mangle)]
760    pub extern "C" fn slint_solve_box_layout(
761        data: &BoxLayoutData,
762        repeater_indexes: Slice<u32>,
763        result: &mut SharedVector<Coord>,
764    ) {
765        *result = super::solve_box_layout(data, repeater_indexes)
766    }
767
768    #[unsafe(no_mangle)]
769    /// Return the LayoutInfo for a BoxLayout with the given cells.
770    pub extern "C" fn slint_box_layout_info(
771        cells: Slice<BoxLayoutCellData>,
772        spacing: Coord,
773        padding: &Padding,
774        alignment: LayoutAlignment,
775    ) -> LayoutInfo {
776        super::box_layout_info(cells, spacing, padding, alignment)
777    }
778
779    #[unsafe(no_mangle)]
780    /// Return the LayoutInfo for a BoxLayout with the given cells.
781    pub extern "C" fn slint_box_layout_info_ortho(
782        cells: Slice<BoxLayoutCellData>,
783        padding: &Padding,
784    ) -> LayoutInfo {
785        super::box_layout_info_ortho(cells, padding)
786    }
787
788    /// Calls [`reorder_dialog_button_layout`].
789    ///
790    /// Safety: `cells` must be a pointer to a mutable array of cell data, the array must have at
791    /// least `roles.len()` elements.
792    #[unsafe(no_mangle)]
793    pub unsafe extern "C" fn slint_reorder_dialog_button_layout(
794        cells: *mut GridLayoutCellData,
795        roles: Slice<DialogButtonRole>,
796    ) {
797        reorder_dialog_button_layout(core::slice::from_raw_parts_mut(cells, roles.len()), &roles);
798    }
799}