// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 //! Runtime support for layouts. // cspell:ignore coord use crate::items::{ CrossAxisAlignment, DialogButtonRole, FlexboxLayoutDirection, FlexboxLayoutWrap, LayoutAlignment, }; use crate::{Coord, SharedVector, slice::Slice}; use alloc::format; use alloc::string::String; use alloc::vec::Vec; use num_traits::Float; pub use crate::items::Orientation; /// The constraint that applies to a layout // Also, the field needs to be in alphabetical order because how the generated code sort fields for struct #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq)] pub struct LayoutInfo { /// The maximum size for the item. pub max: Coord, /// The maximum size in percentage of the parent (value between 0 and 100). pub max_percent: Coord, /// The minimum size for this item. pub min: Coord, /// The minimum size in percentage of the parent (value between 0 and 100). pub min_percent: Coord, /// the preferred size pub preferred: Coord, /// the stretch factor pub stretch: f32, } impl Default for LayoutInfo { fn default() -> Self { LayoutInfo { min: 0 as _, max: Coord::MAX, min_percent: 0 as _, max_percent: 100 as _, preferred: 0 as _, stretch: 0 as _, } } } impl LayoutInfo { // Note: This "logic" is duplicated in the cpp generator's generated code for merging layout // infos, and in the compiler's const_propagation pass, which folds merges of constant // layout infos at compile time. #[must_use] pub fn merge(&self, other: &LayoutInfo) -> Self { Self { min: self.min.max(other.min), max: self.max.min(other.max), min_percent: self.min_percent.max(other.min_percent), max_percent: self.max_percent.min(other.max_percent), preferred: self.preferred.max(other.preferred), stretch: self.stretch.min(other.stretch), } } /// Helper function to return a preferred size which is within the min/max constraints #[must_use] pub fn preferred_bounded(&self) -> Coord { self.preferred.min(self.max).max(self.min) } } impl core::ops::Add for LayoutInfo { type Output = Self; fn add(self, rhs: Self) -> Self::Output { self.merge(&rhs) } } /// Returns the logical min and max sizes given the provided layout constraints. pub fn min_max_size_for_layout_constraints( constraints_horizontal: LayoutInfo, constraints_vertical: LayoutInfo, ) -> (Option, Option) { let min_width = constraints_horizontal.min.min(constraints_horizontal.max) as f32; let min_height = constraints_vertical.min.min(constraints_vertical.max) as f32; let max_width = constraints_horizontal.max.max(constraints_horizontal.min) as f32; let max_height = constraints_vertical.max.max(constraints_vertical.min) as f32; //cfg!(target_arch = "wasm32") is there because wasm32 winit don't like when max size is None: // panicked at 'Property is read only: JsValue(NoModificationAllowedError: CSSStyleDeclaration.removeProperty: Can't remove property 'max-width' from computed style let min_size = if min_width > 0. || min_height > 0. || cfg!(target_arch = "wasm32") { Some(crate::api::LogicalSize::new(min_width, min_height)) } else { None }; let max_size = if (max_width > 0. && max_height > 0. && (max_width < i32::MAX as f32 || max_height < i32::MAX as f32)) || cfg!(target_arch = "wasm32") { // maximum widget size for Qt and a workaround for the winit api not allowing partial constraints let window_size_max = 16_777_215.; Some(crate::api::LogicalSize::new( max_width.min(window_size_max), max_height.min(window_size_max), )) } else { None }; (min_size, max_size) } /// Implement a saturating_add version for both possible value of Coord. /// So that adding the max value does not overflow trait Saturating { fn add(_: Self, _: Self) -> Self; } impl Saturating for i32 { #[inline] fn add(a: Self, b: Self) -> Self { a.saturating_add(b) } } impl Saturating for f32 { #[inline] fn add(a: Self, b: Self) -> Self { a + b } } mod grid_internal { use super::*; fn order_coord(a: &T, b: &T) -> core::cmp::Ordering { a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal) } #[derive(Debug, Clone)] pub struct LayoutData { // inputs pub min: Coord, pub max: Coord, pub pref: Coord, pub stretch: f32, // outputs pub pos: Coord, pub size: Coord, } impl Default for LayoutData { fn default() -> Self { LayoutData { min: 0 as _, max: Coord::MAX, pref: 0 as _, stretch: f32::MAX, pos: 0 as _, size: 0 as _, } } } trait Adjust { fn can_grow(_: &LayoutData) -> Coord; fn to_distribute(expected_size: Coord, current_size: Coord) -> Coord; fn distribute(_: &mut LayoutData, val: Coord); } struct Grow; impl Adjust for Grow { fn can_grow(it: &LayoutData) -> Coord { it.max - it.size } fn to_distribute(expected_size: Coord, current_size: Coord) -> Coord { expected_size - current_size } fn distribute(it: &mut LayoutData, val: Coord) { it.size += val; } } struct Shrink; impl Adjust for Shrink { fn can_grow(it: &LayoutData) -> Coord { it.size - it.min } fn to_distribute(expected_size: Coord, current_size: Coord) -> Coord { current_size - expected_size } fn distribute(it: &mut LayoutData, val: Coord) { it.size -= val; } } #[allow(clippy::unnecessary_cast)] // Coord fn adjust_items(data: &mut [LayoutData], size_without_spacing: Coord) -> Option<()> { loop { let size_cannot_grow: Coord = data .iter() .filter(|it| A::can_grow(it) <= 0 as _) .map(|it| it.size) .fold(0 as Coord, Saturating::add); let total_stretch: f32 = data.iter().filter(|it| A::can_grow(it) > 0 as _).map(|it| it.stretch).sum(); let actual_stretch = |s: f32| if total_stretch <= 0. { 1. } else { s }; let max_grow = data .iter() .filter(|it| A::can_grow(it) > 0 as _) .map(|it| A::can_grow(it) as f32 / actual_stretch(it.stretch)) .min_by(order_coord)?; let current_size: Coord = data .iter() .filter(|it| A::can_grow(it) > 0 as _) .map(|it| it.size) .fold(0 as _, Saturating::add); //let to_distribute = size_without_spacing - (size_cannot_grow + current_size); let to_distribute = A::to_distribute(size_without_spacing, size_cannot_grow + current_size) as f32; if to_distribute <= 0. || max_grow <= 0. { return Some(()); } let grow = if total_stretch <= 0. { to_distribute / (data.iter().filter(|it| A::can_grow(it) > 0 as _).count() as Coord) as f32 } else { to_distribute / total_stretch } .min(max_grow); let mut distributed = 0 as Coord; for it in data.iter_mut().filter(|it| A::can_grow(it) > 0 as Coord) { let val = (grow * actual_stretch(it.stretch)) as Coord; A::distribute(it, val); distributed += val; } if distributed <= 0 as Coord { // This can happen when Coord is integer and there is less then a pixel to add to each elements // just give the pixel to the one with the bigger stretch if let Some(it) = data .iter_mut() .filter(|it| A::can_grow(it) > 0 as _) .max_by(|a, b| actual_stretch(a.stretch).total_cmp(&b.stretch)) { A::distribute(it, to_distribute as Coord); } return Some(()); } } } pub fn layout_items(data: &mut [LayoutData], start_pos: Coord, size: Coord, spacing: Coord) { let size_without_spacing = size - spacing * (data.len() - 1) as Coord; let mut pref = 0 as Coord; for it in data.iter_mut() { it.size = it.pref; pref += it.pref; } if size_without_spacing >= pref { adjust_items::(data, size_without_spacing); } else if size_without_spacing < pref { adjust_items::(data, size_without_spacing); } let mut pos = start_pos; for it in data.iter_mut() { it.pos = pos; pos = Saturating::add(pos, Saturating::add(it.size, spacing)); } } #[test] #[allow(clippy::float_cmp)] // We want bit-wise equality here fn test_layout_items() { let my_items = &mut [ LayoutData { min: 100., max: 200., pref: 100., stretch: 1., ..Default::default() }, LayoutData { min: 50., max: 300., pref: 100., stretch: 1., ..Default::default() }, LayoutData { min: 50., max: 150., pref: 100., stretch: 1., ..Default::default() }, ]; layout_items(my_items, 100., 650., 0.); assert_eq!(my_items[0].size, 200.); assert_eq!(my_items[1].size, 300.); assert_eq!(my_items[2].size, 150.); layout_items(my_items, 100., 200., 0.); assert_eq!(my_items[0].size, 100.); assert_eq!(my_items[1].size, 50.); assert_eq!(my_items[2].size, 50.); layout_items(my_items, 100., 300., 0.); assert_eq!(my_items[0].size, 100.); assert_eq!(my_items[1].size, 100.); assert_eq!(my_items[2].size, 100.); } /// Create a vector of LayoutData (e.g. one per row if Vertical) based on the constraints and organized data /// Used by both solve_grid_layout() and grid_layout_info() pub fn to_layout_data( organized_data: &GridLayoutOrganizedData, constraints: Slice, orientation: Orientation, repeater_indices: Slice, repeater_steps: Slice, spacing: Coord, size: Option, ) -> Vec { assert!(organized_data.len().is_multiple_of(4)); let num = organized_data.max_value( constraints.len(), orientation, &repeater_indices, &repeater_steps, ); if num < 1 { return Default::default(); } let marker_for_empty = -1.; let mut layout_data = alloc::vec![grid_internal::LayoutData { max: 0 as Coord, stretch: marker_for_empty, ..Default::default() }; num]; let mut has_spans = false; for (idx, cell_data) in constraints.iter().enumerate() { let constraint = &cell_data.constraint; let mut max = constraint.max; if let Some(size) = size { max = max.min(size * constraint.max_percent / 100 as Coord); } let (col_or_row, span) = organized_data.col_or_row_and_span( idx, orientation, &repeater_indices, &repeater_steps, ); for c in 0..(span as usize) { let cdata = &mut layout_data[col_or_row as usize + c]; // Initialize max/stretch to proper defaults on first item in this row/col // so that empty rows/columns don't stretch. if cdata.stretch == marker_for_empty { cdata.max = Coord::MAX; cdata.stretch = 1.; } cdata.max = cdata.max.min(max); } if span == 1 { let mut min = constraint.min; if let Some(size) = size { min = min.max(size * constraint.min_percent / 100 as Coord); } let pref = constraint.preferred.min(max).max(min); let cdata = &mut layout_data[col_or_row as usize]; cdata.min = cdata.min.max(min); cdata.pref = cdata.pref.max(pref); cdata.stretch = cdata.stretch.min(constraint.stretch); } else { has_spans = true; } } if has_spans { for (idx, cell_data) in constraints.iter().enumerate() { let constraint = &cell_data.constraint; let (col_or_row, span) = organized_data.col_or_row_and_span( idx, orientation, &repeater_indices, &repeater_steps, ); if span > 1 { let span_data = &mut layout_data [(col_or_row as usize)..(col_or_row as usize + span as usize)]; // Adjust minimum sizes let mut min = constraint.min; if let Some(size) = size { min = min.max(size * constraint.min_percent / 100 as Coord); } grid_internal::layout_items(span_data, 0 as _, min, spacing); for cdata in span_data.iter_mut() { if cdata.min < cdata.size { cdata.min = cdata.size; } } // Adjust maximum sizes let mut max = constraint.max; if let Some(size) = size { max = max.min(size * constraint.max_percent / 100 as Coord); } grid_internal::layout_items(span_data, 0 as _, max, spacing); for cdata in span_data.iter_mut() { if cdata.max > cdata.size { cdata.max = cdata.size; } } // Adjust preferred sizes grid_internal::layout_items(span_data, 0 as _, constraint.preferred, spacing); for cdata in span_data.iter_mut() { cdata.pref = cdata.pref.max(cdata.size).min(cdata.max).max(cdata.min); } // Adjust stretches let total_stretch: f32 = span_data.iter().map(|c| c.stretch).sum(); if total_stretch > constraint.stretch { for cdata in span_data.iter_mut() { cdata.stretch *= constraint.stretch / total_stretch; } } } } } for cdata in layout_data.iter_mut() { if cdata.stretch == marker_for_empty { cdata.stretch = 0.; } // A cell collapsed to a fixed zero size can pull the row/col's // max below its min (#9724). The minimum is the hard constraint, // so raise max to meet it; min == max now, so that's pref's only // legal value too. Guarded: this must not touch rows without a // min/max conflict, since to_layout_data's output also drives // solve_grid_layout. if cdata.max < cdata.min { cdata.max = cdata.min; cdata.pref = cdata.min; } } layout_data } } #[repr(C)] pub struct Constraint { pub min: Coord, pub max: Coord, } impl Default for Constraint { fn default() -> Self { Constraint { min: 0 as Coord, max: Coord::MAX } } } #[repr(C)] #[derive(Copy, Clone, Debug, Default)] pub struct Padding { pub begin: Coord, pub end: Coord, } #[repr(C)] #[derive(Debug)] /// The horizontal or vertical data for all cells of a GridLayout, used as input to solve_grid_layout() pub struct GridLayoutData { pub size: Coord, pub spacing: Coord, pub padding: Padding, pub organized_data: GridLayoutOrganizedData, } /// The input data for a cell of a GridLayout, before row/col determination and before H/V split /// Used as input to organize_grid_layout() #[repr(C)] #[derive(Debug, Clone)] pub struct GridLayoutInputData { /// whether this cell is the first one in a Row element pub new_row: bool, /// col and row number. /// Only ROW_COL_AUTO and the u16 range are valid, values outside of /// that will be clamped with a warning at runtime pub col: f32, pub row: f32, /// colspan and rowspan /// Only the u16 range is valid, values outside of that will be clamped with a warning at runtime pub colspan: f32, pub rowspan: f32, } impl Default for GridLayoutInputData { fn default() -> Self { Self { new_row: false, col: i_slint_common::ROW_COL_AUTO, row: i_slint_common::ROW_COL_AUTO, colspan: 1.0, rowspan: 1.0, } } } /// The organized layout data for a GridLayout, after row/col determination: /// For each cell, stores col, colspan, row, rowspan pub type GridLayoutOrganizedData = SharedVector; impl GridLayoutOrganizedData { fn push_cell(&mut self, col: u16, colspan: u16, row: u16, rowspan: u16) { self.push(col); self.push(colspan); self.push(row); self.push(rowspan); } fn col_or_row_and_span( &self, cell_number: usize, orientation: Orientation, repeater_indices: &Slice, repeater_steps: &Slice, ) -> (u16, u16) { // For every cell, we have 4 entries, each at their own index // But we also need to take into account indirections for repeated items // Two-level indirection for repeated items: // jump_pos = (ri_start_cell - cell_nr_adj) * 4 // data_base = self[jump_pos] (base of this repeater's data) // stride = self[jump_pos + 1] (u16 entries per row = step * 4) // data_idx = data_base + row_in_rep * stride + col_in_rep * 4 let mut final_idx = 0; let mut cell_nr_adj = 0i32; // needs to be signed in case we start with an empty repeater let cell_number = cell_number as i32; // repeater_indices is a list of (start_cell, count) pairs for rep_idx in 0..(repeater_indices.len() / 2) { let ri_start_cell = repeater_indices[rep_idx * 2] as i32; if cell_number < ri_start_cell { break; } let ri_cell_count = repeater_indices[rep_idx * 2 + 1] as i32; let step = repeater_steps.get(rep_idx).copied().unwrap_or(1) as i32; let cells_in_repeater = ri_cell_count * step; if cells_in_repeater > 0 && cell_number >= ri_start_cell && cell_number < ri_start_cell + cells_in_repeater { let cell_in_rep = cell_number - ri_start_cell; let row_in_rep = cell_in_rep / step; let col_in_rep = cell_in_rep % step; let jump_pos = (ri_start_cell - cell_nr_adj) as usize * 4; let data_base = self[jump_pos] as usize; let stride = self[jump_pos + 1] as usize; final_idx = data_base + row_in_rep as usize * stride + col_in_rep as usize * 4; break; } // Each repeater occupies 1 jump cell in the static area but cells_in_repeater cells logically // Note: -1 is correct for an empty repeater (e.g. if false), which occupies 1 jump cell, for 0 real cells cell_nr_adj += cells_in_repeater - 1; } if final_idx == 0 { final_idx = ((cell_number - cell_nr_adj) * 4) as usize; } let offset = if orientation == Orientation::Horizontal { 0 } else { 2 }; (self[final_idx + offset], self[final_idx + offset + 1]) } fn max_value( &self, num_cells: usize, orientation: Orientation, repeater_indices: &Slice, repeater_steps: &Slice, ) -> usize { let mut max = 0; // This could be rewritten more efficiently to avoid a loop calling a loop, by keeping track of the repeaters we saw until now // Not sure it's worth the complexity though for idx in 0..num_cells { let (col_or_row, span) = self.col_or_row_and_span(idx, orientation, repeater_indices, repeater_steps); // Widen to usize: a cell with an out-of-range row/col is clamped to u16::MAX, so adding // the span here in u16 would overflow and under-size the layout vector. max = max.max(col_or_row as usize + span.max(1) as usize); } max } } /// Two-level indirection organized data generator for grid layouts with repeaters. /// Uses 2-level indirection: cache[cache[jump_pos] + ri * stride + col * 4] /// Each jump cell stores [data_base, stride, 0, 0] where stride = step * 4. /// /// Layout: [static_cells (4 u16 each)] [jump_cells (4 u16 each, 1 per repeater)] /// [row_data (rep_count * step * 4 u16)] ... (repeated for each repeater) struct OrganizedDataGenerator<'a> { // Input repeater_indices: &'a [u32], repeater_steps: &'a [u32], // An always increasing counter, the index of the cell being added counter: usize, // The u16 position in result for the next repeater's data section repeat_u16_offset: usize, // The index/2 in repeater_indices (i.e. which repeater we're looking at next) next_rep: usize, // The cell index in result for the next non-repeated item (each cell = 4 u16) current_offset: usize, // Output result: &'a mut GridLayoutOrganizedData, } impl<'a> OrganizedDataGenerator<'a> { fn new( repeater_indices: &'a [u32], repeater_steps: &'a [u32], static_cells: usize, num_repeaters: usize, total_repeated_cells_count: usize, result: &'a mut GridLayoutOrganizedData, ) -> Self { result.resize((static_cells + num_repeaters + total_repeated_cells_count) * 4, 0 as _); let repeat_u16_offset = (static_cells + num_repeaters) * 4; Self { repeater_indices, repeater_steps, counter: 0, repeat_u16_offset, next_rep: 0, current_offset: 0, result, } } fn add(&mut self, col: u16, colspan: u16, row: u16, rowspan: u16) { let res = self.result.make_mut_slice(); loop { if let Some(nr) = self.repeater_indices.get(self.next_rep * 2) { let nr = *nr as usize; let step = self.repeater_steps.get(self.next_rep).copied().unwrap_or(1) as usize; let rep_count = self.repeater_indices[self.next_rep * 2 + 1] as usize; if nr == self.counter { // First cell of this repeater let data_u16_start = self.repeat_u16_offset; let stride = step * 4; // Write jump cell: [data_base, stride, 0, 0] res[self.current_offset * 4] = data_u16_start as _; res[self.current_offset * 4 + 1] = stride as _; self.current_offset += 1; } if self.counter >= nr { let cells_in_repeater = rep_count * step; if self.counter - nr == cells_in_repeater { // Past the end of this repeater — advance past data self.repeat_u16_offset += cells_in_repeater * 4; self.next_rep += 1; continue; } // Write data at the position determined by row/col within repeater let cell_in_rep = self.counter - nr; let row_in_rep = cell_in_rep / step; let col_in_rep = cell_in_rep % step; let data_u16_start = self.repeat_u16_offset; let u16_pos = data_u16_start + row_in_rep * step * 4 + col_in_rep * 4; res[u16_pos] = col; res[u16_pos + 1] = colspan; res[u16_pos + 2] = row; res[u16_pos + 3] = rowspan; self.counter += 1; return; } } // Non-repeated cell res[self.current_offset * 4] = col; res[self.current_offset * 4 + 1] = colspan; res[self.current_offset * 4 + 2] = row; res[self.current_offset * 4 + 3] = rowspan; self.current_offset += 1; self.counter += 1; return; } } } /// Given the cells of a layout of a Dialog, re-order the buttons according to the platform /// This function assume that the `roles` contains the roles of the button which are the first cells in `input_data` pub fn organize_dialog_button_layout( input_data: Slice, dialog_button_roles: Slice, ) -> GridLayoutOrganizedData { let mut organized_data = GridLayoutOrganizedData::default(); organized_data.reserve(input_data.len() * 4); #[cfg(feature = "std")] fn is_kde() -> bool { // assume some Unix, check if XDG_CURRENT_DESKTOP starts with K std::env::var("XDG_CURRENT_DESKTOP") .ok() .and_then(|v| v.as_bytes().first().copied()) .is_some_and(|x| x.eq_ignore_ascii_case(&b'K')) } #[cfg(not(feature = "std"))] let is_kde = || true; let expected_order: &[DialogButtonRole] = match crate::detect_operating_system() { crate::items::OperatingSystemType::Windows => { &[ DialogButtonRole::Reset, DialogButtonRole::None, // spacer DialogButtonRole::Accept, DialogButtonRole::Action, DialogButtonRole::Reject, DialogButtonRole::Apply, DialogButtonRole::Help, ] } crate::items::OperatingSystemType::Macos | crate::items::OperatingSystemType::Ios => { &[ DialogButtonRole::Help, DialogButtonRole::Reset, DialogButtonRole::Apply, DialogButtonRole::Action, DialogButtonRole::None, // spacer DialogButtonRole::Reject, DialogButtonRole::Accept, ] } _ if is_kde() => { // KDE variant &[ DialogButtonRole::Help, DialogButtonRole::Reset, DialogButtonRole::None, // spacer DialogButtonRole::Action, DialogButtonRole::Accept, DialogButtonRole::Apply, DialogButtonRole::Reject, ] } _ => { // GNOME variant and fallback for WASM build &[ DialogButtonRole::Help, DialogButtonRole::Reset, DialogButtonRole::None, // spacer DialogButtonRole::Action, DialogButtonRole::Accept, DialogButtonRole::Apply, DialogButtonRole::Reject, ] } }; // Reorder the actual buttons according to expected_order let mut column_for_input: Vec = Vec::with_capacity(dialog_button_roles.len()); for role in expected_order.iter() { if role == &DialogButtonRole::None { column_for_input.push(usize::MAX); // empty column, ensure nothing will match continue; } for (idx, r) in dialog_button_roles.as_slice().iter().enumerate() { if *r == *role { column_for_input.push(idx); } } } for (input_index, cell) in input_data.as_slice().iter().enumerate() { let col = column_for_input.iter().position(|&x| x == input_index); if let Some(col) = col { organized_data.push_cell(col as _, cell.colspan as _, cell.row as _, cell.rowspan as _); } else { // This is used for the main window (which is the only cell which isn't a button) // Given lower_dialog_layout(), this will always be a single cell at 0,0 with a colspan of number_of_buttons organized_data.push_cell( cell.col as _, cell.colspan as _, cell.row as _, cell.rowspan as _, ); } } organized_data } // GridLayout-specific fn total_repeated_cells<'a>(repeater_indices: &'a [u32], repeater_steps: &'a [u32]) -> usize { repeater_indices .chunks(2) .enumerate() .map(|(i, chunk)| { let count = chunk.get(1).copied().unwrap_or(0) as usize; let step = repeater_steps.get(i).copied().unwrap_or(1) as usize; count * step }) .sum() } type Errors = Vec; pub fn organize_grid_layout( input_data: Slice, repeater_indices: Slice, repeater_steps: Slice, ) -> GridLayoutOrganizedData { let (organized_data, errors) = organize_grid_layout_impl(input_data, repeater_indices, repeater_steps); for error in errors { crate::debug_log!("Slint layout error: {}", error); } organized_data } // Implement "auto" behavior for row/col numbers (unless specified in the slint file). fn organize_grid_layout_impl( input_data: Slice, repeater_indices: Slice, repeater_steps: Slice, ) -> (GridLayoutOrganizedData, Errors) { let mut organized_data = GridLayoutOrganizedData::default(); // Cache size: static_cells * 4 + num_repeaters * 4 (jump cells) // + per repeater: rep_count * step * 4 (data) let num_repeaters = repeater_indices.len() / 2; let total_repeated_cells = total_repeated_cells(repeater_indices.as_slice(), repeater_steps.as_slice()); let static_cells = input_data.len() - total_repeated_cells; let mut generator = OrganizedDataGenerator::new( repeater_indices.as_slice(), repeater_steps.as_slice(), static_cells, num_repeaters, total_repeated_cells, &mut organized_data, ); let mut errors = Vec::new(); fn clamp_to_u16(value: f32, field_name: &str, errors: &mut Vec) -> u16 { if value < 0.0 { errors.push(format!("cell {field_name} {value} is negative, clamping to 0")); 0 } else if value > u16::MAX as f32 { errors .push(format!("cell {field_name} {value} is too large, clamping to {}", u16::MAX)); u16::MAX } else { value as u16 } } let mut row = 0; let mut col = 0; let mut first = true; for cell in input_data.as_slice().iter() { if cell.new_row && !first { row += 1; col = 0; } first = false; if cell.row != i_slint_common::ROW_COL_AUTO { let cell_row = clamp_to_u16(cell.row, "row", &mut errors); if row != cell_row { row = cell_row; col = 0; } } if cell.col != i_slint_common::ROW_COL_AUTO { col = clamp_to_u16(cell.col, "col", &mut errors); } let colspan = clamp_to_u16(cell.colspan, "colspan", &mut errors); let rowspan = clamp_to_u16(cell.rowspan, "rowspan", &mut errors); col = col.min(u16::MAX - colspan); // ensure col + colspan doesn't overflow generator.add(col, colspan, row, rowspan); col += colspan; } (organized_data, errors) } /// Layout cache generator for box layouts. /// The layout cache generator inserts the pos and size into the result array (which becomes the layout cache property), /// including the indirections for repeated items (so that the x,y,width,height properties for repeated items /// can point to indices known at compile time, those that contain the indirections) /// Example: for repeater_indices=[1,4] (meaning that item at index 1 is repeated 4 times), /// 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] /// i.e. pos1, width1, jump to idx 4, jump to idx 5, pos2, width2, pos3, width3, pos4, width4, pos5, width5 struct LayoutCacheGenerator<'a> { // Input repeater_indices: &'a [u32], // An always increasing counter, the index of the cell being added counter: usize, // The index/2 in result in which we should add the next repeated item repeat_offset: usize, // The index/2 in repeater_indices next_rep: usize, // The index/2 in result in which we should add the next non-repeated item current_offset: usize, // Output result: &'a mut SharedVector, } impl<'a> LayoutCacheGenerator<'a> { fn new(repeater_indices: &'a [u32], result: &'a mut SharedVector) -> Self { let total_repeated_cells: usize = repeater_indices .chunks(2) .map(|chunk| chunk.get(1).copied().unwrap_or(0) as usize) .sum(); assert!(result.len() >= total_repeated_cells * 2); let repeat_offset = result.len() / 2 - total_repeated_cells; Self { repeater_indices, counter: 0, repeat_offset, next_rep: 0, current_offset: 0, result } } fn add(&mut self, pos: Coord, size: Coord) { let res = self.result.make_mut_slice(); let o = loop { if let Some(nr) = self.repeater_indices.get(self.next_rep * 2) { let nr = *nr as usize; if nr == self.counter { // Write jump entry for o in 0..2 { res[self.current_offset * 2 + o] = (self.repeat_offset * 2 + o) as _; } self.current_offset += 1; } if self.counter >= nr { let rep_count = self.repeater_indices[self.next_rep * 2 + 1] as usize; if self.counter - nr == rep_count { self.repeat_offset += rep_count; self.next_rep += 1; continue; } let offset = self.repeat_offset + (self.counter - nr); break offset; } } self.current_offset += 1; break self.current_offset - 1; }; res[o * 2] = pos; res[o * 2 + 1] = size; self.counter += 1; } } /// Two-level indirection layout cache generator for grid layouts with repeaters. /// Uses 2-level indirection: cache[cache[jump] + ri * stride + child_offset] /// Each jump cell stores [data_base, stride] where stride = step * 2. struct GridLayoutCacheGenerator<'a> { // Input repeater_indices: &'a [u32], repeater_steps: &'a [u32], // An always increasing counter, the index of the cell being added counter: usize, // The f32 position in result for the next repeater's dynamic data section repeat_f32_offset: usize, // The index/2 in repeater_indices next_rep: usize, // The cell index (index/2) in result for the next non-repeated item current_offset: usize, // Output result: &'a mut SharedVector, } impl<'a> GridLayoutCacheGenerator<'a> { fn new( repeater_indices: &'a [u32], repeater_steps: &'a [u32], static_cells: usize, num_repeaters: usize, total_repeated_cells_count: usize, result: &'a mut SharedVector, ) -> Self { result.resize((static_cells + num_repeaters + total_repeated_cells_count) * 2, 0 as _); let repeat_f32_offset = (static_cells + num_repeaters) * 2; Self { repeater_indices, repeater_steps, counter: 0, repeat_f32_offset, next_rep: 0, current_offset: 0, result, } } fn add(&mut self, pos: Coord, size: Coord) { let res = self.result.make_mut_slice(); loop { if let Some(nr) = self.repeater_indices.get(self.next_rep * 2) { let nr = *nr as usize; let step = self.repeater_steps.get(self.next_rep).copied().unwrap_or(1) as usize; let rep_count = self.repeater_indices[self.next_rep * 2 + 1] as usize; if nr == self.counter { // First cell of this repeater let data_f32_start = self.repeat_f32_offset; let stride = step * 2; // Write 1 jump cell (2 f32): [data_base, stride] res[self.current_offset * 2] = data_f32_start as _; res[self.current_offset * 2 + 1] = stride as _; self.current_offset += 1; } if self.counter >= nr { let cells_in_repeater = rep_count * step; if self.counter - nr == cells_in_repeater { // Past the end of this repeater — advance past data self.repeat_f32_offset += cells_in_repeater * 2; self.next_rep += 1; continue; } // Write data at the position determined by row/col within repeater let cell_in_rep = self.counter - nr; let row_in_rep = cell_in_rep / step; let col_in_rep = cell_in_rep % step; let data_f32_start = self.repeat_f32_offset; let f32_pos = data_f32_start + row_in_rep * step * 2 + col_in_rep * 2; res[f32_pos] = pos; res[f32_pos + 1] = size; self.counter += 1; return; } } // Non-repeated cell res[self.current_offset * 2] = pos; res[self.current_offset * 2 + 1] = size; self.current_offset += 1; self.counter += 1; return; } } } /// return, an array which is of size `data.cells.len() * 2` which for each cell stores: /// pos (x or y), size (width or height) pub fn solve_grid_layout( data: &GridLayoutData, constraints: Slice, orientation: Orientation, repeater_indices: Slice, repeater_steps: Slice, ) -> SharedVector { let mut layout_data = grid_internal::to_layout_data( &data.organized_data, constraints, orientation, repeater_indices, repeater_steps, data.spacing, Some(data.size), ); if layout_data.is_empty() { return Default::default(); } grid_internal::layout_items( &mut layout_data, data.padding.begin, data.size - (data.padding.begin + data.padding.end), data.spacing, ); let mut result = SharedVector::::default(); let num_repeaters = repeater_indices.len() / 2; let total_repeated_cells = total_repeated_cells(repeater_indices.as_slice(), repeater_steps.as_slice()); let static_cells = constraints.len() - total_repeated_cells; let mut generator = GridLayoutCacheGenerator::new( repeater_indices.as_slice(), repeater_steps.as_slice(), static_cells, num_repeaters, total_repeated_cells, &mut result, ); for idx in 0..constraints.len() { let (col_or_row, span) = data.organized_data.col_or_row_and_span( idx, orientation, &repeater_indices, &repeater_steps, ); let cdata = &layout_data[col_or_row as usize]; let size = if span > 0 { let last_cell = &layout_data[col_or_row as usize + span as usize - 1]; last_cell.pos + last_cell.size - cdata.pos } else { 0 as Coord }; generator.add(cdata.pos, size); } result } pub fn grid_layout_info( organized_data: GridLayoutOrganizedData, // not & because the code generator doesn't support it in ExtraBuiltinFunctionCall constraints: Slice, repeater_indices: Slice, repeater_steps: Slice, spacing: Coord, padding: &Padding, orientation: Orientation, ) -> LayoutInfo { let layout_data = grid_internal::to_layout_data( &organized_data, constraints, orientation, repeater_indices, repeater_steps, spacing, None, ); if layout_data.is_empty() { let mut info = LayoutInfo::default(); info.min = padding.begin + padding.end; info.preferred = info.min; info.max = info.min; return info; } let spacing_w = spacing * (layout_data.len() - 1) as Coord + padding.begin + padding.end; let min = layout_data.iter().map(|data| data.min).sum::() + spacing_w; let max = layout_data.iter().map(|data| data.max).fold(spacing_w, Saturating::add); let preferred = layout_data.iter().map(|data| data.pref).sum::() + spacing_w; let stretch = layout_data.iter().map(|data| data.stretch).sum::(); LayoutInfo { min, max, min_percent: 0 as _, max_percent: 100 as _, preferred, stretch } } #[repr(C)] #[derive(Debug)] /// The BoxLayoutData is used to represent both a Horizontal and Vertical layout. /// The width/height x/y correspond to that of a horizontal layout. /// For vertical layout, they are inverted pub struct BoxLayoutData<'a> { pub size: Coord, pub spacing: Coord, pub padding: Padding, pub alignment: LayoutAlignment, pub cells: Slice<'a, LayoutItemInfo>, } /// Input for `solve_box_layout_ortho`. #[repr(C)] #[derive(Debug)] pub struct BoxLayoutOrthoData<'a> { pub size: Coord, pub padding: Padding, pub cross_axis_alignment: CrossAxisAlignment, pub cells: Slice<'a, LayoutItemInfo>, } #[repr(C)] #[derive(Debug)] /// The FlexboxLayoutData is used for a flex layout. pub struct FlexboxLayoutData<'a> { pub width: Coord, pub height: Coord, pub spacing_h: Coord, pub spacing_v: Coord, pub padding_h: Padding, pub padding_v: Padding, pub alignment: LayoutAlignment, pub direction: FlexboxLayoutDirection, pub cross_axis_line_alignment: LayoutAlignment, pub cross_axis_alignment: CrossAxisAlignment, pub flex_wrap: FlexboxLayoutWrap, /// Horizontal constraints (width) for each cell pub cells_h: Slice<'a, LayoutItemInfo>, /// Vertical constraints (height) for each cell pub cells_v: Slice<'a, LayoutItemInfo>, /// Per-item flex properties, one per cell (axis-independent) pub flex_props: Slice<'a, FlexItemProps>, } #[repr(C)] #[derive(Debug, Clone, Default)] /// The information about a single item in a box or grid layout pub struct LayoutItemInfo { pub constraint: LayoutInfo, /// Per-item cross-axis alignment override for box layouts /// (`Auto` = use the container's `cross-axis-alignment`) pub cross_axis_self_alignment: CrossAxisAlignment, /// Visual ordering of box layout cells (lower values appear first, default 0). /// Only [`solve_box_layout`] reads it; the cross-axis solve and both /// layout-info functions ignore it. A FlexboxLayout carries it in /// [`FlexItemProps`] instead, and a GridLayout orders its cells with /// `row`/`col`, so both leave this at 0. pub layout_order: i32, } /// The per-item flex properties of a FlexboxLayout cell. /// /// A cell's size constraint is per-axis (`cells_h`/`cells_v`), but these /// properties apply to the item as a whole, so they live in a single parallel /// array instead of being duplicated into both axes. #[repr(C)] #[derive(Debug, Clone, Copy, Default)] pub struct FlexItemProps { /// Per-item cross-axis alignment override (Auto = use container's cross-axis-alignment) pub cross_axis_self_alignment: CrossAxisAlignment, /// Visual ordering of flex items (lower values appear first, default 0) pub layout_order: i32, } #[repr(C)] #[derive(Debug, Clone, Default)] /// One flexbox cell's full layout info, read from a single item instance via the /// item vtable. The bulk cell data in [`FlexboxLayoutData`] keeps the constraint /// and [`FlexItemProps`] in separate parallel arrays; this bundles both for the /// per-instance query. pub struct FlexboxLayoutItemInfo { pub constraint: LayoutInfo, pub props: FlexItemProps, } impl From for FlexboxLayoutItemInfo { fn from(info: LayoutItemInfo) -> Self { Self { constraint: info.constraint, props: FlexItemProps { cross_axis_self_alignment: info.cross_axis_self_alignment, layout_order: info.layout_order, }, } } } /// Solve a BoxLayout pub fn solve_box_layout(data: &BoxLayoutData, repeater_indices: Slice) -> SharedVector { let mut result = SharedVector::::default(); // One element results into two coordinates in the result vector. 1. Position, 2. Size result.resize(data.cells.len() * 2 + repeater_indices.len(), 0 as _); if data.cells.is_empty() { return result; } let size_without_padding = data.size - data.padding.begin - data.padding.end; let num_spacings = (data.cells.len() - 1) as Coord; let spacings = data.spacing * num_spacings; let content_size = size_without_padding - spacings; // The size the cells can occupy without going outside of the layout let mut layout_data: Vec<_> = data .cells .iter() .map(|c| { let min = c.constraint.min.max(c.constraint.min_percent * content_size / 100 as Coord); let max = c.constraint.max.min(c.constraint.max_percent * content_size / 100 as Coord); grid_internal::LayoutData { min, max, pref: c.constraint.preferred.min(max).max(min), stretch: c.constraint.stretch, ..Default::default() } }) .collect(); // `layout-order` reorders the cells like the CSS `order` property. Solve on // the reordered list; the results are written back in declaration order // below, as a cell's cache slot is fixed by its declaration index. let order_map: Vec = if data.cells.iter().any(|c| c.layout_order != 0) { let mut indices: Vec = (0..layout_data.len()).collect(); // sort_by_key is a stable sort, so equal orders keep declaration order indices.sort_by_key(|&i| data.cells[i].layout_order); layout_data = indices.iter().map(|&i| layout_data[i].clone()).collect(); indices } else { Vec::new() }; let pref_size: Coord = layout_data.iter().map(|it| it.pref).sum(); let align = match data.alignment { LayoutAlignment::Stretch => { grid_internal::layout_items( &mut layout_data, data.padding.begin, size_without_padding, data.spacing, ); None } _ if size_without_padding <= pref_size + spacings => { grid_internal::layout_items( &mut layout_data, data.padding.begin, size_without_padding, data.spacing, ); None } LayoutAlignment::Center => Some(( data.padding.begin + (size_without_padding - pref_size - spacings) / 2 as Coord, data.spacing, )), LayoutAlignment::Start => Some((data.padding.begin, data.spacing)), LayoutAlignment::End => { Some((data.padding.begin + (size_without_padding - pref_size - spacings), data.spacing)) } LayoutAlignment::SpaceBetween => { Some((data.padding.begin, (size_without_padding - pref_size) / num_spacings)) } LayoutAlignment::SpaceAround => { let spacing = (size_without_padding - pref_size) / (num_spacings + 1 as Coord); Some((data.padding.begin + spacing / 2 as Coord, spacing)) } LayoutAlignment::SpaceEvenly => { let spacing = (size_without_padding - pref_size) / (num_spacings + 2 as Coord); Some((data.padding.begin + spacing, spacing)) } }; if let Some((mut pos, spacing)) = align { for it in &mut layout_data { it.pos = pos; it.size = it.pref; pos += spacing + it.size; } } let mut generator = LayoutCacheGenerator::new(&repeater_indices, &mut result); if order_map.is_empty() { for layout in layout_data.iter() { generator.add(layout.pos, layout.size); } } else { let mut geom = alloc::vec![(0 as Coord, 0 as Coord); layout_data.len()]; for (sorted_idx, &declared_idx) in order_map.iter().enumerate() { let layout = &layout_data[sorted_idx]; geom[declared_idx] = (layout.pos, layout.size); } for (pos, size) in geom { generator.add(pos, size); } } result } /// Resolves the effective alignment of an item on the cross axis: `auto` on the /// item uses the container's alignment, and `auto` on the container is `stretch`. fn resolve_cross_axis_alignment( self_alignment: CrossAxisAlignment, container_alignment: CrossAxisAlignment, ) -> CrossAxisAlignment { let alignment = match self_alignment { CrossAxisAlignment::Auto => container_alignment, other => other, }; match alignment { CrossAxisAlignment::Auto => CrossAxisAlignment::Stretch, other => other, } } /// Cross-axis solve: returns (position, size) per cell, like [`solve_box_layout`]. pub fn solve_box_layout_ortho( data: &BoxLayoutOrthoData, repeater_indices: Slice, ) -> SharedVector { let mut result = SharedVector::::default(); result.resize(data.cells.len() * 2 + repeater_indices.len(), 0 as _); if data.cells.is_empty() { return result; } let size_without_padding = data.size - data.padding.begin - data.padding.end; let mut generator = LayoutCacheGenerator::new(&repeater_indices, &mut result); for c in data.cells.iter() { let alignment = resolve_cross_axis_alignment(c.cross_axis_self_alignment, data.cross_axis_alignment); let min = c.constraint.min.max(c.constraint.min_percent * size_without_padding / 100 as Coord); let max = c.constraint.max.min(c.constraint.max_percent * size_without_padding / 100 as Coord); let size = match alignment { CrossAxisAlignment::Stretch => size_without_padding, _ => c.constraint.preferred, } .min(max) .max(min); let pos = match alignment { CrossAxisAlignment::Auto | CrossAxisAlignment::Stretch | CrossAxisAlignment::Start => { data.padding.begin } CrossAxisAlignment::End => data.padding.begin + size_without_padding - size, CrossAxisAlignment::Center => { data.padding.begin + (size_without_padding - size) / 2 as Coord } }; generator.add(pos, size); } result } /// Return the LayoutInfo for a BoxLayout with the given cells. pub fn box_layout_info( cells: Slice, spacing: Coord, padding: &Padding, alignment: LayoutAlignment, ) -> LayoutInfo { let count = cells.len(); let is_stretch = alignment == LayoutAlignment::Stretch; if count < 1 { let mut info = LayoutInfo::default(); info.min = padding.begin + padding.end; info.preferred = info.min; if is_stretch { info.max = info.min; } return info; }; let extra_w = padding.begin + padding.end + spacing * (count - 1) as Coord; let min = cells.iter().map(|c| c.constraint.min).sum::() + extra_w; // Minimum size of the complete layout let max = if is_stretch { (cells.iter().map(|c| c.constraint.max).fold(extra_w, Saturating::add)).max(min) } else { Coord::MAX }; // Maximum size of the complete layout let preferred = cells.iter().map(|c| c.constraint.preferred_bounded()).sum::() + extra_w; let stretch = cells.iter().map(|c| c.constraint.stretch).sum::(); LayoutInfo { min, max, min_percent: 0 as _, max_percent: 100 as _, preferred, stretch } } pub fn box_layout_info_ortho(cells: Slice, padding: &Padding) -> LayoutInfo { let extra_w = padding.begin + padding.end; let mut fold = cells.iter().fold(LayoutInfo { stretch: f32::MAX, ..Default::default() }, |a, b| { a.merge(&b.constraint) }); fold.max = fold.max.max(fold.min); fold.preferred = fold.preferred.clamp(fold.min, fold.max); fold.min += extra_w; fold.max = Saturating::add(fold.max, extra_w); fold.preferred += extra_w; // Don't propagate children's percentage constraints to the parent. // Percentages are relative to the layout's own size, not the grandparent's. fold.min_percent = 0 as _; fold.max_percent = 100 as _; fold } /// Helper module for taffy-based flexbox layout mod flexbox_taffy { use super::{ Coord, CrossAxisAlignment, FlexItemProps, FlexboxLayoutWrap as SlintFlexboxLayoutWrap, LayoutAlignment, LayoutInfo, LayoutItemInfo, Padding, Slice, resolve_cross_axis_alignment, }; use alloc::vec::Vec; pub use taffy::prelude::FlexDirection as TaffyFlexDirection; use taffy::prelude::*; /// Start/End map to FlexStart/FlexEnd to respect the flex direction (including reverse); /// AlignContent::Start/End would ignore the direction and always use the writing mode. fn to_align_content(alignment: LayoutAlignment) -> AlignContent { match alignment { LayoutAlignment::Stretch => AlignContent::Stretch, LayoutAlignment::Start => AlignContent::FlexStart, LayoutAlignment::End => AlignContent::FlexEnd, LayoutAlignment::Center => AlignContent::Center, LayoutAlignment::SpaceBetween => AlignContent::SpaceBetween, LayoutAlignment::SpaceAround => AlignContent::SpaceAround, LayoutAlignment::SpaceEvenly => AlignContent::SpaceEvenly, } } /// How an item's cross-axis size is decided when building the taffy tree. #[derive(Copy, Clone, PartialEq, Eq)] pub enum CrossAxisSizing { /// The item's own preferred cross size, except where it stretches to /// fill its flex line. Preferred, /// `auto`, so the measure callback decides. A column's flex basis is /// `auto` too, so a height-for-width item is measured at the width /// taffy assigns it rather than at its preferred width. FromMeasure, /// `auto` for every item, so one whose measure reports nothing falls /// back to its min constraint. This is how a layout's minimum cross /// size is measured. Minimum, } /// Parameters for FlexboxTaffyBuilder::new pub struct FlexboxLayoutParams<'a> { pub cells_h: &'a Slice<'a, LayoutItemInfo>, pub cells_v: &'a Slice<'a, LayoutItemInfo>, pub flex_props: &'a Slice<'a, FlexItemProps>, pub spacing_h: Coord, pub spacing_v: Coord, pub padding_h: &'a Padding, pub padding_v: &'a Padding, pub alignment: LayoutAlignment, pub cross_axis_line_alignment: LayoutAlignment, pub cross_axis_alignment: CrossAxisAlignment, pub flex_wrap: SlintFlexboxLayoutWrap, /// `flex-shrink` for every item. `1.` while wrapping, `0.` for the /// non-wrapping re-solve in `solve_flexbox_layout_with_measure`, whose /// point is to let the content overflow rather than be compressed. pub flex_shrink: f32, pub flex_direction: TaffyFlexDirection, pub container_width: Option, pub container_height: Option, pub cross_axis_sizing: CrossAxisSizing, } /// Build a taffy tree from Slint layout constraints. /// The NodeContext (usize) stores the original child index for measure callbacks. pub struct FlexboxTaffyBuilder { pub taffy: TaffyTree, pub children: Vec, pub container: NodeId, /// Maps taffy child position -> original cell index (empty if no reordering needed) pub order_map: Vec, } impl FlexboxTaffyBuilder { /// Create a new flexbox layout tree from item constraints pub fn new(params: FlexboxLayoutParams) -> Self { let mut taffy = TaffyTree::::new(); // The container's content box on each axis (the outer size minus that // axis' padding), against which percentage constraints resolve. // `None` when that axis has no finite size: either unknown (an // auto-sized container in the info path) or the `Coord::MAX` // "unbounded" sentinel the info path passes for a no-wrap main axis. // A percentage then has no basis and only the absolute constraint // applies — resolving against `MAX` would overflow the `_percent * s` // product (i32 build) or yield infinity (f32). let content_box = |size: Option, pad: &Padding| -> Option { size.filter(|s| *s < Coord::MAX).map(|s| (s - pad.begin - pad.end).max(0 as Coord)) }; let content_w = content_box(params.container_width, params.padding_h); let content_h = content_box(params.container_height, params.padding_v); // Cross-axis upper bound: an item is never bigger than the // container's content box across the flex direction, so a // height-for-width item (e.g. wrapped `Text`) measures against a real // width, and the container can shrink to the size it reports as its // minimum. let (column_cross_cap, row_cross_cap) = match params.flex_direction { TaffyFlexDirection::Column | TaffyFlexDirection::ColumnReverse => (content_w, None), TaffyFlexDirection::Row | TaffyFlexDirection::RowReverse => (None, content_h), }; // Resolve a percentage min/max against the container's content box and // fold it into the absolute constraint, matching how the box layouts // handle percentages (see `solve_box_layout`). Applied only when the // percentage is non-default and the content size is known, so an item // without a percentage size keeps its plain min/max unchanged. let eff_min = |c: &LayoutInfo, content: Option| -> Coord { match content { Some(s) if c.min_percent > 0 as Coord => { c.min.max(c.min_percent * s / 100 as Coord) } _ => c.min, } }; let eff_max = |c: &LayoutInfo, content: Option| -> Coord { match content { Some(s) if c.max_percent < 100 as Coord => { c.max.min(c.max_percent * s / 100 as Coord) } _ => c.max, } }; // Main-axis growth follows the box layouts: items grow only under // `alignment: stretch`, weighted by the main axis' `*-stretch` // factor. When every factor is 0 (e.g. a row of Buttons, whose // styles set `horizontal-stretch: 0`), fall back to weight 1 so // the line still fills, as it would in a HorizontalLayout. The // all-zero test is container-wide since taffy decides the lines. // The parity has known limits: taffy applies the factors per line // and never hands space to grow-0 items, so a line whose items all // have factor 0 next to a stretchy line won't grow, and space a // max-capped item cannot take stays free instead of going to its // stretch-0 siblings (a box layout redistributes both). let main_cells = match params.flex_direction { TaffyFlexDirection::Row | TaffyFlexDirection::RowReverse => params.cells_h, TaffyFlexDirection::Column | TaffyFlexDirection::ColumnReverse => params.cells_v, }; let any_stretch = main_cells.iter().any(|c| c.constraint.stretch > 0.); // Create child nodes from Slint constraints let mut children: Vec = params .cells_h .iter() .enumerate() .map(|(idx, cell_h)| { let cell_v = params.cells_v.get(idx); let flex = params.flex_props.get(idx).cloned().unwrap_or_default(); let h_constraint = &cell_h.constraint; let v_constraint = cell_v.map(|c| &c.constraint); // Use preferred_bounded() which clamps preferred to min/max bounds let preferred_width = h_constraint.preferred_bounded(); let preferred_height = v_constraint.map(|vc| vc.preferred_bounded()).unwrap_or(0 as Coord); // The basis is the preferred size of the main axis, except // where `auto` defers to the measure callback (below) let flex_basis = match params.flex_direction { TaffyFlexDirection::Row | TaffyFlexDirection::RowReverse => { Dimension::length(preferred_width as _) } // For a column the main axis is the height, so pinning the // basis to `preferred_height` would stop taffy from ever // consulting the measure callback. `auto` lets it size the // item from its content at the width it actually assigns — // `preferred_height` was measured at the container width, // which is too wide for an item that does not stretch. TaffyFlexDirection::Column | TaffyFlexDirection::ColumnReverse if params.cross_axis_sizing == CrossAxisSizing::FromMeasure => { Dimension::auto() } TaffyFlexDirection::Column | TaffyFlexDirection::ColumnReverse => { Dimension::length(preferred_height as _) } }; let max_width = eff_max(h_constraint, content_w) .min(column_cross_cap.unwrap_or(Coord::MAX)); let max_height = v_constraint .map_or(Coord::MAX, |vc| eff_max(vc, content_h)) .min(row_cross_cap.unwrap_or(Coord::MAX)); let max_width_dim = if max_width < Coord::MAX { Dimension::length(max_width as _) } else { Dimension::auto() }; // A stretching item gets `auto` on the cross axis, so it sizes // to its flex *line's* cross size (the per-column width when // wrapped), not the whole container. A per-item // `cross-axis-self-alignment` overrides the container's // alignment. `auto` is also what lets the measure callback // decide the cross size, and what lets the minimum pass reach // the item's min constraint. let stretches = resolve_cross_axis_alignment( flex.cross_axis_self_alignment, params.cross_axis_alignment, ) == CrossAxisAlignment::Stretch; let cross_auto = stretches || params.cross_axis_sizing != CrossAxisSizing::Preferred; let definite_cross = |preferred: Coord| { if cross_auto || preferred <= 0 as Coord { Dimension::auto() } else { Dimension::length(preferred as _) } }; taffy .new_leaf_with_context( Style { flex_basis, size: Size { width: match params.flex_direction { TaffyFlexDirection::Column | TaffyFlexDirection::ColumnReverse => { definite_cross(preferred_width) } _ => Dimension::auto(), }, height: match params.flex_direction { TaffyFlexDirection::Row | TaffyFlexDirection::RowReverse => { definite_cross(preferred_height) } _ => Dimension::auto(), }, }, min_size: Size { width: Dimension::length(eff_min(h_constraint, content_w) as _), height: Dimension::length( v_constraint .map(|vc| eff_min(vc, content_h) as f32) .unwrap_or(0.0), ), }, max_size: Size { width: max_width_dim, height: if max_height < Coord::MAX { Dimension::length(max_height as _) } else { Dimension::auto() }, }, flex_grow: if params.alignment == LayoutAlignment::Stretch { if any_stretch { main_cells.get(idx).map_or(0., |c| c.constraint.stretch) } else { 1. } } else { 0. }, // Under `wrap` this is 1: a line with two or // more items never has negative free space (it // would have wrapped), so shrinking only applies // to a single item alone on its line — and that // item must go down to its min whatever its // stretch factor, as it would in a box layout. // The non-wrapping re-solve passes 0, where a // line does have negative free space. flex_shrink: params.flex_shrink, align_self: match flex.cross_axis_self_alignment { CrossAxisAlignment::Auto => None, CrossAxisAlignment::Stretch => Some(AlignSelf::Stretch), CrossAxisAlignment::Start => Some(AlignSelf::FlexStart), CrossAxisAlignment::End => Some(AlignSelf::FlexEnd), CrossAxisAlignment::Center => Some(AlignSelf::Center), }, ..Default::default() }, idx, ) .unwrap() // cannot fail }) .collect(); // Sort children by CSS `order` property if any item has a non-zero order. // Build a mapping from sorted position -> original index. let has_order = params.flex_props.iter().any(|f| f.layout_order != 0); let order_map: Vec = if has_order { let mut indices: Vec = (0..children.len()).collect(); // sort_by_key is a stable sort, as required by CSS indices.sort_by_key(|&i| params.flex_props.get(i).map_or(0, |f| f.layout_order)); let sorted_children: Vec = indices.iter().map(|&i| children[i]).collect(); children = sorted_children; indices } else { Vec::new() }; // Create container node let container = taffy .new_with_children( Style { display: Display::Flex, flex_direction: params.flex_direction, flex_wrap: match params.flex_wrap { SlintFlexboxLayoutWrap::Wrap => FlexWrap::Wrap, SlintFlexboxLayoutWrap::NoWrap => FlexWrap::NoWrap, SlintFlexboxLayoutWrap::WrapReverse => FlexWrap::WrapReverse, }, justify_content: Some(to_align_content(params.alignment)), align_items: Some(match params.cross_axis_alignment { CrossAxisAlignment::Auto | CrossAxisAlignment::Stretch => { AlignItems::Stretch } CrossAxisAlignment::Start => AlignItems::FlexStart, CrossAxisAlignment::End => AlignItems::FlexEnd, CrossAxisAlignment::Center => AlignItems::Center, }), align_content: Some(to_align_content(params.cross_axis_line_alignment)), gap: Size { width: LengthPercentage::length(params.spacing_h as _), height: LengthPercentage::length(params.spacing_v as _), }, padding: Rect { left: LengthPercentage::length(params.padding_h.begin as _), right: LengthPercentage::length(params.padding_h.end as _), top: LengthPercentage::length(params.padding_v.begin as _), bottom: LengthPercentage::length(params.padding_v.end as _), }, size: Size { width: params .container_width .map(|w| Dimension::length(w as _)) .unwrap_or(Dimension::auto()), height: params .container_height .map(|h| Dimension::length(h as _)) .unwrap_or(Dimension::auto()), }, ..Default::default() }, &children, ) .unwrap(); // cannot fail Self { taffy, children, container, order_map } } /// Compute the layout with the given available space. /// /// The `measure` callback is called by taffy for leaf nodes whose size /// it takes from their content (height-for-width). /// It receives `(child_index, known_width, known_height)` where `known_width` /// / `known_height` are `Some` if taffy has already determined that dimension, /// and returns `(width, height)`. pub fn compute_layout( &mut self, available_width: Coord, available_height: Coord, measure: &mut dyn FnMut(usize, Option, Option) -> (Coord, Coord), ) { let available_space = taffy::prelude::Size { width: if available_width < Coord::MAX { AvailableSpace::Definite(available_width as _) } else { AvailableSpace::MaxContent }, height: if available_height < Coord::MAX { AvailableSpace::Definite(available_height as _) } else { AvailableSpace::MaxContent }, }; self.taffy .compute_layout_with_measure( self.container, available_space, |known_dimensions, _available_space, _node_id, node_context, _style| { // Only the container node has no context. let Some(&mut child_index) = node_context else { return taffy::prelude::Size::ZERO; }; let known_w = known_dimensions.width.map(|w| w as Coord); let known_h = known_dimensions.height.map(|h| h as Coord); let (w, h) = measure(child_index, known_w, known_h); taffy::prelude::Size { width: w as f32, height: h as f32 } }, ) .unwrap_or_else(|e| { crate::debug_log!("FlexboxLayout computation error: {}", e); }); } /// Get the computed container size pub fn container_size(&self) -> (Coord, Coord) { let layout = self.taffy.layout(self.container).unwrap(); (layout.size.width as Coord, layout.size.height as Coord) } /// Get the geometry for a specific child pub fn child_geometry(&self, idx: usize) -> (Coord, Coord, Coord, Coord) { let layout = self.taffy.layout(self.children[idx]).unwrap(); ( layout.location.x as Coord, layout.location.y as Coord, layout.size.width as Coord, layout.size.height as Coord, ) } /// Map a taffy child index to the original cell index (accounting for `order` sorting). pub fn original_index(&self, taffy_idx: usize) -> usize { if self.order_map.is_empty() { taffy_idx } else { self.order_map[taffy_idx] } } } } /// A cache generator for FlexboxLayout that handles 4 values per item (x, y, width, height) struct FlexboxLayoutCacheGenerator<'a> { // Input repeater_indices: &'a [u32], // An always increasing counter, the index of the cell being added counter: usize, // The index/4 in result in which we should add the next repeated item repeat_offset: usize, // The index/4 in repeater_indices next_rep: usize, // The index/4 in result in which we should add the next non-repeated item current_offset: usize, // Output result: &'a mut SharedVector, } impl<'a> FlexboxLayoutCacheGenerator<'a> { fn new(repeater_indices: &'a [u32], result: &'a mut SharedVector) -> Self { // Calculate total repeated cells (count for each repeater) let total_repeated_cells: usize = repeater_indices .chunks(2) .map(|chunk| chunk.get(1).copied().unwrap_or(0) as usize) .sum(); assert!(result.len() >= total_repeated_cells * 4); let repeat_offset = result.len() / 4 - total_repeated_cells; Self { repeater_indices, counter: 0, repeat_offset, next_rep: 0, current_offset: 0, result } } fn add(&mut self, x: Coord, y: Coord, w: Coord, h: Coord) { let res = self.result.make_mut_slice(); let o = loop { if let Some(nr) = self.repeater_indices.get(self.next_rep * 2) { let nr = *nr as usize; if nr == self.counter { // Write jump entries for repeater start // Store the base offset (index into the repeated data region) res[self.current_offset * 4] = (self.repeat_offset * 4) as Coord; res[self.current_offset * 4 + 1] = (self.repeat_offset * 4 + 1) as Coord; res[self.current_offset * 4 + 2] = (self.repeat_offset * 4 + 2) as Coord; res[self.current_offset * 4 + 3] = (self.repeat_offset * 4 + 3) as Coord; self.current_offset += 1; } if self.counter >= nr { let rep_count = self.repeater_indices[self.next_rep * 2 + 1] as usize; if self.counter - nr == rep_count { // Advance repeat_offset past this repeater's data before moving to next self.repeat_offset += rep_count; self.next_rep += 1; continue; } // Calculate offset into repeated data let cell_in_rep = self.counter - nr; let offset = self.repeat_offset + cell_in_rep; break offset; } } self.current_offset += 1; break self.current_offset - 1; }; res[o * 4] = x; res[o * 4 + 1] = y; res[o * 4 + 2] = w; res[o * 4 + 3] = h; self.counter += 1; } } /// Measure callback for height-for-width items in a FlexboxLayout. /// /// Called by taffy during the flex solve for items that need dynamic sizing. /// Receives `(child_index, width, height)` and returns `(width, height)`. /// /// `resolve_measure_defaults` answers a query whose height taffy has already /// settled, so the callback is only asked for a height taffy still needs. /// `height` is therefore the cell's preferred height, or 0 past the last cell. /// Hand it back for a cell whose height does not depend on its width; it is /// not a constraint. /// `width` is the one taffy assigned, or the cell's preferred width when it /// has assigned none. /// Return it unchanged. /// Taffy can use a returned width when it has settled the height instead, /// but `resolve_measure_defaults` answers that query without asking. /// /// The result must be a self-consistent pair, each dimension measured at the /// other: taffy caches it, and answers a later query for one dimension with /// the half of the pair it kept. /// /// `None` means the caller has no callback of its own, so a dimension taffy /// has not assigned falls back to the cell's preferred size. /// Taffy itself is always given a callback, so a forgotten one is a compile /// error rather than a silently zero-sized item. pub type FlexboxMeasureFn<'a> = Option<&'a mut dyn FnMut(usize, Coord, Coord) -> (Coord, Coord)>; /// The measure that reports nothing, so an item taffy sizes from its content /// falls back to its min constraint. /// It is taffy-facing: it does not go through /// [`resolve_measure_defaults`], which would resolve the unknown dimensions to /// the preferred size only to have them thrown away. fn zero_measure(_: usize, _: Option, _: Option) -> (Coord, Coord) { (0 as Coord, 0 as Coord) } /// Adapt a [`FlexboxMeasureFn`] to the taffy-facing closure: resolve the /// dimensions taffy did not supply to the cell's preferred size, so the /// callback always receives concrete ones. /// /// It answers two kinds of query itself, returning the pre-resolved pair: /// /// - one whose height taffy has already settled, /// - any query at all when there is no callback. fn resolve_measure_defaults<'a, 'm: 'a>( cells_h: &'a [LayoutItemInfo], cells_v: &'a [LayoutItemInfo], mut measure: FlexboxMeasureFn<'m>, ) -> impl FnMut(usize, Option, Option) -> (Coord, Coord) + 'a { move |index, known_w, known_h| { let w = known_w.unwrap_or_else(|| { cells_h.get(index).map_or(0 as Coord, |c| c.constraint.preferred_bounded()) }); let h = known_h.unwrap_or_else(|| { cells_v.get(index).map_or(0 as Coord, |c| c.constraint.preferred_bounded()) }); // Answering here rather than measuring saves the work. // Taffy keeps the height it settled and discards the one a callback // returns, so the measurement (a text shaping, for a wrapped `Text`) // would be thrown away. // The callback returns the width unchanged, so the pair is the same // either way. match (known_h, measure.as_mut()) { (None, Some(measure)) => measure(index, w, h), _ => (w, h), } } } pub fn solve_flexbox_layout( data: &FlexboxLayoutData, repeater_indices: Slice, ) -> SharedVector { solve_flexbox_layout_with_measure(data, repeater_indices, None) } /// Solve a FlexboxLayout using Taffy /// Returns: [x1, y1, w1, h1, x2, y2, w2, h2, ...] for each item pub fn solve_flexbox_layout_with_measure( data: &FlexboxLayoutData, repeater_indices: Slice, measure: FlexboxMeasureFn<'_>, ) -> SharedVector { // 4 values per item: x, y, width, height let mut result = SharedVector::::default(); result.resize(data.cells_h.len() * 4 + repeater_indices.len() * 2, 0 as _); if data.cells_h.is_empty() { return result; } let taffy_direction = match data.direction { FlexboxLayoutDirection::Row => flexbox_taffy::TaffyFlexDirection::Row, FlexboxLayoutDirection::RowReverse => flexbox_taffy::TaffyFlexDirection::RowReverse, FlexboxLayoutDirection::Column => flexbox_taffy::TaffyFlexDirection::Column, FlexboxLayoutDirection::ColumnReverse => flexbox_taffy::TaffyFlexDirection::ColumnReverse, }; let (container_width, container_height) = ( if data.width > 0 as Coord { Some(data.width) } else { None }, if data.height > 0 as Coord { Some(data.height) } else { None }, ); let use_measure = measure.is_some(); let build = |flex_wrap, flex_shrink| { flexbox_taffy::FlexboxTaffyBuilder::new(flexbox_taffy::FlexboxLayoutParams { cells_h: &data.cells_h, cells_v: &data.cells_v, flex_props: &data.flex_props, spacing_h: data.spacing_h, spacing_v: data.spacing_v, padding_h: &data.padding_h, padding_v: &data.padding_v, alignment: data.alignment, cross_axis_line_alignment: data.cross_axis_line_alignment, cross_axis_alignment: data.cross_axis_alignment, flex_wrap, flex_shrink, flex_direction: taffy_direction, container_width, container_height, cross_axis_sizing: if use_measure { flexbox_taffy::CrossAxisSizing::FromMeasure } else { flexbox_taffy::CrossAxisSizing::Preferred }, }) }; let mut builder = build(data.flex_wrap, 1.); let (available_width, available_height) = match data.direction { FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => { (data.width, Coord::MAX) } FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => { (Coord::MAX, data.height) } }; // A `None` callback is safe here: the compiler omits one only for layouts // without height-for-width cells, whose pre-computed height is correct for // whatever width taffy assigns. let mut measure = resolve_measure_defaults(&data.cells_h, &data.cells_v, measure); builder.compute_layout(available_width, available_height, &mut measure); // A column flex wraps by height, so its columns can be wider than the // width it was given: one column's when its height is not settled (see // `flexbox_layout_info_cross_axis`). Never overflow sideways into a // sibling: solve without wrapping instead, and let the content overflow // downward, like a wrapped Text given too little height. // // `WrapReverse` is left alone. It anchors its lines at the cross end, which // `NoWrap` does not, so re-solving would move the content to the other side; // and the container's own height still drives the wrapping, so an unbounded // available height does not stop it either. Such a flex keeps wrapping past // its width. let is_column = matches!( data.direction, FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse ); if is_column && data.flex_wrap == FlexboxLayoutWrap::Wrap && data.width > 0 as Coord { // taffy computes in `f32`, so ignore an overflow below half a pixel. // Integer coordinates are exact and need no tolerance. #[cfg(not(slint_int_coord))] const OVERFLOW_TOLERANCE: Coord = 0.5; #[cfg(slint_int_coord)] const OVERFLOW_TOLERANCE: Coord = 0; let (left, right) = (data.padding_h.begin, data.width - data.padding_h.end); // Taffy indices, not original cell ones: `order` may have sorted them. // Asking whether *any* child overflows does not care about the order. let overflows = (0..data.cells_h.len()).any(|idx| { let (x, _, w, _) = builder.child_geometry(idx); x < left - OVERFLOW_TOLERANCE || x + w > right + OVERFLOW_TOLERANCE }); if overflows { // A second full solve, deliberately, and a common one: an // unsettled-height column flex is given one column's width, so any // wrapping at all overflows it. `cross-axis-line-alignment` // places the lines, so a single line wider than the flex lands at a // negative `x` under `center` or `end`, and clamping that one line // back is not enough for the multi-line case this exists for: // taffy has to lay the items out again without wrapping. // `flexbox_column_wrap_line_alignment.slint` covers both. // // Shrink only where the first solve did: items that fit one column // keep the shrinking `wrap` gives them, while content that needed // more than one column is meant to overflow rather than be // compressed into the height that made it wrap // (`flexbox_column_wrap_shrink.slint`). A lone item never wrapped, // however far it overflows. // `cells_h` is the array the children were built from, so it is // the child count; `cells_v` is its main-axis twin, one entry per // cell. The sum uses the cells' preferred sizes, which is what // taffy wraps on too, though a height-for-width cell it measured // may end up a little taller: close enough to tell one column from // several, which is all this decides. let one_column = data.cells_h.len() < 2 || flexbox_layout_unwrapped_main( Slice::from_slice(data.cells_v.as_slice()), data.spacing_v, &data.padding_v, ) <= data.height; builder = build(FlexboxLayoutWrap::NoWrap, if one_column { 1. } else { 0. }); builder.compute_layout(available_width, available_height, &mut measure); } } // Extract results using the cache generator to handle repeaters. // If `order` sorting was applied, we need to collect results by original index first, // because the cache generator expects items in their original declaration order. if builder.order_map.is_empty() { let mut generator = FlexboxLayoutCacheGenerator::new(&repeater_indices, &mut result); for idx in 0..data.cells_h.len() { let (x, y, w, h) = builder.child_geometry(idx); generator.add(x, y, w, h); } } else { let count = data.cells_h.len(); let mut geom = alloc::vec![(0 as Coord, 0 as Coord, 0 as Coord, 0 as Coord); count]; for taffy_idx in 0..count { let orig_idx = builder.original_index(taffy_idx); geom[orig_idx] = builder.child_geometry(taffy_idx); } let mut generator = FlexboxLayoutCacheGenerator::new(&repeater_indices, &mut result); for (x, y, w, h) in geom { generator.add(x, y, w, h); } } result } /// The flex's natural single-line (no-wrap) main-axis size: the size it /// occupies when all items sit on one line. A perpendicular parent uses this /// to give a non-stretch wrapping-flex cell its natural size (and only wrap /// when the available cross size is smaller), instead of the compact /// `sqrt`-area "square" that [`flexbox_layout_info_main_axis`] reports as /// `preferred`. pub fn flexbox_layout_unwrapped_main( cells: Slice, spacing: Coord, padding: &Padding, ) -> Coord { let extra_pad = padding.begin + padding.end; if cells.is_empty() { return extra_pad; } let num_spacings = cells.len().saturating_sub(1) as Coord; cells.iter().map(|c| c.constraint.preferred_bounded()).sum::() + spacing * num_spacings + extra_pad } /// Return main-axis LayoutInfo for a FlexboxLayout. /// Only needs the same-axis cells, avoiding a cross-axis binding loop. /// The reported `max` is always unbounded, even when every item is max-capped: /// unlike `box_layout_info`, wrapping makes a sum-of-maxes cap ill-defined. pub fn flexbox_layout_info_main_axis( cells: Slice, spacing: Coord, padding: &Padding, flex_wrap: FlexboxLayoutWrap, ) -> LayoutInfo { let extra_pad = padding.begin + padding.end; if cells.is_empty() { return LayoutInfo { min: extra_pad, preferred: extra_pad, max: extra_pad, ..Default::default() }; } let num_spacings = cells.len().saturating_sub(1) as Coord; let min = if matches!(flex_wrap, FlexboxLayoutWrap::NoWrap) { cells.iter().map(|c| c.constraint.min).sum::() + spacing * num_spacings + extra_pad } else { // Wrapping: the widest single item must fit cells.iter().map(|c| c.constraint.min).fold(0.0 as Coord, |a, b| a.max(b)) + extra_pad }; let preferred = if matches!(flex_wrap, FlexboxLayoutWrap::NoWrap) { // No wrapping: all items on one line flexbox_layout_unwrapped_main(cells, spacing, padding) } else { // Wrapping: aim for a roughly square (pixel-area) arrangement, using only // main-axis sizes so this stays independent of the cross axis. The square // side is the sqrt of the total area; each item is approximated as a // (size + spacing) square, so the gaps count toward the area and the grid // stays roughly square as spacing grows (otherwise a spacing-blind target // is reached with fewer items, skewing the grid taller). Snap that up to a // whole number of items, so a line holds a clean grid row instead of // wrapping mid-item (which would over-count columns: e.g. 3 equal items // want `A B / C`, not `A B C`). // Accumulate the area in f64: with the integer Coord build, Coord-typed // products (and their sum) would overflow for large items. let total_area: f64 = cells .iter() .map(|c| c.constraint.preferred_bounded() as f64 + spacing as f64) .map(|w| w * w) .sum(); let target = Float::sqrt(total_area as f32) as Coord; let mut acc = 0 as Coord; let mut started = false; for c in cells.iter() { // taffy breaks the lines on the hypothetical size (the preferred size, // before any growing), so the line fitting has to measure the items // the same way. let size = c.constraint.preferred_bounded(); acc += if started { spacing + size } else { size }; started = true; // `acc` is the real row width (no trailing gap), but `target` budgets // a gap per item, so add one back before comparing — else a row grabs // one item too many near an integer sqrt. if acc + spacing >= target { break; } } acc + extra_pad }; let stretch = cells.iter().map(|c| c.constraint.stretch).sum::(); LayoutInfo { min, max: Coord::MAX, min_percent: 0 as _, max_percent: 100 as _, preferred, stretch, } } /// Return cross-axis LayoutInfo for a FlexboxLayout. /// /// The minimum and the preferred cross size are two different measurements of /// the same items, so this runs the flex algorithm twice: once with every item /// at its min constraint, once at its preferred size. /// /// `constraint_size` is the main-axis container dimension (width for row, /// height for column). When valid (> 0 and < MAX), it's used as the taffy /// constraint for accurate wrapping. When invalid (e.g. 0, negative, or /// MAX — which can happen due to circular dependencies in nested /// perpendicular flexboxes), falls back to a heuristic based on /// `flexbox_layout_info_main_axis`. #[allow(clippy::too_many_arguments)] pub fn flexbox_layout_info_cross_axis( cells_h: Slice, cells_v: Slice, flex_props: Slice, spacing_h: Coord, spacing_v: Coord, padding_h: &Padding, padding_v: &Padding, direction: FlexboxLayoutDirection, alignment: LayoutAlignment, flex_wrap: FlexboxLayoutWrap, constraint_size: Coord, ) -> LayoutInfo { flexbox_layout_info_cross_axis_with_measure( cells_h, cells_v, flex_props, spacing_h, spacing_v, padding_h, padding_v, direction, alignment, flex_wrap, constraint_size, None, ) } /// Same as [`flexbox_layout_info_cross_axis`], with a measure callback so /// height-for-width cells (e.g. a nested wrapping flexbox) are measured at the /// main-axis size taffy actually assigns them, not at the pre-computed cell /// size (which was measured at the container width). /// /// `alignment` is the container's `alignment`: under `stretch` the solve grows /// the cells along the main axis, which changes a height-for-width cell's /// cross size, so this measurement must grow them the same way. #[allow(clippy::too_many_arguments)] pub fn flexbox_layout_info_cross_axis_with_measure( cells_h: Slice, cells_v: Slice, flex_props: Slice, spacing_h: Coord, spacing_v: Coord, padding_h: &Padding, padding_v: &Padding, direction: FlexboxLayoutDirection, alignment: LayoutAlignment, flex_wrap: FlexboxLayoutWrap, constraint_size: Coord, measure: FlexboxMeasureFn<'_>, ) -> LayoutInfo { debug_assert_eq!(cells_h.len(), cells_v.len()); debug_assert_eq!(cells_h.len(), flex_props.len()); if cells_h.is_empty() { assert!(cells_v.is_empty()); let orientation = match direction { FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => { Orientation::Vertical } FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => { Orientation::Horizontal } }; let padding = match orientation { Orientation::Horizontal => padding_h, Orientation::Vertical => padding_v, }; let pad = padding.begin + padding.end; return LayoutInfo { min: pad, preferred: pad, max: pad, ..Default::default() }; } // Determine which axis is cross let cross_cells = match direction { FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => &cells_v, FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => &cells_h, }; // Compute the main-axis preferred size to use as the constraint for taffy, // using the same heuristic as flexbox_layout_info_main_axis. let (main_cells, main_spacing, main_padding) = match direction { FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => { (&cells_h, spacing_h, padding_h) } FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => { (&cells_v, spacing_v, padding_v) } }; let main_extra_pad = main_padding.begin + main_padding.end; let main_axis_constraint = if constraint_size > 0 as Coord && constraint_size < Coord::MAX { // Use the actual container main-axis dimension (accurate) constraint_size } else if matches!(flex_wrap, FlexboxLayoutWrap::NoWrap) || constraint_size >= Coord::MAX { // No-wrap mode, or caller signalled "unconstrained" via MAX // (used when no real main-axis dimension is in scope, e.g. // a nested perpendicular flex queried via vtable): treat the // main axis as unbounded so items don't wrap. This gives the // natural max-cell-cross-axis result rather than the // sqrt(item-areas) heuristic. Coord::MAX } else { // Use actual item areas (main * cross) for the heuristic, since both // axes' cells are available here (unlike flexbox_layout_info_main_axis). // Accumulate in f64: with the integer Coord build, Coord-typed products // (and their sum) would overflow for large items. let total_area: f64 = main_cells .iter() .zip(cross_cells.iter()) .map(|(m, c)| { m.constraint.preferred_bounded() as f64 * c.constraint.preferred_bounded() as f64 }) .sum(); let count = main_cells.len(); Float::sqrt(total_area as f32) as Coord + main_spacing * (count - 1) as Coord + main_extra_pad }; let taffy_direction = match direction { FlexboxLayoutDirection::Row => flexbox_taffy::TaffyFlexDirection::Row, FlexboxLayoutDirection::RowReverse => flexbox_taffy::TaffyFlexDirection::RowReverse, FlexboxLayoutDirection::Column => flexbox_taffy::TaffyFlexDirection::Column, FlexboxLayoutDirection::ColumnReverse => flexbox_taffy::TaffyFlexDirection::ColumnReverse, }; let (container_width, container_height) = match direction { FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => { (Some(main_axis_constraint), None) } FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => { (None, Some(main_axis_constraint)) } }; let params = |cross_axis_sizing| flexbox_taffy::FlexboxLayoutParams { cells_h: &cells_h, cells_v: &cells_v, flex_props: &flex_props, spacing_h, spacing_v, padding_h, padding_v, alignment, cross_axis_line_alignment: LayoutAlignment::Stretch, cross_axis_alignment: CrossAxisAlignment::Stretch, flex_wrap, flex_shrink: 1., flex_direction: taffy_direction, container_width, container_height, cross_axis_sizing, }; let (available_width, available_height) = match direction { FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => { (main_axis_constraint, Coord::MAX) } FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => { (Coord::MAX, main_axis_constraint) } }; let cross_of = |(width, height): (Coord, Coord)| match direction { FlexboxLayoutDirection::Row | FlexboxLayoutDirection::RowReverse => height, FlexboxLayoutDirection::Column | FlexboxLayoutDirection::ColumnReverse => width, }; let mut builder = flexbox_taffy::FlexboxTaffyBuilder::new(params(flexbox_taffy::CrossAxisSizing::Minimum)); // Report nothing, so every item falls back to its min constraint. let mut zero = zero_measure; builder.compute_layout(available_width, available_height, &mut zero); let cross_size = cross_of(builder.container_size()); // The pass above resolved every `auto` cross size to the item's minimum. // Measure again, which resolves `auto` to the item's preferred size // instead, and sums the flex lines when the items wrap. let preferred = { let mut builder = flexbox_taffy::FlexboxTaffyBuilder::new(params( flexbox_taffy::CrossAxisSizing::FromMeasure, )); let mut resolved = resolve_measure_defaults(&cells_h, &cells_v, measure); builder.compute_layout(available_width, available_height, &mut resolved); cross_of(builder.container_size()) }; LayoutInfo { min: cross_size, max: Coord::MAX, min_percent: 0 as _, max_percent: 100 as _, preferred, stretch: 0.0, } } #[cfg(feature = "ffi")] pub(crate) mod ffi { #![allow(unsafe_code)] use super::*; #[unsafe(no_mangle)] pub extern "C" fn slint_organize_grid_layout( input_data: Slice, repeater_indices: Slice, repeater_steps: Slice, result: &mut GridLayoutOrganizedData, ) { *result = super::organize_grid_layout(input_data, repeater_indices, repeater_steps); } #[unsafe(no_mangle)] pub extern "C" fn slint_organize_dialog_button_layout( input_data: Slice, dialog_button_roles: Slice, result: &mut GridLayoutOrganizedData, ) { *result = super::organize_dialog_button_layout(input_data, dialog_button_roles); } #[unsafe(no_mangle)] pub extern "C" fn slint_solve_grid_layout( data: &GridLayoutData, constraints: Slice, orientation: Orientation, repeater_indices: Slice, repeater_steps: Slice, result: &mut SharedVector, ) { *result = super::solve_grid_layout( data, constraints, orientation, repeater_indices, repeater_steps, ) } #[unsafe(no_mangle)] pub extern "C" fn slint_grid_layout_info( organized_data: &GridLayoutOrganizedData, constraints: Slice, repeater_indices: Slice, repeater_steps: Slice, spacing: Coord, padding: &Padding, orientation: Orientation, ) -> LayoutInfo { super::grid_layout_info( organized_data.clone(), constraints, repeater_indices, repeater_steps, spacing, padding, orientation, ) } #[unsafe(no_mangle)] pub extern "C" fn slint_solve_box_layout( data: &BoxLayoutData, repeater_indices: Slice, result: &mut SharedVector, ) { *result = super::solve_box_layout(data, repeater_indices) } #[unsafe(no_mangle)] pub extern "C" fn slint_solve_box_layout_ortho( data: &BoxLayoutOrthoData, repeater_indices: Slice, result: &mut SharedVector, ) { *result = super::solve_box_layout_ortho(data, repeater_indices) } #[unsafe(no_mangle)] /// Return the LayoutInfo for a BoxLayout with the given cells. pub extern "C" fn slint_box_layout_info( cells: Slice, spacing: Coord, padding: &Padding, alignment: LayoutAlignment, ) -> LayoutInfo { super::box_layout_info(cells, spacing, padding, alignment) } #[unsafe(no_mangle)] /// Return the LayoutInfo for a BoxLayout with the given cells. pub extern "C" fn slint_box_layout_info_ortho( cells: Slice, padding: &Padding, ) -> LayoutInfo { super::box_layout_info_ortho(cells, padding) } /// The measure callback for C FFI. Returns (width, height) via out pointers. /// A dimension taffy has not determined arrives pre-resolved to the cell's /// preferred size, so both are concrete. /// A null function pointer means no measure callback. pub type FlexboxMeasureFnC = unsafe extern "C" fn( user_data: *mut core::ffi::c_void, child_index: usize, width: Coord, height: Coord, out_width: *mut Coord, out_height: *mut Coord, ); /// Turn a C measure callback (nullable fn pointer + user data) into the /// closure form used internally. /// /// # Safety /// `measure_fn`, when non-null, must be a valid `FlexboxMeasureFnC` /// function pointer, passed as `*const c_void` because cbindgen can't /// represent `Option` in C++. unsafe fn measure_closure_from_c( measure_fn: *const core::ffi::c_void, measure_user_data: *mut core::ffi::c_void, ) -> Option (Coord, Coord)> { const { assert!( core::mem::size_of::<*const core::ffi::c_void>() == core::mem::size_of::() ); } if measure_fn.is_null() { return None; } let c_measure = unsafe { core::mem::transmute::<*const core::ffi::c_void, FlexboxMeasureFnC>(measure_fn) }; Some(move |child_index: usize, w: Coord, h: Coord| { let mut out_w: Coord = 0 as _; let mut out_h: Coord = 0 as _; // Safety: c_measure is a valid function pointer provided by the caller, // and out_w/out_h are valid mutable pointers. unsafe { c_measure(measure_user_data, child_index, w, h, &mut out_w, &mut out_h); } (out_w, out_h) }) } #[unsafe(no_mangle)] pub extern "C" fn slint_solve_flexbox_layout( data: &FlexboxLayoutData, repeater_indices: Slice, result: &mut SharedVector, measure_fn: *const core::ffi::c_void, measure_user_data: *mut core::ffi::c_void, ) { // Safety: the caller guarantees `measure_fn` is a valid `FlexboxMeasureFnC` // when non-null (see `measure_closure_from_c`). let measure = unsafe { measure_closure_from_c(measure_fn, measure_user_data) }; if let Some(mut measure) = measure { *result = super::solve_flexbox_layout_with_measure( data, repeater_indices, Some(&mut measure), ); } else { *result = super::solve_flexbox_layout(data, repeater_indices); } } #[unsafe(no_mangle)] /// Return main-axis LayoutInfo for a FlexboxLayout (single-axis, no cross-axis dependency). pub extern "C" fn slint_flexbox_layout_info_main_axis( cells: Slice, spacing: Coord, padding: &Padding, flex_wrap: FlexboxLayoutWrap, ) -> LayoutInfo { super::flexbox_layout_info_main_axis(cells, spacing, padding, flex_wrap) } #[unsafe(no_mangle)] /// Return the flex's natural single-line (no-wrap) main-axis size. pub extern "C" fn slint_flexbox_layout_unwrapped_main( cells: Slice, spacing: Coord, padding: &Padding, ) -> Coord { super::flexbox_layout_unwrapped_main(cells, spacing, padding) } #[unsafe(no_mangle)] /// Return cross-axis LayoutInfo for a FlexboxLayout. pub extern "C" fn slint_flexbox_layout_info_cross_axis( cells_h: Slice, cells_v: Slice, flex_props: Slice, spacing_h: Coord, spacing_v: Coord, padding_h: &Padding, padding_v: &Padding, direction: FlexboxLayoutDirection, alignment: LayoutAlignment, flex_wrap: FlexboxLayoutWrap, constraint_size: Coord, ) -> LayoutInfo { super::flexbox_layout_info_cross_axis( cells_h, cells_v, flex_props, spacing_h, spacing_v, padding_h, padding_v, direction, alignment, flex_wrap, constraint_size, ) } #[unsafe(no_mangle)] /// Like `slint_flexbox_layout_info_cross_axis`, with a measure callback so /// height-for-width cells are re-measured at the size taffy assigns them. pub extern "C" fn slint_flexbox_layout_info_cross_axis_with_measure( cells_h: Slice, cells_v: Slice, flex_props: Slice, spacing_h: Coord, spacing_v: Coord, padding_h: &Padding, padding_v: &Padding, direction: FlexboxLayoutDirection, alignment: LayoutAlignment, flex_wrap: FlexboxLayoutWrap, constraint_size: Coord, measure_fn: *const core::ffi::c_void, measure_user_data: *mut core::ffi::c_void, ) -> LayoutInfo { // Safety: the caller guarantees `measure_fn` is a valid `FlexboxMeasureFnC` // when non-null (see `measure_closure_from_c`). let mut measure = unsafe { measure_closure_from_c(measure_fn, measure_user_data) }; super::flexbox_layout_info_cross_axis_with_measure( cells_h, cells_v, flex_props, spacing_h, spacing_v, padding_h, padding_v, direction, alignment, flex_wrap, constraint_size, measure.as_mut().map(|m| m as _), ) } } #[cfg(test)] mod tests { use super::*; fn collect_from_organized_data( organized_data: &GridLayoutOrganizedData, num_cells: usize, repeater_indices: Slice, repeater_steps: Slice, ) -> Vec<(u16, u16, u16, u16)> { let mut result = Vec::new(); for i in 0..num_cells { let col_and_span = organized_data.col_or_row_and_span( i, Orientation::Horizontal, &repeater_indices, &repeater_steps, ); let row_and_span = organized_data.col_or_row_and_span( i, Orientation::Vertical, &repeater_indices, &repeater_steps, ); result.push((col_and_span.0, col_and_span.1, row_and_span.0, row_and_span.1)); } result } #[test] fn test_organized_data_generator_2_fixed_cells() { // 2 fixed cells let mut result = GridLayoutOrganizedData::default(); let num_cells = 2; let mut generator = OrganizedDataGenerator::new(&[], &[], num_cells, 0, 0, &mut result); generator.add(0, 1, 0, 1); generator.add(1, 2, 0, 3); assert_eq!(result.as_slice(), &[0, 1, 0, 1, 1, 2, 0, 3]); let repeater_indices = Slice::from_slice(&[]); let empty_steps = Slice::from_slice(&[]); let collected_data = collect_from_organized_data(&result, num_cells, repeater_indices, empty_steps); assert_eq!(collected_data.as_slice(), &[(0, 1, 0, 1), (1, 2, 0, 3)]); assert_eq!( result.max_value(num_cells, Orientation::Horizontal, &repeater_indices, &empty_steps), 3 ); assert_eq!( result.max_value(num_cells, Orientation::Vertical, &repeater_indices, &empty_steps), 3 ); } #[test] fn test_organized_data_generator_1_fixed_cell_1_repeater() { // 4 cells: 1 fixed cell, 1 repeater with 3 repeated cells let mut result = GridLayoutOrganizedData::default(); let num_cells = 4; let repeater_indices = &[1u32, 3u32]; let mut generator = OrganizedDataGenerator::new(repeater_indices, &[], 1, 1, 3, &mut result); generator.add(0, 1, 0, 2); // fixed generator.add(1, 2, 1, 3); // repeated generator.add(1, 1, 2, 4); generator.add(2, 2, 3, 5); assert_eq!( result.as_slice(), &[ 0, 1, 0, 2, // fixed cell 8, 4, 0, 0, // jump cell: data_base=8, stride=4 (step=1, epi=4) 1, 2, 1, 3, // repeated cell 1 1, 1, 2, 4, // repeated cell 2 2, 2, 3, 5, // repeated cell 3 ] ); let repeater_indices = Slice::from_slice(repeater_indices); let empty_steps = Slice::from_slice(&[]); let collected_data = collect_from_organized_data(&result, num_cells, repeater_indices, empty_steps); assert_eq!( collected_data.as_slice(), &[(0, 1, 0, 2), (1, 2, 1, 3), (1, 1, 2, 4), (2, 2, 3, 5)] ); assert_eq!( result.max_value(num_cells, Orientation::Horizontal, &repeater_indices, &empty_steps), 4 ); assert_eq!( result.max_value(num_cells, Orientation::Vertical, &repeater_indices, &empty_steps), 8 ); } #[test] fn test_organize_data_with_auto_and_spans() { let auto = i_slint_common::ROW_COL_AUTO; let input = std::vec![ GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 2., rowspan: -1. }, GridLayoutInputData { new_row: false, col: auto, row: auto, colspan: 1., rowspan: 2. }, GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 2., rowspan: 1. }, GridLayoutInputData { new_row: true, col: -2., row: 80000., colspan: 2., rowspan: 1. }, ]; let repeater_indices = Slice::from_slice(&[]); let (organized_data, errors) = organize_grid_layout_impl( Slice::from_slice(&input), repeater_indices, Slice::from_slice(&[]), ); assert_eq!( organized_data.as_slice(), &[ 0, 2, 0, 0, // row 0, col 0, rowspan 0 (see below) 2, 1, 0, 2, // row 0, col 2 (due to colspan of first cell) 0, 2, 1, 1, // row 1, col 0 0, 2, 65535, 1, // row 65535, col 0 ] ); assert_eq!(errors.len(), 3); // Note that a rowspan of 0 is valid, it means the cell doesn't occupy any row assert_eq!(errors[0], "cell rowspan -1 is negative, clamping to 0"); assert_eq!(errors[1], "cell row 80000 is too large, clamping to 65535"); assert_eq!(errors[2], "cell col -2 is negative, clamping to 0"); let empty_steps = Slice::from_slice(&[]); let collected_data = collect_from_organized_data( &organized_data, input.len(), repeater_indices, empty_steps, ); assert_eq!( collected_data.as_slice(), &[(0, 2, 0, 0), (2, 1, 0, 2), (0, 2, 1, 1), (0, 2, 65535, 1)] ); assert_eq!( organized_data.max_value(3, Orientation::Horizontal, &repeater_indices, &empty_steps), 3 ); assert_eq!( organized_data.max_value(3, Orientation::Vertical, &repeater_indices, &empty_steps), 2 ); } #[test] fn test_organize_data_1_empty_repeater() { // Row { Text {} if false: Text {} }, this test shows why we need i32 for cell_nr_adj let auto = i_slint_common::ROW_COL_AUTO; let cell = GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 1., rowspan: 1. }; let input = std::vec![cell]; let repeater_indices = Slice::from_slice(&[1u32, 0u32]); let (organized_data, errors) = organize_grid_layout_impl( Slice::from_slice(&input), repeater_indices, Slice::from_slice(&[]), ); assert_eq!( organized_data.as_slice(), &[ 0, 1, 0, 1, // fixed 0, 0, 0, 0 ] // jump to repeater data (not used) ); assert_eq!(errors.len(), 0); let empty_steps = Slice::from_slice(&[]); let collected_data = collect_from_organized_data( &organized_data, input.len(), repeater_indices, empty_steps, ); assert_eq!(collected_data.as_slice(), &[(0, 1, 0, 1)]); assert_eq!( organized_data.max_value(1, Orientation::Horizontal, &repeater_indices, &empty_steps), 1 ); } #[test] fn test_organize_data_4_repeaters() { let auto = i_slint_common::ROW_COL_AUTO; let mut cell = GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 1., rowspan: 1. }; let mut input = std::vec![cell.clone()]; for _ in 0..8 { cell.new_row = false; input.push(cell.clone()); } let repeater_indices = Slice::from_slice(&[0u32, 0u32, 1u32, 4u32, 6u32, 2u32, 8u32, 0u32]); let (organized_data, errors) = organize_grid_layout_impl( Slice::from_slice(&input), repeater_indices, Slice::from_slice(&[]), ); assert_eq!( organized_data.as_slice(), &[ 28, 4, 0, 0, // rep0 jump: data at 28, stride=4 (empty) 0, 1, 0, 1, // fixed cell (col=0) 28, 4, 0, 0, // rep1 jump: data at 28, stride=4 (4 rows) 5, 1, 0, 1, // fixed cell (col=5) 44, 4, 0, 0, // rep2 jump: data at 44, stride=4 (2 rows) 52, 4, 0, 0, // rep3 jump: data at 52, stride=4 (empty) 8, 1, 0, 1, // fixed cell (col=8) 1, 1, 0, 1, // rep1 row 0 2, 1, 0, 1, // rep1 row 1 3, 1, 0, 1, // rep1 row 2 4, 1, 0, 1, // rep1 row 3 6, 1, 0, 1, // rep2 row 0 7, 1, 0, 1, // rep2 row 1 ] ); assert_eq!(errors.len(), 0); let empty_steps = Slice::from_slice(&[]); let collected_data = collect_from_organized_data( &organized_data, input.len(), repeater_indices, empty_steps, ); assert_eq!( collected_data.as_slice(), &[ (0, 1, 0, 1), (1, 1, 0, 1), (2, 1, 0, 1), (3, 1, 0, 1), (4, 1, 0, 1), (5, 1, 0, 1), (6, 1, 0, 1), (7, 1, 0, 1), (8, 1, 0, 1), ] ); let empty_steps = Slice::from_slice(&[]); assert_eq!( organized_data.max_value( input.len(), Orientation::Horizontal, &repeater_indices, &empty_steps ), 9 ); } #[test] fn test_organize_data_repeated_rows() { let auto = i_slint_common::ROW_COL_AUTO; let mut input = Vec::new(); let num_rows: u32 = 3; let num_columns: u32 = 2; // 3 rows of 2 columns each for _ in 0..num_rows { let mut cell = GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 1., rowspan: 1., }; input.push(cell.clone()); cell.new_row = false; input.push(cell.clone()); } // Repeater 0: starts at index 0, has 3 instances of 2 elements let repeater_indices_arr = [0_u32, num_rows]; let repeater_steps_arr = [num_columns]; let repeater_steps = Slice::from_slice(&repeater_steps_arr); let repeater_indices = Slice::from_slice(&repeater_indices_arr); let (organized_data, errors) = organize_grid_layout_impl(Slice::from_slice(&input), repeater_indices, repeater_steps); assert_eq!( organized_data.as_slice(), &[ 4, 8, 0, 0, // jump cell: data at u16 idx 4, stride=8 (=step*4=2*4) 0, 1, 0, 1, 1, 1, 0, 1, // row 0: col 0, col 1 0, 1, 1, 1, 1, 1, 1, 1, // row 1: col 0, col 1 0, 1, 2, 1, 1, 1, 2, 1, // row 2: col 0, col 1 ] ); assert_eq!(errors.len(), 0); let collected_data = collect_from_organized_data( &organized_data, input.len(), repeater_indices, repeater_steps, ); assert_eq!( collected_data.as_slice(), // (col, colspan, row, rowspan) for each cell in input order &[(0, 1, 0, 1), (1, 1, 0, 1), (0, 1, 1, 1), (1, 1, 1, 1), (0, 1, 2, 1), (1, 1, 2, 1),] ); assert_eq!( organized_data.max_value( input.len(), Orientation::Horizontal, &repeater_indices, &repeater_steps ), 2 ); assert_eq!( organized_data.max_value( input.len(), Orientation::Vertical, &repeater_indices, &repeater_steps ), 3 ); // Now test GridLayoutCacheGenerator let mut layout_cache_v = SharedVector::::default(); let mut generator = GridLayoutCacheGenerator::new( repeater_indices.as_slice(), repeater_steps.as_slice(), 0, // static_cells 1, // num_repeaters 6, // total_repeated_cells (3 rows * 2 columns) &mut layout_cache_v, ); // Row 0 generator.add(0., 50.); generator.add(0., 50.); // Row 1 generator.add(50., 50.); generator.add(50., 50.); // Row 2 generator.add(100., 50.); generator.add(100., 50.); assert_eq!( layout_cache_v.as_slice(), &[ 2., 4., // jump cell: data at pos 2, stride=4 (=step*2=2*2) 0., 50., 0., 50., // row 0 50., 50., 50., 50., // row 1 100., 50., 100., 50., // row 2 ] ); // GridRepeaterCacheAccess: cache[cache[jump_index] + ri * stride + child_offset] let layout_cache_v_access = |jump_index: usize, repeater_index: usize, stride: usize, child_offset: usize| -> Coord { let base = layout_cache_v[jump_index] as usize; let data_idx = base + repeater_index * stride + child_offset; layout_cache_v[data_idx] }; // stride=4 (step=2, entries_per_item=2) // Y pos for child 0 (child_offset=0) assert_eq!(layout_cache_v_access(0, 0, 4, 0), 0.); assert_eq!(layout_cache_v_access(0, 1, 4, 0), 50.); assert_eq!(layout_cache_v_access(0, 2, 4, 0), 100.); // Y pos for child 1 (child_offset=2) assert_eq!(layout_cache_v_access(0, 0, 4, 2), 0.); assert_eq!(layout_cache_v_access(0, 1, 4, 2), 50.); assert_eq!(layout_cache_v_access(0, 2, 4, 2), 100.); } #[test] fn test_organize_data_repeated_rows_multiple_repeaters() { let auto = i_slint_common::ROW_COL_AUTO; let mut input = Vec::new(); let num_rows: u32 = 5; let mut cell = GridLayoutInputData { new_row: true, col: auto, row: auto, colspan: 1., rowspan: 1. }; // 3 rows of 2 columns each for _ in 0..3 { cell.new_row = true; input.push(cell.clone()); cell.new_row = false; input.push(cell.clone()); } // 2 rows of 3 columns each for _ in 0..2 { cell.new_row = true; input.push(cell.clone()); cell.new_row = false; input.push(cell.clone()); cell.new_row = false; input.push(cell.clone()); } // Repeater 0: starts at index 0, has 3 instances of 2 elements // Repeater 1: starts at index 6 (after repeater 0's 3*2=6 cells), has 2 instances of 3 elements let repeater_indices_arr = [0_u32, 3, 6, 2]; let repeater_steps_arr = [2, 3]; let repeater_steps = Slice::from_slice(&repeater_steps_arr); let repeater_indices = Slice::from_slice(&repeater_indices_arr); let (organized_data, errors) = organize_grid_layout_impl(Slice::from_slice(&input), repeater_indices, repeater_steps); assert_eq!( organized_data.as_slice(), &[ 8, 8, 0, 0, // repeater 0 jump: data at 8, stride=8 (=step*4=2*4) 32, 12, 0, 0, // repeater 1 jump: data at 32, stride=12 (=step*4=3*4) // Repeater 0 data 0, 1, 0, 1, 1, 1, 0, 1, // row 0: col 0, col 1 0, 1, 1, 1, 1, 1, 1, 1, // row 1: col 0, col 1 0, 1, 2, 1, 1, 1, 2, 1, // row 2: col 0, col 1 // Repeater 1 data 0, 1, 3, 1, 1, 1, 3, 1, 2, 1, 3, 1, // row 0: col 0, col 1, col 2 0, 1, 4, 1, 1, 1, 4, 1, 2, 1, 4, 1, // row 1: col 0, col 1, col 2 ] ); assert_eq!(errors.len(), 0); let collected_data = collect_from_organized_data( &organized_data, input.len(), repeater_indices, repeater_steps, ); assert_eq!( collected_data.as_slice(), // (col, colspan, row, rowspan) for each cell in input order &[ (0, 1, 0, 1), (1, 1, 0, 1), (0, 1, 1, 1), (1, 1, 1, 1), (0, 1, 2, 1), (1, 1, 2, 1), (0, 1, 3, 1), (1, 1, 3, 1), (2, 1, 3, 1), (0, 1, 4, 1), (1, 1, 4, 1), (2, 1, 4, 1) ] ); assert_eq!( organized_data.max_value( input.len(), Orientation::Horizontal, &repeater_indices, &repeater_steps ), 3 // max col (2) + colspan (1) = 3 ); assert_eq!( organized_data.max_value( input.len(), Orientation::Vertical, &repeater_indices, &repeater_steps ), num_rows as usize // max row (4) + rowspan (1) = 5 ); // Now test GridLayoutCacheGenerator let mut layout_cache_v = SharedVector::::default(); let mut generator = GridLayoutCacheGenerator::new( repeater_indices.as_slice(), repeater_steps.as_slice(), 0, // static_cells 2, // num_repeaters 12, // total_repeated_cells (3*2 + 2*3) &mut layout_cache_v, ); // Row 0 generator.add(0., 50.); generator.add(0., 50.); // Row 1 generator.add(50., 50.); generator.add(50., 50.); // Row 2 generator.add(100., 50.); generator.add(100., 50.); // Row 3 generator.add(150., 50.); generator.add(150., 50.); generator.add(150., 50.); // Row 4 generator.add(200., 50.); generator.add(200., 50.); generator.add(200., 50.); assert_eq!( layout_cache_v.as_slice(), &[ 4., 4., // repeater 0 jump: data at pos 4, stride=4 (=step*2=2*2) 16., 6., // repeater 1 jump: data at pos 16, stride=6 (=step*2=3*2) 0., 50., 0., 50., // repeater 0 row 0 data 50., 50., 50., 50., // repeater 0 row 1 data 100., 50., 100., 50., // repeater 0 row 2 data 150., 50., 150., 50., 150., 50., // repeater 1 row 3 data 200., 50., 200., 50., 200., 50., // repeater 1 row 4 data ] ); // GridRepeaterCacheAccess: cache[cache[jump_index] + ri * stride + child_offset] let layout_cache_v_access = |jump_index: usize, repeater_index: usize, stride: usize, child_offset: usize| -> Coord { let base = layout_cache_v[jump_index] as usize; let data_idx = base + repeater_index * stride + child_offset; layout_cache_v[data_idx] }; // Repeater 0: Y pos for child 0 (child_offset=0), stride=4 assert_eq!(layout_cache_v_access(0, 0, 4, 0), 0.); assert_eq!(layout_cache_v_access(0, 1, 4, 0), 50.); assert_eq!(layout_cache_v_access(0, 2, 4, 0), 100.); // Repeater 0: Y pos for child 1 (child_offset=2), stride=4 assert_eq!(layout_cache_v_access(0, 0, 4, 2), 0.); assert_eq!(layout_cache_v_access(0, 1, 4, 2), 50.); assert_eq!(layout_cache_v_access(0, 2, 4, 2), 100.); // Repeater 1: Y pos for child 0 (child_offset=0), jump at index 2, stride=6 assert_eq!(layout_cache_v_access(2, 0, 6, 0), 150.); assert_eq!(layout_cache_v_access(2, 1, 6, 0), 200.); // Repeater 1: Y pos for child 2 (child_offset=4), jump at index 2, stride=6 assert_eq!(layout_cache_v_access(2, 0, 6, 4), 150.); assert_eq!(layout_cache_v_access(2, 1, 6, 4), 200.); } #[test] fn test_layout_cache_generator_2_fixed_cells() { // 2 fixed cells let mut result = SharedVector::::default(); result.resize(2 * 2, 0 as _); let mut generator = LayoutCacheGenerator::new(&[], &mut result); generator.add(0., 50.); // fixed generator.add(80., 50.); // fixed assert_eq!(result.as_slice(), &[0., 50., 80., 50.]); } #[test] fn test_layout_cache_generator_1_fixed_cell_1_repeater() { // 4 cells: 1 fixed cell, 1 repeater with 3 repeated cells let mut result = SharedVector::::default(); let repeater_indices = &[1, 3]; result.resize(4 * 2 + repeater_indices.len(), 0 as _); let mut generator = LayoutCacheGenerator::new(repeater_indices, &mut result); generator.add(0., 50.); // fixed generator.add(80., 50.); // repeated generator.add(160., 50.); generator.add(240., 50.); assert_eq!( result.as_slice(), &[ 0., 50., // fixed 4., 5., // jump to repeater data 80., 50., 160., 50., 240., 50. // repeater data ] ); } #[test] fn test_layout_cache_generator_4_repeaters() { // 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 let mut result = SharedVector::::default(); let repeater_indices = &[1, 0, 1, 4, 6, 2, 8, 0]; result.resize(8 * 2 + repeater_indices.len(), 0 as _); let mut generator = LayoutCacheGenerator::new(repeater_indices, &mut result); generator.add(0., 50.); // fixed generator.add(80., 10.); // repeated generator.add(160., 10.); generator.add(240., 10.); generator.add(320., 10.); // end of second repeater generator.add(400., 80.); // fixed generator.add(500., 20.); // repeated generator.add(600., 20.); // end of third repeater assert_eq!( result.as_slice(), &[ 0., 50., // fixed 12., 13., // jump to first (empty) repeater (not used) 12., 13., // jump to second repeater data 400., 80., // fixed 20., 21., // jump to third repeater data 0., 0., // slot for jumping to fourth repeater (currently empty) 80., 10., 160., 10., 240., 10., 320., 10., // first repeater data 500., 20., 600., 20. // second repeater data ] ); } /// `flexbox_layout_unwrapped_main()` computes the container's max-content main /// size by hand, because the main-axis layout-info path may not read the /// cross-axis cells (that would be a binding loop). taffy computes the same /// thing from the same cells, so this pins the two together and fails the day /// the hand-rolled version drifts. /// /// Only meaningful for `no-wrap`: at max-content taffy never wraps (it reports a /// single line), while the wrapping branch of `flexbox_layout_info_main_axis()` /// deliberately reports a roughly square arrangement instead. mod max_content_matches_taffy { use super::*; fn cell(preferred: Coord, stretch: f32) -> FlexboxLayoutItemInfo { FlexboxLayoutItemInfo { constraint: LayoutInfo { preferred, stretch, ..Default::default() }, ..Default::default() } } /// The constraint half of the bundled test cells, as the parallel /// array the runtime takes. fn constraints(cells: &[FlexboxLayoutItemInfo]) -> Vec { cells .iter() .map(|c| LayoutItemInfo { constraint: c.constraint.clone(), ..Default::default() }) .collect() } /// Split the bundled test cells into the parallel (constraint, flex-props) /// arrays the runtime takes. fn split(cells: &[FlexboxLayoutItemInfo]) -> (Vec, Vec) { (constraints(cells), cells.iter().map(|c| c.props).collect()) } /// What taffy makes of the same cells, asked for its max-content main size. fn taffy_max_content_main(cells: &[FlexboxLayoutItemInfo]) -> Coord { let (main, flex) = split(cells); // The cross axis is deliberately left unconstrained: the main-axis result // must not depend on it, which is what makes the loop-free path sound. let cross: Vec = cells .iter() .map(|_| LayoutItemInfo { constraint: LayoutInfo { max: Coord::MAX, ..Default::default() }, ..Default::default() }) .collect(); let cells_h = Slice::from_slice(&main); let cells_v = Slice::from_slice(&cross); let flex_props = Slice::from_slice(&flex); let pad = Padding::default(); let mut builder = flexbox_taffy::FlexboxTaffyBuilder::new(flexbox_taffy::FlexboxLayoutParams { cells_h: &cells_h, cells_v: &cells_v, flex_props: &flex_props, spacing_h: 0 as Coord, spacing_v: 0 as Coord, padding_h: &pad, padding_v: &pad, // Stretch: the strongest growing mode, to show the stretch // factors cancel out of the max-content size. alignment: LayoutAlignment::Stretch, cross_axis_line_alignment: LayoutAlignment::Stretch, cross_axis_alignment: CrossAxisAlignment::Stretch, flex_wrap: FlexboxLayoutWrap::NoWrap, flex_shrink: 1., flex_direction: flexbox_taffy::TaffyFlexDirection::Row, container_width: None, container_height: None, cross_axis_sizing: flexbox_taffy::CrossAxisSizing::Preferred, }); // A row item gets `size.width: auto`, so taffy asks the measure callback // for its content size. That callback is how Slint reports an item's // natural size, so the comparison is only fair if it answers here too. let mut measure = |idx: usize, known_w: Option, known_h: Option| { ( known_w.unwrap_or_else(|| cells[idx].constraint.preferred_bounded()), known_h.unwrap_or(0 as Coord), ) }; builder.compute_layout(Coord::MAX, Coord::MAX, &mut measure); builder.container_size().0 } fn ours(cells: &[FlexboxLayoutItemInfo]) -> Coord { let main = constraints(cells); flexbox_layout_unwrapped_main(Slice::from_slice(&main), 0 as Coord, &Padding::default()) } #[track_caller] fn assert_agrees(name: &str, cells: &[FlexboxLayoutItemInfo]) { let (ours, theirs) = (ours(cells), taffy_max_content_main(cells)); assert!((ours - theirs).abs() <= 1 as Coord, "{name}: ours={ours} taffy={theirs}"); } /// The grow factor (derived from the stretch factor) cancels out of /// taffy's max-content flex fraction, so an item contributes its /// content size and we must land on the same number. #[test] fn agrees() { assert_agrees("plain", &[cell(50., 0.), cell(250., 0.)]); assert_agrees("equal stretch", &[cell(50., 1.), cell(250., 1.)]); assert_agrees("uneven stretch", &[cell(60., 1.), cell(60., 3.)]); assert_agrees("fractional stretch", &[cell(60., 0.5), cell(40., 1.5)]); assert_agrees("mixed stretch and not", &[cell(50., 1.), cell(100., 0.)]); // `preferred-width: 0` with stretch collapses the max-content size // to nothing in taffy too, as it does in a HorizontalLayout. assert_agrees("zero preferred, stretchy", &[cell(0., 1.), cell(0., 1.)]); } /// The same cells asked of a *column* container, where the main-axis size /// comes from the basis (the preferred height), not the measure callback. fn taffy_column_main(cells: &[FlexboxLayoutItemInfo]) -> Coord { let (main, flex) = split(cells); // For a column the main axis is vertical: main-axis sizes live in cells_v, // while the cross-axis (width) constraint is left unbounded. let h: Vec = cells .iter() .map(|_| LayoutItemInfo { constraint: LayoutInfo { max: Coord::MAX, ..Default::default() }, ..Default::default() }) .collect(); let cells_h = Slice::from_slice(&h); let cells_v = Slice::from_slice(&main); let flex_props = Slice::from_slice(&flex); let pad = Padding::default(); let mut builder = flexbox_taffy::FlexboxTaffyBuilder::new(flexbox_taffy::FlexboxLayoutParams { cells_h: &cells_h, cells_v: &cells_v, flex_props: &flex_props, spacing_h: 0 as Coord, spacing_v: 0 as Coord, padding_h: &pad, padding_v: &pad, alignment: LayoutAlignment::Stretch, cross_axis_line_alignment: LayoutAlignment::Stretch, cross_axis_alignment: CrossAxisAlignment::Stretch, flex_wrap: FlexboxLayoutWrap::NoWrap, flex_shrink: 1., flex_direction: flexbox_taffy::TaffyFlexDirection::Column, container_width: None, container_height: None, cross_axis_sizing: flexbox_taffy::CrossAxisSizing::Preferred, }); let mut measure = |idx: usize, known_w: Option, known_h: Option| { ( known_w.unwrap_or(0 as Coord), known_h.unwrap_or_else(|| cells[idx].constraint.preferred_bounded()), ) }; builder.compute_layout(Coord::MAX, Coord::MAX, &mut measure); builder.container_size().1 } #[track_caller] fn assert_agrees_column(name: &str, cells: &[FlexboxLayoutItemInfo]) { let (o, t) = (ours(cells), taffy_column_main(cells)); assert!((o - t).abs() <= 1 as Coord, "{name}: ours={o} taffy={t}"); } /// Both axes report the sum of the preferred sizes, so the column case must /// agree just like the row case, even though taffy computes it from the /// basis there instead of the measure callback. #[test] fn agrees_for_a_column() { assert_agrees_column("plain", &[cell(50., 0.), cell(250., 0.)]); assert_agrees_column("equal stretch", &[cell(50., 1.), cell(250., 1.)]); assert_agrees_column("zero preferred, stretchy", &[cell(0., 1.), cell(0., 1.)]); } } /// The taffy grow/shrink factors are derived from the container's /// `alignment` and the cells' main-axis stretch factors (see /// `FlexboxTaffyBuilder::new`), mirroring how a box layout distributes /// free space. mod stretch_driven_grow { use super::*; fn info(preferred: Coord, stretch: f32) -> LayoutInfo { LayoutInfo { preferred, stretch, ..Default::default() } } /// Solve a row of `constraints` in a 400px-wide container and return /// the resulting widths. fn solve_row(alignment: LayoutAlignment, constraints: &[LayoutInfo]) -> Vec { let cells_h: Vec = constraints .iter() .map(|c| LayoutItemInfo { constraint: c.clone(), ..Default::default() }) .collect(); let cells_v: Vec = constraints.iter().map(|_| LayoutItemInfo::default()).collect(); let flex_props: Vec = constraints.iter().map(|_| FlexItemProps::default()).collect(); let pad = Padding::default(); let mut builder = flexbox_taffy::FlexboxTaffyBuilder::new(flexbox_taffy::FlexboxLayoutParams { cells_h: &Slice::from_slice(&cells_h), cells_v: &Slice::from_slice(&cells_v), flex_props: &Slice::from_slice(&flex_props), spacing_h: 0 as Coord, spacing_v: 0 as Coord, padding_h: &pad, padding_v: &pad, alignment, cross_axis_line_alignment: LayoutAlignment::Stretch, cross_axis_alignment: CrossAxisAlignment::Stretch, flex_wrap: FlexboxLayoutWrap::Wrap, flex_shrink: 1., flex_direction: flexbox_taffy::TaffyFlexDirection::Row, container_width: Some(400 as Coord), container_height: None, cross_axis_sizing: flexbox_taffy::CrossAxisSizing::Preferred, }); builder.compute_layout(400 as Coord, Coord::MAX, &mut zero_measure); (0..constraints.len()).map(|i| builder.child_geometry(i).2).collect() } #[test] fn grows_by_stretch_only_under_alignment_stretch() { // 200 free pixels split by the stretch weights 3:1. let cells = [info(100., 3.), info(100., 1.)]; assert_eq!(solve_row(LayoutAlignment::Stretch, &cells), [250., 150.]); // A zero factor next to a non-zero one stays at its preferred size. let cells = [info(100., 1.), info(100., 0.)]; assert_eq!(solve_row(LayoutAlignment::Stretch, &cells), [300., 100.]); // All-zero factors split the free space evenly, like a HorizontalLayout. let cells = [info(100., 0.), info(100., 0.)]; assert_eq!(solve_row(LayoutAlignment::Stretch, &cells), [200., 200.]); // Growing requires `alignment: stretch` on the container. let cells = [info(100., 3.), info(100., 1.)]; assert_eq!(solve_row(LayoutAlignment::Start, &cells), [100., 100.]); } /// An item alone on its line and larger than the container shrinks to /// fit whatever its stretch factor says (the constant taffy shrink /// factor of 1); only its `min` refuses shrinking. #[test] fn lone_oversized_item_shrinks_regardless_of_stretch() { let squeezable = LayoutInfo { preferred: 500., min: 200., stretch: 0., ..Default::default() }; assert_eq!(solve_row(LayoutAlignment::Start, &[squeezable]), [400.]); let rigid = LayoutInfo { preferred: 500., min: 450., stretch: 0., ..Default::default() }; assert_eq!(solve_row(LayoutAlignment::Start, &[rigid]), [450.]); } } /// A percentage constraint must not be resolved against the `Coord::MAX` /// "unbounded" sentinel that the info path passes for a no-wrap main axis. /// `min_percent * MAX / 100` overflows the i32 build (debug panic) and gives /// infinity in the f32 build, which taffy then bakes into the item's size. /// When the container axis is unbounded a percentage has no basis, so the /// item keeps its natural (here zero) size instead. /// /// Checked at the builder because the containing info function reports only /// the *cross* size, while the overflow lands on the item's *main*-axis size; /// the f32 build hides it at the container level but not at the item. #[test] fn percentage_against_unbounded_container_leaves_item_finite() { // A single row cell with `width: 50%` (min_percent == max_percent == 50). let cells_h = [LayoutItemInfo { constraint: LayoutInfo { min_percent: 50 as Coord, max_percent: 50 as Coord, max: Coord::MAX, ..Default::default() }, ..Default::default() }]; let cells_v = [LayoutItemInfo { constraint: LayoutInfo { preferred: 30 as Coord, max: Coord::MAX, ..Default::default() }, ..Default::default() }]; let flex_props = [FlexItemProps::default()]; let pad = Padding::default(); let mut builder = flexbox_taffy::FlexboxTaffyBuilder::new(flexbox_taffy::FlexboxLayoutParams { cells_h: &Slice::from_slice(&cells_h), cells_v: &Slice::from_slice(&cells_v), flex_props: &Slice::from_slice(&flex_props), spacing_h: 0 as Coord, spacing_v: 0 as Coord, padding_h: &pad, padding_v: &pad, alignment: LayoutAlignment::Start, cross_axis_line_alignment: LayoutAlignment::Stretch, cross_axis_alignment: CrossAxisAlignment::Stretch, flex_wrap: FlexboxLayoutWrap::NoWrap, flex_shrink: 1., flex_direction: flexbox_taffy::TaffyFlexDirection::Row, // The unbounded main axis: the value the info path feeds here. container_width: Some(Coord::MAX), container_height: None, cross_axis_sizing: flexbox_taffy::CrossAxisSizing::Preferred, }); builder.compute_layout(Coord::MAX, Coord::MAX, &mut zero_measure); let (_x, _y, w, _h) = builder.child_geometry(0); // Without the fix the dropped `50% * MAX` makes this the f32 sentinel // (or panics under i32); with it the item just takes its natural size. assert!(w.is_finite() && w < Coord::MAX, "item main-axis size was {w}"); } /// Runs `grid_internal::to_layout_data` for a single row made of one /// non-spanning cell per constraint, and returns that row's combined /// LayoutData (min/max/pref), for testing the row/col aggregation in /// isolation from the rest of GridLayout. fn row_layout_data(constraints: &[LayoutInfo]) -> grid_internal::LayoutData { let mut organized_data = GridLayoutOrganizedData::default(); let mut generator = OrganizedDataGenerator::new(&[], &[], constraints.len(), 0, 0, &mut organized_data); for col in 0..constraints.len() { generator.add(col as u16, 1, 0, 1); } let items: Vec = constraints .iter() .map(|constraint| LayoutItemInfo { constraint: *constraint, ..Default::default() }) .collect(); let mut layout_data = grid_internal::to_layout_data( &organized_data, Slice::from_slice(&items), Orientation::Vertical, Slice::from_slice(&[]), Slice::from_slice(&[]), 0 as _, None, ); assert_eq!(layout_data.len(), 1); layout_data.remove(0) } #[test] fn test_grid_row_collapsed_cell_raises_max_and_pulls_pref_down_with_it() { // Row with a cell collapsed to a fixed zero size (the `height: cond ? // x : 0px` idiom, #9724) next to a cell with an explicit min/preferred // and no max (e.g. `min-height: 5px; preferred-height: 50px;`). let row = row_layout_data(&[ LayoutInfo { min: 0 as _, max: 0 as _, preferred: 0 as _, ..Default::default() }, LayoutInfo { min: 5 as _, preferred: 50 as _, ..Default::default() }, ]); // The collapsed cell's fixed zero pulls the row's max down to 0 while // its sibling's min pulls the row's min up to 5: a real conflict, so // max must be raised to meet min, and the sibling's now out-of-range // preferred (50) must be pulled down with it, not summed into the // row's LayoutInfo as-is. assert_eq!(row.min, 5 as Coord); assert_eq!(row.max, 5 as Coord); assert_eq!(row.pref, 5 as Coord); } #[test] fn test_grid_row_without_conflicting_constraints_keeps_its_own_pref() { // No cell forces max below min here (max 10 >= min 0): not a #9724 // conflict, so the fix must leave this row untouched. The second // cell's preferred (50) legitimately exceeds the row's own max (10) // already on master; to_layout_data's output also drives // solve_grid_layout, so clamping pref here would silently shrink // rows that never had a min/max conflict in the first place. let row = row_layout_data(&[ LayoutInfo { min: 0 as _, max: 10 as _, preferred: 5 as _, ..Default::default() }, LayoutInfo { min: 0 as _, preferred: 50 as _, ..Default::default() }, ]); assert_eq!(row.min, 0 as Coord); assert_eq!(row.max, 10 as Coord); assert_eq!(row.pref, 50 as Coord); } }