use taffy::{
Dimension, Display, FlexDirection, FlexWrap, GridPlacement, LengthPercentage,
LengthPercentageAuto, Style,
};
pub use taffy::{AlignItems, AvailableSpace, JustifyContent};
use crate::direction::Direction;
use crate::track::TemplateTrack;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SizeDimension {
Px(f32),
Percent(f32),
Auto,
}
impl From<f32> for SizeDimension {
fn from(px: f32) -> Self {
SizeDimension::Px(px)
}
}
impl From<SizeDimension> for Dimension {
fn from(d: SizeDimension) -> Self {
match d {
SizeDimension::Px(v) => Dimension::length(v),
SizeDimension::Percent(v) => Dimension::percent(v),
SizeDimension::Auto => Dimension::auto(),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub(crate) struct LogicalStyle {
pub(crate) padding_start: Option<f32>,
pub(crate) padding_end: Option<f32>,
pub(crate) margin_start: Option<f32>,
pub(crate) margin_end: Option<f32>,
pub(crate) inset_start: Option<f32>,
pub(crate) inset_end: Option<f32>,
pub(crate) row_follows_direction: bool,
pub(crate) row_forced: bool,
pub(crate) hidden: bool,
pub(crate) min_height_override: Option<f32>,
pub(crate) leading_margin: Option<(bool, f32)>,
}
impl LogicalStyle {
pub(crate) fn has_edges(&self) -> bool {
self.padding_start.is_some()
|| self.padding_end.is_some()
|| self.margin_start.is_some()
|| self.margin_end.is_some()
|| self.inset_start.is_some()
|| self.inset_end.is_some()
}
pub(crate) fn needs_tracking(&self) -> bool {
self.has_edges()
|| self.row_forced
|| self.hidden
|| self.min_height_override.is_some()
|| self.leading_margin.is_some()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct Margin {
pub block_start: f32,
pub block_end: f32,
pub inline_start: f32,
pub inline_end: f32,
}
impl Margin {
pub fn all(px: f32) -> Self {
Self {
block_start: px,
block_end: px,
inline_start: px,
inline_end: px,
}
}
pub fn symmetric(block: f32, inline: f32) -> Self {
Self {
block_start: block,
block_end: block,
inline_start: inline,
inline_end: inline,
}
}
}
#[derive(Clone)]
pub struct LayoutStyle {
pub(crate) inner: Style,
pub(crate) logical: LogicalStyle,
}
impl LayoutStyle {
pub fn new() -> Self {
Self {
inner: Style {
display: Display::Block,
..Style::default()
},
logical: LogicalStyle::default(),
}
}
pub fn flex_row(mut self) -> Self {
self.inner.display = Display::Flex;
self.inner.flex_direction = FlexDirection::Row;
self.logical.row_follows_direction = true;
self
}
pub fn flex_row_reverse(mut self) -> Self {
self.inner.display = Display::Flex;
self.inner.flex_direction = FlexDirection::RowReverse;
self.logical.row_follows_direction = false;
self
}
pub fn flex_column(mut self) -> Self {
self.inner.display = Display::Flex;
self.inner.flex_direction = FlexDirection::Column;
self.logical.row_follows_direction = false;
self
}
pub fn flex_wrap(mut self) -> Self {
self.inner.flex_wrap = FlexWrap::Wrap;
self
}
pub fn display_none(mut self) -> Self {
self.logical.hidden = true;
self
}
pub fn absolute_fill(mut self) -> Self {
self.inner.position = taffy::Position::Absolute;
let zero = LengthPercentageAuto::length(0.0);
self.inner.inset = taffy::Rect {
left: zero,
right: zero,
top: zero,
bottom: zero,
};
self
}
pub fn absolute(mut self) -> Self {
self.inner.position = taffy::Position::Absolute;
self
}
pub fn inset_top(mut self, px: f32) -> Self {
self.inner.inset.top = LengthPercentageAuto::length(px);
self
}
pub fn inset_bottom(mut self, px: f32) -> Self {
self.inner.inset.bottom = LengthPercentageAuto::length(px);
self
}
pub fn width_px(&self) -> Option<f32> {
self.inner.size.width.into_option()
}
pub fn is_width_auto(&self) -> bool {
self.inner.size.width.is_auto()
}
pub fn width(mut self, dim: impl Into<SizeDimension>) -> Self {
self.inner.size.width = dim.into().into();
self
}
pub fn height_px(&self) -> Option<f32> {
self.inner.size.height.into_option()
}
pub fn is_height_auto(&self) -> bool {
self.inner.size.height.is_auto()
}
pub fn height(mut self, dim: impl Into<SizeDimension>) -> Self {
self.inner.size.height = dim.into().into();
self
}
pub fn min_width(mut self, dim: impl Into<SizeDimension>) -> Self {
self.inner.min_size.width = dim.into().into();
self
}
pub fn min_height(mut self, dim: impl Into<SizeDimension>) -> Self {
self.inner.min_size.height = dim.into().into();
self
}
pub fn max_width_px(&self) -> Option<f32> {
self.inner.max_size.width.into_option()
}
pub fn max_width(mut self, dim: impl Into<SizeDimension>) -> Self {
self.inner.max_size.width = dim.into().into();
self
}
pub fn max_height(mut self, dim: impl Into<SizeDimension>) -> Self {
self.inner.max_size.height = dim.into().into();
self
}
pub fn flex_grow(mut self, grow: f32) -> Self {
self.inner.flex_grow = grow;
self
}
pub fn flex_shrink(mut self, shrink: f32) -> Self {
self.inner.flex_shrink = shrink;
self
}
pub fn flex_basis(mut self, dim: impl Into<SizeDimension>) -> Self {
self.inner.flex_basis = dim.into().into();
self
}
pub fn padding_all(mut self, px: f32) -> Self {
let value = LengthPercentage::length(px);
self.inner.padding = taffy::geometry::Rect {
left: value,
right: value,
top: value,
bottom: value,
};
self
}
pub fn padding_horizontal(mut self, px: f32) -> Self {
self.inner.padding.left = LengthPercentage::length(px);
self.inner.padding.right = LengthPercentage::length(px);
self
}
pub fn padding_vertical(mut self, px: f32) -> Self {
self.inner.padding.top = LengthPercentage::length(px);
self.inner.padding.bottom = LengthPercentage::length(px);
self
}
pub fn padding_top(mut self, px: f32) -> Self {
self.inner.padding.top = LengthPercentage::length(px);
self
}
pub fn padding_bottom(mut self, px: f32) -> Self {
self.inner.padding.bottom = LengthPercentage::length(px);
self
}
pub fn padding_left(mut self, px: f32) -> Self {
self.inner.padding.left = LengthPercentage::length(px);
self
}
pub fn padding_right(mut self, px: f32) -> Self {
self.inner.padding.right = LengthPercentage::length(px);
self
}
pub fn padding_start(mut self, px: f32) -> Self {
self.logical.padding_start = Some(px);
self
}
pub fn padding_end(mut self, px: f32) -> Self {
self.logical.padding_end = Some(px);
self
}
pub fn margin(self, m: Margin) -> Self {
self.margin_block_start(m.block_start)
.margin_block_end(m.block_end)
.margin_inline_start(m.inline_start)
.margin_inline_end(m.inline_end)
}
pub fn margin_block_start(mut self, px: f32) -> Self {
self.inner.margin.top = LengthPercentageAuto::length(px);
self
}
pub fn margin_block_end(mut self, px: f32) -> Self {
self.inner.margin.bottom = LengthPercentageAuto::length(px);
self
}
pub fn margin_inline_start(mut self, px: f32) -> Self {
self.logical.margin_start = Some(px);
self
}
pub fn margin_inline_end(mut self, px: f32) -> Self {
self.logical.margin_end = Some(px);
self
}
pub fn margin_from_left(mut self, px: f32) -> Self {
self.inner.margin.left = LengthPercentageAuto::length(px);
self
}
pub fn inset_start(mut self, px: f32) -> Self {
self.logical.inset_start = Some(px);
self
}
pub fn inset_end(mut self, px: f32) -> Self {
self.logical.inset_end = Some(px);
self
}
pub fn gap(mut self, px: f32) -> Self {
self.inner.gap = taffy::geometry::Size {
width: LengthPercentage::length(px),
height: LengthPercentage::length(px),
};
self
}
pub fn gap_x(mut self, px: f32) -> Self {
self.inner.gap.width = LengthPercentage::length(px);
self
}
pub fn gap_y(mut self, px: f32) -> Self {
self.inner.gap.height = LengthPercentage::length(px);
self
}
pub fn align_items(mut self, value: AlignItems) -> Self {
self.inner.align_items = Some(value);
self
}
pub fn align_self_stretch(mut self) -> Self {
self.inner.align_self = Some(taffy::AlignSelf::STRETCH);
self
}
pub fn align_self_center(mut self) -> Self {
self.inner.align_self = Some(taffy::AlignSelf::CENTER);
self
}
pub fn align_self_start(mut self) -> Self {
self.inner.align_self = Some(taffy::AlignSelf::FLEX_START);
self
}
pub fn align_self_end(mut self) -> Self {
self.inner.align_self = Some(taffy::AlignSelf::FLEX_END);
self
}
pub fn justify_content(mut self, value: JustifyContent) -> Self {
self.inner.justify_content = Some(value);
self
}
pub fn display_grid(mut self) -> Self {
self.inner.display = Display::Grid;
self
}
pub fn grid_template_columns(mut self, tracks: Vec<TemplateTrack>) -> Self {
self.inner.grid_template_columns = tracks
.into_iter()
.map(|t| t.into_template_component())
.collect();
self
}
pub fn grid_column_span(mut self, count: u16) -> Self {
self.inner.grid_column = taffy::geometry::Line {
start: GridPlacement::Span(count),
end: GridPlacement::Auto,
};
self
}
pub fn grid_row_span(mut self, count: u16) -> Self {
self.inner.grid_row = taffy::geometry::Line {
start: GridPlacement::Span(count),
end: GridPlacement::Auto,
};
self
}
pub fn aspect_ratio(mut self, ratio: f32) -> Self {
self.inner.aspect_ratio = Some(ratio);
self
}
pub(crate) fn resolve(&self, direction: Direction) -> Style {
let mut style = self.inner.clone();
let logical = &self.logical;
if logical.row_follows_direction || logical.row_forced {
style.flex_direction = if direction.is_rtl() {
FlexDirection::RowReverse
} else {
FlexDirection::Row
};
}
if logical.hidden {
style.display = Display::None;
}
if let Some(min_height) = logical.min_height_override {
style.min_size.height = Dimension::length(min_height);
}
let (start, end) = if direction.is_rtl() {
(Edge::Right, Edge::Left)
} else {
(Edge::Left, Edge::Right)
};
for (edge, px) in [(start, logical.padding_start), (end, logical.padding_end)] {
if let Some(px) = px {
*edge.of_mut(&mut style.padding) = LengthPercentage::length(px);
}
}
for (edge, px) in [(start, logical.margin_start), (end, logical.margin_end)] {
if let Some(px) = px {
*edge.of_mut(&mut style.margin) = LengthPercentageAuto::length(px);
}
}
for (edge, px) in [(start, logical.inset_start), (end, logical.inset_end)] {
if let Some(px) = px {
*edge.of_mut(&mut style.inset) = LengthPercentageAuto::length(px);
}
}
style
}
}
#[derive(Clone, Copy)]
enum Edge {
Left,
Right,
}
impl Edge {
fn of_mut<T>(self, rect: &mut taffy::geometry::Rect<T>) -> &mut T {
match self {
Edge::Left => &mut rect.left,
Edge::Right => &mut rect.right,
}
}
}
impl Default for LayoutStyle {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn logical_padding_resolves_to_the_edge_the_direction_starts_from() {
let style = LayoutStyle::new().padding_start(8.0).padding_end(2.0);
let ltr = style.resolve(Direction::Ltr);
assert_eq!(ltr.padding.left, LengthPercentage::length(8.0));
assert_eq!(ltr.padding.right, LengthPercentage::length(2.0));
let rtl = style.resolve(Direction::Rtl);
assert_eq!(rtl.padding.right, LengthPercentage::length(8.0));
assert_eq!(rtl.padding.left, LengthPercentage::length(2.0));
}
#[test]
fn a_physical_edge_is_left_alone_by_the_direction() {
let style = LayoutStyle::new().padding_left(12.0);
for direction in [Direction::Ltr, Direction::Rtl] {
let resolved = style.resolve(direction);
assert_eq!(resolved.padding.left, LengthPercentage::length(12.0));
assert_eq!(resolved.padding.right, LengthPercentage::length(0.0));
}
}
#[test]
fn resolving_twice_does_not_accumulate() {
let style = LayoutStyle::new().padding_start(8.0);
let _ = style.resolve(Direction::Rtl);
let back = style.resolve(Direction::Ltr);
assert_eq!(back.padding.left, LengthPercentage::length(8.0));
assert_eq!(back.padding.right, LengthPercentage::length(0.0));
}
#[test]
fn a_row_reverses_under_rtl_but_an_explicit_reverse_does_not_flip_back() {
let row = LayoutStyle::new().flex_row();
assert_eq!(
row.resolve(Direction::Ltr).flex_direction,
FlexDirection::Row
);
assert_eq!(
row.resolve(Direction::Rtl).flex_direction,
FlexDirection::RowReverse
);
let reversed = LayoutStyle::new().flex_row_reverse();
for direction in [Direction::Ltr, Direction::Rtl] {
assert_eq!(
reversed.resolve(direction).flex_direction,
FlexDirection::RowReverse,
"an explicit reverse is physical"
);
}
}
#[test]
fn a_column_is_unaffected_by_direction() {
let col = LayoutStyle::new().flex_column();
for direction in [Direction::Ltr, Direction::Rtl] {
assert_eq!(col.resolve(direction).flex_direction, FlexDirection::Column);
}
}
#[test]
fn only_logical_edges_need_the_style_kept_for_a_flip() {
assert!(!LayoutStyle::new().flex_row().logical.has_edges());
assert!(
LayoutStyle::new()
.margin_inline_start(4.0)
.logical
.has_edges()
);
assert!(LayoutStyle::new().inset_end(4.0).logical.has_edges());
}
#[test]
fn style_default_is_block() {
let style = LayoutStyle::new();
assert_eq!(style.inner.display, Display::Block);
}
#[test]
fn style_width_sets_dimension() {
let style = LayoutStyle::new().width(120.0);
assert_eq!(style.inner.size.width, Dimension::length(120.0));
}
#[test]
fn style_width_px_reads_back_length() {
let style = LayoutStyle::new().width(120.0);
assert_eq!(style.width_px(), Some(120.0));
assert!(!style.is_width_auto());
}
#[test]
fn style_width_px_none_for_percent_or_default() {
assert_eq!(LayoutStyle::new().width_px(), None);
assert!(LayoutStyle::new().is_width_auto());
let percent = LayoutStyle::new().width(SizeDimension::Percent(0.5));
assert_eq!(percent.width_px(), None);
assert!(!percent.is_width_auto());
}
#[test]
fn style_width_percent_sets_dimension() {
let style = LayoutStyle::new().width(SizeDimension::Percent(0.5));
assert_eq!(style.inner.size.width, Dimension::percent(0.5));
}
#[test]
fn style_height_sets_dimension() {
let style = LayoutStyle::new().height(80.0);
assert_eq!(style.inner.size.height, Dimension::length(80.0));
}
#[test]
fn style_max_width_sets_dimension() {
let style = LayoutStyle::new().max_width(200.0);
assert_eq!(style.inner.max_size.width, Dimension::length(200.0));
}
#[test]
fn style_max_height_sets_dimension() {
let style = LayoutStyle::new().max_height(150.0);
assert_eq!(style.inner.max_size.height, Dimension::length(150.0));
}
#[test]
fn style_flex_basis_percent_sets_dimension() {
let style = LayoutStyle::new().flex_basis(SizeDimension::Percent(0.5));
assert_eq!(style.inner.flex_basis, Dimension::percent(0.5));
}
#[test]
fn style_flex_row_sets_direction() {
let style = LayoutStyle::new().flex_row();
assert_eq!(style.inner.flex_direction, FlexDirection::Row);
}
#[test]
fn style_flex_column_sets_direction() {
let style = LayoutStyle::new().flex_column();
assert_eq!(style.inner.flex_direction, FlexDirection::Column);
}
#[test]
fn style_align_items_center_sets_field() {
let style = LayoutStyle::new().align_items(AlignItems::CENTER);
assert_eq!(style.inner.align_items, Some(taffy::AlignItems::CENTER));
}
#[test]
fn style_justify_center_sets_field() {
let style = LayoutStyle::new().justify_content(JustifyContent::CENTER);
assert_eq!(
style.inner.justify_content,
Some(taffy::JustifyContent::CENTER)
);
}
#[test]
fn style_default_impl_matches_new() {
let style = LayoutStyle::default();
assert_eq!(style.inner.display, Display::Block);
}
#[test]
fn style_padding_horizontal_sets_left_right() {
let style = LayoutStyle::new().padding_horizontal(10.0);
assert_eq!(style.inner.padding.left, LengthPercentage::length(10.0));
assert_eq!(style.inner.padding.right, LengthPercentage::length(10.0));
}
#[test]
fn style_padding_vertical_sets_top_bottom() {
let style = LayoutStyle::new().padding_vertical(8.0);
assert_eq!(style.inner.padding.top, LengthPercentage::length(8.0));
assert_eq!(style.inner.padding.bottom, LengthPercentage::length(8.0));
}
#[test]
fn style_padding_top_sets_field() {
let style = LayoutStyle::new().padding_top(4.0);
assert_eq!(style.inner.padding.top, LengthPercentage::length(4.0));
}
#[test]
fn margin_writes_the_block_pair_directly_and_defers_the_inline_pair() {
let style = LayoutStyle::new().margin(Margin::symmetric(6.0, 12.0));
assert_eq!(style.inner.margin.top, LengthPercentageAuto::length(6.0));
assert_eq!(style.inner.margin.bottom, LengthPercentageAuto::length(6.0));
assert_eq!(style.logical.margin_start, Some(12.0));
assert_eq!(style.logical.margin_end, Some(12.0));
}
#[test]
fn a_margin_from_the_left_stays_left_under_rtl() {
let style = LayoutStyle::new().margin_from_left(20.0);
let ltr = style.resolve(Direction::Ltr);
let rtl = style.resolve(Direction::Rtl);
assert_eq!(ltr.margin.left, LengthPercentageAuto::length(20.0));
assert_eq!(rtl.margin.left, LengthPercentageAuto::length(20.0));
}
}