presentar_widgets/
stack.rs

1//! Stack widget for z-axis overlapping children.
2
3use presentar_core::{
4    widget::LayoutResult, Canvas, Constraints, Event, Rect, Size, TypeId, Widget,
5};
6use serde::{Deserialize, Serialize};
7use std::any::Any;
8
9/// How to align children within the stack.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
11pub enum StackAlignment {
12    /// Align to top-left corner
13    #[default]
14    TopLeft,
15    /// Align to top center
16    TopCenter,
17    /// Align to top-right corner
18    TopRight,
19    /// Align to center-left
20    CenterLeft,
21    /// Center both axes
22    Center,
23    /// Align to center-right
24    CenterRight,
25    /// Align to bottom-left corner
26    BottomLeft,
27    /// Align to bottom center
28    BottomCenter,
29    /// Align to bottom-right corner
30    BottomRight,
31}
32
33impl StackAlignment {
34    /// Get horizontal offset ratio (0.0 = left, 0.5 = center, 1.0 = right).
35    #[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    /// Get vertical offset ratio (0.0 = top, 0.5 = center, 1.0 = bottom).
45    #[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/// How to size the stack.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
57pub enum StackFit {
58    /// Size to the largest child
59    #[default]
60    Loose,
61    /// Expand to fill available space
62    Expand,
63}
64
65/// Stack widget for overlaying children.
66///
67/// Children are painted in order, with later children on top.
68#[derive(Serialize, Deserialize)]
69pub struct Stack {
70    /// Alignment for non-positioned children
71    alignment: StackAlignment,
72    /// How to size the stack
73    fit: StackFit,
74    /// Children widgets
75    #[serde(skip)]
76    children: Vec<Box<dyn Widget>>,
77    /// Test ID
78    test_id_value: Option<String>,
79    /// Cached bounds
80    #[serde(skip)]
81    bounds: Rect,
82}
83
84impl Default for Stack {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90impl Stack {
91    /// Create a new empty stack.
92    #[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    /// Set alignment.
104    #[must_use]
105    pub const fn alignment(mut self, alignment: StackAlignment) -> Self {
106        self.alignment = alignment;
107        self
108    }
109
110    /// Set fit mode.
111    #[must_use]
112    pub const fn fit(mut self, fit: StackFit) -> Self {
113        self.fit = fit;
114        self
115    }
116
117    /// Add a child widget.
118    pub fn child(mut self, widget: impl Widget + 'static) -> Self {
119        self.children.push(Box::new(widget));
120        self
121    }
122
123    /// Set test ID.
124    #[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    /// Get alignment.
131    #[must_use]
132    pub const fn get_alignment(&self) -> StackAlignment {
133        self.alignment
134    }
135
136    /// Get fit mode.
137    #[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        // Measure all children - size is the largest child
160        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        // Layout all children with alignment
176        for child in &mut self.children {
177            let child_constraints = Constraints::loose(bounds.size());
178            let child_size = child.measure(child_constraints);
179
180            // Calculate position based on alignment
181            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        // Paint children in order (first = bottom, last = top)
197        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        // Process events in reverse order (top-most first)
204        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    // =========================================================================
231    // StackAlignment Tests - TESTS FIRST
232    // =========================================================================
233
234    #[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        // Left alignments
242        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        // Center alignments
247        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        // Right alignments
252        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        // Top alignments
260        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        // Center alignments
265        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        // Bottom alignments
270        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    // =========================================================================
276    // StackFit Tests - TESTS FIRST
277    // =========================================================================
278
279    #[test]
280    fn test_stack_fit_default() {
281        assert_eq!(StackFit::default(), StackFit::Loose);
282    }
283
284    // =========================================================================
285    // Stack Construction Tests - TESTS FIRST
286    // =========================================================================
287
288    #[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    // =========================================================================
316    // Stack Measure Tests - TESTS FIRST
317    // =========================================================================
318
319    #[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    // =========================================================================
334    // Stack Widget Trait Tests - TESTS FIRST
335    // =========================================================================
336
337    #[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    // =========================================================================
357    // StackAlignment Tests - TESTS FIRST
358    // =========================================================================
359
360    #[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}