use kurbo::Size;
fn non_negative(size: Size) -> Size {
Size::new(size.width.max(0.0), size.height.max(0.0))
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BoxConstraints {
min: Size,
max: Size,
}
impl BoxConstraints {
pub fn new(min: Size, max: Size) -> Self {
let min = non_negative(min);
let max = non_negative(max);
#[cfg(debug_assertions)]
if max.width < min.width || max.height < min.height {
eprintln!(
"frust-core: BoxConstraints::new received inverted bounds \
(min {min:?} > max {max:?}); widening max to min — likely an \
upstream layout bug"
);
}
let max = Size::new(max.width.max(min.width), max.height.max(min.height));
Self { min, max }
}
pub fn tight(size: Size) -> Self {
let size = non_negative(size);
Self {
min: size,
max: size,
}
}
pub fn loose(max: Size) -> Self {
Self {
min: Size::ZERO,
max: non_negative(max),
}
}
pub fn min(&self) -> Size {
self.min
}
pub fn max(&self) -> Size {
self.max
}
pub fn constrain(&self, size: Size) -> Size {
Size::new(
size.width.clamp(self.min.width, self.max.width),
size.height.clamp(self.min.height, self.max.height),
)
}
pub fn loosen(&self) -> Self {
Self {
min: Size::ZERO,
max: self.max,
}
}
pub fn tighten(&self, size: Size) -> Self {
let size = self.constrain(size);
Self {
min: size,
max: size,
}
}
pub fn is_tight(&self) -> bool {
self.min == self.max
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constrain_clamps_into_range() {
let bc = BoxConstraints::new(Size::new(10.0, 10.0), Size::new(100.0, 100.0));
assert_eq!(bc.constrain(Size::new(5.0, 5.0)), Size::new(10.0, 10.0));
assert_eq!(
bc.constrain(Size::new(200.0, 200.0)),
Size::new(100.0, 100.0)
);
assert_eq!(bc.constrain(Size::new(50.0, 40.0)), Size::new(50.0, 40.0));
}
#[test]
fn loose_and_loosen_zero_the_minimum() {
let bc = BoxConstraints::loose(Size::new(80.0, 60.0));
assert_eq!(bc.min(), Size::ZERO);
assert_eq!(bc.max(), Size::new(80.0, 60.0));
let tight = BoxConstraints::tight(Size::new(30.0, 30.0));
let loosened = tight.loosen();
assert_eq!(loosened.min(), Size::ZERO);
assert_eq!(loosened.max(), Size::new(30.0, 30.0));
}
#[test]
fn tight_forces_exact_size() {
let bc = BoxConstraints::tight(Size::new(42.0, 24.0));
assert!(bc.is_tight());
assert_eq!(bc.min(), bc.max());
assert_eq!(
bc.constrain(Size::new(1000.0, 1000.0)),
Size::new(42.0, 24.0)
);
}
#[test]
fn tighten_clamps_then_locks() {
let bc = BoxConstraints::new(Size::new(10.0, 10.0), Size::new(100.0, 100.0));
let t = bc.tighten(Size::new(500.0, 5.0));
assert!(t.is_tight());
assert_eq!(t.min(), Size::new(100.0, 10.0));
}
#[test]
fn constructors_reject_negative_sizes() {
let bc = BoxConstraints::loose(Size::new(-5.0, -5.0));
assert_eq!(bc.max(), Size::ZERO);
}
#[test]
fn new_widens_max_when_one_axis_is_inverted() {
let bc = BoxConstraints::new(Size::new(100.0, 10.0), Size::new(10.0, 100.0));
assert!(bc.min().width <= bc.max().width);
assert!(bc.min().height <= bc.max().height);
assert_eq!(bc.min(), Size::new(100.0, 10.0));
assert_eq!(bc.max(), Size::new(100.0, 100.0));
let constrained = bc.constrain(Size::new(0.0, 0.0));
assert_eq!(constrained, Size::new(100.0, 10.0));
}
#[test]
fn new_widens_max_when_both_axes_are_inverted() {
let bc = BoxConstraints::new(Size::new(100.0, 100.0), Size::new(10.0, 10.0));
assert!(bc.min().width <= bc.max().width);
assert!(bc.min().height <= bc.max().height);
assert_eq!(bc.min(), Size::new(100.0, 100.0));
assert_eq!(bc.max(), Size::new(100.0, 100.0));
assert!(bc.is_tight());
let constrained = bc.constrain(Size::new(1000.0, 1000.0));
assert_eq!(constrained, Size::new(100.0, 100.0));
}
}