1use presentar_core::{
4 widget::{Brick, BrickAssertion, BrickBudget, BrickVerification, LayoutResult},
5 Canvas, Constraints, Event, Rect, Size, TypeId, Widget,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9use std::time::Duration;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
13pub enum MainAxisAlignment {
14 #[default]
16 Start,
17 End,
19 Center,
21 SpaceBetween,
23 SpaceAround,
25 SpaceEvenly,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
31pub enum CrossAxisAlignment {
32 Start,
34 End,
36 #[default]
38 Center,
39 Stretch,
41}
42
43#[derive(Serialize, Deserialize)]
45pub struct Row {
46 main_axis_alignment: MainAxisAlignment,
48 cross_axis_alignment: CrossAxisAlignment,
50 gap: f32,
52 #[serde(skip)]
54 children: Vec<Box<dyn Widget>>,
55 test_id_value: Option<String>,
57 #[serde(skip)]
59 bounds: Rect,
60 #[serde(skip)]
62 child_bounds: Vec<Rect>,
63}
64
65impl Default for Row {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl Row {
72 #[must_use]
74 pub fn new() -> Self {
75 Self {
76 main_axis_alignment: MainAxisAlignment::Start,
77 cross_axis_alignment: CrossAxisAlignment::Center,
78 gap: 0.0,
79 children: Vec::new(),
80 test_id_value: None,
81 bounds: Rect::default(),
82 child_bounds: Vec::new(),
83 }
84 }
85
86 #[must_use]
88 pub const fn main_axis_alignment(mut self, alignment: MainAxisAlignment) -> Self {
89 self.main_axis_alignment = alignment;
90 self
91 }
92
93 #[must_use]
95 pub const fn cross_axis_alignment(mut self, alignment: CrossAxisAlignment) -> Self {
96 self.cross_axis_alignment = alignment;
97 self
98 }
99
100 #[must_use]
102 pub const fn gap(mut self, gap: f32) -> Self {
103 self.gap = gap;
104 self
105 }
106
107 pub fn child(mut self, widget: impl Widget + 'static) -> Self {
109 self.children.push(Box::new(widget));
110 self
111 }
112
113 #[must_use]
115 pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
116 self.test_id_value = Some(id.into());
117 self
118 }
119}
120
121impl Widget for Row {
122 fn type_id(&self) -> TypeId {
123 TypeId::of::<Self>()
124 }
125
126 fn measure(&self, constraints: Constraints) -> Size {
127 if self.children.is_empty() {
128 return Size::ZERO;
129 }
130
131 let mut total_width = 0.0f32;
132 let mut max_height = 0.0f32;
133
134 for (i, child) in self.children.iter().enumerate() {
136 let child_constraints = Constraints::new(
137 0.0,
138 (constraints.max_width - total_width).max(0.0),
139 0.0,
140 constraints.max_height,
141 );
142
143 let child_size = child.measure(child_constraints);
144 total_width += child_size.width;
145 max_height = max_height.max(child_size.height);
146
147 if i < self.children.len() - 1 {
148 total_width += self.gap;
149 }
150 }
151
152 constraints.constrain(Size::new(total_width, max_height))
153 }
154
155 fn layout(&mut self, bounds: Rect) -> LayoutResult {
156 self.bounds = bounds;
157 self.child_bounds.clear();
158
159 if self.children.is_empty() {
160 return LayoutResult { size: Size::ZERO };
161 }
162
163 let mut child_sizes: Vec<Size> = Vec::with_capacity(self.children.len());
165 let mut total_width = 0.0f32;
166
167 for child in &self.children {
168 let child_constraints = Constraints::loose(bounds.size());
169 let size = child.measure(child_constraints);
170 total_width += size.width;
171 child_sizes.push(size);
172 }
173
174 let gaps_width = self.gap * (self.children.len() - 1).max(0) as f32;
175 let content_width = total_width + gaps_width;
176 let remaining_space = (bounds.width - content_width).max(0.0);
177
178 let (mut x, extra_gap) = match self.main_axis_alignment {
180 MainAxisAlignment::Start => (bounds.x, 0.0),
181 MainAxisAlignment::End => (bounds.x + remaining_space, 0.0),
182 MainAxisAlignment::Center => (bounds.x + remaining_space / 2.0, 0.0),
183 MainAxisAlignment::SpaceBetween => {
184 if self.children.len() > 1 {
185 (bounds.x, remaining_space / (self.children.len() - 1) as f32)
186 } else {
187 (bounds.x, 0.0)
188 }
189 }
190 MainAxisAlignment::SpaceAround => {
191 let gap = remaining_space / self.children.len() as f32;
192 (bounds.x + gap / 2.0, gap)
193 }
194 MainAxisAlignment::SpaceEvenly => {
195 let gap = remaining_space / (self.children.len() + 1) as f32;
196 (bounds.x + gap, gap)
197 }
198 };
199
200 let num_children = self.children.len();
202 for (i, (child, size)) in self.children.iter_mut().zip(child_sizes.iter()).enumerate() {
203 let y = match self.cross_axis_alignment {
204 CrossAxisAlignment::Start | CrossAxisAlignment::Stretch => bounds.y,
205 CrossAxisAlignment::End => bounds.y + bounds.height - size.height,
206 CrossAxisAlignment::Center => bounds.y + (bounds.height - size.height) / 2.0,
207 };
208
209 let height = if self.cross_axis_alignment == CrossAxisAlignment::Stretch {
210 bounds.height
211 } else {
212 size.height
213 };
214
215 let child_bounds = Rect::new(x, y, size.width, height);
216 child.layout(child_bounds);
217 self.child_bounds.push(child_bounds);
218
219 if i < num_children - 1 {
221 x += size.width;
222 if self.main_axis_alignment == MainAxisAlignment::SpaceBetween {
223 x += extra_gap;
225 } else {
226 x += self.gap + extra_gap;
227 }
228 }
229 }
230
231 LayoutResult {
232 size: bounds.size(),
233 }
234 }
235
236 fn paint(&self, canvas: &mut dyn Canvas) {
237 for child in &self.children {
238 child.paint(canvas);
239 }
240 }
241
242 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
243 for child in &mut self.children {
244 if let Some(msg) = child.event(event) {
245 return Some(msg);
246 }
247 }
248 None
249 }
250
251 fn children(&self) -> &[Box<dyn Widget>] {
252 &self.children
253 }
254
255 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
256 &mut self.children
257 }
258
259 fn test_id(&self) -> Option<&str> {
260 self.test_id_value.as_deref()
261 }
262}
263
264impl Brick for Row {
266 fn brick_name(&self) -> &'static str {
267 "Row"
268 }
269
270 fn assertions(&self) -> &[BrickAssertion] {
271 &[BrickAssertion::MaxLatencyMs(16)]
272 }
273
274 fn budget(&self) -> BrickBudget {
275 BrickBudget::uniform(16)
276 }
277
278 fn verify(&self) -> BrickVerification {
279 BrickVerification {
280 passed: self.assertions().to_vec(),
281 failed: vec![],
282 verification_time: Duration::from_micros(10),
283 }
284 }
285
286 fn to_html(&self) -> String {
287 r#"<div class="brick-row"></div>"#.to_string()
288 }
289
290 fn to_css(&self) -> String {
291 ".brick-row { display: flex; flex-direction: row; }".to_string()
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use presentar_core::widget::AccessibleRole;
299 use presentar_core::{Brick, BrickBudget, BrickVerification, Widget};
300 use std::time::Duration;
301
302 struct FixedWidget {
304 size: Size,
305 }
306
307 impl FixedWidget {
308 fn new(width: f32, height: f32) -> Self {
309 Self {
310 size: Size::new(width, height),
311 }
312 }
313 }
314
315 impl Brick for FixedWidget {
316 fn brick_name(&self) -> &'static str {
317 "FixedWidget"
318 }
319
320 fn assertions(&self) -> &[presentar_core::BrickAssertion] {
321 &[]
322 }
323
324 fn budget(&self) -> BrickBudget {
325 BrickBudget::uniform(16)
326 }
327
328 fn verify(&self) -> BrickVerification {
329 BrickVerification {
330 passed: vec![],
331 failed: vec![],
332 verification_time: Duration::from_micros(1),
333 }
334 }
335
336 fn to_html(&self) -> String {
337 String::new()
338 }
339
340 fn to_css(&self) -> String {
341 String::new()
342 }
343 }
344
345 impl Widget for FixedWidget {
346 fn type_id(&self) -> TypeId {
347 TypeId::of::<Self>()
348 }
349
350 fn measure(&self, constraints: Constraints) -> Size {
351 constraints.constrain(self.size)
352 }
353
354 fn layout(&mut self, _bounds: Rect) -> LayoutResult {
355 LayoutResult { size: self.size }
356 }
357
358 fn paint(&self, _canvas: &mut dyn Canvas) {}
359
360 fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
361 None
362 }
363
364 fn children(&self) -> &[Box<dyn Widget>] {
365 &[]
366 }
367
368 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
369 &mut []
370 }
371
372 fn accessible_role(&self) -> AccessibleRole {
373 AccessibleRole::Generic
374 }
375 }
376
377 #[test]
380 fn test_row_empty() {
381 let row = Row::new();
382 let size = row.measure(Constraints::loose(Size::new(100.0, 100.0)));
383 assert_eq!(size, Size::ZERO);
384 }
385
386 #[test]
387 fn test_row_builder() {
388 let row = Row::new()
389 .gap(10.0)
390 .main_axis_alignment(MainAxisAlignment::Center)
391 .cross_axis_alignment(CrossAxisAlignment::Start)
392 .with_test_id("my-row");
393
394 assert_eq!(row.gap, 10.0);
395 assert_eq!(row.main_axis_alignment, MainAxisAlignment::Center);
396 assert_eq!(row.cross_axis_alignment, CrossAxisAlignment::Start);
397 assert_eq!(Widget::test_id(&row), Some("my-row"));
398 }
399
400 #[test]
401 fn test_row_default() {
402 let row = Row::default();
403 assert_eq!(row.main_axis_alignment, MainAxisAlignment::Start);
404 assert_eq!(row.cross_axis_alignment, CrossAxisAlignment::Center);
405 assert_eq!(row.gap, 0.0);
406 }
407
408 #[test]
409 fn test_row_type_id() {
410 let row = Row::new();
411 assert_eq!(Widget::type_id(&row), TypeId::of::<Row>());
412 }
413
414 #[test]
415 fn test_row_children() {
416 let row = Row::new()
417 .child(FixedWidget::new(50.0, 30.0))
418 .child(FixedWidget::new(50.0, 30.0));
419 assert_eq!(row.children().len(), 2);
420 }
421
422 #[test]
425 fn test_row_measure_single_child() {
426 let row = Row::new().child(FixedWidget::new(50.0, 30.0));
427 let size = row.measure(Constraints::loose(Size::new(200.0, 100.0)));
428 assert_eq!(size, Size::new(50.0, 30.0));
429 }
430
431 #[test]
432 fn test_row_measure_multiple_children() {
433 let row = Row::new()
434 .child(FixedWidget::new(50.0, 30.0))
435 .child(FixedWidget::new(60.0, 40.0));
436 let size = row.measure(Constraints::loose(Size::new(200.0, 100.0)));
437 assert_eq!(size, Size::new(110.0, 40.0));
438 }
439
440 #[test]
441 fn test_row_measure_with_gap() {
442 let row = Row::new()
443 .gap(10.0)
444 .child(FixedWidget::new(50.0, 30.0))
445 .child(FixedWidget::new(50.0, 30.0));
446 let size = row.measure(Constraints::loose(Size::new(200.0, 100.0)));
447 assert_eq!(size, Size::new(110.0, 30.0)); }
449
450 #[test]
451 fn test_row_measure_constrained() {
452 let row = Row::new()
453 .child(FixedWidget::new(100.0, 50.0))
454 .child(FixedWidget::new(100.0, 50.0));
455 let size = row.measure(Constraints::tight(Size::new(150.0, 40.0)));
456 assert_eq!(size, Size::new(150.0, 40.0)); }
458
459 #[test]
462 fn test_row_alignment_start() {
463 let mut row = Row::new()
464 .main_axis_alignment(MainAxisAlignment::Start)
465 .child(FixedWidget::new(30.0, 20.0))
466 .child(FixedWidget::new(30.0, 20.0));
467
468 row.layout(Rect::new(0.0, 0.0, 200.0, 50.0));
469
470 assert_eq!(row.child_bounds.len(), 2);
471 assert_eq!(row.child_bounds[0].x, 0.0);
472 assert_eq!(row.child_bounds[1].x, 30.0);
473 }
474
475 #[test]
476 fn test_row_alignment_end() {
477 let mut row = Row::new()
478 .main_axis_alignment(MainAxisAlignment::End)
479 .child(FixedWidget::new(30.0, 20.0))
480 .child(FixedWidget::new(30.0, 20.0));
481
482 row.layout(Rect::new(0.0, 0.0, 200.0, 50.0));
483
484 assert_eq!(row.child_bounds[0].x, 140.0);
486 assert_eq!(row.child_bounds[1].x, 170.0);
487 }
488
489 #[test]
490 fn test_row_alignment_center() {
491 let mut row = Row::new()
492 .main_axis_alignment(MainAxisAlignment::Center)
493 .child(FixedWidget::new(30.0, 20.0))
494 .child(FixedWidget::new(30.0, 20.0));
495
496 row.layout(Rect::new(0.0, 0.0, 200.0, 50.0));
497
498 assert_eq!(row.child_bounds[0].x, 70.0);
500 assert_eq!(row.child_bounds[1].x, 100.0);
501 }
502
503 #[test]
504 fn test_row_alignment_space_between() {
505 let mut row = Row::new()
506 .main_axis_alignment(MainAxisAlignment::SpaceBetween)
507 .child(FixedWidget::new(30.0, 20.0))
508 .child(FixedWidget::new(30.0, 20.0));
509
510 row.layout(Rect::new(0.0, 0.0, 200.0, 50.0));
511
512 assert_eq!(row.child_bounds[0].x, 0.0);
514 assert_eq!(row.child_bounds[1].x, 170.0); }
516
517 #[test]
518 fn test_row_alignment_space_between_single_child() {
519 let mut row = Row::new()
520 .main_axis_alignment(MainAxisAlignment::SpaceBetween)
521 .child(FixedWidget::new(30.0, 20.0));
522
523 row.layout(Rect::new(0.0, 0.0, 200.0, 50.0));
524
525 assert_eq!(row.child_bounds[0].x, 0.0);
527 }
528
529 #[test]
530 fn test_row_alignment_space_between_three_children() {
531 let mut row = Row::new()
532 .main_axis_alignment(MainAxisAlignment::SpaceBetween)
533 .child(FixedWidget::new(30.0, 20.0))
534 .child(FixedWidget::new(30.0, 20.0))
535 .child(FixedWidget::new(30.0, 20.0));
536
537 row.layout(Rect::new(0.0, 0.0, 200.0, 50.0));
538
539 assert_eq!(row.child_bounds[0].x, 0.0);
541 assert_eq!(row.child_bounds[1].x, 85.0); assert_eq!(row.child_bounds[2].x, 170.0); }
544
545 #[test]
546 fn test_row_alignment_space_around() {
547 let mut row = Row::new()
548 .main_axis_alignment(MainAxisAlignment::SpaceAround)
549 .child(FixedWidget::new(40.0, 20.0))
550 .child(FixedWidget::new(40.0, 20.0));
551
552 row.layout(Rect::new(0.0, 0.0, 200.0, 50.0));
553
554 assert_eq!(row.child_bounds[0].x, 30.0);
557 assert_eq!(row.child_bounds[1].x, 130.0);
558 }
559
560 #[test]
561 fn test_row_alignment_space_evenly() {
562 let mut row = Row::new()
563 .main_axis_alignment(MainAxisAlignment::SpaceEvenly)
564 .child(FixedWidget::new(40.0, 20.0))
565 .child(FixedWidget::new(40.0, 20.0));
566
567 row.layout(Rect::new(0.0, 0.0, 200.0, 50.0));
568
569 assert_eq!(row.child_bounds[0].x, 40.0);
572 assert_eq!(row.child_bounds[1].x, 120.0);
573 }
574
575 #[test]
578 fn test_row_cross_alignment_start() {
579 let mut row = Row::new()
580 .cross_axis_alignment(CrossAxisAlignment::Start)
581 .child(FixedWidget::new(30.0, 20.0));
582
583 row.layout(Rect::new(0.0, 0.0, 200.0, 100.0));
584
585 assert_eq!(row.child_bounds[0].y, 0.0);
586 assert_eq!(row.child_bounds[0].height, 20.0);
587 }
588
589 #[test]
590 fn test_row_cross_alignment_end() {
591 let mut row = Row::new()
592 .cross_axis_alignment(CrossAxisAlignment::End)
593 .child(FixedWidget::new(30.0, 20.0));
594
595 row.layout(Rect::new(0.0, 0.0, 200.0, 100.0));
596
597 assert_eq!(row.child_bounds[0].y, 80.0); assert_eq!(row.child_bounds[0].height, 20.0);
599 }
600
601 #[test]
602 fn test_row_cross_alignment_center() {
603 let mut row = Row::new()
604 .cross_axis_alignment(CrossAxisAlignment::Center)
605 .child(FixedWidget::new(30.0, 20.0));
606
607 row.layout(Rect::new(0.0, 0.0, 200.0, 100.0));
608
609 assert_eq!(row.child_bounds[0].y, 40.0); assert_eq!(row.child_bounds[0].height, 20.0);
611 }
612
613 #[test]
614 fn test_row_cross_alignment_stretch() {
615 let mut row = Row::new()
616 .cross_axis_alignment(CrossAxisAlignment::Stretch)
617 .child(FixedWidget::new(30.0, 20.0));
618
619 row.layout(Rect::new(0.0, 0.0, 200.0, 100.0));
620
621 assert_eq!(row.child_bounds[0].y, 0.0);
622 assert_eq!(row.child_bounds[0].height, 100.0); }
624
625 #[test]
628 fn test_row_gap_single_child() {
629 let mut row = Row::new().gap(20.0).child(FixedWidget::new(30.0, 20.0));
630
631 row.layout(Rect::new(0.0, 0.0, 200.0, 50.0));
632
633 assert_eq!(row.child_bounds[0].x, 0.0);
635 }
636
637 #[test]
638 fn test_row_gap_multiple_children() {
639 let mut row = Row::new()
640 .gap(15.0)
641 .child(FixedWidget::new(30.0, 20.0))
642 .child(FixedWidget::new(30.0, 20.0))
643 .child(FixedWidget::new(30.0, 20.0));
644
645 row.layout(Rect::new(0.0, 0.0, 200.0, 50.0));
646
647 assert_eq!(row.child_bounds[0].x, 0.0);
648 assert_eq!(row.child_bounds[1].x, 45.0); assert_eq!(row.child_bounds[2].x, 90.0); }
651
652 #[test]
653 fn test_row_gap_with_alignment_center() {
654 let mut row = Row::new()
655 .gap(10.0)
656 .main_axis_alignment(MainAxisAlignment::Center)
657 .child(FixedWidget::new(30.0, 20.0))
658 .child(FixedWidget::new(30.0, 20.0));
659
660 row.layout(Rect::new(0.0, 0.0, 200.0, 50.0));
661
662 assert_eq!(row.child_bounds[0].x, 65.0);
664 assert_eq!(row.child_bounds[1].x, 105.0); }
666
667 #[test]
670 fn test_row_layout_empty() {
671 let mut row = Row::new();
672 let result = row.layout(Rect::new(0.0, 0.0, 200.0, 50.0));
673 assert_eq!(result.size, Size::ZERO);
674 }
675
676 #[test]
677 fn test_row_content_larger_than_bounds() {
678 let mut row = Row::new()
679 .child(FixedWidget::new(100.0, 30.0))
680 .child(FixedWidget::new(100.0, 30.0))
681 .child(FixedWidget::new(100.0, 30.0));
682
683 row.layout(Rect::new(0.0, 0.0, 200.0, 50.0));
685
686 assert_eq!(row.child_bounds[0].x, 0.0);
688 assert_eq!(row.child_bounds[1].x, 100.0);
689 assert_eq!(row.child_bounds[2].x, 200.0);
690 }
691
692 #[test]
693 fn test_row_with_offset_bounds() {
694 let mut row = Row::new()
695 .child(FixedWidget::new(30.0, 20.0))
696 .child(FixedWidget::new(30.0, 20.0));
697
698 row.layout(Rect::new(50.0, 25.0, 200.0, 50.0));
699
700 assert_eq!(row.child_bounds[0].x, 50.0);
702 assert_eq!(row.child_bounds[0].y, 40.0); assert_eq!(row.child_bounds[1].x, 80.0);
704 }
705
706 #[test]
707 fn test_row_varying_child_heights() {
708 let mut row = Row::new()
709 .cross_axis_alignment(CrossAxisAlignment::Center)
710 .child(FixedWidget::new(30.0, 20.0))
711 .child(FixedWidget::new(30.0, 60.0))
712 .child(FixedWidget::new(30.0, 40.0));
713
714 row.layout(Rect::new(0.0, 0.0, 200.0, 100.0));
715
716 assert_eq!(row.child_bounds[0].y, 40.0); assert_eq!(row.child_bounds[1].y, 20.0); assert_eq!(row.child_bounds[2].y, 30.0); }
721
722 #[test]
725 fn test_main_axis_alignment_default() {
726 assert_eq!(MainAxisAlignment::default(), MainAxisAlignment::Start);
727 }
728
729 #[test]
730 fn test_cross_axis_alignment_default() {
731 assert_eq!(CrossAxisAlignment::default(), CrossAxisAlignment::Center);
732 }
733}