use super::spatial::{Bounds, Placement, Resolved};
use crate::{Node, ValidationError, Vector2};
#[cfg(test)]
mod tests;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Alignment {
#[default]
Start,
Center,
End,
Baseline,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Layout {
Row {
gap: f64,
alignment: Alignment,
},
Column {
gap: f64,
alignment: Alignment,
},
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Row {
gap: f64,
alignment: Alignment,
}
impl Row {
pub fn new() -> Self {
Self::default()
}
pub fn gap(mut self, gap: f64) -> Self {
self.gap = gap;
self
}
pub fn alignment(mut self, alignment: Alignment) -> Self {
self.alignment = alignment;
self
}
pub fn children<Child: Into<Node>>(self, children: impl IntoIterator<Item = Child>) -> Node {
Node::layout(
Layout::Row {
gap: self.gap,
alignment: self.alignment,
},
children,
)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Column {
gap: f64,
alignment: Alignment,
}
impl Column {
pub fn new() -> Self {
Self::default()
}
pub fn gap(mut self, gap: f64) -> Self {
self.gap = gap;
self
}
pub fn alignment(mut self, alignment: Alignment) -> Self {
self.alignment = alignment;
self
}
pub fn children<Child: Into<Node>>(self, children: impl IntoIterator<Item = Child>) -> Node {
Node::layout(
Layout::Column {
gap: self.gap,
alignment: self.alignment,
},
children,
)
}
}
impl Layout {
pub(super) fn baselines(
self,
children: &[Node],
context: &crate::DrawContext,
path: &str,
) -> Result<Vec<Option<f64>>, ValidationError> {
if !matches!(
self,
Self::Row {
alignment: Alignment::Baseline,
..
}
) {
return Ok(vec![None; children.len()]);
}
children
.iter()
.enumerate()
.map(|(index, child)| child.baseline(context, &format!("{path}.children[{index}]")))
.collect()
}
pub(super) fn placements(
self,
children: &[(usize, Bounds)],
baselines: &[Option<f64>],
path: &str,
) -> Result<Vec<Option<(Resolved, Bounds)>>, ValidationError> {
let (horizontal, gap, alignment) = match self {
Self::Row { gap, alignment } => (true, gap, alignment),
Self::Column { gap, alignment } => (false, gap, alignment),
};
if !gap.is_finite() {
return Err(ValidationError::invalid(
path,
"gap",
"Layout gap must be finite",
));
}
if alignment == Alignment::Baseline && !horizontal {
return Err(ValidationError::unsupported(
path,
"alignment",
"Baseline alignment is supported only in horizontal rows",
));
}
let mut cross_extent = 0.0_f64;
let mut common_baseline = 0.0_f64;
for (index, bounds) in children {
if matches!(bounds, Bounds::Empty) {
continue;
}
let rect = bounds.rect(&format!("{path}.children[{index}]"))?;
if alignment == Alignment::Baseline {
let baseline = baselines[*index].ok_or_else(|| {
ValidationError::unsupported(
path,
"alignment",
"Baseline requires text metrics for every nonempty child",
)
})?;
let offset = baseline - rect.top;
if !offset.is_finite() {
return Err(ValidationError::invalid(
path,
"baseline",
"Baseline offset must be finite",
));
}
common_baseline = common_baseline.max(offset);
}
if !rect.width().is_finite() || !rect.height().is_finite() {
return Err(ValidationError::invalid(
path,
"layout_bounds",
"Layout spans must be finite",
));
}
cross_extent = cross_extent.max(if horizontal {
rect.height()
} else {
rect.width()
});
}
let mut cursor = 0.0_f64;
let mut occupied = false;
let mut placements = Vec::with_capacity(children.len());
for (index, bounds) in children {
if matches!(bounds, Bounds::Empty) {
placements.push(None);
continue;
}
let child_path = format!("{path}.children[{index}]");
let rect = bounds.rect(&child_path)?;
if occupied {
cursor += gap;
}
let (main_size, cross_size) = if horizontal {
(rect.width(), rect.height())
} else {
(rect.height(), rect.width())
};
let cross = match alignment {
Alignment::Start => 0.0,
Alignment::Center => (cross_extent - cross_size) * 0.5,
Alignment::End => cross_extent - cross_size,
Alignment::Baseline => {
common_baseline - (baselines[*index].expect("validated baseline") - rect.top)
}
};
let offset = if horizontal {
Vector2::new(cursor - rect.left, cross - rect.top)
} else {
Vector2::new(cross - rect.left, cursor - rect.top)
};
let placement = Placement {
translation: offset,
..Placement::default()
}
.resolve(*bounds, &child_path)?;
let placed_bounds = bounds.transformed(placement.transform, &child_path)?;
placements.push(Some((placement, placed_bounds)));
cursor += main_size;
if !cursor.is_finite() {
return Err(ValidationError::invalid(
path,
"layout_bounds",
"Layout extent must remain finite",
));
}
occupied = true;
}
Ok(placements)
}
}