use crate::compat::{vec, Vec};
use crate::core::{ObjectId, Size};
use crate::style::EdgeOffsets;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AxisHints {
pub min: u32,
pub pref: u32,
pub max: u32,
}
impl AxisHints {
pub fn new(min: u32, pref: u32, max: u32) -> Self {
let max = max.max(pref);
let min = min.min(max);
let pref = pref.clamp(min, max);
Self { min, pref, max }
}
pub fn fixed(size: u32) -> Self {
Self { min: size, pref: size, max: size }
}
pub fn at_least(min: u32) -> Self {
Self { min, pref: min, max: u32::MAX }
}
pub fn at_most(max: u32) -> Self {
Self { min: 0, pref: max, max }
}
pub fn unconstrained() -> Self {
Self { min: 0, pref: 0, max: u32::MAX }
}
pub fn clamp(&self, value: u32) -> u32 {
value.clamp(self.min, self.max)
}
pub fn is_fixed(&self) -> bool {
self.min == self.max
}
}
impl Default for AxisHints {
fn default() -> Self {
Self::unconstrained()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Hints {
pub width: AxisHints,
pub height: AxisHints,
}
impl Hints {
pub fn fixed(width: u32, height: u32) -> Self {
Self { width: AxisHints::fixed(width), height: AxisHints::fixed(height) }
}
pub fn at_least(width: u32, height: u32) -> Self {
Self { width: AxisHints::at_least(width), height: AxisHints::at_least(height) }
}
pub fn preferred(&self) -> Size {
Size::new(self.width.pref, self.height.pref)
}
pub fn clamp_to(&self, bounds: Size) -> Size {
Size::new(self.width.clamp(bounds.width), self.height.clamp(bounds.height))
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct LayoutParams {
pub fill: bool,
pub stretch: u32,
pub margins: EdgeOffsets,
}
impl LayoutParams {
pub fn new() -> Self {
Self::default()
}
pub fn filled() -> Self {
Self { fill: true, ..Self::default() }
}
pub fn stretched(stretch: u32) -> Self {
Self { fill: true, stretch, ..Self::default() }
}
pub fn with_margins(mut self, margins: EdgeOffsets) -> Self {
self.margins = margins;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChildInfo {
pub id: ObjectId,
pub hints: Hints,
pub params: LayoutParams,
}
impl ChildInfo {
pub fn new(id: ObjectId, hints: Hints) -> Self {
Self { id, hints, params: LayoutParams::new() }
}
pub fn with_params(mut self, params: LayoutParams) -> Self {
self.params = params;
self
}
pub fn find(children: &[ChildInfo], id: ObjectId) -> Option<&ChildInfo> {
children.iter().find(|child| child.id == id)
}
pub fn bounds(&self) -> Size {
let preferred = self.hints.preferred();
Size::new(
preferred.width.saturating_add(self.params.margins.horizontal_total()),
preferred.height.saturating_add(self.params.margins.vertical_total()),
)
}
}
pub fn total_bounds(children: &[ChildInfo], vertical: bool) -> u32 {
children
.iter()
.map(|child| if vertical { child.bounds().height } else { child.bounds().width })
.sum()
}
pub fn total_preferred(children: &[ChildInfo], vertical: bool) -> u32 {
children.iter().map(|child| axis(&child.hints, vertical).pref).sum()
}
pub fn total_minimum(children: &[ChildInfo], vertical: bool) -> u32 {
children.iter().map(|child| axis(&child.hints, vertical).min).sum()
}
pub fn max_preferred(children: &[ChildInfo], vertical: bool) -> u32 {
children.iter().map(|child| axis(&child.hints, vertical).pref).max().unwrap_or(0)
}
pub fn axis(hints: &Hints, vertical: bool) -> AxisHints {
if vertical {
hints.height
} else {
hints.width
}
}
pub fn placement_order(children: &[ChildInfo]) -> Vec<ObjectId> {
children.iter().map(|child| child.id).collect::<Vec<_>>()
}
pub fn filling(children: &[ChildInfo]) -> Vec<ObjectId> {
let mut ids = vec![];
for child in children {
if child.params.fill {
ids.push(child.id);
}
}
ids
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_new_hint_is_always_ordered() {
let hint = AxisHints::new(30, 10, 50);
assert_eq!(hint, AxisHints { min: 30, pref: 30, max: 50 });
let hint = AxisHints::new(10, 60, 50);
assert_eq!(hint, AxisHints { min: 10, pref: 60, max: 60 });
for (min, pref, max) in [(0, 0, 0), (5, 5, 5), (100, 1, 2), (1, 100, 2), (50, 40, 30)] {
let hint = AxisHints::new(min, pref, max);
assert!(
hint.min <= hint.pref && hint.pref <= hint.max,
"{min}/{pref}/{max} normalised to {hint:?}"
);
}
}
#[test]
fn a_contradictory_range_stays_representable() {
let hint = AxisHints::new(50, 40, 30);
assert!(hint.min <= hint.pref && hint.pref <= hint.max);
assert_eq!(hint.pref, 40, "the caller's preferred size is the anchor");
assert!(hint.clamp(1) >= hint.min);
assert!(hint.clamp(999) <= hint.max);
}
#[test]
fn the_three_constructors_express_the_three_shapes() {
let fixed = AxisHints::fixed(40);
assert!(fixed.is_fixed());
assert_eq!(fixed.clamp(0), 40);
assert_eq!(fixed.clamp(100), 40);
let floor = AxisHints::at_least(30);
assert_eq!(floor.clamp(5), 30, "a floor cannot be squeezed past");
assert_eq!(floor.clamp(500), 500, "and imposes no ceiling");
let ceiling = AxisHints::at_most(30);
assert_eq!(ceiling.clamp(5), 5, "a ceiling does not force growth");
assert_eq!(ceiling.clamp(500), 30, "but it does stop growth");
}
#[test]
fn the_default_hint_preserves_the_old_size_hint_behaviour() {
let none = Hints::default();
assert_eq!(none.preferred(), Size::new(0, 0));
assert_eq!(none.width.clamp(1234), 1234);
assert_eq!(none.height.clamp(0), 0);
}
#[test]
fn both_axes_are_kept_separate() {
let hints = Hints { width: AxisHints::at_least(120), height: AxisHints::fixed(24) };
assert_eq!(hints.height.clamp(999), 24);
assert_eq!(hints.width.clamp(999), 999);
assert_eq!(hints.preferred(), Size::new(120, 24));
}
#[test]
fn clamping_to_bounds_respects_each_axis_independently() {
let hints = Hints::at_least(100, 50);
let clamped = hints.clamp_to(Size::new(60, 200));
assert_eq!(clamped, Size::new(100, 200));
}
#[test]
fn fill_is_declared_separately_from_size() {
let button = ChildInfo::new(1, Hints::fixed(64, 40));
let slider = ChildInfo::new(2, Hints::fixed(64, 40)).with_params(LayoutParams::filled());
assert_eq!(button.hints, slider.hints);
assert!(!button.params.fill);
assert!(slider.params.fill);
assert_eq!(filling(&[button, slider]), vec![2]);
}
#[test]
fn the_axis_selector_reads_the_requested_axis() {
let hints = Hints { width: AxisHints::fixed(7), height: AxisHints::fixed(9) };
assert_eq!(axis(&hints, false).pref, 7);
assert_eq!(axis(&hints, true).pref, 9);
}
#[test]
fn the_aggregates_sum_and_maximise_over_the_right_axis() {
let children = vec![
ChildInfo::new(1, Hints::at_least(10, 4)),
ChildInfo::new(2, Hints::at_least(20, 6)),
];
assert_eq!(total_preferred(&children, false), 30);
assert_eq!(total_minimum(&children, false), 30);
assert_eq!(total_preferred(&children, true), 10);
assert_eq!(max_preferred(&children, true), 6);
assert_eq!(max_preferred(&[], false), 0, "an empty list has no largest child");
}
#[test]
fn a_child_can_be_looked_up_by_id() {
let children =
vec![ChildInfo::new(1, Hints::fixed(1, 1)), ChildInfo::new(2, Hints::fixed(2, 2))];
assert_eq!(ChildInfo::find(&children, 2).map(|c| c.hints.width.pref), Some(2));
assert!(ChildInfo::find(&children, 9).is_none());
assert_eq!(placement_order(&children), vec![1, 2]);
}
#[test]
fn stretched_params_carry_their_weight_and_margins() {
let params = LayoutParams::stretched(3).with_margins(EdgeOffsets::symmetric(4, 8));
assert!(params.fill);
assert_eq!(params.stretch, 3);
assert_eq!(params.margins.horizontal_total(), 16);
}
}