1use crate::vdom::VNode;
4use ratatui::layout::Rect;
5
6#[derive(Debug, Clone, Copy, PartialEq, Default)]
8pub enum FlexDirection {
9 #[default]
10 Row,
11 Column,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Default)]
16pub enum JustifyContent {
17 #[default]
18 Start,
19 Center,
20 End,
21 SpaceBetween,
22 SpaceAround,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Default)]
27pub enum AlignItems {
28 #[default]
29 Start,
30 Center,
31 End,
32 Stretch,
33}
34
35#[derive(Debug, Clone, Copy, Default)]
37pub struct FlexStyle {
38 pub direction: FlexDirection,
39 pub justify_content: JustifyContent,
40 pub align_items: AlignItems,
41 pub gap: u16,
42 pub flex_grow: f32,
43 pub flex_shrink: f32,
44}
45
46impl FlexStyle {
47 pub fn new() -> Self {
48 Self::default()
49 }
50
51 pub fn row(mut self) -> Self {
52 self.direction = FlexDirection::Row;
53 self
54 }
55
56 pub fn column(mut self) -> Self {
57 self.direction = FlexDirection::Column;
58 self
59 }
60
61 pub fn justify(mut self, justify: JustifyContent) -> Self {
62 self.justify_content = justify;
63 self
64 }
65
66 pub fn align(mut self, align: AlignItems) -> Self {
67 self.align_items = align;
68 self
69 }
70
71 pub fn gap(mut self, gap: u16) -> Self {
72 self.gap = gap;
73 self
74 }
75
76 pub fn flex_grow(mut self, grow: f32) -> Self {
77 self.flex_grow = grow;
78 self
79 }
80
81 pub fn flex_shrink(mut self, shrink: f32) -> Self {
82 self.flex_shrink = shrink;
83 self
84 }
85}
86
87#[derive(Debug, Clone, Copy, Default)]
89pub struct FlexboxLayout;
90
91impl FlexboxLayout {
92 pub fn new() -> Self {
93 Self
94 }
95
96 pub fn calculate(node: &VNode, constraints: Rect) -> Vec<Rect> {
98 let children = match node.children() {
99 Some(children) if !children.is_empty() => children,
100 _ => return vec![],
101 };
102
103 let style = Self::extract_style(node);
105
106 let base_sizes: Vec<Rect> = children
108 .iter()
109 .map(|_| Rect {
110 x: 0,
111 y: 0,
112 width: 1, height: 1, })
115 .collect();
116
117 Self::layout_children(&base_sizes, constraints, &style, children.len())
118 }
119
120 fn extract_style(node: &VNode) -> FlexStyle {
122 let attrs = node.attrs().unwrap();
123
124 FlexStyle {
125 direction: attrs
126 .get("direction")
127 .and_then(|v| match v.as_str() {
128 "row" => Some(FlexDirection::Row),
129 "column" => Some(FlexDirection::Column),
130 _ => None,
131 })
132 .unwrap_or(FlexDirection::Row),
133
134 justify_content: attrs
135 .get("justify")
136 .and_then(|v| match v.as_str() {
137 "start" => Some(JustifyContent::Start),
138 "center" => Some(JustifyContent::Center),
139 "end" => Some(JustifyContent::End),
140 "space-between" => Some(JustifyContent::SpaceBetween),
141 "space-around" => Some(JustifyContent::SpaceAround),
142 _ => None,
143 })
144 .unwrap_or(JustifyContent::Start),
145
146 align_items: attrs
147 .get("align")
148 .and_then(|v| match v.as_str() {
149 "start" => Some(AlignItems::Start),
150 "center" => Some(AlignItems::Center),
151 "end" => Some(AlignItems::End),
152 "stretch" => Some(AlignItems::Stretch),
153 _ => None,
154 })
155 .unwrap_or(AlignItems::Start),
156
157 gap: attrs.get("gap").and_then(|v| v.parse().ok()).unwrap_or(0),
158
159 flex_grow: attrs
160 .get("flex-grow")
161 .and_then(|v| v.parse().ok())
162 .unwrap_or(0.0),
163
164 flex_shrink: attrs
165 .get("flex-shrink")
166 .and_then(|v| v.parse().ok())
167 .unwrap_or(1.0),
168 }
169 }
170
171 fn layout_children(
173 _base_sizes: &[Rect],
174 constraints: Rect,
175 style: &FlexStyle,
176 child_count: usize,
177 ) -> Vec<Rect> {
178 if child_count == 0 {
179 return vec![];
180 }
181
182 let main_axis_size = match style.direction {
183 FlexDirection::Row => constraints.width,
184 FlexDirection::Column => constraints.height,
185 };
186
187 let cross_axis_size = match style.direction {
188 FlexDirection::Row => constraints.height,
189 FlexDirection::Column => constraints.width,
190 };
191
192 let total_gap = style
194 .gap
195 .saturating_mul(child_count.saturating_sub(1) as u16);
196 let available_space = main_axis_size.saturating_sub(total_gap);
197
198 let main_positions =
200 Self::distribute_main_axis(available_space, style, child_count, main_axis_size);
201
202 let cross_positions =
204 Self::distribute_cross_axis(cross_axis_size, &style.align_items, child_count);
205
206 let mut results = Vec::with_capacity(child_count);
208
209 for i in 0..child_count {
210 let child_width = match style.direction {
211 FlexDirection::Row => main_positions[i].size,
212 FlexDirection::Column => cross_axis_size,
213 };
214
215 let child_height = match style.direction {
216 FlexDirection::Row => cross_axis_size,
217 FlexDirection::Column => main_positions[i].size,
218 };
219
220 let x = match style.direction {
221 FlexDirection::Row => constraints.x + main_positions[i].pos,
222 FlexDirection::Column => constraints.x + cross_positions[i].pos,
223 };
224
225 let y = match style.direction {
226 FlexDirection::Row => constraints.y + cross_positions[i].pos,
227 FlexDirection::Column => constraints.y + main_positions[i].pos,
228 };
229
230 results.push(Rect {
231 x,
232 y,
233 width: child_width,
234 height: child_height,
235 });
236 }
237
238 results
239 }
240
241 fn distribute_main_axis(
243 available_space: u16,
244 style: &FlexStyle,
245 child_count: usize,
246 _container_size: u16,
247 ) -> Vec<PositionInfo> {
248 let mut positions = Vec::with_capacity(child_count);
249
250 if child_count == 0 {
251 return positions;
252 }
253
254 let gap_space = style
256 .gap
257 .saturating_mul(child_count.saturating_sub(1) as u16);
258 let usable_space = available_space.saturating_sub(gap_space);
259
260 let total_flex_grow = if style.flex_grow > 0.0 {
262 style.flex_grow * child_count as f32
263 } else {
264 child_count as f32 };
266
267 let base_size = if total_flex_grow > 0.0 {
269 (usable_space as f32 / total_flex_grow) as u16
270 } else {
271 0
272 };
273
274 let total_used = base_size
276 .saturating_mul(child_count as u16)
277 .saturating_add(gap_space);
278
279 let mut current_pos = match style.justify_content {
280 JustifyContent::Start => 0,
281 JustifyContent::Center => available_space.saturating_sub(total_used) / 2,
282 JustifyContent::End => available_space.saturating_sub(total_used),
283 JustifyContent::SpaceBetween => {
284 if child_count > 1 {
285 let _extra_gap = (usable_space
286 .saturating_sub(base_size.saturating_mul(child_count as u16)))
287 .saturating_div(child_count.saturating_sub(1) as u16);
288 0
289 } else {
290 0
291 }
292 }
293 JustifyContent::SpaceAround => {
294 let extra_gap = (usable_space
295 .saturating_sub(base_size.saturating_mul(child_count as u16)))
296 .saturating_div(child_count as u16);
297 extra_gap / 2
298 }
299 };
300
301 let mut gap = style.gap;
302
303 if style.justify_content == JustifyContent::SpaceBetween && child_count > 1 {
304 let extra = usable_space.saturating_sub(base_size.saturating_mul(child_count as u16));
305 gap = gap.saturating_add(extra / (child_count.saturating_sub(1) as u16));
306 }
307
308 if style.justify_content == JustifyContent::SpaceAround {
309 let extra = usable_space.saturating_sub(base_size.saturating_mul(child_count as u16));
310 let extra_gap = extra / (child_count as u16);
311 gap = extra_gap;
312 }
313
314 for _i in 0..child_count {
315 positions.push(PositionInfo {
316 pos: current_pos,
317 size: base_size,
318 });
319
320 current_pos = current_pos.saturating_add(base_size).saturating_add(gap);
321 }
322
323 positions
324 }
325
326 fn distribute_cross_axis(
328 cross_axis_size: u16,
329 style: &AlignItems,
330 child_count: usize,
331 ) -> Vec<PositionInfo> {
332 let mut positions = Vec::with_capacity(child_count);
333
334 if child_count == 0 {
335 return positions;
336 }
337
338 let child_size = cross_axis_size;
340
341 for _i in 0..child_count {
342 let pos = match style {
343 AlignItems::Start => 0,
344 AlignItems::Center => 0, AlignItems::End => 0, AlignItems::Stretch => 0,
347 };
348
349 positions.push(PositionInfo {
350 pos,
351 size: child_size,
352 });
353 }
354
355 positions
356 }
357}
358
359#[derive(Debug, Clone, Copy)]
361struct PositionInfo {
362 pos: u16,
363 size: u16,
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use crate::vdom::VNode;
370 use std::collections::HashMap;
371
372 fn create_flex_container(attrs: HashMap<String, String>, children: Vec<VNode>) -> VNode {
373 VNode::Element {
374 tag: "flex".to_string(),
375 attrs,
376 children,
377 }
378 }
379
380 fn create_text_node(text: &str) -> VNode {
381 VNode::Text(text.to_string())
382 }
383
384 #[test]
385 fn test_layout_simple_row() {
386 let children = vec![
387 create_text_node("A"),
388 create_text_node("B"),
389 create_text_node("C"),
390 ];
391
392 let container = create_flex_container(HashMap::new(), children);
393
394 let constraints = Rect {
395 x: 0,
396 y: 0,
397 width: 20,
398 height: 10,
399 };
400
401 let results = FlexboxLayout::calculate(&container, constraints);
402
403 assert_eq!(results.len(), 3);
404
405 assert!(results[0].x < results[1].x);
407 assert!(results[1].x < results[2].x);
408
409 assert_eq!(results[0].height, 10);
411 assert_eq!(results[1].height, 10);
412 assert_eq!(results[2].height, 10);
413
414 assert_eq!(results[0].y, 0);
416 assert_eq!(results[1].y, 0);
417 assert_eq!(results[2].y, 0);
418 }
419
420 #[test]
421 fn test_layout_simple_column() {
422 let children = vec![
423 create_text_node("A"),
424 create_text_node("B"),
425 create_text_node("C"),
426 ];
427
428 let mut attrs = HashMap::new();
429 attrs.insert("direction".to_string(), "column".to_string());
430
431 let container = create_flex_container(attrs, children);
432
433 let constraints = Rect {
434 x: 0,
435 y: 0,
436 width: 10,
437 height: 20,
438 };
439
440 let results = FlexboxLayout::calculate(&container, constraints);
441
442 assert_eq!(results.len(), 3);
443
444 assert!(results[0].y < results[1].y);
446 assert!(results[1].y < results[2].y);
447
448 assert_eq!(results[0].width, 10);
450 assert_eq!(results[1].width, 10);
451 assert_eq!(results[2].width, 10);
452
453 assert_eq!(results[0].x, 0);
455 assert_eq!(results[1].x, 0);
456 assert_eq!(results[2].x, 0);
457 }
458
459 #[test]
460 fn test_layout_justify_center() {
461 let children = vec![
462 create_text_node("A"),
463 create_text_node("B"),
464 create_text_node("C"),
465 ];
466
467 let mut attrs = HashMap::new();
468 attrs.insert("justify".to_string(), "center".to_string());
469
470 let container = create_flex_container(attrs, children);
471
472 let constraints = Rect {
473 x: 0,
474 y: 0,
475 width: 20,
476 height: 10,
477 };
478
479 let results = FlexboxLayout::calculate(&container, constraints);
480
481 assert_eq!(results.len(), 3);
482
483 let total_width = results[2].x + results[2].width;
485 assert!(total_width <= 20);
486 assert!(results[0].x > 0 || total_width == 20);
488 }
489
490 #[test]
491 fn test_layout_flex_grow() {
492 let children = vec![
493 create_text_node("A"),
494 create_text_node("B"),
495 create_text_node("C"),
496 ];
497
498 let mut attrs = HashMap::new();
499 attrs.insert("flex-grow".to_string(), "1.0".to_string());
500
501 let container = create_flex_container(attrs, children);
502
503 let constraints = Rect {
504 x: 0,
505 y: 0,
506 width: 20,
507 height: 10,
508 };
509 let results = FlexboxLayout::calculate(&container, constraints);
510
511 assert_eq!(results.len(), 3);
512
513 assert!(results[0].width > 0);
515 assert!(results[1].width > 0);
516 assert!(results[2].width > 0);
517 }
518
519 #[test]
520 fn test_layout_gap() {
521 let children = vec![
522 create_text_node("A"),
523 create_text_node("B"),
524 create_text_node("C"),
525 ];
526
527 let mut attrs = HashMap::new();
528 attrs.insert("gap".to_string(), "2".to_string());
529
530 let container = create_flex_container(attrs, children);
531
532 let constraints = Rect {
533 x: 0,
534 y: 0,
535 width: 20,
536 height: 10,
537 };
538
539 let results = FlexboxLayout::calculate(&container, constraints);
540
541 assert_eq!(results.len(), 3);
542
543 let gap_1 = results[1].x - (results[0].x + results[0].width);
546 let gap_2 = results[2].x - (results[1].x + results[1].width);
547
548 assert!(gap_1 >= 2 || gap_2 >= 2);
550 }
551
552 #[test]
553 fn test_layout_nested() {
554 let inner_children = vec![create_text_node("A"), create_text_node("B")];
556
557 let mut inner_attrs = HashMap::new();
558 inner_attrs.insert("direction".to_string(), "row".to_string());
559
560 let inner_container = create_flex_container(inner_attrs, inner_children);
561
562 let outer_children = vec![
564 create_text_node("X"),
565 inner_container,
566 create_text_node("Y"),
567 ];
568
569 let mut outer_attrs = HashMap::new();
570 outer_attrs.insert("direction".to_string(), "column".to_string());
571
572 let outer_container = create_flex_container(outer_attrs, outer_children);
573
574 let constraints = Rect {
575 x: 0,
576 y: 0,
577 width: 20,
578 height: 30,
579 };
580
581 let results = FlexboxLayout::calculate(&outer_container, constraints);
582
583 assert_eq!(results.len(), 3);
585
586 assert!(results[0].y < results[1].y);
588 assert!(results[1].y < results[2].y);
589
590 assert_eq!(results[0].width, 20);
592 assert_eq!(results[1].width, 20);
593 assert_eq!(results[2].width, 20);
594 }
595
596 #[test]
597 fn test_flex_style_builder() {
598 let style = FlexStyle::new()
599 .row()
600 .justify(JustifyContent::Center)
601 .align(AlignItems::Stretch)
602 .gap(2)
603 .flex_grow(1.0)
604 .flex_shrink(0.5);
605
606 assert_eq!(style.direction, FlexDirection::Row);
607 assert_eq!(style.justify_content, JustifyContent::Center);
608 assert_eq!(style.align_items, AlignItems::Stretch);
609 assert_eq!(style.gap, 2);
610 assert_eq!(style.flex_grow, 1.0);
611 assert_eq!(style.flex_shrink, 0.5);
612 }
613
614 #[test]
615 fn test_empty_children() {
616 let container = create_flex_container(HashMap::new(), vec![]);
617
618 let constraints = Rect {
619 x: 0,
620 y: 0,
621 width: 20,
622 height: 10,
623 };
624
625 let results = FlexboxLayout::calculate(&container, constraints);
626
627 assert_eq!(results.len(), 0);
628 }
629
630 #[test]
631 fn test_single_child() {
632 let children = vec![create_text_node("A")];
633
634 let container = create_flex_container(HashMap::new(), children);
635
636 let constraints = Rect {
637 x: 0,
638 y: 0,
639 width: 20,
640 height: 10,
641 };
642
643 let results = FlexboxLayout::calculate(&container, constraints);
644
645 assert_eq!(results.len(), 1);
646 assert_eq!(results[0].x, 0);
647 assert_eq!(results[0].y, 0);
648 assert_eq!(results[0].height, 10);
649 }
650
651 #[test]
652 fn test_justify_end() {
653 let children = vec![create_text_node("A"), create_text_node("B")];
654
655 let mut attrs = HashMap::new();
656 attrs.insert("justify".to_string(), "end".to_string());
657
658 let container = create_flex_container(attrs, children);
659
660 let constraints = Rect {
661 x: 0,
662 y: 0,
663 width: 20,
664 height: 10,
665 };
666
667 let results = FlexboxLayout::calculate(&container, constraints);
668
669 assert_eq!(results.len(), 2);
670
671 let total_width = results[1].x + results[1].width;
673 assert!(total_width <= 20);
674 }
675
676 #[test]
677 fn test_space_between() {
678 let children = vec![
679 create_text_node("A"),
680 create_text_node("B"),
681 create_text_node("C"),
682 ];
683
684 let mut attrs = HashMap::new();
685 attrs.insert("justify".to_string(), "space-between".to_string());
686
687 let container = create_flex_container(attrs, children);
688
689 let constraints = Rect {
690 x: 0,
691 y: 0,
692 width: 20,
693 height: 10,
694 };
695
696 let results = FlexboxLayout::calculate(&container, constraints);
697
698 assert_eq!(results.len(), 3);
699
700 assert_eq!(results[0].x, 0);
702
703 let last_end = results[2].x + results[2].width;
705 assert!(last_end <= 20);
706 }
707
708 #[test]
709 fn test_space_around() {
710 let children = vec![
711 create_text_node("A"),
712 create_text_node("B"),
713 create_text_node("C"),
714 ];
715
716 let mut attrs = HashMap::new();
717 attrs.insert("justify".to_string(), "space-around".to_string());
718
719 let container = create_flex_container(attrs, children);
720
721 let constraints = Rect {
722 x: 0,
723 y: 0,
724 width: 20,
725 height: 10,
726 };
727
728 let results = FlexboxLayout::calculate(&container, constraints);
729
730 assert_eq!(results.len(), 3);
731
732 assert!(!results.is_empty());
734 assert!(results[0].width > 0);
735 }
736}