Three root causes, all fixed and verified on the Pixel 9 Pro: 1. Present phase (backend + renderer): VSYNC_LOCKED_RENDER defers the render to the vsync wake; RENDER_AHEAD presents the previous frame at the start of render() (phase = wake + ~1.1 ms) and holds the new frame one slot. Crash fix: a held wgpu frame must be PRESENTED, never merely dropped, before the next acquire (AlreadyAcquired panic at launch otherwise); discard_pending_frame presents too. 2. Full-res prefetch churn (src/lib.rs): the 8-slot full cache cycled through a ~20-cell viewport forever (137 decodes/20 s) and each eviction free()'d a 50 MB Vec on the UI thread (20-52 ms CPU spikes, ~7/s = the residual SLOWEV NonMain jank). Fix: 60 s tombstone cooldown on evicted paths (idle prefetch skips, urgent tap bypasses) + off-thread reaper for the freed blobs (insert: 21 ms -> 4 us). 3. (earlier) Skia final flush + Mailbox present mode (vendor, M3). Instrumentation (all behind --features perf): OTHR/INVK/INVK_TAG/FLCK lines for cross-thread closure timing and push-site tagging. Warm measured swipe, 8580-file grid: cadence median 8.33 ms (exact 120 Hz), p99 15.5 ms, 5/552 gaps >16.7 ms (0.9%, max 28.3 ms, zero >33 ms); PRES phase median ~994 us, zero late frames. Docs updated (docs/BUGS/scroll-jank-120hz.md, full scorecard + line reference). Also: vendored i-slint-core + wgpu-hal (vkshim interposition patch), vkshim profiling tooling, backend made a non-optional dep, debuggable flag for simpleperf (REMOVE before shipping).
3673 lines
149 KiB
Rust
3673 lines
149 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// 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<crate::api::LogicalSize>, Option<crate::api::LogicalSize>) {
|
|
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<T: PartialOrd>(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<A: Adjust>(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::<Grow>(data, size_without_spacing);
|
|
} else if size_without_spacing < pref {
|
|
adjust_items::<Shrink>(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<LayoutItemInfo>,
|
|
orientation: Orientation,
|
|
repeater_indices: Slice<u32>,
|
|
repeater_steps: Slice<u32>,
|
|
spacing: Coord,
|
|
size: Option<Coord>,
|
|
) -> Vec<LayoutData> {
|
|
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<u16>;
|
|
|
|
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<u32>,
|
|
repeater_steps: &Slice<u32>,
|
|
) -> (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<u32>,
|
|
repeater_steps: &Slice<u32>,
|
|
) -> 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<GridLayoutInputData>,
|
|
dialog_button_roles: Slice<DialogButtonRole>,
|
|
) -> 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<usize> = 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<String>;
|
|
|
|
pub fn organize_grid_layout(
|
|
input_data: Slice<GridLayoutInputData>,
|
|
repeater_indices: Slice<u32>,
|
|
repeater_steps: Slice<u32>,
|
|
) -> 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<GridLayoutInputData>,
|
|
repeater_indices: Slice<u32>,
|
|
repeater_steps: Slice<u32>,
|
|
) -> (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<String>) -> 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<Coord>,
|
|
}
|
|
|
|
impl<'a> LayoutCacheGenerator<'a> {
|
|
fn new(repeater_indices: &'a [u32], result: &'a mut SharedVector<Coord>) -> 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<Coord>,
|
|
}
|
|
|
|
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<Coord>,
|
|
) -> 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<LayoutItemInfo>,
|
|
orientation: Orientation,
|
|
repeater_indices: Slice<u32>,
|
|
repeater_steps: Slice<u32>,
|
|
) -> SharedVector<Coord> {
|
|
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::<Coord>::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<LayoutItemInfo>,
|
|
repeater_indices: Slice<u32>,
|
|
repeater_steps: Slice<u32>,
|
|
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::<Coord>() + 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::<Coord>() + spacing_w;
|
|
let stretch = layout_data.iter().map(|data| data.stretch).sum::<f32>();
|
|
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<LayoutItemInfo> 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<u32>) -> SharedVector<Coord> {
|
|
let mut result = SharedVector::<Coord>::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<usize> = if data.cells.iter().any(|c| c.layout_order != 0) {
|
|
let mut indices: Vec<usize> = (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<u32>,
|
|
) -> SharedVector<Coord> {
|
|
let mut result = SharedVector::<Coord>::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<LayoutItemInfo>,
|
|
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::<Coord>() + 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::<Coord>() + extra_w;
|
|
let stretch = cells.iter().map(|c| c.constraint.stretch).sum::<f32>();
|
|
LayoutInfo { min, max, min_percent: 0 as _, max_percent: 100 as _, preferred, stretch }
|
|
}
|
|
|
|
pub fn box_layout_info_ortho(cells: Slice<LayoutItemInfo>, 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<Coord>,
|
|
pub container_height: Option<Coord>,
|
|
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<usize>,
|
|
pub children: Vec<NodeId>,
|
|
pub container: NodeId,
|
|
/// Maps taffy child position -> original cell index (empty if no reordering needed)
|
|
pub order_map: Vec<usize>,
|
|
}
|
|
|
|
impl FlexboxTaffyBuilder {
|
|
/// Create a new flexbox layout tree from item constraints
|
|
pub fn new(params: FlexboxLayoutParams) -> Self {
|
|
let mut taffy = TaffyTree::<usize>::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<Coord>, pad: &Padding| -> Option<Coord> {
|
|
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>| -> 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>| -> 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<NodeId> = 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<usize> = if has_order {
|
|
let mut indices: Vec<usize> = (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<NodeId> = 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<Coord>, Option<Coord>) -> (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<Coord>,
|
|
}
|
|
|
|
impl<'a> FlexboxLayoutCacheGenerator<'a> {
|
|
fn new(repeater_indices: &'a [u32], result: &'a mut SharedVector<Coord>) -> 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<Coord>, _: Option<Coord>) -> (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<Coord>, Option<Coord>) -> (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<u32>,
|
|
) -> SharedVector<Coord> {
|
|
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<u32>,
|
|
measure: FlexboxMeasureFn<'_>,
|
|
) -> SharedVector<Coord> {
|
|
// 4 values per item: x, y, width, height
|
|
let mut result = SharedVector::<Coord>::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<LayoutItemInfo>,
|
|
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::<Coord>()
|
|
+ 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<LayoutItemInfo>,
|
|
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::<Coord>() + 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::<f32>();
|
|
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<LayoutItemInfo>,
|
|
cells_v: Slice<LayoutItemInfo>,
|
|
flex_props: Slice<FlexItemProps>,
|
|
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<LayoutItemInfo>,
|
|
cells_v: Slice<LayoutItemInfo>,
|
|
flex_props: Slice<FlexItemProps>,
|
|
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<GridLayoutInputData>,
|
|
repeater_indices: Slice<u32>,
|
|
repeater_steps: Slice<u32>,
|
|
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<GridLayoutInputData>,
|
|
dialog_button_roles: Slice<DialogButtonRole>,
|
|
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<LayoutItemInfo>,
|
|
orientation: Orientation,
|
|
repeater_indices: Slice<u32>,
|
|
repeater_steps: Slice<u32>,
|
|
result: &mut SharedVector<Coord>,
|
|
) {
|
|
*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<LayoutItemInfo>,
|
|
repeater_indices: Slice<u32>,
|
|
repeater_steps: Slice<u32>,
|
|
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<u32>,
|
|
result: &mut SharedVector<Coord>,
|
|
) {
|
|
*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<u32>,
|
|
result: &mut SharedVector<Coord>,
|
|
) {
|
|
*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<LayoutItemInfo>,
|
|
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<LayoutItemInfo>,
|
|
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<fn pointer>` in C++.
|
|
unsafe fn measure_closure_from_c(
|
|
measure_fn: *const core::ffi::c_void,
|
|
measure_user_data: *mut core::ffi::c_void,
|
|
) -> Option<impl FnMut(usize, Coord, Coord) -> (Coord, Coord)> {
|
|
const {
|
|
assert!(
|
|
core::mem::size_of::<*const core::ffi::c_void>()
|
|
== core::mem::size_of::<FlexboxMeasureFnC>()
|
|
);
|
|
}
|
|
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<u32>,
|
|
result: &mut SharedVector<Coord>,
|
|
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<LayoutItemInfo>,
|
|
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<LayoutItemInfo>,
|
|
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<LayoutItemInfo>,
|
|
cells_v: Slice<LayoutItemInfo>,
|
|
flex_props: Slice<FlexItemProps>,
|
|
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<LayoutItemInfo>,
|
|
cells_v: Slice<LayoutItemInfo>,
|
|
flex_props: Slice<FlexItemProps>,
|
|
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<u32>,
|
|
repeater_steps: Slice<u32>,
|
|
) -> 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::<Coord>::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::<Coord>::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::<Coord>::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::<Coord>::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::<Coord>::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<LayoutItemInfo> {
|
|
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<LayoutItemInfo>, Vec<FlexItemProps>) {
|
|
(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<LayoutItemInfo> = 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<Coord>, known_h: Option<Coord>| {
|
|
(
|
|
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<LayoutItemInfo> = 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<Coord>, known_h: Option<Coord>| {
|
|
(
|
|
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<Coord> {
|
|
let cells_h: Vec<LayoutItemInfo> = constraints
|
|
.iter()
|
|
.map(|c| LayoutItemInfo { constraint: c.clone(), ..Default::default() })
|
|
.collect();
|
|
let cells_v: Vec<LayoutItemInfo> =
|
|
constraints.iter().map(|_| LayoutItemInfo::default()).collect();
|
|
let flex_props: Vec<FlexItemProps> =
|
|
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<LayoutItemInfo> = 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);
|
|
}
|
|
}
|