1use presentar_core::{
4 widget::LayoutResult, Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color,
5 Constraints, CornerRadius, Event, Rect, Size, TypeId, Widget,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9use std::time::Duration;
10
11#[derive(Serialize, Deserialize)]
13pub struct Container {
14 pub background: Option<Color>,
16 pub corner_radius: CornerRadius,
18 pub padding: f32,
20 pub min_width: Option<f32>,
22 pub min_height: Option<f32>,
24 pub max_width: Option<f32>,
26 pub max_height: Option<f32>,
28 #[serde(skip)]
30 children: Vec<Box<dyn Widget>>,
31 test_id_value: Option<String>,
33 #[serde(skip)]
35 bounds: Rect,
36}
37
38impl Default for Container {
39 fn default() -> Self {
40 Self {
41 background: None,
42 corner_radius: CornerRadius::ZERO,
43 padding: 0.0,
44 min_width: None,
45 min_height: None,
46 max_width: None,
47 max_height: None,
48 children: Vec::new(),
49 test_id_value: None,
50 bounds: Rect::default(),
51 }
52 }
53}
54
55impl Container {
56 #[must_use]
58 pub fn new() -> Self {
59 Self::default()
60 }
61
62 #[must_use]
64 pub const fn background(mut self, color: Color) -> Self {
65 self.background = Some(color);
66 self
67 }
68
69 #[must_use]
71 pub const fn corner_radius(mut self, radius: CornerRadius) -> Self {
72 self.corner_radius = radius;
73 self
74 }
75
76 #[must_use]
78 pub const fn padding(mut self, padding: f32) -> Self {
79 self.padding = padding;
80 self
81 }
82
83 #[must_use]
85 pub const fn min_width(mut self, width: f32) -> Self {
86 self.min_width = Some(width);
87 self
88 }
89
90 #[must_use]
92 pub const fn min_height(mut self, height: f32) -> Self {
93 self.min_height = Some(height);
94 self
95 }
96
97 #[must_use]
99 pub const fn max_width(mut self, width: f32) -> Self {
100 self.max_width = Some(width);
101 self
102 }
103
104 #[must_use]
106 pub const fn max_height(mut self, height: f32) -> Self {
107 self.max_height = Some(height);
108 self
109 }
110
111 pub fn child(mut self, widget: impl Widget + 'static) -> Self {
113 self.children.push(Box::new(widget));
114 self
115 }
116
117 #[must_use]
119 pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
120 self.test_id_value = Some(id.into());
121 self
122 }
123}
124
125impl Widget for Container {
126 fn type_id(&self) -> TypeId {
127 TypeId::of::<Self>()
128 }
129
130 fn measure(&self, constraints: Constraints) -> Size {
131 let padding2 = self.padding * 2.0;
132
133 let child_constraints = Constraints::new(
135 0.0,
136 (constraints.max_width - padding2).max(0.0),
137 0.0,
138 (constraints.max_height - padding2).max(0.0),
139 );
140
141 let mut child_size = Size::ZERO;
142 for child in &self.children {
143 let size = child.measure(child_constraints);
144 child_size.width = child_size.width.max(size.width);
145 child_size.height = child_size.height.max(size.height);
146 }
147
148 let mut size = Size::new(child_size.width + padding2, child_size.height + padding2);
150
151 if let Some(min_w) = self.min_width {
153 size.width = size.width.max(min_w);
154 }
155 if let Some(min_h) = self.min_height {
156 size.height = size.height.max(min_h);
157 }
158 if let Some(max_w) = self.max_width {
159 size.width = size.width.min(max_w);
160 }
161 if let Some(max_h) = self.max_height {
162 size.height = size.height.min(max_h);
163 }
164
165 constraints.constrain(size)
166 }
167
168 fn layout(&mut self, bounds: Rect) -> LayoutResult {
169 self.bounds = bounds;
170
171 let child_bounds = bounds.inset(self.padding);
173 for child in &mut self.children {
174 child.layout(child_bounds);
175 }
176
177 LayoutResult {
178 size: bounds.size(),
179 }
180 }
181
182 fn paint(&self, canvas: &mut dyn Canvas) {
183 if let Some(color) = self.background {
185 canvas.fill_rect(self.bounds, color);
186 }
187
188 for child in &self.children {
190 child.paint(canvas);
191 }
192 }
193
194 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
195 for child in &mut self.children {
197 if let Some(msg) = child.event(event) {
198 return Some(msg);
199 }
200 }
201 None
202 }
203
204 fn children(&self) -> &[Box<dyn Widget>] {
205 &self.children
206 }
207
208 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
209 &mut self.children
210 }
211
212 fn test_id(&self) -> Option<&str> {
213 self.test_id_value.as_deref()
214 }
215}
216
217impl Brick for Container {
219 fn brick_name(&self) -> &'static str {
220 "Container"
221 }
222
223 fn assertions(&self) -> &[BrickAssertion] {
224 &[BrickAssertion::MaxLatencyMs(16)]
225 }
226
227 fn budget(&self) -> BrickBudget {
228 BrickBudget::uniform(16)
229 }
230
231 fn verify(&self) -> BrickVerification {
232 BrickVerification {
233 passed: self.assertions().to_vec(),
234 failed: vec![],
235 verification_time: Duration::from_micros(10),
236 }
237 }
238
239 fn to_html(&self) -> String {
240 let test_id = self.test_id_value.as_deref().unwrap_or("container");
241 format!(r#"<div class="brick-container" data-testid="{test_id}"></div>"#)
242 }
243
244 fn to_css(&self) -> String {
245 ".brick-container { display: block; }".into()
246 }
247
248 fn test_id(&self) -> Option<&str> {
249 self.test_id_value.as_deref()
250 }
251}
252
253#[cfg(test)]
254#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn test_container_default() {
260 let c = Container::new();
261 assert!(c.background.is_none());
262 assert_eq!(c.padding, 0.0);
263 assert!(c.children.is_empty());
264 }
265
266 #[test]
267 fn test_container_builder() {
268 let c = Container::new()
269 .background(Color::WHITE)
270 .padding(10.0)
271 .min_width(100.0)
272 .with_test_id("my-container");
273
274 assert_eq!(c.background, Some(Color::WHITE));
275 assert_eq!(c.padding, 10.0);
276 assert_eq!(c.min_width, Some(100.0));
277 assert_eq!(Widget::test_id(&c), Some("my-container"));
278 }
279
280 #[test]
281 fn test_container_measure_empty() {
282 let c = Container::new().padding(10.0);
283 let size = c.measure(Constraints::loose(Size::new(100.0, 100.0)));
284 assert_eq!(size, Size::new(20.0, 20.0)); }
286
287 #[test]
288 fn test_container_measure_with_min_size() {
289 let c = Container::new().min_width(50.0).min_height(50.0);
290 let size = c.measure(Constraints::loose(Size::new(100.0, 100.0)));
291 assert_eq!(size, Size::new(50.0, 50.0));
292 }
293
294 #[test]
295 fn test_container_measure_with_max_size() {
296 let c = Container::new()
297 .max_width(30.0)
298 .max_height(30.0)
299 .min_width(100.0);
300 let size = c.measure(Constraints::loose(Size::new(200.0, 200.0)));
301 assert_eq!(size.width, 30.0); }
303
304 #[test]
305 fn test_container_corner_radius() {
306 let c = Container::new().corner_radius(CornerRadius::uniform(8.0));
307 assert_eq!(c.corner_radius, CornerRadius::uniform(8.0));
308 }
309
310 #[test]
311 fn test_container_type_id() {
312 let c = Container::new();
313 assert_eq!(Widget::type_id(&c), TypeId::of::<Container>());
314 }
315
316 #[test]
317 fn test_container_layout_sets_bounds() {
318 let mut c = Container::new().padding(10.0);
319 let result = c.layout(Rect::new(0.0, 0.0, 100.0, 80.0));
320 assert_eq!(result.size, Size::new(100.0, 80.0));
321 assert_eq!(c.bounds, Rect::new(0.0, 0.0, 100.0, 80.0));
322 }
323
324 #[test]
325 fn test_container_children_empty() {
326 let c = Container::new();
327 assert!(c.children().is_empty());
328 }
329
330 #[test]
331 fn test_container_event_no_children_returns_none() {
332 let mut c = Container::new();
333 c.layout(Rect::new(0.0, 0.0, 100.0, 100.0));
334 let result = c.event(&Event::MouseEnter);
335 assert!(result.is_none());
336 }
337
338 use presentar_core::draw::DrawCommand;
340 use presentar_core::RecordingCanvas;
341
342 #[test]
343 fn test_container_paint_no_background() {
344 let mut c = Container::new();
345 c.layout(Rect::new(0.0, 0.0, 100.0, 100.0));
346 let mut canvas = RecordingCanvas::new();
347 c.paint(&mut canvas);
348 assert_eq!(canvas.command_count(), 0);
349 }
350
351 #[test]
352 fn test_container_paint_with_background() {
353 let mut c = Container::new().background(Color::RED);
354 c.layout(Rect::new(0.0, 0.0, 100.0, 50.0));
355 let mut canvas = RecordingCanvas::new();
356 c.paint(&mut canvas);
357 assert_eq!(canvas.command_count(), 1);
358 match &canvas.commands()[0] {
359 DrawCommand::Rect { bounds, style, .. } => {
360 assert_eq!(bounds.width, 100.0);
361 assert_eq!(bounds.height, 50.0);
362 assert_eq!(style.fill, Some(Color::RED));
363 }
364 _ => panic!("Expected Rect"),
365 }
366 }
367
368 #[test]
373 fn test_container_min_height_builder() {
374 let c = Container::new().min_height(75.0);
375 assert_eq!(c.min_height, Some(75.0));
376 }
377
378 #[test]
379 fn test_container_max_height_builder() {
380 let c = Container::new().max_height(150.0);
381 assert_eq!(c.max_height, Some(150.0));
382 }
383
384 #[test]
385 fn test_container_max_width_builder() {
386 let c = Container::new().max_width(200.0);
387 assert_eq!(c.max_width, Some(200.0));
388 }
389
390 #[test]
391 fn test_container_all_constraints() {
392 let c = Container::new()
393 .min_width(50.0)
394 .max_width(200.0)
395 .min_height(30.0)
396 .max_height(150.0);
397 assert_eq!(c.min_width, Some(50.0));
398 assert_eq!(c.max_width, Some(200.0));
399 assert_eq!(c.min_height, Some(30.0));
400 assert_eq!(c.max_height, Some(150.0));
401 }
402
403 #[test]
404 fn test_container_chained_all_builders() {
405 let c = Container::new()
406 .background(Color::BLUE)
407 .corner_radius(CornerRadius::uniform(10.0))
408 .padding(5.0)
409 .min_width(100.0)
410 .min_height(80.0)
411 .max_width(300.0)
412 .max_height(200.0)
413 .with_test_id("full-container");
414
415 assert_eq!(c.background, Some(Color::BLUE));
416 assert_eq!(c.corner_radius, CornerRadius::uniform(10.0));
417 assert_eq!(c.padding, 5.0);
418 assert_eq!(c.min_width, Some(100.0));
419 assert_eq!(c.min_height, Some(80.0));
420 assert_eq!(c.max_width, Some(300.0));
421 assert_eq!(c.max_height, Some(200.0));
422 assert_eq!(Widget::test_id(&c), Some("full-container"));
423 }
424
425 #[test]
430 fn test_container_measure_tight_constraints() {
431 let c = Container::new().padding(10.0);
432 let size = c.measure(Constraints::tight(Size::new(50.0, 50.0)));
433 assert_eq!(size, Size::new(50.0, 50.0));
435 }
436
437 #[test]
438 fn test_container_measure_unbounded() {
439 let c = Container::new().min_width(100.0).min_height(50.0);
440 let size = c.measure(Constraints::unbounded());
441 assert_eq!(size, Size::new(100.0, 50.0));
442 }
443
444 #[test]
445 fn test_container_measure_min_overrides_content() {
446 let c = Container::new().min_width(200.0).min_height(200.0);
447 let size = c.measure(Constraints::loose(Size::new(500.0, 500.0)));
448 assert!(size.width >= 200.0);
449 assert!(size.height >= 200.0);
450 }
451
452 #[test]
453 fn test_container_measure_max_clamps() {
454 let c = Container::new().min_width(300.0).max_width(150.0); let size = c.measure(Constraints::loose(Size::new(500.0, 500.0)));
456 assert_eq!(size.width, 150.0);
458 }
459
460 #[test]
461 fn test_container_measure_padding_only() {
462 let c = Container::new().padding(25.0);
463 let size = c.measure(Constraints::loose(Size::new(100.0, 100.0)));
464 assert_eq!(size, Size::new(50.0, 50.0)); }
466
467 #[test]
472 fn test_container_layout_with_offset() {
473 let mut c = Container::new();
474 let result = c.layout(Rect::new(20.0, 30.0, 100.0, 80.0));
475 assert_eq!(result.size, Size::new(100.0, 80.0));
476 assert_eq!(c.bounds.x, 20.0);
477 assert_eq!(c.bounds.y, 30.0);
478 }
479
480 #[test]
481 fn test_container_layout_zero_size() {
482 let mut c = Container::new();
483 let result = c.layout(Rect::new(0.0, 0.0, 0.0, 0.0));
484 assert_eq!(result.size, Size::new(0.0, 0.0));
485 }
486
487 #[test]
488 fn test_container_layout_large_bounds() {
489 let mut c = Container::new();
490 let result = c.layout(Rect::new(0.0, 0.0, 10000.0, 10000.0));
491 assert_eq!(result.size, Size::new(10000.0, 10000.0));
492 }
493
494 #[test]
499 fn test_container_children_mut_access() {
500 let mut c = Container::new();
501 assert!(c.children_mut().is_empty());
502 }
503
504 #[test]
509 fn test_container_test_id_none_by_default() {
510 let c = Container::new();
511 assert!(Widget::test_id(&c).is_none());
512 }
513
514 #[test]
515 fn test_container_test_id_with_str() {
516 let c = Container::new().with_test_id("simple-id");
517 assert_eq!(Widget::test_id(&c), Some("simple-id"));
518 }
519
520 #[test]
521 fn test_container_test_id_with_string() {
522 let id = String::from("dynamic-id");
523 let c = Container::new().with_test_id(id);
524 assert_eq!(Widget::test_id(&c), Some("dynamic-id"));
525 }
526
527 #[test]
532 fn test_container_corner_radius_zero() {
533 let c = Container::new().corner_radius(CornerRadius::ZERO);
534 assert_eq!(c.corner_radius, CornerRadius::ZERO);
535 }
536
537 #[test]
538 fn test_container_corner_radius_asymmetric() {
539 let radius = CornerRadius {
540 top_left: 5.0,
541 top_right: 10.0,
542 bottom_left: 15.0,
543 bottom_right: 20.0,
544 };
545 let c = Container::new().corner_radius(radius);
546 assert_eq!(c.corner_radius.top_left, 5.0);
547 assert_eq!(c.corner_radius.bottom_right, 20.0);
548 }
549
550 #[test]
555 fn test_container_default_all_none() {
556 let c = Container::default();
557 assert!(c.background.is_none());
558 assert!(c.min_width.is_none());
559 assert!(c.min_height.is_none());
560 assert!(c.max_width.is_none());
561 assert!(c.max_height.is_none());
562 assert!(c.test_id_value.is_none());
563 }
564
565 #[test]
566 fn test_container_default_corner_radius_zero() {
567 let c = Container::default();
568 assert_eq!(c.corner_radius, CornerRadius::ZERO);
569 }
570
571 #[test]
572 fn test_container_default_bounds_zero() {
573 let c = Container::default();
574 assert_eq!(c.bounds, Rect::default());
575 }
576
577 #[test]
582 fn test_container_serialize() {
583 let c = Container::new()
584 .background(Color::GREEN)
585 .padding(15.0)
586 .min_width(100.0);
587 let json = serde_json::to_string(&c).unwrap();
588 assert!(json.contains("background"));
589 assert!(json.contains("padding"));
590 assert!(json.contains("15"));
591 }
592
593 #[test]
594 fn test_container_deserialize() {
595 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}"#;
596 let c: Container = serde_json::from_str(json).unwrap();
597 assert_eq!(c.padding, 10.0);
598 assert_eq!(c.min_width, Some(50.0));
599 }
600
601 #[test]
602 fn test_container_roundtrip_serialization() {
603 let original = Container::new()
604 .background(Color::BLUE)
605 .padding(20.0)
606 .min_width(75.0)
607 .max_height(300.0);
608 let json = serde_json::to_string(&original).unwrap();
609 let deserialized: Container = serde_json::from_str(&json).unwrap();
610 assert_eq!(original.padding, deserialized.padding);
611 assert_eq!(original.min_width, deserialized.min_width);
612 assert_eq!(original.max_height, deserialized.max_height);
613 assert_eq!(original.background, deserialized.background);
614 }
615
616 #[test]
621 fn test_container_paint_transparent_background() {
622 let mut c = Container::new().background(Color::TRANSPARENT);
623 c.layout(Rect::new(0.0, 0.0, 100.0, 100.0));
624 let mut canvas = RecordingCanvas::new();
625 c.paint(&mut canvas);
626 assert_eq!(canvas.command_count(), 1);
628 }
629
630 #[test]
631 fn test_container_paint_after_layout() {
632 let mut c = Container::new().background(Color::WHITE);
633 c.layout(Rect::new(50.0, 50.0, 80.0, 60.0));
635 let mut canvas = RecordingCanvas::new();
636 c.paint(&mut canvas);
637 match &canvas.commands()[0] {
638 DrawCommand::Rect { bounds, .. } => {
639 assert_eq!(bounds.x, 50.0);
640 assert_eq!(bounds.y, 50.0);
641 assert_eq!(bounds.width, 80.0);
642 assert_eq!(bounds.height, 60.0);
643 }
644 _ => panic!("Expected Rect"),
645 }
646 }
647
648 #[test]
653 fn test_container_zero_padding_measure() {
654 let c = Container::new().padding(0.0);
655 let size = c.measure(Constraints::loose(Size::new(100.0, 100.0)));
656 assert_eq!(size, Size::new(0.0, 0.0)); }
658
659 #[test]
660 fn test_container_negative_constraints_handled() {
661 let c = Container::new().padding(10.0);
663 let size = c.measure(Constraints::new(0.0, 5.0, 0.0, 5.0));
664 assert_eq!(size, Size::new(5.0, 5.0));
666 }
667}