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    /// `None` follows the children, as an iced column does: a stack with a
54    /// `Fill` child fills.
55    width: Option<Length>,
56    height: Option<Length>,
57    wrap: bool,
58}
59
60impl<Message> std::fmt::Debug for Stack<'_, Message> {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        f.debug_struct("Stack")
63            .field("axis", &self.axis)
64            .field("children", &self.children.len())
65            .field("gap", &self.gap)
66            .field("align", &self.align)
67            .field("wrap", &self.wrap)
68            .finish_non_exhaustive()
69    }
70}
71
72/// Stacks children top to bottom.
73pub fn vstack<'a, Message>(
74    children: impl IntoIterator<Item = Element<'a, Message>>,
75) -> Stack<'a, Message> {
76    Stack::new(Axis::Vertical, children)
77}
78
79/// Stacks children left to right.
80pub fn hstack<'a, Message>(
81    children: impl IntoIterator<Item = Element<'a, Message>>,
82) -> Stack<'a, Message> {
83    Stack::new(Axis::Horizontal, children)
84}
85
86impl<'a, Message> Stack<'a, Message> {
87    fn new(axis: Axis, children: impl IntoIterator<Item = Element<'a, Message>>) -> Self {
88        Self {
89            axis,
90            children: children.into_iter().collect(),
91            gap: Gap::default(),
92            align: Alignment::Start,
93            padding: Padding::ZERO,
94            width: None,
95            height: None,
96            wrap: false,
97        }
98    }
99
100    /// Adds a child at the end.
101    pub fn push(mut self, child: impl Into<Element<'a, Message>>) -> Self {
102        self.children.push(child.into());
103        self
104    }
105
106    pub fn gap(mut self, gap: Gap) -> Self {
107        self.gap = gap;
108        self
109    }
110
111    /// Aligns children across the stack: horizontally in a `vstack`,
112    /// vertically in an `hstack`.
113    pub fn align(mut self, align: Alignment) -> Self {
114        self.align = align;
115        self
116    }
117
118    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
119        self.padding = padding.into();
120        self
121    }
122
123    /// Overrides the width. Without it the stack shrinks to its children,
124    /// or fills when a child fills.
125    pub fn width(mut self, width: impl Into<Length>) -> Self {
126        self.width = Some(width.into());
127        self
128    }
129
130    /// Overrides the height, which otherwise follows the children as the
131    /// width does.
132    pub fn height(mut self, height: impl Into<Length>) -> Self {
133        self.height = Some(height.into());
134        self
135    }
136
137    /// Moves children onto a new line when they run out of room.
138    pub fn wrap(mut self) -> Self {
139        self.wrap = true;
140        self
141    }
142
143    pub fn axis(&self) -> Axis {
144        self.axis
145    }
146
147    pub fn len(&self) -> usize {
148        self.children.len()
149    }
150
151    pub fn is_empty(&self) -> bool {
152        self.children.is_empty()
153    }
154}
155
156impl<'a, Message: 'a> From<Stack<'a, Message>> for Element<'a, Message> {
157    fn from(stack: Stack<'a, Message>) -> Self {
158        let gap = stack.gap.pixels();
159        match stack.axis {
160            Axis::Vertical => {
161                let mut column = Column::with_children(stack.children)
162                    .spacing(gap)
163                    .align_x(stack.align)
164                    .padding(stack.padding);
165                if let Some(width) = stack.width {
166                    column = column.width(width);
167                }
168                if let Some(height) = stack.height {
169                    column = column.height(height);
170                }
171                if stack.wrap {
172                    column.wrap().into()
173                } else {
174                    column.into()
175                }
176            }
177            Axis::Horizontal => {
178                let mut row = Row::with_children(stack.children)
179                    .spacing(gap)
180                    .align_y(stack.align)
181                    .padding(stack.padding);
182                if let Some(width) = stack.width {
183                    row = row.width(width);
184                }
185                if let Some(height) = stack.height {
186                    row = row.height(height);
187                }
188                if stack.wrap {
189                    row.wrap().into()
190                } else {
191                    row.into()
192                }
193            }
194        }
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn default_builder_uses_medium_gap_and_start_alignment() {
204        let s: Stack<'_, ()> = vstack([]);
205        assert_eq!(s.axis(), Axis::Vertical);
206        assert_eq!(s.gap, Gap::Md);
207        assert_eq!(s.align, Alignment::Start);
208        assert_eq!(s.width, None);
209        assert_eq!(s.height, None);
210        assert!(!s.wrap);
211        assert!(s.is_empty());
212    }
213
214    #[test]
215    fn hstack_is_horizontal() {
216        let s: Stack<'_, ()> = hstack([]);
217        assert_eq!(s.axis(), Axis::Horizontal);
218    }
219
220    #[test]
221    fn push_appends_children() {
222        let s: Stack<'_, ()> = vstack(["a".into()]).push("b").push("c");
223        assert_eq!(s.len(), 3);
224    }
225
226    #[test]
227    fn gaps_follow_the_spacing_scale() {
228        assert_eq!(Gap::None.pixels(), 0.0);
229        assert_eq!(Gap::Xs.pixels(), space::XS);
230        assert_eq!(Gap::Xl.pixels(), space::XL);
231        let pixels = Gap::ALL.map(Gap::pixels);
232        assert!(pixels.windows(2).all(|w| w[0] < w[1]));
233    }
234
235    #[test]
236    fn options_are_kept() {
237        let s: Stack<'_, ()> = hstack([])
238            .gap(Gap::Xl)
239            .align(Alignment::Center)
240            .width(Length::Fill)
241            .wrap();
242        assert_eq!(s.gap, Gap::Xl);
243        assert_eq!(s.align, Alignment::Center);
244        assert_eq!(s.width, Some(Length::Fill));
245        assert!(s.wrap);
246    }
247
248    #[test]
249    fn an_unset_size_follows_the_children() {
250        let filling: Element<'_, ()> =
251            vstack([iced::widget::space().width(Length::Fill).into()]).into();
252        assert_eq!(filling.as_widget().size().width, Length::Fill);
253
254        let shrinking: Element<'_, ()> = vstack(["a".into()]).into();
255        assert_eq!(shrinking.as_widget().size().width, Length::Shrink);
256
257        let fixed: Element<'_, ()> = hstack([iced::widget::space().width(Length::Fill).into()])
258            .width(120)
259            .into();
260        assert_eq!(fixed.as_widget().size().width, Length::Fixed(120.0));
261    }
262}