pub mod flex;
pub mod flex_layout;
pub mod zstack;
pub use flex::{Constraints, Size};
pub use flex_layout::Flex;
pub use zstack::ZStack;
use crate::tree::{LayoutCtx, WidgetId};
pub trait IntoF32 {
fn into_f32(self) -> f32;
}
impl IntoF32 for f32 {
fn into_f32(self) -> f32 {
self
}
}
impl IntoF32 for f64 {
fn into_f32(self) -> f32 {
self as f32
}
}
impl IntoF32 for i32 {
fn into_f32(self) -> f32 {
self as f32
}
}
impl IntoF32 for u16 {
fn into_f32(self) -> f32 {
self as f32
}
}
impl IntoF32 for u32 {
fn into_f32(self) -> f32 {
self as f32
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Length {
extent: Extent,
min: f32,
max: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum Extent {
#[default]
Auto,
Exact(f32),
Fill,
Fraction(f32),
}
impl Extent {
pub fn declared_size(&self) -> Option<f32> {
match self {
Extent::Exact(size) | Extent::Fraction(size) => Some(*size),
Extent::Auto | Extent::Fill => None,
}
}
}
const NO_MIN: f32 = f32::NEG_INFINITY;
const NO_MAX: f32 = f32::INFINITY;
impl Default for Length {
fn default() -> Self {
Self {
extent: Extent::default(),
min: NO_MIN,
max: NO_MAX,
}
}
}
impl Length {
pub fn extent(&self) -> Extent {
self.extent
}
pub fn exact_size(&self) -> Option<f32> {
match self.extent {
Extent::Exact(size) => Some(size),
_ => None,
}
}
pub fn is_fill(&self) -> bool {
matches!(self.extent, Extent::Fill)
}
pub(crate) fn settled_without_content(&self, available: f32) -> bool {
match self.extent {
Extent::Exact(_) => true,
Extent::Fill => available.is_finite(),
Extent::Auto | Extent::Fraction(_) => false,
}
}
pub fn min(&self) -> Option<f32> {
(self.min != NO_MIN).then_some(self.min)
}
pub fn max(&self) -> Option<f32> {
(self.max != NO_MAX).then_some(self.max)
}
pub(crate) fn clamp(&self, size: f32) -> f32 {
size.max(self.min).min(self.max)
}
pub(crate) fn clamp_max(&self, size: f32) -> f32 {
size.min(self.max)
}
pub(crate) fn resolve_fraction(&mut self, available: f32) {
if let Extent::Fraction(share) = self.extent
&& available.is_finite()
{
self.extent = Extent::Exact((available * share).max(0.0));
}
}
fn with(self, extent: Extent) -> Self {
Self { extent, ..self }
}
pub fn exact(value: impl IntoF32) -> Self {
Length::default().with(Extent::Exact(value.into_f32()))
}
pub fn at_least(mut self, min: impl IntoF32) -> Self {
self.min = min.into_f32();
self
}
pub fn at_most(mut self, max: impl IntoF32) -> Self {
self.max = max.into_f32();
self
}
}
pub fn at_least(min: impl IntoF32) -> Length {
Length::default().at_least(min)
}
pub fn at_most(max: impl IntoF32) -> Length {
Length::default().at_most(max)
}
pub fn fill() -> Length {
Length::default().with(Extent::Fill)
}
pub fn fraction(f: impl IntoF32) -> Length {
Length::default().with(Extent::Fraction(f.into_f32()))
}
impl From<f32> for Length {
fn from(value: f32) -> Self {
Length::exact(value)
}
}
impl From<f64> for Length {
fn from(value: f64) -> Self {
Length::from(value as f32)
}
}
impl From<i32> for Length {
fn from(value: i32) -> Self {
Length::from(value as f32)
}
}
impl From<u16> for Length {
fn from(value: u16) -> Self {
Length::from(value as f32)
}
}
impl From<u32> for Length {
fn from(value: u32) -> Self {
Length::from(value as f32)
}
}
pub trait Layout {
fn layout(
&mut self,
ctx: &mut LayoutCtx,
children: &[WidgetId],
constraints: Constraints,
origin: (f32, f32),
) -> Size;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Axis {
Horizontal,
Vertical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MainAlignment {
Start,
Center,
End,
SpaceBetween,
SpaceAround,
SpaceEvenly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CrossAlignment {
Start,
Center,
End,
Stretch,
Baseline,
}
crate::reactive::converts!(
f32 => Length,
f64 => Length,
i32 => Length,
u32 => Length,
u16 => Length,
);
#[cfg(test)]
mod a_length_is_what_it_carries {
use super::*;
use crate::reactive::Prop;
use std::mem::size_of;
#[test]
fn and_a_property_holding_one_weighs_the_same() {
assert_eq!(
size_of::<Length>(),
16,
"a length is an extent and two bounds"
);
assert_eq!(
size_of::<Prop<Length>>(),
16,
"and the property tag rides in a niche rather than a word of its own"
);
}
#[test]
fn an_extent_carries_a_number_only_when_it_names_one() {
assert_eq!(
Extent::Auto.declared_size(),
None,
"sizing to content is not a number"
);
assert_eq!(
Extent::Fill.declared_size(),
None,
"nor is taking what is offered"
);
assert_eq!(Extent::Exact(12.0).declared_size(), Some(12.0));
assert_eq!(Extent::Fraction(0.25).declared_size(), Some(0.25));
assert!(
Length::exact(f32::NAN)
.extent()
.declared_size()
.is_some_and(f32::is_nan)
);
assert!(
fraction(f32::NAN)
.extent()
.declared_size()
.is_some_and(f32::is_nan)
);
}
#[test]
fn the_four_sizing_cases_are_mutually_exclusive() {
assert_eq!(Length::default().extent(), Extent::Auto);
assert_eq!(Length::from(10.0f32).extent(), Extent::Exact(10.0));
assert_eq!(fill().extent(), Extent::Fill);
assert_eq!(fraction(0.5).extent(), Extent::Fraction(0.5));
assert_eq!(fill().at_least(4.0).extent(), Extent::Fill);
assert_eq!(at_least(4.0).extent(), Extent::Auto);
}
#[test]
fn an_undeclared_bound_changes_nothing_it_is_applied_to() {
let unbounded = Length::default();
assert_eq!(unbounded.clamp(-10.0), -10.0, "no minimum floors nothing");
assert_eq!(unbounded.clamp(1.0e30), 1.0e30, "no maximum caps nothing");
assert_eq!(
unbounded.clamp(f32::INFINITY),
f32::INFINITY,
"and an unbounded size passes through, which is what a scrolled \
axis hands down"
);
assert_eq!(at_least(4.0).clamp(-10.0), 4.0);
assert_eq!(at_most(40.0).clamp(1.0e30), 40.0);
assert_eq!(at_least(400.0).at_most(50.0).clamp(100.0), 50.0);
}
#[test]
fn a_bound_nobody_declared_reads_as_none() {
assert_eq!(Length::default().min(), None);
assert_eq!(Length::default().max(), None);
assert_eq!(at_least(50.0).min(), Some(50.0));
assert_eq!(at_least(50.0).max(), None);
assert_eq!(at_most(400.0).max(), Some(400.0));
assert_eq!(at_least(50.0).at_most(400.0).min(), Some(50.0));
assert_eq!(at_least(50.0).at_most(400.0).max(), Some(400.0));
assert_eq!(at_most(f32::INFINITY).max(), None);
assert_eq!(at_least(f32::NEG_INFINITY).min(), None);
assert!(at_most(f32::NAN).max().is_some_and(f32::is_nan));
}
}