1use iced::widget::{Column, Row};
7use iced::{Alignment, Element, Length, Padding};
8
9use crate::theme::space;
10
11#[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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum Axis {
42 Vertical,
43 Horizontal,
44}
45
46pub 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
70pub fn vstack<'a, Message>(
72 children: impl IntoIterator<Item = Element<'a, Message>>,
73) -> Stack<'a, Message> {
74 Stack::new(Axis::Vertical, children)
75}
76
77pub 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 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 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 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}