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: 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
72pub fn vstack<'a, Message>(
74 children: impl IntoIterator<Item = Element<'a, Message>>,
75) -> Stack<'a, Message> {
76 Stack::new(Axis::Vertical, children)
77}
78
79pub 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 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 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 pub fn width(mut self, width: impl Into<Length>) -> Self {
126 self.width = Some(width.into());
127 self
128 }
129
130 pub fn height(mut self, height: impl Into<Length>) -> Self {
133 self.height = Some(height.into());
134 self
135 }
136
137 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}