use iced::widget::{Column, Row};
use iced::{Alignment, Element, Length, Padding};
use crate::theme::space;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Gap {
None,
Xs,
Sm,
#[default]
Md,
Lg,
Xl,
}
impl Gap {
pub const ALL: [Gap; 6] = [Gap::None, Gap::Xs, Gap::Sm, Gap::Md, Gap::Lg, Gap::Xl];
pub const fn pixels(self) -> f32 {
match self {
Gap::None => 0.0,
Gap::Xs => space::XS,
Gap::Sm => space::SM,
Gap::Md => space::MD,
Gap::Lg => space::LG,
Gap::Xl => space::XL,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Axis {
Vertical,
Horizontal,
}
pub struct Stack<'a, Message> {
axis: Axis,
children: Vec<Element<'a, Message>>,
gap: Gap,
align: Alignment,
padding: Padding,
width: Option<Length>,
height: Option<Length>,
wrap: bool,
}
impl<Message> std::fmt::Debug for Stack<'_, Message> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Stack")
.field("axis", &self.axis)
.field("children", &self.children.len())
.field("gap", &self.gap)
.field("align", &self.align)
.field("wrap", &self.wrap)
.finish_non_exhaustive()
}
}
pub fn vstack<'a, Message>(
children: impl IntoIterator<Item = Element<'a, Message>>,
) -> Stack<'a, Message> {
Stack::new(Axis::Vertical, children)
}
pub fn hstack<'a, Message>(
children: impl IntoIterator<Item = Element<'a, Message>>,
) -> Stack<'a, Message> {
Stack::new(Axis::Horizontal, children)
}
impl<'a, Message> Stack<'a, Message> {
fn new(axis: Axis, children: impl IntoIterator<Item = Element<'a, Message>>) -> Self {
Self {
axis,
children: children.into_iter().collect(),
gap: Gap::default(),
align: Alignment::Start,
padding: Padding::ZERO,
width: None,
height: None,
wrap: false,
}
}
pub fn push(mut self, child: impl Into<Element<'a, Message>>) -> Self {
self.children.push(child.into());
self
}
pub fn gap(mut self, gap: Gap) -> Self {
self.gap = gap;
self
}
pub fn align(mut self, align: Alignment) -> Self {
self.align = align;
self
}
pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
self.padding = padding.into();
self
}
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = Some(width.into());
self
}
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = Some(height.into());
self
}
pub fn wrap(mut self) -> Self {
self.wrap = true;
self
}
pub fn axis(&self) -> Axis {
self.axis
}
pub fn len(&self) -> usize {
self.children.len()
}
pub fn is_empty(&self) -> bool {
self.children.is_empty()
}
}
impl<'a, Message: 'a> From<Stack<'a, Message>> for Element<'a, Message> {
fn from(stack: Stack<'a, Message>) -> Self {
let gap = stack.gap.pixels();
match stack.axis {
Axis::Vertical => {
let mut column = Column::with_children(stack.children)
.spacing(gap)
.align_x(stack.align)
.padding(stack.padding);
if let Some(width) = stack.width {
column = column.width(width);
}
if let Some(height) = stack.height {
column = column.height(height);
}
if stack.wrap {
column.wrap().into()
} else {
column.into()
}
}
Axis::Horizontal => {
let mut row = Row::with_children(stack.children)
.spacing(gap)
.align_y(stack.align)
.padding(stack.padding);
if let Some(width) = stack.width {
row = row.width(width);
}
if let Some(height) = stack.height {
row = row.height(height);
}
if stack.wrap {
row.wrap().into()
} else {
row.into()
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_builder_uses_medium_gap_and_start_alignment() {
let s: Stack<'_, ()> = vstack([]);
assert_eq!(s.axis(), Axis::Vertical);
assert_eq!(s.gap, Gap::Md);
assert_eq!(s.align, Alignment::Start);
assert_eq!(s.width, None);
assert_eq!(s.height, None);
assert!(!s.wrap);
assert!(s.is_empty());
}
#[test]
fn hstack_is_horizontal() {
let s: Stack<'_, ()> = hstack([]);
assert_eq!(s.axis(), Axis::Horizontal);
}
#[test]
fn push_appends_children() {
let s: Stack<'_, ()> = vstack(["a".into()]).push("b").push("c");
assert_eq!(s.len(), 3);
}
#[test]
fn gaps_follow_the_spacing_scale() {
assert_eq!(Gap::None.pixels(), 0.0);
assert_eq!(Gap::Xs.pixels(), space::XS);
assert_eq!(Gap::Xl.pixels(), space::XL);
let pixels = Gap::ALL.map(Gap::pixels);
assert!(pixels.windows(2).all(|w| w[0] < w[1]));
}
#[test]
fn options_are_kept() {
let s: Stack<'_, ()> = hstack([])
.gap(Gap::Xl)
.align(Alignment::Center)
.width(Length::Fill)
.wrap();
assert_eq!(s.gap, Gap::Xl);
assert_eq!(s.align, Alignment::Center);
assert_eq!(s.width, Some(Length::Fill));
assert!(s.wrap);
}
#[test]
fn an_unset_size_follows_the_children() {
let filling: Element<'_, ()> =
vstack([iced::widget::space().width(Length::Fill).into()]).into();
assert_eq!(filling.as_widget().size().width, Length::Fill);
let shrinking: Element<'_, ()> = vstack(["a".into()]).into();
assert_eq!(shrinking.as_widget().size().width, Length::Shrink);
let fixed: Element<'_, ()> = hstack([iced::widget::space().width(Length::Fill).into()])
.width(120)
.into();
assert_eq!(fixed.as_widget().size().width, Length::Fixed(120.0));
}
}