presentar_widgets/
container.rs

1//! Container widget for layout grouping.
2
3use presentar_core::{
4    widget::LayoutResult, Canvas, Color, Constraints, CornerRadius, Event, Rect, Size, TypeId,
5    Widget,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9
10/// Container widget for grouping and styling children.
11#[derive(Serialize, Deserialize)]
12pub struct Container {
13    /// Background color
14    pub background: Option<Color>,
15    /// Corner radius for rounded corners
16    pub corner_radius: CornerRadius,
17    /// Padding (all sides)
18    pub padding: f32,
19    /// Minimum width constraint
20    pub min_width: Option<f32>,
21    /// Minimum height constraint
22    pub min_height: Option<f32>,
23    /// Maximum width constraint
24    pub max_width: Option<f32>,
25    /// Maximum height constraint
26    pub max_height: Option<f32>,
27    /// Children widgets
28    #[serde(skip)]
29    children: Vec<Box<dyn Widget>>,
30    /// Test ID for this widget
31    test_id_value: Option<String>,
32    /// Cached bounds after layout
33    #[serde(skip)]
34    bounds: Rect,
35}
36
37impl Default for Container {
38    fn default() -> Self {
39        Self {
40            background: None,
41            corner_radius: CornerRadius::ZERO,
42            padding: 0.0,
43            min_width: None,
44            min_height: None,
45            max_width: None,
46            max_height: None,
47            children: Vec::new(),
48            test_id_value: None,
49            bounds: Rect::default(),
50        }
51    }
52}
53
54impl Container {
55    /// Create a new empty container.
56    #[must_use]
57    pub fn new() -> Self {
58        Self::default()
59    }
60
61    /// Set the background color.
62    #[must_use]
63    pub const fn background(mut self, color: Color) -> Self {
64        self.background = Some(color);
65        self
66    }
67
68    /// Set the corner radius.
69    #[must_use]
70    pub const fn corner_radius(mut self, radius: CornerRadius) -> Self {
71        self.corner_radius = radius;
72        self
73    }
74
75    /// Set uniform padding on all sides.
76    #[must_use]
77    pub const fn padding(mut self, padding: f32) -> Self {
78        self.padding = padding;
79        self
80    }
81
82    /// Set minimum width.
83    #[must_use]
84    pub const fn min_width(mut self, width: f32) -> Self {
85        self.min_width = Some(width);
86        self
87    }
88
89    /// Set minimum height.
90    #[must_use]
91    pub const fn min_height(mut self, height: f32) -> Self {
92        self.min_height = Some(height);
93        self
94    }
95
96    /// Set maximum width.
97    #[must_use]
98    pub const fn max_width(mut self, width: f32) -> Self {
99        self.max_width = Some(width);
100        self
101    }
102
103    /// Set maximum height.
104    #[must_use]
105    pub const fn max_height(mut self, height: f32) -> Self {
106        self.max_height = Some(height);
107        self
108    }
109
110    /// Add a child widget.
111    pub fn child(mut self, widget: impl Widget + 'static) -> Self {
112        self.children.push(Box::new(widget));
113        self
114    }
115
116    /// Set the test ID.
117    #[must_use]
118    pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
119        self.test_id_value = Some(id.into());
120        self
121    }
122}
123
124impl Widget for Container {
125    fn type_id(&self) -> TypeId {
126        TypeId::of::<Self>()
127    }
128
129    fn measure(&self, constraints: Constraints) -> Size {
130        let padding2 = self.padding * 2.0;
131
132        // Measure children
133        let child_constraints = Constraints::new(
134            0.0,
135            (constraints.max_width - padding2).max(0.0),
136            0.0,
137            (constraints.max_height - padding2).max(0.0),
138        );
139
140        let mut child_size = Size::ZERO;
141        for child in &self.children {
142            let size = child.measure(child_constraints);
143            child_size.width = child_size.width.max(size.width);
144            child_size.height = child_size.height.max(size.height);
145        }
146
147        // Add padding and apply constraints
148        let mut size = Size::new(child_size.width + padding2, child_size.height + padding2);
149
150        // Apply min/max constraints
151        if let Some(min_w) = self.min_width {
152            size.width = size.width.max(min_w);
153        }
154        if let Some(min_h) = self.min_height {
155            size.height = size.height.max(min_h);
156        }
157        if let Some(max_w) = self.max_width {
158            size.width = size.width.min(max_w);
159        }
160        if let Some(max_h) = self.max_height {
161            size.height = size.height.min(max_h);
162        }
163
164        constraints.constrain(size)
165    }
166
167    fn layout(&mut self, bounds: Rect) -> LayoutResult {
168        self.bounds = bounds;
169
170        // Layout children within padded bounds
171        let child_bounds = bounds.inset(self.padding);
172        for child in &mut self.children {
173            child.layout(child_bounds);
174        }
175
176        LayoutResult {
177            size: bounds.size(),
178        }
179    }
180
181    fn paint(&self, canvas: &mut dyn Canvas) {
182        // Draw background
183        if let Some(color) = self.background {
184            canvas.fill_rect(self.bounds, color);
185        }
186
187        // Paint children
188        for child in &self.children {
189            child.paint(canvas);
190        }
191    }
192
193    fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
194        // Propagate to children
195        for child in &mut self.children {
196            if let Some(msg) = child.event(event) {
197                return Some(msg);
198            }
199        }
200        None
201    }
202
203    fn children(&self) -> &[Box<dyn Widget>] {
204        &self.children
205    }
206
207    fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
208        &mut self.children
209    }
210
211    fn test_id(&self) -> Option<&str> {
212        self.test_id_value.as_deref()
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_container_default() {
222        let c = Container::new();
223        assert!(c.background.is_none());
224        assert_eq!(c.padding, 0.0);
225        assert!(c.children.is_empty());
226    }
227
228    #[test]
229    fn test_container_builder() {
230        let c = Container::new()
231            .background(Color::WHITE)
232            .padding(10.0)
233            .min_width(100.0)
234            .with_test_id("my-container");
235
236        assert_eq!(c.background, Some(Color::WHITE));
237        assert_eq!(c.padding, 10.0);
238        assert_eq!(c.min_width, Some(100.0));
239        assert_eq!(Widget::test_id(&c), Some("my-container"));
240    }
241
242    #[test]
243    fn test_container_measure_empty() {
244        let c = Container::new().padding(10.0);
245        let size = c.measure(Constraints::loose(Size::new(100.0, 100.0)));
246        assert_eq!(size, Size::new(20.0, 20.0)); // padding * 2
247    }
248
249    #[test]
250    fn test_container_measure_with_min_size() {
251        let c = Container::new().min_width(50.0).min_height(50.0);
252        let size = c.measure(Constraints::loose(Size::new(100.0, 100.0)));
253        assert_eq!(size, Size::new(50.0, 50.0));
254    }
255
256    #[test]
257    fn test_container_measure_with_max_size() {
258        let c = Container::new()
259            .max_width(30.0)
260            .max_height(30.0)
261            .min_width(100.0);
262        let size = c.measure(Constraints::loose(Size::new(200.0, 200.0)));
263        assert_eq!(size.width, 30.0); // max wins over min
264    }
265
266    #[test]
267    fn test_container_corner_radius() {
268        let c = Container::new().corner_radius(CornerRadius::uniform(8.0));
269        assert_eq!(c.corner_radius, CornerRadius::uniform(8.0));
270    }
271
272    #[test]
273    fn test_container_type_id() {
274        let c = Container::new();
275        assert_eq!(Widget::type_id(&c), TypeId::of::<Container>());
276    }
277
278    #[test]
279    fn test_container_layout_sets_bounds() {
280        let mut c = Container::new().padding(10.0);
281        let result = c.layout(Rect::new(0.0, 0.0, 100.0, 80.0));
282        assert_eq!(result.size, Size::new(100.0, 80.0));
283        assert_eq!(c.bounds, Rect::new(0.0, 0.0, 100.0, 80.0));
284    }
285
286    #[test]
287    fn test_container_children_empty() {
288        let c = Container::new();
289        assert!(c.children().is_empty());
290    }
291
292    #[test]
293    fn test_container_event_no_children_returns_none() {
294        let mut c = Container::new();
295        c.layout(Rect::new(0.0, 0.0, 100.0, 100.0));
296        let result = c.event(&Event::MouseEnter);
297        assert!(result.is_none());
298    }
299
300    // Paint tests
301    use presentar_core::draw::DrawCommand;
302    use presentar_core::RecordingCanvas;
303
304    #[test]
305    fn test_container_paint_no_background() {
306        let mut c = Container::new();
307        c.layout(Rect::new(0.0, 0.0, 100.0, 100.0));
308        let mut canvas = RecordingCanvas::new();
309        c.paint(&mut canvas);
310        assert_eq!(canvas.command_count(), 0);
311    }
312
313    #[test]
314    fn test_container_paint_with_background() {
315        let mut c = Container::new().background(Color::RED);
316        c.layout(Rect::new(0.0, 0.0, 100.0, 50.0));
317        let mut canvas = RecordingCanvas::new();
318        c.paint(&mut canvas);
319        assert_eq!(canvas.command_count(), 1);
320        match &canvas.commands()[0] {
321            DrawCommand::Rect { bounds, style, .. } => {
322                assert_eq!(bounds.width, 100.0);
323                assert_eq!(bounds.height, 50.0);
324                assert_eq!(style.fill, Some(Color::RED));
325            }
326            _ => panic!("Expected Rect"),
327        }
328    }
329
330    // =========================================================================
331    // Builder Pattern Tests
332    // =========================================================================
333
334    #[test]
335    fn test_container_min_height_builder() {
336        let c = Container::new().min_height(75.0);
337        assert_eq!(c.min_height, Some(75.0));
338    }
339
340    #[test]
341    fn test_container_max_height_builder() {
342        let c = Container::new().max_height(150.0);
343        assert_eq!(c.max_height, Some(150.0));
344    }
345
346    #[test]
347    fn test_container_max_width_builder() {
348        let c = Container::new().max_width(200.0);
349        assert_eq!(c.max_width, Some(200.0));
350    }
351
352    #[test]
353    fn test_container_all_constraints() {
354        let c = Container::new()
355            .min_width(50.0)
356            .max_width(200.0)
357            .min_height(30.0)
358            .max_height(150.0);
359        assert_eq!(c.min_width, Some(50.0));
360        assert_eq!(c.max_width, Some(200.0));
361        assert_eq!(c.min_height, Some(30.0));
362        assert_eq!(c.max_height, Some(150.0));
363    }
364
365    #[test]
366    fn test_container_chained_all_builders() {
367        let c = Container::new()
368            .background(Color::BLUE)
369            .corner_radius(CornerRadius::uniform(10.0))
370            .padding(5.0)
371            .min_width(100.0)
372            .min_height(80.0)
373            .max_width(300.0)
374            .max_height(200.0)
375            .with_test_id("full-container");
376
377        assert_eq!(c.background, Some(Color::BLUE));
378        assert_eq!(c.corner_radius, CornerRadius::uniform(10.0));
379        assert_eq!(c.padding, 5.0);
380        assert_eq!(c.min_width, Some(100.0));
381        assert_eq!(c.min_height, Some(80.0));
382        assert_eq!(c.max_width, Some(300.0));
383        assert_eq!(c.max_height, Some(200.0));
384        assert_eq!(Widget::test_id(&c), Some("full-container"));
385    }
386
387    // =========================================================================
388    // Measure Tests
389    // =========================================================================
390
391    #[test]
392    fn test_container_measure_tight_constraints() {
393        let c = Container::new().padding(10.0);
394        let size = c.measure(Constraints::tight(Size::new(50.0, 50.0)));
395        // With tight constraints, padding is still applied but constrained
396        assert_eq!(size, Size::new(50.0, 50.0));
397    }
398
399    #[test]
400    fn test_container_measure_unbounded() {
401        let c = Container::new().min_width(100.0).min_height(50.0);
402        let size = c.measure(Constraints::unbounded());
403        assert_eq!(size, Size::new(100.0, 50.0));
404    }
405
406    #[test]
407    fn test_container_measure_min_overrides_content() {
408        let c = Container::new().min_width(200.0).min_height(200.0);
409        let size = c.measure(Constraints::loose(Size::new(500.0, 500.0)));
410        assert!(size.width >= 200.0);
411        assert!(size.height >= 200.0);
412    }
413
414    #[test]
415    fn test_container_measure_max_clamps() {
416        let c = Container::new().min_width(300.0).max_width(150.0); // max < min
417        let size = c.measure(Constraints::loose(Size::new(500.0, 500.0)));
418        // max wins after min is applied
419        assert_eq!(size.width, 150.0);
420    }
421
422    #[test]
423    fn test_container_measure_padding_only() {
424        let c = Container::new().padding(25.0);
425        let size = c.measure(Constraints::loose(Size::new(100.0, 100.0)));
426        assert_eq!(size, Size::new(50.0, 50.0)); // 25 * 2 on each axis
427    }
428
429    // =========================================================================
430    // Layout Tests
431    // =========================================================================
432
433    #[test]
434    fn test_container_layout_with_offset() {
435        let mut c = Container::new();
436        let result = c.layout(Rect::new(20.0, 30.0, 100.0, 80.0));
437        assert_eq!(result.size, Size::new(100.0, 80.0));
438        assert_eq!(c.bounds.x, 20.0);
439        assert_eq!(c.bounds.y, 30.0);
440    }
441
442    #[test]
443    fn test_container_layout_zero_size() {
444        let mut c = Container::new();
445        let result = c.layout(Rect::new(0.0, 0.0, 0.0, 0.0));
446        assert_eq!(result.size, Size::new(0.0, 0.0));
447    }
448
449    #[test]
450    fn test_container_layout_large_bounds() {
451        let mut c = Container::new();
452        let result = c.layout(Rect::new(0.0, 0.0, 10000.0, 10000.0));
453        assert_eq!(result.size, Size::new(10000.0, 10000.0));
454    }
455
456    // =========================================================================
457    // Children Tests
458    // =========================================================================
459
460    #[test]
461    fn test_container_children_mut_access() {
462        let mut c = Container::new();
463        assert!(c.children_mut().is_empty());
464    }
465
466    // =========================================================================
467    // Test ID Tests
468    // =========================================================================
469
470    #[test]
471    fn test_container_test_id_none_by_default() {
472        let c = Container::new();
473        assert!(Widget::test_id(&c).is_none());
474    }
475
476    #[test]
477    fn test_container_test_id_with_str() {
478        let c = Container::new().with_test_id("simple-id");
479        assert_eq!(Widget::test_id(&c), Some("simple-id"));
480    }
481
482    #[test]
483    fn test_container_test_id_with_string() {
484        let id = String::from("dynamic-id");
485        let c = Container::new().with_test_id(id);
486        assert_eq!(Widget::test_id(&c), Some("dynamic-id"));
487    }
488
489    // =========================================================================
490    // Corner Radius Tests
491    // =========================================================================
492
493    #[test]
494    fn test_container_corner_radius_zero() {
495        let c = Container::new().corner_radius(CornerRadius::ZERO);
496        assert_eq!(c.corner_radius, CornerRadius::ZERO);
497    }
498
499    #[test]
500    fn test_container_corner_radius_asymmetric() {
501        let radius = CornerRadius {
502            top_left: 5.0,
503            top_right: 10.0,
504            bottom_left: 15.0,
505            bottom_right: 20.0,
506        };
507        let c = Container::new().corner_radius(radius);
508        assert_eq!(c.corner_radius.top_left, 5.0);
509        assert_eq!(c.corner_radius.bottom_right, 20.0);
510    }
511
512    // =========================================================================
513    // Default Tests
514    // =========================================================================
515
516    #[test]
517    fn test_container_default_all_none() {
518        let c = Container::default();
519        assert!(c.background.is_none());
520        assert!(c.min_width.is_none());
521        assert!(c.min_height.is_none());
522        assert!(c.max_width.is_none());
523        assert!(c.max_height.is_none());
524        assert!(c.test_id_value.is_none());
525    }
526
527    #[test]
528    fn test_container_default_corner_radius_zero() {
529        let c = Container::default();
530        assert_eq!(c.corner_radius, CornerRadius::ZERO);
531    }
532
533    #[test]
534    fn test_container_default_bounds_zero() {
535        let c = Container::default();
536        assert_eq!(c.bounds, Rect::default());
537    }
538
539    // =========================================================================
540    // Serialization Tests
541    // =========================================================================
542
543    #[test]
544    fn test_container_serialize() {
545        let c = Container::new()
546            .background(Color::GREEN)
547            .padding(15.0)
548            .min_width(100.0);
549        let json = serde_json::to_string(&c).unwrap();
550        assert!(json.contains("background"));
551        assert!(json.contains("padding"));
552        assert!(json.contains("15"));
553    }
554
555    #[test]
556    fn test_container_deserialize() {
557        let json = r##"{"background":{"r":1.0,"g":0.0,"b":0.0,"a":1.0},"corner_radius":{"top_left":0.0,"top_right":0.0,"bottom_left":0.0,"bottom_right":0.0},"padding":10.0,"min_width":50.0,"min_height":null,"max_width":null,"max_height":null,"test_id_value":null}"##;
558        let c: Container = serde_json::from_str(json).unwrap();
559        assert_eq!(c.padding, 10.0);
560        assert_eq!(c.min_width, Some(50.0));
561    }
562
563    #[test]
564    fn test_container_roundtrip_serialization() {
565        let original = Container::new()
566            .background(Color::BLUE)
567            .padding(20.0)
568            .min_width(75.0)
569            .max_height(300.0);
570        let json = serde_json::to_string(&original).unwrap();
571        let deserialized: Container = serde_json::from_str(&json).unwrap();
572        assert_eq!(original.padding, deserialized.padding);
573        assert_eq!(original.min_width, deserialized.min_width);
574        assert_eq!(original.max_height, deserialized.max_height);
575        assert_eq!(original.background, deserialized.background);
576    }
577
578    // =========================================================================
579    // Paint Edge Cases
580    // =========================================================================
581
582    #[test]
583    fn test_container_paint_transparent_background() {
584        let mut c = Container::new().background(Color::TRANSPARENT);
585        c.layout(Rect::new(0.0, 0.0, 100.0, 100.0));
586        let mut canvas = RecordingCanvas::new();
587        c.paint(&mut canvas);
588        // Should still paint even with transparent
589        assert_eq!(canvas.command_count(), 1);
590    }
591
592    #[test]
593    fn test_container_paint_after_layout() {
594        let mut c = Container::new().background(Color::WHITE);
595        // Layout at specific position
596        c.layout(Rect::new(50.0, 50.0, 80.0, 60.0));
597        let mut canvas = RecordingCanvas::new();
598        c.paint(&mut canvas);
599        match &canvas.commands()[0] {
600            DrawCommand::Rect { bounds, .. } => {
601                assert_eq!(bounds.x, 50.0);
602                assert_eq!(bounds.y, 50.0);
603                assert_eq!(bounds.width, 80.0);
604                assert_eq!(bounds.height, 60.0);
605            }
606            _ => panic!("Expected Rect"),
607        }
608    }
609
610    // =========================================================================
611    // Edge Cases
612    // =========================================================================
613
614    #[test]
615    fn test_container_zero_padding_measure() {
616        let c = Container::new().padding(0.0);
617        let size = c.measure(Constraints::loose(Size::new(100.0, 100.0)));
618        assert_eq!(size, Size::new(0.0, 0.0)); // No content, no padding
619    }
620
621    #[test]
622    fn test_container_negative_constraints_handled() {
623        // Constraints with negative max should clamp to 0
624        let c = Container::new().padding(10.0);
625        let size = c.measure(Constraints::new(0.0, 5.0, 0.0, 5.0));
626        // Padding is 20 total but max is 5, so constrained
627        assert_eq!(size, Size::new(5.0, 5.0));
628    }
629}