1use presentar_core::{
4 widget::LayoutResult, Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas,
5 Constraints, Event, Rect, Size, TypeId, Widget,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9use std::time::Duration;
10
11use crate::row::{CrossAxisAlignment, MainAxisAlignment};
12
13#[derive(Serialize, Deserialize)]
15pub struct Column {
16 main_axis_alignment: MainAxisAlignment,
18 cross_axis_alignment: CrossAxisAlignment,
20 gap: f32,
22 #[serde(skip)]
24 children: Vec<Box<dyn Widget>>,
25 test_id_value: Option<String>,
27 #[serde(skip)]
29 bounds: Rect,
30 #[serde(skip)]
32 child_bounds: Vec<Rect>,
33}
34
35impl Default for Column {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41impl Column {
42 #[must_use]
44 pub fn new() -> Self {
45 Self {
46 main_axis_alignment: MainAxisAlignment::Start,
47 cross_axis_alignment: CrossAxisAlignment::Center,
48 gap: 0.0,
49 children: Vec::new(),
50 test_id_value: None,
51 bounds: Rect::default(),
52 child_bounds: Vec::new(),
53 }
54 }
55
56 #[must_use]
58 pub const fn main_axis_alignment(mut self, alignment: MainAxisAlignment) -> Self {
59 self.main_axis_alignment = alignment;
60 self
61 }
62
63 #[must_use]
65 pub const fn cross_axis_alignment(mut self, alignment: CrossAxisAlignment) -> Self {
66 self.cross_axis_alignment = alignment;
67 self
68 }
69
70 #[must_use]
72 pub const fn gap(mut self, gap: f32) -> Self {
73 self.gap = gap;
74 self
75 }
76
77 pub fn child(mut self, widget: impl Widget + 'static) -> Self {
79 self.children.push(Box::new(widget));
80 self
81 }
82
83 #[must_use]
85 pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
86 self.test_id_value = Some(id.into());
87 self
88 }
89}
90
91impl Widget for Column {
92 fn type_id(&self) -> TypeId {
93 TypeId::of::<Self>()
94 }
95
96 fn measure(&self, constraints: Constraints) -> Size {
97 if self.children.is_empty() {
98 return Size::ZERO;
99 }
100
101 let mut max_width = 0.0f32;
102 let mut total_height = 0.0f32;
103
104 for (i, child) in self.children.iter().enumerate() {
106 let child_constraints = Constraints::new(
107 0.0,
108 constraints.max_width,
109 0.0,
110 (constraints.max_height - total_height).max(0.0),
111 );
112
113 let child_size = child.measure(child_constraints);
114 max_width = max_width.max(child_size.width);
115 total_height += child_size.height;
116
117 if i < self.children.len() - 1 {
118 total_height += self.gap;
119 }
120 }
121
122 constraints.constrain(Size::new(max_width, total_height))
123 }
124
125 fn layout(&mut self, bounds: Rect) -> LayoutResult {
126 self.bounds = bounds;
127 self.child_bounds.clear();
128
129 if self.children.is_empty() {
130 return LayoutResult { size: Size::ZERO };
131 }
132
133 let mut child_sizes: Vec<Size> = Vec::with_capacity(self.children.len());
135 let mut total_height = 0.0f32;
136
137 for child in &self.children {
138 let child_constraints = Constraints::loose(bounds.size());
139 let size = child.measure(child_constraints);
140 total_height += size.height;
141 child_sizes.push(size);
142 }
143
144 let gaps_height = self.gap * (self.children.len() - 1).max(0) as f32;
145 let content_height = total_height + gaps_height;
146 let remaining_space = (bounds.height - content_height).max(0.0);
147
148 let (mut y, extra_gap) = match self.main_axis_alignment {
150 MainAxisAlignment::Start => (bounds.y, 0.0),
151 MainAxisAlignment::End => (bounds.y + remaining_space, 0.0),
152 MainAxisAlignment::Center => (bounds.y + remaining_space / 2.0, 0.0),
153 MainAxisAlignment::SpaceBetween => {
154 if self.children.len() > 1 {
155 (bounds.y, remaining_space / (self.children.len() - 1) as f32)
156 } else {
157 (bounds.y, 0.0)
158 }
159 }
160 MainAxisAlignment::SpaceAround => {
161 let gap = remaining_space / self.children.len() as f32;
162 (bounds.y + gap / 2.0, gap)
163 }
164 MainAxisAlignment::SpaceEvenly => {
165 let gap = remaining_space / (self.children.len() + 1) as f32;
166 (bounds.y + gap, gap)
167 }
168 };
169
170 let num_children = self.children.len();
172 for (i, (child, size)) in self.children.iter_mut().zip(child_sizes.iter()).enumerate() {
173 let x = match self.cross_axis_alignment {
174 CrossAxisAlignment::Start | CrossAxisAlignment::Stretch => bounds.x,
175 CrossAxisAlignment::End => bounds.x + bounds.width - size.width,
176 CrossAxisAlignment::Center => bounds.x + (bounds.width - size.width) / 2.0,
177 };
178
179 let width = if self.cross_axis_alignment == CrossAxisAlignment::Stretch {
180 bounds.width
181 } else {
182 size.width
183 };
184
185 let child_bounds = Rect::new(x, y, width, size.height);
186 child.layout(child_bounds);
187 self.child_bounds.push(child_bounds);
188
189 if i < num_children - 1 {
191 y += size.height;
192 if self.main_axis_alignment == MainAxisAlignment::SpaceBetween {
193 y += extra_gap;
195 } else {
196 y += self.gap + extra_gap;
197 }
198 }
199 }
200
201 LayoutResult {
202 size: bounds.size(),
203 }
204 }
205
206 fn paint(&self, canvas: &mut dyn Canvas) {
207 for child in &self.children {
208 child.paint(canvas);
209 }
210 }
211
212 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
213 for child in &mut self.children {
214 if let Some(msg) = child.event(event) {
215 return Some(msg);
216 }
217 }
218 None
219 }
220
221 fn children(&self) -> &[Box<dyn Widget>] {
222 &self.children
223 }
224
225 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
226 &mut self.children
227 }
228
229 fn test_id(&self) -> Option<&str> {
230 self.test_id_value.as_deref()
231 }
232}
233
234impl Brick for Column {
236 fn brick_name(&self) -> &'static str {
237 "Column"
238 }
239
240 fn assertions(&self) -> &[BrickAssertion] {
241 &[BrickAssertion::MaxLatencyMs(16)]
242 }
243
244 fn budget(&self) -> BrickBudget {
245 BrickBudget::uniform(16)
246 }
247
248 fn verify(&self) -> BrickVerification {
249 BrickVerification {
250 passed: self.assertions().to_vec(),
251 failed: vec![],
252 verification_time: Duration::from_micros(10),
253 }
254 }
255
256 fn to_html(&self) -> String {
257 let test_id = self.test_id_value.as_deref().unwrap_or("column");
258 format!(r#"<div class="brick-column" data-testid="{test_id}"></div>"#)
259 }
260
261 fn to_css(&self) -> String {
262 ".brick-column { display: flex; flex-direction: column; }".into()
263 }
264
265 fn test_id(&self) -> Option<&str> {
266 self.test_id_value.as_deref()
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273 use presentar_core::widget::AccessibleRole;
274 use presentar_core::{Brick, BrickBudget, BrickVerification, Widget};
275 use std::time::Duration;
276
277 struct FixedWidget {
279 size: Size,
280 }
281
282 impl FixedWidget {
283 fn new(width: f32, height: f32) -> Self {
284 Self {
285 size: Size::new(width, height),
286 }
287 }
288 }
289
290 impl Brick for FixedWidget {
291 fn brick_name(&self) -> &'static str {
292 "FixedWidget"
293 }
294
295 fn assertions(&self) -> &[presentar_core::BrickAssertion] {
296 &[]
297 }
298
299 fn budget(&self) -> BrickBudget {
300 BrickBudget::uniform(16)
301 }
302
303 fn verify(&self) -> BrickVerification {
304 BrickVerification {
305 passed: vec![],
306 failed: vec![],
307 verification_time: Duration::from_micros(1),
308 }
309 }
310
311 fn to_html(&self) -> String {
312 String::new()
313 }
314
315 fn to_css(&self) -> String {
316 String::new()
317 }
318 }
319
320 impl Widget for FixedWidget {
321 fn type_id(&self) -> TypeId {
322 TypeId::of::<Self>()
323 }
324
325 fn measure(&self, constraints: Constraints) -> Size {
326 constraints.constrain(self.size)
327 }
328
329 fn layout(&mut self, _bounds: Rect) -> LayoutResult {
330 LayoutResult { size: self.size }
331 }
332
333 fn paint(&self, _canvas: &mut dyn Canvas) {}
334
335 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
336 None
337 }
338
339 fn children(&self) -> &[Box<dyn Widget>] {
340 &[]
341 }
342
343 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
344 &mut []
345 }
346
347 fn accessible_role(&self) -> AccessibleRole {
348 AccessibleRole::Generic
349 }
350 }
351
352 #[test]
355 fn test_column_empty() {
356 let col = Column::new();
357 let size = col.measure(Constraints::loose(Size::new(100.0, 100.0)));
358 assert_eq!(size, Size::ZERO);
359 }
360
361 #[test]
362 fn test_column_builder() {
363 let col = Column::new()
364 .gap(10.0)
365 .main_axis_alignment(MainAxisAlignment::Center)
366 .cross_axis_alignment(CrossAxisAlignment::Start)
367 .with_test_id("my-column");
368
369 assert_eq!(col.gap, 10.0);
370 assert_eq!(col.main_axis_alignment, MainAxisAlignment::Center);
371 assert_eq!(col.cross_axis_alignment, CrossAxisAlignment::Start);
372 assert_eq!(Widget::test_id(&col), Some("my-column"));
373 }
374
375 #[test]
376 fn test_column_default() {
377 let col = Column::default();
378 assert_eq!(col.main_axis_alignment, MainAxisAlignment::Start);
379 assert_eq!(col.cross_axis_alignment, CrossAxisAlignment::Center);
380 assert_eq!(col.gap, 0.0);
381 }
382
383 #[test]
384 fn test_column_type_id() {
385 let col = Column::new();
386 assert_eq!(Widget::type_id(&col), TypeId::of::<Column>());
387 }
388
389 #[test]
390 fn test_column_children() {
391 let col = Column::new()
392 .child(FixedWidget::new(50.0, 30.0))
393 .child(FixedWidget::new(50.0, 30.0));
394 assert_eq!(col.children().len(), 2);
395 }
396
397 #[test]
400 fn test_column_measure_single_child() {
401 let col = Column::new().child(FixedWidget::new(50.0, 30.0));
402 let size = col.measure(Constraints::loose(Size::new(200.0, 200.0)));
403 assert_eq!(size, Size::new(50.0, 30.0));
404 }
405
406 #[test]
407 fn test_column_measure_multiple_children() {
408 let col = Column::new()
409 .child(FixedWidget::new(50.0, 30.0))
410 .child(FixedWidget::new(60.0, 40.0));
411 let size = col.measure(Constraints::loose(Size::new(200.0, 200.0)));
412 assert_eq!(size, Size::new(60.0, 70.0)); }
414
415 #[test]
416 fn test_column_measure_with_gap() {
417 let col = Column::new()
418 .gap(10.0)
419 .child(FixedWidget::new(50.0, 30.0))
420 .child(FixedWidget::new(50.0, 30.0));
421 let size = col.measure(Constraints::loose(Size::new(200.0, 200.0)));
422 assert_eq!(size, Size::new(50.0, 70.0)); }
424
425 #[test]
426 fn test_column_measure_constrained() {
427 let col = Column::new()
428 .child(FixedWidget::new(100.0, 100.0))
429 .child(FixedWidget::new(100.0, 100.0));
430 let size = col.measure(Constraints::tight(Size::new(80.0, 150.0)));
431 assert_eq!(size, Size::new(80.0, 150.0)); }
433
434 #[test]
437 fn test_column_alignment_start() {
438 let mut col = Column::new()
439 .main_axis_alignment(MainAxisAlignment::Start)
440 .child(FixedWidget::new(30.0, 20.0))
441 .child(FixedWidget::new(30.0, 20.0));
442
443 col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
444
445 assert_eq!(col.child_bounds.len(), 2);
446 assert_eq!(col.child_bounds[0].y, 0.0);
447 assert_eq!(col.child_bounds[1].y, 20.0);
448 }
449
450 #[test]
451 fn test_column_alignment_end() {
452 let mut col = Column::new()
453 .main_axis_alignment(MainAxisAlignment::End)
454 .child(FixedWidget::new(30.0, 20.0))
455 .child(FixedWidget::new(30.0, 20.0));
456
457 col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
458
459 assert_eq!(col.child_bounds[0].y, 160.0);
461 assert_eq!(col.child_bounds[1].y, 180.0);
462 }
463
464 #[test]
465 fn test_column_alignment_center() {
466 let mut col = Column::new()
467 .main_axis_alignment(MainAxisAlignment::Center)
468 .child(FixedWidget::new(30.0, 20.0))
469 .child(FixedWidget::new(30.0, 20.0));
470
471 col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
472
473 assert_eq!(col.child_bounds[0].y, 80.0);
475 assert_eq!(col.child_bounds[1].y, 100.0);
476 }
477
478 #[test]
479 fn test_column_alignment_space_between() {
480 let mut col = Column::new()
481 .main_axis_alignment(MainAxisAlignment::SpaceBetween)
482 .child(FixedWidget::new(30.0, 20.0))
483 .child(FixedWidget::new(30.0, 20.0));
484
485 col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
486
487 assert_eq!(col.child_bounds[0].y, 0.0);
489 assert_eq!(col.child_bounds[1].y, 180.0); }
491
492 #[test]
493 fn test_column_alignment_space_between_single_child() {
494 let mut col = Column::new()
495 .main_axis_alignment(MainAxisAlignment::SpaceBetween)
496 .child(FixedWidget::new(30.0, 20.0));
497
498 col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
499
500 assert_eq!(col.child_bounds[0].y, 0.0);
502 }
503
504 #[test]
505 fn test_column_alignment_space_between_three_children() {
506 let mut col = Column::new()
507 .main_axis_alignment(MainAxisAlignment::SpaceBetween)
508 .child(FixedWidget::new(30.0, 20.0))
509 .child(FixedWidget::new(30.0, 20.0))
510 .child(FixedWidget::new(30.0, 20.0));
511
512 col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
513
514 assert_eq!(col.child_bounds[0].y, 0.0);
516 assert_eq!(col.child_bounds[1].y, 90.0); assert_eq!(col.child_bounds[2].y, 180.0); }
519
520 #[test]
521 fn test_column_alignment_space_around() {
522 let mut col = Column::new()
523 .main_axis_alignment(MainAxisAlignment::SpaceAround)
524 .child(FixedWidget::new(30.0, 40.0))
525 .child(FixedWidget::new(30.0, 40.0));
526
527 col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
528
529 assert_eq!(col.child_bounds[0].y, 30.0);
532 assert_eq!(col.child_bounds[1].y, 130.0);
533 }
534
535 #[test]
536 fn test_column_alignment_space_evenly() {
537 let mut col = Column::new()
538 .main_axis_alignment(MainAxisAlignment::SpaceEvenly)
539 .child(FixedWidget::new(30.0, 40.0))
540 .child(FixedWidget::new(30.0, 40.0));
541
542 col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
543
544 assert_eq!(col.child_bounds[0].y, 40.0);
547 assert_eq!(col.child_bounds[1].y, 120.0);
548 }
549
550 #[test]
553 fn test_column_cross_alignment_start() {
554 let mut col = Column::new()
555 .cross_axis_alignment(CrossAxisAlignment::Start)
556 .child(FixedWidget::new(30.0, 20.0));
557
558 col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
559
560 assert_eq!(col.child_bounds[0].x, 0.0);
561 assert_eq!(col.child_bounds[0].width, 30.0);
562 }
563
564 #[test]
565 fn test_column_cross_alignment_end() {
566 let mut col = Column::new()
567 .cross_axis_alignment(CrossAxisAlignment::End)
568 .child(FixedWidget::new(30.0, 20.0));
569
570 col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
571
572 assert_eq!(col.child_bounds[0].x, 70.0); assert_eq!(col.child_bounds[0].width, 30.0);
574 }
575
576 #[test]
577 fn test_column_cross_alignment_center() {
578 let mut col = Column::new()
579 .cross_axis_alignment(CrossAxisAlignment::Center)
580 .child(FixedWidget::new(30.0, 20.0));
581
582 col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
583
584 assert_eq!(col.child_bounds[0].x, 35.0); assert_eq!(col.child_bounds[0].width, 30.0);
586 }
587
588 #[test]
589 fn test_column_cross_alignment_stretch() {
590 let mut col = Column::new()
591 .cross_axis_alignment(CrossAxisAlignment::Stretch)
592 .child(FixedWidget::new(30.0, 20.0));
593
594 col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
595
596 assert_eq!(col.child_bounds[0].x, 0.0);
597 assert_eq!(col.child_bounds[0].width, 100.0); }
599
600 #[test]
603 fn test_column_gap_single_child() {
604 let mut col = Column::new().gap(20.0).child(FixedWidget::new(30.0, 20.0));
605
606 col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
607
608 assert_eq!(col.child_bounds[0].y, 0.0);
610 }
611
612 #[test]
613 fn test_column_gap_multiple_children() {
614 let mut col = Column::new()
615 .gap(15.0)
616 .child(FixedWidget::new(30.0, 20.0))
617 .child(FixedWidget::new(30.0, 20.0))
618 .child(FixedWidget::new(30.0, 20.0));
619
620 col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
621
622 assert_eq!(col.child_bounds[0].y, 0.0);
623 assert_eq!(col.child_bounds[1].y, 35.0); assert_eq!(col.child_bounds[2].y, 70.0); }
626
627 #[test]
628 fn test_column_gap_with_alignment_center() {
629 let mut col = Column::new()
630 .gap(10.0)
631 .main_axis_alignment(MainAxisAlignment::Center)
632 .child(FixedWidget::new(30.0, 20.0))
633 .child(FixedWidget::new(30.0, 20.0));
634
635 col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
636
637 assert_eq!(col.child_bounds[0].y, 75.0);
639 assert_eq!(col.child_bounds[1].y, 105.0); }
641
642 #[test]
645 fn test_column_layout_empty() {
646 let mut col = Column::new();
647 let result = col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
648 assert_eq!(result.size, Size::ZERO);
649 }
650
651 #[test]
652 fn test_column_content_larger_than_bounds() {
653 let mut col = Column::new()
654 .child(FixedWidget::new(30.0, 100.0))
655 .child(FixedWidget::new(30.0, 100.0))
656 .child(FixedWidget::new(30.0, 100.0));
657
658 col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
660
661 assert_eq!(col.child_bounds[0].y, 0.0);
663 assert_eq!(col.child_bounds[1].y, 100.0);
664 assert_eq!(col.child_bounds[2].y, 200.0);
665 }
666
667 #[test]
668 fn test_column_with_offset_bounds() {
669 let mut col = Column::new()
670 .child(FixedWidget::new(30.0, 20.0))
671 .child(FixedWidget::new(30.0, 20.0));
672
673 col.layout(Rect::new(25.0, 50.0, 100.0, 200.0));
674
675 assert_eq!(col.child_bounds[0].y, 50.0);
677 assert_eq!(col.child_bounds[0].x, 60.0); assert_eq!(col.child_bounds[1].y, 70.0);
679 }
680
681 #[test]
682 fn test_column_varying_child_widths() {
683 let mut col = Column::new()
684 .cross_axis_alignment(CrossAxisAlignment::Center)
685 .child(FixedWidget::new(20.0, 30.0))
686 .child(FixedWidget::new(60.0, 30.0))
687 .child(FixedWidget::new(40.0, 30.0));
688
689 col.layout(Rect::new(0.0, 0.0, 100.0, 200.0));
690
691 assert_eq!(col.child_bounds[0].x, 40.0); assert_eq!(col.child_bounds[1].x, 20.0); assert_eq!(col.child_bounds[2].x, 30.0); }
696}