#[cfg(test)]
use alloc::vec::Vec;
use smallvec::SmallVec;
use super::{Axis, stack_stretch_axis};
use crate::{HorizontalAlignment, ProposalSize, Size, SubView, VerticalAlignment, ViewDimensions};
pub(super) struct ChildMeasurement {
pub(super) dimensions: ViewDimensions,
pub(super) proposal: ProposalSize,
main_stretch: bool,
cross_stretch: bool,
}
impl ChildMeasurement {
pub(super) const fn size(&self) -> Size {
self.dimensions.size
}
pub(super) const fn stretches_main_axis(&self) -> bool {
self.main_stretch
}
pub(super) const fn stretches_cross_axis(&self) -> bool {
self.cross_stretch
}
pub(super) fn vertical_guide(&self, alignment: VerticalAlignment) -> f32 {
self.dimensions.vertical(alignment)
}
pub(super) fn horizontal_guide(&self, alignment: HorizontalAlignment) -> f32 {
self.dimensions.horizontal(alignment)
}
}
const fn main_extent(axis: Axis, size: Size) -> f32 {
if axis.is_horizontal() {
size.width
} else {
size.height
}
}
const fn main_proposal(axis: Axis, proposal: ProposalSize) -> Option<f32> {
if axis.is_horizontal() {
proposal.width
} else {
proposal.height
}
}
const fn with_main(axis: Axis, proposal: ProposalSize, main: Option<f32>) -> ProposalSize {
if axis.is_horizontal() {
proposal.with_width(main)
} else {
proposal.with_height(main)
}
}
pub(super) const fn place_cross_extent(measured: f32, available: f32, stretch: bool) -> f32 {
if measured.is_infinite() {
available
} else if stretch {
measured.max(available)
} else {
measured
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LineAnchor {
Leading,
Center,
Trailing,
Custom,
}
impl LineAnchor {
pub fn vertical(alignment: VerticalAlignment) -> Self {
if alignment == VerticalAlignment::Top {
Self::Leading
} else if alignment == VerticalAlignment::Center {
Self::Center
} else if alignment == VerticalAlignment::Bottom {
Self::Trailing
} else {
Self::Custom
}
}
pub fn horizontal(alignment: HorizontalAlignment) -> Self {
if alignment == HorizontalAlignment::Leading {
Self::Leading
} else if alignment == HorizontalAlignment::Center {
Self::Center
} else if alignment == HorizontalAlignment::Trailing {
Self::Trailing
} else {
Self::Custom
}
}
}
pub fn cross_envelope(children: impl IntoIterator<Item = (f32, f32)>) -> (f32, f32) {
let mut above = f32::NEG_INFINITY;
let mut below = f32::NEG_INFINITY;
for (extent, guide) in children {
if !extent.is_finite() {
continue;
}
above = above.max(guide);
below = below.max(extent - guide);
}
if above.is_finite() {
(above, below)
} else {
(0.0, 0.0)
}
}
pub fn container_line(extent: f32, anchor: LineAnchor, above: f32, below: f32) -> f32 {
let slack = extent - (above + below);
let offset = match anchor {
LineAnchor::Leading | LineAnchor::Custom => 0.0,
LineAnchor::Center => slack * 0.5,
LineAnchor::Trailing => slack,
};
offset + above
}
pub(super) fn stack_spacing(spacing: f32, count: usize) -> f32 {
spacing * usize_to_f32(count.saturating_sub(1))
}
pub(super) fn measure_stack(
axis: Axis,
proposal: ProposalSize,
spacing: f32,
children: &[&dyn SubView],
) -> SmallVec<[ChildMeasurement; 4]> {
let main = main_proposal(axis, proposal);
let probe = if main == Some(0.0) || main == Some(f32::INFINITY) {
proposal
} else {
with_main(axis, proposal, None)
};
let mut measured: SmallVec<[ChildMeasurement; 4]> = children
.iter()
.map(|child| {
let physical = stack_stretch_axis(axis, &[child.stretch_axis()]);
let horizontal = physical.stretches_horizontal();
let vertical = physical.stretches_vertical();
ChildMeasurement {
dimensions: child.measure(probe),
proposal: probe,
main_stretch: if axis.is_horizontal() {
horizontal
} else {
vertical
},
cross_stretch: if axis.is_horizontal() {
vertical
} else {
horizontal
},
}
})
.collect();
let Some(main) = main.filter(|value| value.is_finite() && *value != 0.0) else {
return measured;
};
let available = (main - stack_spacing(spacing, children.len())).max(0.0);
let extents: SmallVec<[Extent; 4]> = children
.iter()
.zip(&measured)
.map(|(child, measurement)| {
let minimum = main_extent(
axis,
child.measure(with_main(axis, proposal, Some(0.0))).size,
);
Extent {
ideal: if measurement.stretches_main_axis() {
available.max(minimum)
} else {
main_extent(
axis,
child
.measure(with_main(axis, proposal, Some(f32::INFINITY)))
.size,
)
.min(available)
.max(minimum)
},
min: minimum,
priority: child.priority(),
}
})
.collect();
for ((child, measurement), allocated) in children
.iter()
.zip(&mut measured)
.zip(compress_to_fit(&extents, available))
{
measurement.proposal = with_main(axis, proposal, Some(allocated));
measurement.dimensions = child.measure(measurement.proposal);
if measurement.stretches_main_axis() {
if axis.is_horizontal() {
measurement.dimensions.size.width = measurement.size().width.max(allocated);
} else {
measurement.dimensions.size.height = measurement.size().height.max(allocated);
}
}
}
measured
}
#[derive(Clone, Copy, Debug)]
pub struct Extent {
pub ideal: f32,
pub min: f32,
pub priority: i32,
}
pub fn compress_to_fit(extents: &[Extent], available: f32) -> SmallVec<[f32; 4]> {
let mut resolved: SmallVec<[f32; 4]> = extents.iter().map(|extent| extent.ideal).collect();
let total: f64 = resolved.iter().copied().map(f64::from).sum();
if !exceeds(total, available, resolved.len()) {
return resolved;
}
let mut priorities: SmallVec<[i32; 4]> = extents.iter().map(|extent| extent.priority).collect();
priorities.sort_unstable_by(|left, right| right.cmp(left));
priorities.dedup();
let mut allocated = 0.0;
for priority in priorities {
let band: SmallVec<[usize; 4]> = extents
.iter()
.enumerate()
.filter(|(_, extent)| extent.priority == priority)
.map(|(index, _)| index)
.collect();
let reserved: f64 = extents
.iter()
.filter(|extent| extent.priority < priority)
.map(|extent| f64::from(extent.min))
.sum();
let band_extents: SmallVec<[Extent; 4]> =
band.iter().map(|&index| extents[index]).collect();
let budget = f64::from(available) - allocated - reserved;
for (&index, extent) in band.iter().zip(water_fill(&band_extents, budget)) {
resolved[index] = extent;
allocated += f64::from(extent);
}
}
resolved
}
fn water_fill(extents: &[Extent], target: f64) -> SmallVec<[f32; 4]> {
use num_traits::ToPrimitive;
let mut total: f64 = extents.iter().map(|extent| f64::from(extent.min)).sum();
if target <= total {
return extents.iter().map(|extent| extent.min).collect();
}
let mut breakpoints: SmallVec<[(f64, f64); 8]> = extents
.iter()
.flat_map(|extent| {
[
(f64::from(extent.min), 1.0),
(f64::from(extent.ideal), -1.0),
]
})
.collect();
breakpoints.sort_unstable_by(|left, right| left.0.total_cmp(&right.0));
let mut cap = 0.0;
let mut active = 0.0;
for (breakpoint, change) in breakpoints {
let next_total = total + active * (breakpoint - cap);
if active > 0.0 && target <= next_total {
let cap = (cap + (target - total) / active)
.to_f32()
.expect("water-filling cap must be representable as f32");
return extents
.iter()
.map(|extent| extent.ideal.min(cap).max(extent.min))
.collect();
}
total = next_total;
cap = breakpoint;
active += change;
}
extents.iter().map(|extent| extent.ideal).collect()
}
pub fn exceeds(total: f64, available: f32, terms: usize) -> bool {
let available = f64::from(available);
let magnitude = total.abs().max(available.abs()).max(1.0);
total > available
&& (total.is_infinite()
|| total - available
> f64::from(f32::EPSILON) * magnitude * f64::from(usize_to_f32(terms.max(1))))
}
fn usize_to_f32(value: usize) -> f32 {
use num_traits::ToPrimitive;
value
.to_f32()
.expect("child count must be representable as f32")
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
fn extent(ideal: f32, min: f32) -> Extent {
Extent {
ideal,
min,
priority: 0,
}
}
#[test]
fn insufficient_space_preserves_exact_minima() {
let extents = [
extent(120.0, 120.0),
extent(164.0, 72.0),
extent(120.0, 120.0),
];
assert_eq!(
compress_to_fit(&extents, 164.0).as_slice(),
&[120.0, 72.0, 120.0]
);
}
#[test]
fn one_flexible_child_receives_the_exact_remainder() {
let extents = [extent(50.0, 50.0), extent(280.0, 0.0), extent(80.0, 80.0)];
assert_eq!(
compress_to_fit(&extents, 280.0).as_slice(),
&[50.0, 150.0, 80.0]
);
}
#[test]
fn large_ideals_do_not_erase_a_small_budget() {
let extents = [extent(1.0e30, 0.0), extent(1.0e30, 0.0)];
assert_eq!(compress_to_fit(&extents, 2.0).as_slice(), &[1.0, 1.0]);
}
#[test]
fn everything_fits_is_a_no_op() {
let extents = vec![extent(30.0, 10.0), extent(40.0, 10.0)];
assert_eq!(compress_to_fit(&extents, 100.0).as_slice(), &[30.0, 40.0]);
}
#[test]
fn equal_children_shrink_equally() {
let extents: Vec<Extent> = (0..7).map(|_| extent(40.0, 0.0)).collect();
let resolved = compress_to_fit(&extents, 224.0);
for width in resolved {
assert!((width - 32.0).abs() < 0.01, "expected 32pt, got {width}");
}
}
#[test]
fn the_widest_child_absorbs_the_deficit_alone() {
let extents = vec![extent(50.0, 0.0), extent(200.0, 0.0)];
let resolved = compress_to_fit(&extents, 140.0);
assert!((resolved[0] - 50.0).abs() < 0.01, "got {}", resolved[0]);
assert!((resolved[1] - 90.0).abs() < 0.01, "got {}", resolved[1]);
}
#[test]
fn a_child_never_shrinks_below_its_reported_minimum() {
let extents = vec![extent(100.0, 20.0), extent(100.0, 80.0)];
let resolved = compress_to_fit(&extents, 120.0);
assert!(resolved[1] >= 80.0 - 0.01, "got {}", resolved[1]);
assert!(resolved[0] >= 20.0 - 0.01, "got {}", resolved[0]);
}
#[test]
fn lower_priority_gives_way_first() {
let extents = vec![
Extent {
ideal: 100.0,
min: 0.0,
priority: 1,
},
Extent {
ideal: 100.0,
min: 0.0,
priority: 0,
},
];
let resolved = compress_to_fit(&extents, 150.0);
assert!(
(resolved[0] - 100.0).abs() < 0.01,
"the prioritized child keeps its ideal, got {}",
resolved[0]
);
assert!(
(resolved[1] - 50.0).abs() < 0.01,
"the lower-priority child gives up the whole deficit, got {}",
resolved[1]
);
}
}