1use 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#[derive(Serialize, Deserialize)]
12pub struct Container {
13 pub background: Option<Color>,
15 pub corner_radius: CornerRadius,
17 pub padding: f32,
19 pub min_width: Option<f32>,
21 pub min_height: Option<f32>,
23 pub max_width: Option<f32>,
25 pub max_height: Option<f32>,
27 #[serde(skip)]
29 children: Vec<Box<dyn Widget>>,
30 test_id_value: Option<String>,
32 #[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 #[must_use]
57 pub fn new() -> Self {
58 Self::default()
59 }
60
61 #[must_use]
63 pub const fn background(mut self, color: Color) -> Self {
64 self.background = Some(color);
65 self
66 }
67
68 #[must_use]
70 pub const fn corner_radius(mut self, radius: CornerRadius) -> Self {
71 self.corner_radius = radius;
72 self
73 }
74
75 #[must_use]
77 pub const fn padding(mut self, padding: f32) -> Self {
78 self.padding = padding;
79 self
80 }
81
82 #[must_use]
84 pub const fn min_width(mut self, width: f32) -> Self {
85 self.min_width = Some(width);
86 self
87 }
88
89 #[must_use]
91 pub const fn min_height(mut self, height: f32) -> Self {
92 self.min_height = Some(height);
93 self
94 }
95
96 #[must_use]
98 pub const fn max_width(mut self, width: f32) -> Self {
99 self.max_width = Some(width);
100 self
101 }
102
103 #[must_use]
105 pub const fn max_height(mut self, height: f32) -> Self {
106 self.max_height = Some(height);
107 self
108 }
109
110 pub fn child(mut self, widget: impl Widget + 'static) -> Self {
112 self.children.push(Box::new(widget));
113 self
114 }
115
116 #[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 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 let mut size = Size::new(child_size.width + padding2, child_size.height + padding2);
149
150 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 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 if let Some(color) = self.background {
184 canvas.fill_rect(self.bounds, color);
185 }
186
187 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 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)); }
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); }
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 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 #[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 #[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 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); let size = c.measure(Constraints::loose(Size::new(500.0, 500.0)));
418 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)); }
428
429 #[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 #[test]
461 fn test_container_children_mut_access() {
462 let mut c = Container::new();
463 assert!(c.children_mut().is_empty());
464 }
465
466 #[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 #[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 #[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 #[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 #[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 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 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 #[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)); }
620
621 #[test]
622 fn test_container_negative_constraints_handled() {
623 let c = Container::new().padding(10.0);
625 let size = c.measure(Constraints::new(0.0, 5.0, 0.0, 5.0));
626 assert_eq!(size, Size::new(5.0, 5.0));
628 }
629}