Skip to main content

iced_cube/layout/
stack.rs

1//! Vertical and horizontal stacks with token spacing.
2//!
3//! These are thin wrappers over iced's `column` and `row` that only take
4//! spacing from the theme's scale.
5
6use iced::widget::{Column, Row};
7use iced::{Alignment, Element, Length, Padding};
8
9use crate::theme::space;
10
11/// Space between children, taken from the spacing scale.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
13pub enum Gap {
14    None,
15    Xs,
16    Sm,
17    #[default]
18    Md,
19    Lg,
20    Xl,
21}
22
23impl Gap {
24    pub const ALL: [Gap; 6] = [Gap::None, Gap::Xs, Gap::Sm, Gap::Md, Gap::Lg, Gap::Xl];
25
26    /// The gap in logical pixels.
27    pub const fn pixels(self) -> f32 {
28        match self {
29            Gap::None => 0.0,
30            Gap::Xs => space::XS,
31            Gap::Sm => space::SM,
32            Gap::Md => space::MD,
33            Gap::Lg => space::LG,
34            Gap::Xl => space::XL,
35        }
36    }
37}
38
39/// Which way a stack lays out its children.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum Axis {
42    Vertical,
43    Horizontal,
44}
45
46/// A stack builder. Convert it into an [`Element`] to render.
47pub struct Stack<'a, Message> {
48    axis: Axis,
49    children: Vec<Element<'a, Message>>,
50    gap: Gap,
51    align: Alignment,
52    padding: Padding,
53    width: Length,
54    height: Length,
55    wrap: bool,
56}
57
58impl<Message> std::fmt::Debug for Stack<'_, Message> {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        f.debug_struct("Stack")
61            .field("axis", &self.axis)
62            .field("children", &self.children.len())
63            .field("gap", &self.gap)
64            .field("align", &self.align)
65            .field("wrap", &self.wrap)
66            .finish_non_exhaustive()
67    }
68}
69
70/// Stacks children top to bottom.
71pub fn vstack<'a, Message>(
72    children: impl IntoIterator<Item = Element<'a, Message>>,
73) -> Stack<'a, Message> {
74    Stack::new(Axis::Vertical, children)
75}
76
77/// Stacks children left to right.
78pub fn hstack<'a, Message>(
79    children: impl IntoIterator<Item = Element<'a, Message>>,
80) -> Stack<'a, Message> {
81    Stack::new(Axis::Horizontal, children)
82}
83
84impl<'a, Message> Stack<'a, Message> {
85    fn new(axis: Axis, children: impl IntoIterator<Item = Element<'a, Message>>) -> Self {
86        Self {
87            axis,
88            children: children.into_iter().collect(),
89            gap: Gap::default(),
90            align: Alignment::Start,
91            padding: Padding::ZERO,
92            width: Length::Shrink,
93            height: Length::Shrink,
94            wrap: false,
95        }
96    }
97
98    /// Adds a child at the end.
99    pub fn push(mut self, child: impl Into<Element<'a, Message>>) -> Self {
100        self.children.push(child.into());
101        self
102    }
103
104    pub fn gap(mut self, gap: Gap) -> Self {
105        self.gap = gap;
106        self
107    }
108
109    /// Aligns children across the stack: horizontally in a `vstack`,
110    /// vertically in an `hstack`.
111    pub fn align(mut self, align: Alignment) -> Self {
112        self.align = align;
113        self
114    }
115
116    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
117        self.padding = padding.into();
118        self
119    }
120
121    pub fn width(mut self, width: impl Into<Length>) -> Self {
122        self.width = width.into();
123        self
124    }
125
126    pub fn height(mut self, height: impl Into<Length>) -> Self {
127        self.height = height.into();
128        self
129    }
130
131    /// Moves children onto a new line when they run out of room.
132    pub fn wrap(mut self) -> Self {
133        self.wrap = true;
134        self
135    }
136
137    pub fn axis(&self) -> Axis {
138        self.axis
139    }
140
141    pub fn len(&self) -> usize {
142        self.children.len()
143    }
144
145    pub fn is_empty(&self) -> bool {
146        self.children.is_empty()
147    }
148}
149
150impl<'a, Message: 'a> From<Stack<'a, Message>> for Element<'a, Message> {
151    fn from(stack: Stack<'a, Message>) -> Self {
152        let gap = stack.gap.pixels();
153        match stack.axis {
154            Axis::Vertical => {
155                let column = Column::from_vec(stack.children)
156                    .spacing(gap)
157                    .align_x(stack.align)
158                    .padding(stack.padding)
159                    .width(stack.width)
160                    .height(stack.height);
161                if stack.wrap {
162                    column.wrap().into()
163                } else {
164                    column.into()
165                }
166            }
167            Axis::Horizontal => {
168                let row = Row::from_vec(stack.children)
169                    .spacing(gap)
170                    .align_y(stack.align)
171                    .padding(stack.padding)
172                    .width(stack.width)
173                    .height(stack.height);
174                if stack.wrap {
175                    row.wrap().into()
176                } else {
177                    row.into()
178                }
179            }
180        }
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn default_builder_uses_medium_gap_and_start_alignment() {
190        let s: Stack<'_, ()> = vstack([]);
191        assert_eq!(s.axis(), Axis::Vertical);
192        assert_eq!(s.gap, Gap::Md);
193        assert_eq!(s.align, Alignment::Start);
194        assert_eq!(s.width, Length::Shrink);
195        assert!(!s.wrap);
196        assert!(s.is_empty());
197    }
198
199    #[test]
200    fn hstack_is_horizontal() {
201        let s: Stack<'_, ()> = hstack([]);
202        assert_eq!(s.axis(), Axis::Horizontal);
203    }
204
205    #[test]
206    fn push_appends_children() {
207        let s: Stack<'_, ()> = vstack(["a".into()]).push("b").push("c");
208        assert_eq!(s.len(), 3);
209    }
210
211    #[test]
212    fn gaps_follow_the_spacing_scale() {
213        assert_eq!(Gap::None.pixels(), 0.0);
214        assert_eq!(Gap::Xs.pixels(), space::XS);
215        assert_eq!(Gap::Xl.pixels(), space::XL);
216        let pixels = Gap::ALL.map(Gap::pixels);
217        assert!(pixels.windows(2).all(|w| w[0] < w[1]));
218    }
219
220    #[test]
221    fn options_are_kept() {
222        let s: Stack<'_, ()> = hstack([])
223            .gap(Gap::Xl)
224            .align(Alignment::Center)
225            .width(Length::Fill)
226            .wrap();
227        assert_eq!(s.gap, Gap::Xl);
228        assert_eq!(s.align, Alignment::Center);
229        assert_eq!(s.width, Length::Fill);
230        assert!(s.wrap);
231    }
232}