1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial |
3 | |
4 | //! Runtime support for layouts. |
5 | |
6 | // cspell:ignore coord |
7 | |
8 | use crate::items::{DialogButtonRole, LayoutAlignment}; |
9 | use crate::{slice::Slice, Coord, SharedVector}; |
10 | use alloc::vec::Vec; |
11 | |
12 | pub 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)] |
18 | pub 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 | |
33 | impl 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 | |
46 | impl 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 | |
67 | impl 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. |
76 | pub 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 |
114 | trait Saturating { |
115 | fn add(_: Self, _: Self) -> Self; |
116 | } |
117 | impl Saturating for i32 { |
118 | #[inline ] |
119 | fn add(a: Self, b: Self) -> Self { |
120 | a.saturating_add(b) |
121 | } |
122 | } |
123 | impl Saturating for f32 { |
124 | #[inline ] |
125 | fn add(a: Self, b: Self) -> Self { |
126 | a + b |
127 | } |
128 | } |
129 | |
130 | mod 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 = 0; |
315 | for cell in data { |
316 | num = num.max(cell.col_or_row + cell.span); |
317 | } |
318 | if num < 1 { |
319 | return Default::default(); |
320 | } |
321 | let mut layout_data = alloc::vec![grid_internal::LayoutData { stretch: 1., ..Default::default() }; num as usize]; |
322 | let mut has_spans = false; |
323 | for cell in data { |
324 | let constraint = &cell.constraint; |
325 | let mut max = constraint.max; |
326 | if let Some(size) = size { |
327 | max = max.min(size * constraint.max_percent / 100 as Coord); |
328 | } |
329 | for c in 0..(cell.span as usize) { |
330 | let cdata = &mut layout_data[cell.col_or_row as usize + c]; |
331 | cdata.max = cdata.max.min(max); |
332 | } |
333 | if cell.span == 1 { |
334 | let mut min = constraint.min; |
335 | if let Some(size) = size { |
336 | min = min.max(size * constraint.min_percent / 100 as Coord); |
337 | } |
338 | let pref = constraint.preferred.min(max).max(min); |
339 | let cdata = &mut layout_data[cell.col_or_row as usize]; |
340 | cdata.min = cdata.min.max(min); |
341 | cdata.pref = cdata.pref.max(pref); |
342 | cdata.stretch = cdata.stretch.min(constraint.stretch); |
343 | } else { |
344 | has_spans = true; |
345 | } |
346 | } |
347 | if has_spans { |
348 | // Adjust minimum sizes |
349 | for cell in data.iter().filter(|cell| cell.span > 1) { |
350 | let span_data = &mut layout_data |
351 | [(cell.col_or_row as usize)..(cell.col_or_row + cell.span) as usize]; |
352 | let mut min = cell.constraint.min; |
353 | if let Some(size) = size { |
354 | min = min.max(size * cell.constraint.min_percent / 100 as Coord); |
355 | } |
356 | grid_internal::layout_items(span_data, 0 as _, min, spacing); |
357 | for cdata in span_data { |
358 | if cdata.min < cdata.size { |
359 | cdata.min = cdata.size; |
360 | } |
361 | } |
362 | } |
363 | // Adjust maximum sizes |
364 | for cell in data.iter().filter(|cell| cell.span > 1) { |
365 | let span_data = &mut layout_data |
366 | [(cell.col_or_row as usize)..(cell.col_or_row + cell.span) as usize]; |
367 | let mut max = cell.constraint.max; |
368 | if let Some(size) = size { |
369 | max = max.min(size * cell.constraint.max_percent / 100 as Coord); |
370 | } |
371 | grid_internal::layout_items(span_data, 0 as _, max, spacing); |
372 | for cdata in span_data { |
373 | if cdata.max > cdata.size { |
374 | cdata.max = cdata.size; |
375 | } |
376 | } |
377 | } |
378 | // Adjust preferred sizes |
379 | for cell in data.iter().filter(|cell| cell.span > 1) { |
380 | let span_data = &mut layout_data |
381 | [(cell.col_or_row as usize)..(cell.col_or_row + cell.span) as usize]; |
382 | grid_internal::layout_items(span_data, 0 as _, cell.constraint.preferred, spacing); |
383 | for cdata in span_data { |
384 | cdata.pref = cdata.pref.max(cdata.size).min(cdata.max).max(cdata.min); |
385 | } |
386 | } |
387 | // Adjust stretches |
388 | for cell in data.iter().filter(|cell| cell.span > 1) { |
389 | let span_data = &mut layout_data |
390 | [(cell.col_or_row as usize)..(cell.col_or_row + cell.span) as usize]; |
391 | let total_stretch: f32 = span_data.iter().map(|c| c.stretch).sum(); |
392 | if total_stretch > cell.constraint.stretch { |
393 | for cdata in span_data { |
394 | cdata.stretch *= cell.constraint.stretch / total_stretch; |
395 | } |
396 | } |
397 | } |
398 | } |
399 | layout_data |
400 | } |
401 | } |
402 | |
403 | #[repr (C)] |
404 | pub struct Constraint { |
405 | pub min: Coord, |
406 | pub max: Coord, |
407 | } |
408 | |
409 | impl Default for Constraint { |
410 | fn default() -> Self { |
411 | Constraint { min: 0 as Coord, max: Coord::MAX } |
412 | } |
413 | } |
414 | |
415 | #[repr (C)] |
416 | #[derive (Copy, Clone, Debug, Default)] |
417 | pub struct Padding { |
418 | pub begin: Coord, |
419 | pub end: Coord, |
420 | } |
421 | |
422 | #[repr (C)] |
423 | #[derive (Debug)] |
424 | pub struct GridLayoutData<'a> { |
425 | pub size: Coord, |
426 | pub spacing: Coord, |
427 | pub padding: Padding, |
428 | pub cells: Slice<'a, GridLayoutCellData>, |
429 | } |
430 | |
431 | #[repr (C)] |
432 | #[derive (Default, Debug)] |
433 | pub struct GridLayoutCellData { |
434 | /// col, or row. |
435 | pub col_or_row: u16, |
436 | /// colspan or rowspan |
437 | pub span: u16, |
438 | pub constraint: LayoutInfo, |
439 | } |
440 | |
441 | /// return, an array which is of size `data.cells.len() * 2` which for each cell we give the pos, size |
442 | pub fn solve_grid_layout(data: &GridLayoutData) -> SharedVector<Coord> { |
443 | let mut layout_data = |
444 | grid_internal::to_layout_data(data.cells.as_slice(), data.spacing, Some(data.size)); |
445 | |
446 | if layout_data.is_empty() { |
447 | return Default::default(); |
448 | } |
449 | |
450 | grid_internal::layout_items( |
451 | &mut layout_data, |
452 | data.padding.begin, |
453 | data.size - (data.padding.begin + data.padding.end), |
454 | data.spacing, |
455 | ); |
456 | |
457 | let mut result = SharedVector::with_capacity(4 * data.cells.len()); |
458 | for cell in data.cells.iter() { |
459 | let cdata = &layout_data[cell.col_or_row as usize]; |
460 | result.push(cdata.pos); |
461 | result.push({ |
462 | let first_cell = &layout_data[cell.col_or_row as usize]; |
463 | let last_cell = &layout_data[cell.col_or_row as usize + cell.span as usize - 1]; |
464 | last_cell.pos + last_cell.size - first_cell.pos |
465 | }); |
466 | } |
467 | result |
468 | } |
469 | |
470 | pub fn grid_layout_info( |
471 | cells: Slice<GridLayoutCellData>, |
472 | spacing: Coord, |
473 | padding: &Padding, |
474 | ) -> LayoutInfo { |
475 | let layout_data: Vec = grid_internal::to_layout_data(data:cells.as_slice(), spacing, size:None); |
476 | if layout_data.is_empty() { |
477 | return Default::default(); |
478 | } |
479 | let spacing_w: f32 = spacing * (layout_data.len() - 1) as Coord + padding.begin + padding.end; |
480 | let min: f32 = layout_data.iter().map(|data: &LayoutData| data.min).sum::<Coord>() + spacing_w; |
481 | let max: f32 = layout_data.iter().map(|data| data.max).fold(init:spacing_w, f:Saturating::add); |
482 | let preferred: f32 = layout_data.iter().map(|data: &LayoutData| data.pref).sum::<Coord>() + spacing_w; |
483 | let stretch: f32 = layout_data.iter().map(|data: &LayoutData| data.stretch).sum::<f32>(); |
484 | LayoutInfo { min, max, min_percent: 0 as _, max_percent: 100 as _, preferred, stretch } |
485 | } |
486 | |
487 | #[repr (C)] |
488 | #[derive (Debug)] |
489 | /// The BoxLayoutData is used to represent both a Horizontal and Vertical layout. |
490 | /// The width/height x/y correspond to that of a horizontal layout. |
491 | /// For vertical layout, they are inverted |
492 | pub struct BoxLayoutData<'a> { |
493 | pub size: Coord, |
494 | pub spacing: Coord, |
495 | pub padding: Padding, |
496 | pub alignment: LayoutAlignment, |
497 | pub cells: Slice<'a, BoxLayoutCellData>, |
498 | } |
499 | |
500 | #[repr (C)] |
501 | #[derive (Default, Debug, Clone)] |
502 | pub struct BoxLayoutCellData { |
503 | pub constraint: LayoutInfo, |
504 | } |
505 | |
506 | /// Solve a BoxLayout |
507 | pub fn solve_box_layout(data: &BoxLayoutData, repeater_indexes: Slice<u32>) -> SharedVector<Coord> { |
508 | let mut result = SharedVector::<Coord>::default(); |
509 | result.resize(data.cells.len() * 2 + repeater_indexes.len(), 0 as _); |
510 | |
511 | if data.cells.is_empty() { |
512 | return result; |
513 | } |
514 | |
515 | let mut layout_data: Vec<_> = data |
516 | .cells |
517 | .iter() |
518 | .map(|c| { |
519 | let min = c.constraint.min.max(c.constraint.min_percent * data.size / 100 as Coord); |
520 | let max = c.constraint.max.min(c.constraint.max_percent * data.size / 100 as Coord); |
521 | grid_internal::LayoutData { |
522 | min, |
523 | max, |
524 | pref: c.constraint.preferred.min(max).max(min), |
525 | stretch: c.constraint.stretch, |
526 | ..Default::default() |
527 | } |
528 | }) |
529 | .collect(); |
530 | |
531 | let size_without_padding = data.size - data.padding.begin - data.padding.end; |
532 | let pref_size: Coord = layout_data.iter().map(|it| it.pref).sum(); |
533 | let num_spacings = (layout_data.len() - 1) as Coord; |
534 | let spacings = data.spacing * num_spacings; |
535 | |
536 | let align = match data.alignment { |
537 | LayoutAlignment::Stretch => { |
538 | grid_internal::layout_items( |
539 | &mut layout_data, |
540 | data.padding.begin, |
541 | size_without_padding, |
542 | data.spacing, |
543 | ); |
544 | None |
545 | } |
546 | _ if size_without_padding <= pref_size + spacings => { |
547 | grid_internal::layout_items( |
548 | &mut layout_data, |
549 | data.padding.begin, |
550 | size_without_padding, |
551 | data.spacing, |
552 | ); |
553 | None |
554 | } |
555 | LayoutAlignment::Center => Some(( |
556 | data.padding.begin + (size_without_padding - pref_size - spacings) / 2 as Coord, |
557 | data.spacing, |
558 | )), |
559 | LayoutAlignment::Start => Some((data.padding.begin, data.spacing)), |
560 | LayoutAlignment::End => { |
561 | Some((data.padding.begin + (size_without_padding - pref_size - spacings), data.spacing)) |
562 | } |
563 | LayoutAlignment::SpaceBetween => { |
564 | Some((data.padding.begin, (size_without_padding - pref_size) / num_spacings)) |
565 | } |
566 | LayoutAlignment::SpaceAround => { |
567 | let spacing = (size_without_padding - pref_size) / (num_spacings + 1 as Coord); |
568 | Some((data.padding.begin + spacing / 2 as Coord, spacing)) |
569 | } |
570 | }; |
571 | if let Some((mut pos, spacing)) = align { |
572 | for it in &mut layout_data { |
573 | it.pos = pos; |
574 | it.size = it.pref; |
575 | pos += spacing + it.size; |
576 | } |
577 | } |
578 | |
579 | let res = result.make_mut_slice(); |
580 | |
581 | // The index/2 in result in which we should add the next repeated item |
582 | let mut repeat_offset = |
583 | res.len() / 2 - repeater_indexes.iter().skip(1).step_by(2).sum::<u32>() as usize; |
584 | // The index/2 in repeater_indexes |
585 | let mut next_rep = 0; |
586 | // The index/2 in result in which we should add the next non-repeated item |
587 | let mut current_offset = 0; |
588 | for (idx, layout) in layout_data.iter().enumerate() { |
589 | let o = loop { |
590 | if let Some(nr) = repeater_indexes.get(next_rep * 2) { |
591 | let nr = *nr as usize; |
592 | if nr == idx { |
593 | for o in 0..2 { |
594 | res[current_offset * 2 + o] = (repeat_offset * 2 + o) as _; |
595 | } |
596 | current_offset += 1; |
597 | } |
598 | if idx >= nr { |
599 | if idx - nr == repeater_indexes[next_rep * 2 + 1] as usize { |
600 | next_rep += 1; |
601 | continue; |
602 | } |
603 | repeat_offset += 1; |
604 | break repeat_offset - 1; |
605 | } |
606 | } |
607 | current_offset += 1; |
608 | break current_offset - 1; |
609 | }; |
610 | res[o * 2] = layout.pos; |
611 | res[o * 2 + 1] = layout.size; |
612 | } |
613 | result |
614 | } |
615 | |
616 | /// Return the LayoutInfo for a BoxLayout with the given cells. |
617 | pub fn box_layout_info( |
618 | cells: Slice<BoxLayoutCellData>, |
619 | spacing: Coord, |
620 | padding: &Padding, |
621 | alignment: LayoutAlignment, |
622 | ) -> LayoutInfo { |
623 | let count: usize = cells.len(); |
624 | if count < 1 { |
625 | return LayoutInfo { max: 0 as _, ..LayoutInfo::default() }; |
626 | }; |
627 | let is_stretch: bool = alignment == LayoutAlignment::Stretch; |
628 | let extra_w: f32 = padding.begin + padding.end + spacing * (count - 1) as Coord; |
629 | let min: f32 = cells.iter().map(|c: &BoxLayoutCellData| c.constraint.min).sum::<Coord>() + extra_w; |
630 | let max: f32 = if is_stretch { |
631 | (cells.iter().map(|c| c.constraint.max).fold(init:extra_w, f:Saturating::add)).max(min) |
632 | } else { |
633 | Coord::MAX |
634 | }; |
635 | let preferred: f32 = cells.iter().map(|c: &BoxLayoutCellData| c.constraint.preferred_bounded()).sum::<Coord>() + extra_w; |
636 | let stretch: f32 = cells.iter().map(|c: &BoxLayoutCellData| c.constraint.stretch).sum::<f32>(); |
637 | LayoutInfo { min, max, min_percent: 0 as _, max_percent: 100 as _, preferred, stretch } |
638 | } |
639 | |
640 | pub fn box_layout_info_ortho(cells: Slice<BoxLayoutCellData>, padding: &Padding) -> LayoutInfo { |
641 | let count: usize = cells.len(); |
642 | if count < 1 { |
643 | return LayoutInfo { max: 0 as _, ..LayoutInfo::default() }; |
644 | }; |
645 | let extra_w: f32 = padding.begin + padding.end; |
646 | |
647 | let mut fold: LayoutInfo = |
648 | cells.iter().fold(init:LayoutInfo { stretch: f32::MAX, ..Default::default() }, |a: LayoutInfo, b: &BoxLayoutCellData| { |
649 | a.merge(&b.constraint) |
650 | }); |
651 | fold.max = fold.max.max(fold.min); |
652 | fold.preferred = fold.preferred.clamp(fold.min, fold.max); |
653 | fold.min += extra_w; |
654 | fold.max = Saturating::add(fold.max, extra_w); |
655 | fold.preferred += extra_w; |
656 | fold |
657 | } |
658 | |
659 | /// Given the cells of a layout of a Dialog, re-order the button according to the platform |
660 | /// |
661 | /// This function assume that the `roles` contains the roles of the button which are the first `cells` |
662 | /// It will simply change the column field of the cell |
663 | pub fn reorder_dialog_button_layout(cells: &mut [GridLayoutCellData], roles: &[DialogButtonRole]) { |
664 | fn add_buttons( |
665 | cells: &mut [GridLayoutCellData], |
666 | roles: &[DialogButtonRole], |
667 | idx: &mut u16, |
668 | role: DialogButtonRole, |
669 | ) { |
670 | for (cell, r) in cells.iter_mut().zip(roles.iter()) { |
671 | if *r == role { |
672 | cell.col_or_row = *idx; |
673 | *idx += 1; |
674 | } |
675 | } |
676 | } |
677 | |
678 | #[cfg (feature = "std" )] |
679 | fn is_kde() -> bool { |
680 | // assume some unix check if XDG_CURRENT_DESKTOP stats with K |
681 | std::env::var("XDG_CURRENT_DESKTOP" ) |
682 | .ok() |
683 | .and_then(|v| v.as_bytes().first().copied()) |
684 | .map_or(false, |x| x.to_ascii_uppercase() == b'K' ) |
685 | } |
686 | #[cfg (not(feature = "std" ))] |
687 | let is_kde = || true; |
688 | |
689 | let mut idx = 0; |
690 | |
691 | if cfg!(windows) { |
692 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Reset); |
693 | idx += 1; |
694 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Accept); |
695 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Action); |
696 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Reject); |
697 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Apply); |
698 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Help); |
699 | } else if cfg!(target_os = "macos" ) { |
700 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Help); |
701 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Reset); |
702 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Apply); |
703 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Action); |
704 | idx += 1; |
705 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Reject); |
706 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Accept); |
707 | } else if is_kde() { |
708 | // KDE variant |
709 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Help); |
710 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Reset); |
711 | idx += 1; |
712 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Action); |
713 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Accept); |
714 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Apply); |
715 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Reject); |
716 | } else { |
717 | // GNOME variant and fallback for WASM build |
718 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Help); |
719 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Reset); |
720 | idx += 1; |
721 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Action); |
722 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Apply); |
723 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Reject); |
724 | add_buttons(cells, roles, &mut idx, DialogButtonRole::Accept); |
725 | } |
726 | } |
727 | |
728 | #[cfg (feature = "ffi" )] |
729 | pub(crate) mod ffi { |
730 | #![allow (unsafe_code)] |
731 | |
732 | use super::*; |
733 | |
734 | #[no_mangle ] |
735 | pub extern "C" fn slint_solve_grid_layout( |
736 | data: &GridLayoutData, |
737 | result: &mut SharedVector<Coord>, |
738 | ) { |
739 | *result = super::solve_grid_layout(data) |
740 | } |
741 | |
742 | #[no_mangle ] |
743 | pub extern "C" fn slint_grid_layout_info( |
744 | cells: Slice<GridLayoutCellData>, |
745 | spacing: Coord, |
746 | padding: &Padding, |
747 | ) -> LayoutInfo { |
748 | super::grid_layout_info(cells, spacing, padding) |
749 | } |
750 | |
751 | #[no_mangle ] |
752 | pub extern "C" fn slint_solve_box_layout( |
753 | data: &BoxLayoutData, |
754 | repeater_indexes: Slice<u32>, |
755 | result: &mut SharedVector<Coord>, |
756 | ) { |
757 | *result = super::solve_box_layout(data, repeater_indexes) |
758 | } |
759 | |
760 | #[no_mangle ] |
761 | /// Return the LayoutInfo for a BoxLayout with the given cells. |
762 | pub extern "C" fn slint_box_layout_info( |
763 | cells: Slice<BoxLayoutCellData>, |
764 | spacing: Coord, |
765 | padding: &Padding, |
766 | alignment: LayoutAlignment, |
767 | ) -> LayoutInfo { |
768 | super::box_layout_info(cells, spacing, padding, alignment) |
769 | } |
770 | |
771 | #[no_mangle ] |
772 | /// Return the LayoutInfo for a BoxLayout with the given cells. |
773 | pub extern "C" fn slint_box_layout_info_ortho( |
774 | cells: Slice<BoxLayoutCellData>, |
775 | padding: &Padding, |
776 | ) -> LayoutInfo { |
777 | super::box_layout_info_ortho(cells, padding) |
778 | } |
779 | |
780 | /// Calls [`reorder_dialog_button_layout`]. |
781 | /// |
782 | /// Safety: `cells` must be a pointer to a mutable array of cell data, the array must have at |
783 | /// least `roles.len()` elements. |
784 | #[no_mangle ] |
785 | pub unsafe extern "C" fn slint_reorder_dialog_button_layout( |
786 | cells: *mut GridLayoutCellData, |
787 | roles: Slice<DialogButtonRole>, |
788 | ) { |
789 | reorder_dialog_button_layout(core::slice::from_raw_parts_mut(cells, roles.len()), &roles); |
790 | } |
791 | } |
792 | |