1use presentar_core::{
4 widget::LayoutResult, Canvas, Constraints, Event, Rect, Size, TypeId, Widget,
5};
6use serde::{Deserialize, Serialize};
7use std::any::Any;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
11pub enum StackAlignment {
12 #[default]
14 TopLeft,
15 TopCenter,
17 TopRight,
19 CenterLeft,
21 Center,
23 CenterRight,
25 BottomLeft,
27 BottomCenter,
29 BottomRight,
31}
32
33impl StackAlignment {
34 #[must_use]
36 pub const fn horizontal_ratio(&self) -> f32 {
37 match self {
38 Self::TopLeft | Self::CenterLeft | Self::BottomLeft => 0.0,
39 Self::TopCenter | Self::Center | Self::BottomCenter => 0.5,
40 Self::TopRight | Self::CenterRight | Self::BottomRight => 1.0,
41 }
42 }
43
44 #[must_use]
46 pub const fn vertical_ratio(&self) -> f32 {
47 match self {
48 Self::TopLeft | Self::TopCenter | Self::TopRight => 0.0,
49 Self::CenterLeft | Self::Center | Self::CenterRight => 0.5,
50 Self::BottomLeft | Self::BottomCenter | Self::BottomRight => 1.0,
51 }
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
57pub enum StackFit {
58 #[default]
60 Loose,
61 Expand,
63}
64
65#[derive(Serialize, Deserialize)]
69pub struct Stack {
70 alignment: StackAlignment,
72 fit: StackFit,
74 #[serde(skip)]
76 children: Vec<Box<dyn Widget>>,
77 test_id_value: Option<String>,
79 #[serde(skip)]
81 bounds: Rect,
82}
83
84impl Default for Stack {
85 fn default() -> Self {
86 Self::new()
87 }
88}
89
90impl Stack {
91 #[must_use]
93 pub fn new() -> Self {
94 Self {
95 alignment: StackAlignment::TopLeft,
96 fit: StackFit::Loose,
97 children: Vec::new(),
98 test_id_value: None,
99 bounds: Rect::default(),
100 }
101 }
102
103 #[must_use]
105 pub const fn alignment(mut self, alignment: StackAlignment) -> Self {
106 self.alignment = alignment;
107 self
108 }
109
110 #[must_use]
112 pub const fn fit(mut self, fit: StackFit) -> Self {
113 self.fit = fit;
114 self
115 }
116
117 pub fn child(mut self, widget: impl Widget + 'static) -> Self {
119 self.children.push(Box::new(widget));
120 self
121 }
122
123 #[must_use]
125 pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
126 self.test_id_value = Some(id.into());
127 self
128 }
129
130 #[must_use]
132 pub const fn get_alignment(&self) -> StackAlignment {
133 self.alignment
134 }
135
136 #[must_use]
138 pub const fn get_fit(&self) -> StackFit {
139 self.fit
140 }
141}
142
143impl Widget for Stack {
144 fn type_id(&self) -> TypeId {
145 TypeId::of::<Self>()
146 }
147
148 fn measure(&self, constraints: Constraints) -> Size {
149 if self.children.is_empty() {
150 return match self.fit {
151 StackFit::Loose => Size::ZERO,
152 StackFit::Expand => Size::new(constraints.max_width, constraints.max_height),
153 };
154 }
155
156 let mut max_width = 0.0f32;
157 let mut max_height = 0.0f32;
158
159 for child in &self.children {
161 let child_size = child.measure(constraints);
162 max_width = max_width.max(child_size.width);
163 max_height = max_height.max(child_size.height);
164 }
165
166 match self.fit {
167 StackFit::Loose => constraints.constrain(Size::new(max_width, max_height)),
168 StackFit::Expand => Size::new(constraints.max_width, constraints.max_height),
169 }
170 }
171
172 fn layout(&mut self, bounds: Rect) -> LayoutResult {
173 self.bounds = bounds;
174
175 for child in &mut self.children {
177 let child_constraints = Constraints::loose(bounds.size());
178 let child_size = child.measure(child_constraints);
179
180 let x = (bounds.width - child_size.width)
182 .mul_add(self.alignment.horizontal_ratio(), bounds.x);
183 let y = (bounds.height - child_size.height)
184 .mul_add(self.alignment.vertical_ratio(), bounds.y);
185
186 let child_bounds = Rect::new(x, y, child_size.width, child_size.height);
187 child.layout(child_bounds);
188 }
189
190 LayoutResult {
191 size: bounds.size(),
192 }
193 }
194
195 fn paint(&self, canvas: &mut dyn Canvas) {
196 for child in &self.children {
198 child.paint(canvas);
199 }
200 }
201
202 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
203 for child in self.children.iter_mut().rev() {
205 if let Some(msg) = child.event(event) {
206 return Some(msg);
207 }
208 }
209 None
210 }
211
212 fn children(&self) -> &[Box<dyn Widget>] {
213 &self.children
214 }
215
216 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
217 &mut self.children
218 }
219
220 fn test_id(&self) -> Option<&str> {
221 self.test_id_value.as_deref()
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use presentar_core::Widget;
229
230 #[test]
235 fn test_stack_alignment_default() {
236 assert_eq!(StackAlignment::default(), StackAlignment::TopLeft);
237 }
238
239 #[test]
240 fn test_stack_alignment_horizontal_ratio() {
241 assert_eq!(StackAlignment::TopLeft.horizontal_ratio(), 0.0);
243 assert_eq!(StackAlignment::CenterLeft.horizontal_ratio(), 0.0);
244 assert_eq!(StackAlignment::BottomLeft.horizontal_ratio(), 0.0);
245
246 assert_eq!(StackAlignment::TopCenter.horizontal_ratio(), 0.5);
248 assert_eq!(StackAlignment::Center.horizontal_ratio(), 0.5);
249 assert_eq!(StackAlignment::BottomCenter.horizontal_ratio(), 0.5);
250
251 assert_eq!(StackAlignment::TopRight.horizontal_ratio(), 1.0);
253 assert_eq!(StackAlignment::CenterRight.horizontal_ratio(), 1.0);
254 assert_eq!(StackAlignment::BottomRight.horizontal_ratio(), 1.0);
255 }
256
257 #[test]
258 fn test_stack_alignment_vertical_ratio() {
259 assert_eq!(StackAlignment::TopLeft.vertical_ratio(), 0.0);
261 assert_eq!(StackAlignment::TopCenter.vertical_ratio(), 0.0);
262 assert_eq!(StackAlignment::TopRight.vertical_ratio(), 0.0);
263
264 assert_eq!(StackAlignment::CenterLeft.vertical_ratio(), 0.5);
266 assert_eq!(StackAlignment::Center.vertical_ratio(), 0.5);
267 assert_eq!(StackAlignment::CenterRight.vertical_ratio(), 0.5);
268
269 assert_eq!(StackAlignment::BottomLeft.vertical_ratio(), 1.0);
271 assert_eq!(StackAlignment::BottomCenter.vertical_ratio(), 1.0);
272 assert_eq!(StackAlignment::BottomRight.vertical_ratio(), 1.0);
273 }
274
275 #[test]
280 fn test_stack_fit_default() {
281 assert_eq!(StackFit::default(), StackFit::Loose);
282 }
283
284 #[test]
289 fn test_stack_new() {
290 let stack = Stack::new();
291 assert_eq!(stack.get_alignment(), StackAlignment::TopLeft);
292 assert_eq!(stack.get_fit(), StackFit::Loose);
293 assert!(stack.children().is_empty());
294 }
295
296 #[test]
297 fn test_stack_default() {
298 let stack = Stack::default();
299 assert_eq!(stack.get_alignment(), StackAlignment::TopLeft);
300 assert_eq!(stack.get_fit(), StackFit::Loose);
301 }
302
303 #[test]
304 fn test_stack_builder() {
305 let stack = Stack::new()
306 .alignment(StackAlignment::Center)
307 .fit(StackFit::Expand)
308 .with_test_id("my-stack");
309
310 assert_eq!(stack.get_alignment(), StackAlignment::Center);
311 assert_eq!(stack.get_fit(), StackFit::Expand);
312 assert_eq!(Widget::test_id(&stack), Some("my-stack"));
313 }
314
315 #[test]
320 fn test_stack_empty_loose() {
321 let stack = Stack::new().fit(StackFit::Loose);
322 let size = stack.measure(Constraints::loose(Size::new(100.0, 100.0)));
323 assert_eq!(size, Size::ZERO);
324 }
325
326 #[test]
327 fn test_stack_empty_expand() {
328 let stack = Stack::new().fit(StackFit::Expand);
329 let size = stack.measure(Constraints::loose(Size::new(100.0, 100.0)));
330 assert_eq!(size, Size::new(100.0, 100.0));
331 }
332
333 #[test]
338 fn test_stack_type_id() {
339 let stack = Stack::new();
340 let type_id = Widget::type_id(&stack);
341 assert_eq!(type_id, TypeId::of::<Stack>());
342 }
343
344 #[test]
345 fn test_stack_test_id_none() {
346 let stack = Stack::new();
347 assert_eq!(Widget::test_id(&stack), None);
348 }
349
350 #[test]
351 fn test_stack_test_id_some() {
352 let stack = Stack::new().with_test_id("test-stack");
353 assert_eq!(Widget::test_id(&stack), Some("test-stack"));
354 }
355
356 #[test]
361 fn test_stack_alignment_horizontal_ratios() {
362 assert_eq!(StackAlignment::TopLeft.horizontal_ratio(), 0.0);
363 assert_eq!(StackAlignment::TopCenter.horizontal_ratio(), 0.5);
364 assert_eq!(StackAlignment::TopRight.horizontal_ratio(), 1.0);
365 assert_eq!(StackAlignment::Center.horizontal_ratio(), 0.5);
366 assert_eq!(StackAlignment::BottomRight.horizontal_ratio(), 1.0);
367 }
368
369 #[test]
370 fn test_stack_alignment_vertical_ratios() {
371 assert_eq!(StackAlignment::TopLeft.vertical_ratio(), 0.0);
372 assert_eq!(StackAlignment::CenterLeft.vertical_ratio(), 0.5);
373 assert_eq!(StackAlignment::BottomLeft.vertical_ratio(), 1.0);
374 assert_eq!(StackAlignment::Center.vertical_ratio(), 0.5);
375 assert_eq!(StackAlignment::BottomRight.vertical_ratio(), 1.0);
376 }
377
378 #[test]
379 fn test_stack_alignment_default_is_top_left() {
380 let align = StackAlignment::default();
381 assert_eq!(align, StackAlignment::TopLeft);
382 }
383
384 #[test]
385 fn test_stack_fit_default_is_loose() {
386 let fit = StackFit::default();
387 assert_eq!(fit, StackFit::Loose);
388 }
389
390 #[test]
391 fn test_stack_layout_sets_bounds() {
392 let mut stack = Stack::new();
393 let result = stack.layout(Rect::new(10.0, 20.0, 100.0, 80.0));
394 assert_eq!(result.size, Size::new(100.0, 80.0));
395 assert_eq!(stack.bounds, Rect::new(10.0, 20.0, 100.0, 80.0));
396 }
397
398 #[test]
399 fn test_stack_children_empty() {
400 let stack = Stack::new();
401 assert!(stack.children().is_empty());
402 }
403
404 #[test]
405 fn test_stack_event_no_children() {
406 let mut stack = Stack::new();
407 stack.layout(Rect::new(0.0, 0.0, 100.0, 100.0));
408 let result = stack.event(&Event::MouseEnter);
409 assert!(result.is_none());
410 }
411}