slint/sixtyfps_runtime/corelib/layout.rs
Olivier Goffart fafcbfde2c Fix panic when trying to access layout cache of destroyed items
This can be reproduced by deleting the last item of the printer queue in the
printer demo.
It is a regression showing up because we now emit the MouseExit event after
the mouse grab as released.
The problem is that we upgrade the weak item, and call geometry() on it.
Calling geometry will re-evaluate the layout cache which will re-evaluate
the model which will result in the component being removed and the cache
entry having less item than expected.

It is ok to simply return 0. for these layout location since the item will
disapear anyway.
2021-09-08 14:42:08 +02:00

782 lines
26 KiB
Rust

/* LICENSE BEGIN
This file is part of the SixtyFPS Project -- https://sixtyfps.io
Copyright (c) 2021 Olivier Goffart <olivier.goffart@sixtyfps.io>
Copyright (c) 2021 Simon Hausmann <simon.hausmann@sixtyfps.io>
SPDX-License-Identifier: GPL-3.0-only
This file is also available under commercial licensing terms.
Please contact info@sixtyfps.io for more information.
LICENSE END */
//! Runtime support for layouts.
// cspell:ignore coord
use crate::{slice::Slice, SharedVector};
/// Vertical or Horizontal orientation
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[repr(u8)]
pub enum Orientation {
Horizontal,
Vertical,
}
type Coord = f32;
/// The constraint that applies to an item
// NOTE: when adding new fields, the C++ operator== also need updates
// 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: f32,
/// The maximum size in percentage of the parent (value between 0 and 100).
pub max_percent: f32,
/// The minimum size for this item.
pub min: f32,
/// The minimum size in percentage of the parent (value between 0 and 100).
pub min_percent: f32,
/// the preferred size
pub preferred: f32,
/// the stretch factor
pub stretch: f32,
}
impl Default for LayoutInfo {
fn default() -> Self {
LayoutInfo {
min: 0.,
max: f32::MAX,
min_percent: 0.,
max_percent: 100.,
preferred: 0.,
stretch: 0.,
}
}
}
impl LayoutInfo {
// Note: This "logic" is duplicated in the cpp generator's generated code for merging layout infos.
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
pub fn preferred_bounded(&self) -> f32 {
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)
}
}
mod grid_internal {
use super::*;
fn order_coord(a: &Coord, b: &Coord) -> std::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., max: Coord::MAX, pref: 0., stretch: f32::MAX, pos: 0., size: 0. }
}
}
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;
}
}
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.).map(|it| it.size).sum();
let total_stretch: f32 =
data.iter().filter(|it| A::can_grow(it) > 0.).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.)
.map(|it| A::can_grow(it) / actual_stretch(it.stretch))
.min_by(order_coord)?;
let current_size: Coord =
data.iter().filter(|it| A::can_grow(it) > 0.).map(|it| it.size).sum();
//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);
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.).count() as Coord)
} else {
to_distribute / total_stretch
}
.min(max_grow);
for it in data.iter_mut().filter(|it| A::can_grow(it) > 0.) {
A::distribute(it, grow * actual_stretch(it.stretch));
}
}
}
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.;
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 += 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 for an array of GridLayoutCellData
pub fn to_layout_data(
data: &[GridLayoutCellData],
spacing: Coord,
size: Option<Coord>,
) -> Vec<LayoutData> {
let mut num = 0;
for cell in data {
num = num.max(cell.col_or_row + cell.span);
}
if num < 1 {
return Default::default();
}
let mut layout_data =
vec![grid_internal::LayoutData { stretch: 1., ..Default::default() }; num as usize];
let mut has_spans = false;
for cell in data {
let constraint = &cell.constraint;
let mut max = constraint.max;
if let Some(size) = size {
max = max.min(size * constraint.max_percent / 100.);
}
for c in 0..(cell.span as usize) {
let cdata = &mut layout_data[cell.col_or_row as usize + c];
cdata.max = cdata.max.min(max);
}
if cell.span == 1 {
let mut min = constraint.min;
if let Some(size) = size {
min = min.max(size * constraint.min_percent / 100.);
}
let pref = constraint.preferred.min(max).max(min);
let cdata = &mut layout_data[cell.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 {
// Adjust minimum sizes
for cell in data.iter().filter(|cell| cell.span > 1) {
let span_data = &mut layout_data
[(cell.col_or_row as usize)..(cell.col_or_row + cell.span) as usize];
let mut min = cell.constraint.min;
if let Some(size) = size {
min = min.max(size * cell.constraint.min_percent / 100.);
}
grid_internal::layout_items(span_data, 0., min, spacing);
for cdata in span_data {
if cdata.min < cdata.size {
cdata.min = cdata.size;
}
}
}
// Adjust maximum sizes
for cell in data.iter().filter(|cell| cell.span > 1) {
let span_data = &mut layout_data
[(cell.col_or_row as usize)..(cell.col_or_row + cell.span) as usize];
let mut max = cell.constraint.max;
if let Some(size) = size {
max = max.min(size * cell.constraint.max_percent / 100.);
}
grid_internal::layout_items(span_data, 0., max, spacing);
for cdata in span_data {
if cdata.max > cdata.size {
cdata.max = cdata.size;
}
}
}
// Adjust preferred sizes
for cell in data.iter().filter(|cell| cell.span > 1) {
let span_data = &mut layout_data
[(cell.col_or_row as usize)..(cell.col_or_row + cell.span) as usize];
grid_internal::layout_items(span_data, 0., cell.constraint.preferred, spacing);
for cdata in span_data {
cdata.pref = cdata.pref.max(cdata.size).min(cdata.max).max(cdata.min);
}
}
// Adjust stretches
for cell in data.iter().filter(|cell| cell.span > 1) {
let span_data = &mut layout_data
[(cell.col_or_row as usize)..(cell.col_or_row + cell.span) as usize];
let total_stretch: f32 = span_data.iter().map(|c| c.stretch).sum();
if total_stretch > cell.constraint.stretch {
for cdata in span_data {
cdata.stretch *= cell.constraint.stretch / total_stretch;
}
}
}
}
layout_data
}
}
#[repr(C)]
pub struct Constraint {
pub min: Coord,
pub max: Coord,
}
impl Default for Constraint {
fn default() -> Self {
Constraint { min: 0., max: Coord::MAX }
}
}
#[repr(C)]
#[derive(Debug, Default)]
pub struct Padding {
pub begin: Coord,
pub end: Coord,
}
#[repr(C)]
#[derive(Debug)]
pub struct GridLayoutData<'a> {
pub size: Coord,
pub spacing: Coord,
pub padding: &'a Padding,
pub cells: Slice<'a, GridLayoutCellData>,
}
#[repr(C)]
#[derive(Default, Debug)]
pub struct GridLayoutCellData {
/// col, or row.
pub col_or_row: u16,
/// colspan or rowspan
pub span: u16,
pub constraint: LayoutInfo,
}
/// return, an array which is of size `data.cells.len() * 2` which for each cell we give the pos, size
pub fn solve_grid_layout(data: &GridLayoutData) -> SharedVector<Coord> {
let mut layout_data =
grid_internal::to_layout_data(data.cells.as_slice(), 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::with_capacity(4 * data.cells.len());
for cell in data.cells.iter() {
let cdata = &layout_data[cell.col_or_row as usize];
result.push(cdata.pos);
result.push({
let first_cell = &layout_data[cell.col_or_row as usize];
let last_cell = &layout_data[cell.col_or_row as usize + cell.span as usize - 1];
last_cell.pos + last_cell.size - first_cell.pos
});
}
result
}
pub fn grid_layout_info(
cells: Slice<GridLayoutCellData>,
spacing: Coord,
padding: &Padding,
) -> LayoutInfo {
let layout_data = grid_internal::to_layout_data(cells.as_slice(), spacing, None);
if layout_data.is_empty() {
return Default::default();
}
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).sum::<Coord>() + spacing_w;
let preferred = layout_data.iter().map(|data| data.pref).sum::<Coord>() + spacing_w;
let stretch = layout_data.iter().map(|data| data.stretch).sum::<Coord>();
LayoutInfo { min, max, min_percent: 0., max_percent: 100., preferred, stretch }
}
/// Enum representing the alignment property of a BoxLayout or HorizontalLayout
#[derive(Copy, Clone, Debug, PartialEq, strum_macros::EnumString, strum_macros::Display)]
#[repr(C)]
#[allow(non_camel_case_types)]
pub enum LayoutAlignment {
stretch,
center,
start,
end,
space_between,
space_around,
}
impl Default for LayoutAlignment {
fn default() -> Self {
Self::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: &'a Padding,
pub alignment: LayoutAlignment,
pub cells: Slice<'a, BoxLayoutCellData>,
}
#[repr(C)]
#[derive(Default, Debug, Clone)]
pub struct BoxLayoutCellData {
pub constraint: LayoutInfo,
}
/// Solve a BoxLayout
pub fn solve_box_layout(data: &BoxLayoutData, repeater_indexes: Slice<u32>) -> SharedVector<Coord> {
let mut result = SharedVector::<f32>::default();
result.resize(data.cells.len() * 2 + repeater_indexes.len(), 0.);
if data.cells.is_empty() {
return result;
}
let mut layout_data: Vec<_> = data
.cells
.iter()
.map(|c| {
let min = c.constraint.min.max(c.constraint.min_percent * data.size / 100.);
let max = c.constraint.max.min(c.constraint.max_percent * data.size / 100.);
grid_internal::LayoutData {
min,
max,
pref: c.constraint.preferred.min(max).max(min),
stretch: c.constraint.stretch,
..Default::default()
}
})
.collect();
let size_without_padding = data.size - data.padding.begin - data.padding.end;
let pref_size: Coord = layout_data.iter().map(|it| it.pref).sum();
let num_spacings = (layout_data.len() - 1) as Coord;
let spacings = data.spacing * num_spacings;
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.,
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::space_between => {
Some((data.padding.begin, (size_without_padding - pref_size) / num_spacings))
}
LayoutAlignment::space_around => {
let spacing = (size_without_padding - pref_size) / (num_spacings + 1.);
Some((data.padding.begin + spacing / 2., 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 res = result.make_mut_slice();
// The index/2 in result in which we should add the next repeated item
let mut repeat_offset =
res.len() / 2 - repeater_indexes.iter().skip(1).step_by(2).sum::<u32>() as usize;
// The index/2 in repeater_indexes
let mut next_rep = 0;
// The index/2 in result in which we should add the next non-repeated item
let mut current_offset = 0;
for (idx, layout) in layout_data.iter().enumerate() {
let o = loop {
if let Some(nr) = repeater_indexes.get(next_rep * 2) {
let nr = *nr as usize;
if nr == idx {
for o in 0..2 {
res[current_offset * 2 + o] = (repeat_offset * 2 + o) as _;
}
current_offset += 1;
}
if idx >= nr {
if idx - nr == repeater_indexes[next_rep * 2 + 1] as usize {
next_rep += 1;
continue;
}
repeat_offset += 1;
break repeat_offset - 1;
}
}
current_offset += 1;
break current_offset - 1;
};
res[o * 2] = layout.pos;
res[o * 2 + 1] = layout.size;
}
result
}
/// Return the LayoutInfo for a BoxLayout with the given cells.
pub fn box_layout_info(
cells: Slice<BoxLayoutCellData>,
spacing: Coord,
padding: &Padding,
alignment: LayoutAlignment,
) -> LayoutInfo {
let count = cells.len();
if count < 1 {
return LayoutInfo { max: 0., ..LayoutInfo::default() };
};
let is_stretch = alignment == LayoutAlignment::stretch;
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;
let max = if is_stretch {
(cells.iter().map(|c| c.constraint.max).sum::<Coord>() + extra_w).max(min)
} else {
f32::MAX
};
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., max_percent: 100., preferred, stretch }
}
pub fn box_layout_info_ortho(cells: Slice<BoxLayoutCellData>, padding: &Padding) -> LayoutInfo {
let count = cells.len();
if count < 1 {
return LayoutInfo { max: 0., ..LayoutInfo::default() };
};
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 += extra_w;
fold.preferred += extra_w;
fold
}
#[repr(C)]
pub struct PathLayoutData<'a> {
pub elements: &'a crate::graphics::PathData,
pub item_count: u32,
pub x: Coord,
pub y: Coord,
pub width: Coord,
pub height: Coord,
pub offset: f32,
}
#[repr(C)]
#[derive(Default)]
pub struct PathLayoutItemData {
pub width: Coord,
pub height: Coord,
}
pub fn solve_path_layout(data: &PathLayoutData, repeater_indexes: Slice<u32>) -> SharedVector<f32> {
use lyon_geom::*;
use lyon_path::iterator::PathIterator;
// Clone of path elements is cheap because it's a clone of underlying SharedVector
let mut path_iter = data.elements.clone().iter();
path_iter.fit(data.width, data.height, None);
let tolerance: f32 = 0.1; // lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE
let segment_lengths: Vec<Coord> = path_iter
.iter()
.bezier_segments()
.map(|segment| match segment {
BezierSegment::Linear(line_segment) => line_segment.length(),
BezierSegment::Quadratic(quadratic_segment) => {
quadratic_segment.approximate_length(tolerance)
}
BezierSegment::Cubic(cubic_segment) => cubic_segment.approximate_length(tolerance),
})
.collect();
let path_length: Coord = segment_lengths.iter().sum();
// the max(2) is there to put the item in the middle when there is a single item
let item_distance = 1. / ((data.item_count - 1) as f32).max(2.);
let mut i = 0;
let mut next_t: f32 = data.offset;
if data.item_count == 1 {
next_t += item_distance;
}
let mut result = SharedVector::<f32>::default();
result.resize(data.item_count as usize * 2 + repeater_indexes.len(), 0.);
let res = result.make_mut_slice();
// The index/2 in result in which we should add the next repeated item
let mut repeat_offset =
res.len() / 2 - repeater_indexes.iter().skip(1).step_by(2).sum::<u32>() as usize;
// The index/2 in repeater_indexes
let mut next_rep = 0;
// The index/2 in result in which we should add the next non-repeated item
let mut current_offset = 0;
'main_loop: while i < data.item_count {
let mut current_length: f32 = 0.;
next_t %= 1.;
for (seg_idx, segment) in path_iter.iter().bezier_segments().enumerate() {
let seg_len = segment_lengths[seg_idx];
let seg_start = current_length;
current_length += seg_len;
let seg_end_t = (seg_start + seg_len) / path_length;
while next_t <= seg_end_t {
let local_t = ((next_t * path_length) - seg_start) / seg_len;
let item_pos = segment.sample(local_t);
let o = loop {
if let Some(nr) = repeater_indexes.get(next_rep * 2) {
let nr = *nr;
if nr == i {
for o in 0..4 {
res[current_offset * 4 + o] = (repeat_offset * 4 + o) as _;
}
current_offset += 1;
}
if i >= nr {
if i - nr == repeater_indexes[next_rep * 2 + 1] {
next_rep += 1;
continue;
}
repeat_offset += 1;
break repeat_offset - 1;
}
}
current_offset += 1;
break current_offset - 1;
};
res[o * 2] = item_pos.x + data.x;
res[o * 2 + 1] = item_pos.y + data.y;
i += 1;
next_t += item_distance;
if i >= data.item_count {
break 'main_loop;
}
}
if next_t > 1. {
break;
}
}
}
result
}
#[cfg(feature = "ffi")]
pub(crate) mod ffi {
#![allow(unsafe_code)]
use super::*;
#[no_mangle]
pub extern "C" fn sixtyfps_solve_grid_layout(
data: &GridLayoutData,
result: &mut SharedVector<Coord>,
) {
*result = super::solve_grid_layout(data)
}
#[no_mangle]
pub extern "C" fn sixtyfps_grid_layout_info(
cells: Slice<GridLayoutCellData>,
spacing: Coord,
padding: &Padding,
) -> LayoutInfo {
super::grid_layout_info(cells, spacing, padding)
}
#[no_mangle]
pub extern "C" fn sixtyfps_solve_box_layout(
data: &BoxLayoutData,
repeater_indexes: Slice<u32>,
result: &mut SharedVector<Coord>,
) {
*result = super::solve_box_layout(data, repeater_indexes)
}
#[no_mangle]
/// Return the LayoutInfo for a BoxLayout with the given cells.
pub extern "C" fn sixtyfps_box_layout_info(
cells: Slice<BoxLayoutCellData>,
spacing: Coord,
padding: &Padding,
alignment: LayoutAlignment,
) -> LayoutInfo {
super::box_layout_info(cells, spacing, padding, alignment)
}
#[no_mangle]
/// Return the LayoutInfo for a BoxLayout with the given cells.
pub extern "C" fn sixtyfps_box_layout_info_ortho(
cells: Slice<BoxLayoutCellData>,
padding: &Padding,
) -> LayoutInfo {
super::box_layout_info_ortho(cells, padding)
}
#[no_mangle]
pub extern "C" fn sixtyfps_solve_path_layout(
data: &PathLayoutData,
repeater_indexes: Slice<u32>,
result: &mut SharedVector<Coord>,
) {
*result = super::solve_path_layout(data, repeater_indexes)
}
}