1pub mod engine;
7pub mod scroll;
8pub mod style_converter;
9
10pub use engine::{LayoutEngine, LayoutError, LayoutRect};
11pub use scroll::{OverflowBehavior, ScrollManager, ScrollState};
12pub use style_converter::computed_to_taffy;
13
14use crate::geometry::Rect;
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum Direction {
19 Vertical,
21 Horizontal,
23}
24
25#[derive(Clone, Copy, Debug, PartialEq)]
27pub enum Constraint {
28 Fixed(u16),
30 Min(u16),
32 Max(u16),
34 Percentage(u8),
36 Fill,
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub enum Dock {
43 Top,
45 Bottom,
47 Left,
49 Right,
51}
52
53pub struct Layout;
55
56impl Layout {
57 pub fn split(area: Rect, direction: Direction, constraints: &[Constraint]) -> Vec<Rect> {
61 if constraints.is_empty() {
62 return Vec::new();
63 }
64
65 let total = match direction {
66 Direction::Vertical => area.size.height,
67 Direction::Horizontal => area.size.width,
68 };
69
70 let sizes = solve_constraints(total, constraints);
71
72 let mut results = Vec::with_capacity(constraints.len());
73 let mut offset: u16 = 0;
74
75 for &size in &sizes {
76 let rect = match direction {
77 Direction::Vertical => Rect::new(
78 area.position.x,
79 area.position.y + offset,
80 area.size.width,
81 size,
82 ),
83 Direction::Horizontal => Rect::new(
84 area.position.x + offset,
85 area.position.y,
86 size,
87 area.size.height,
88 ),
89 };
90 results.push(rect);
91 offset = offset.saturating_add(size);
92 }
93
94 results
95 }
96
97 pub fn dock(area: Rect, dock: Dock, size: u16) -> (Rect, Rect) {
101 match dock {
102 Dock::Top => {
103 let s = size.min(area.size.height);
104 (
105 Rect::new(area.position.x, area.position.y, area.size.width, s),
106 Rect::new(
107 area.position.x,
108 area.position.y + s,
109 area.size.width,
110 area.size.height.saturating_sub(s),
111 ),
112 )
113 }
114 Dock::Bottom => {
115 let s = size.min(area.size.height);
116 (
117 Rect::new(
118 area.position.x,
119 area.position.y + area.size.height.saturating_sub(s),
120 area.size.width,
121 s,
122 ),
123 Rect::new(
124 area.position.x,
125 area.position.y,
126 area.size.width,
127 area.size.height.saturating_sub(s),
128 ),
129 )
130 }
131 Dock::Left => {
132 let s = size.min(area.size.width);
133 (
134 Rect::new(area.position.x, area.position.y, s, area.size.height),
135 Rect::new(
136 area.position.x + s,
137 area.position.y,
138 area.size.width.saturating_sub(s),
139 area.size.height,
140 ),
141 )
142 }
143 Dock::Right => {
144 let s = size.min(area.size.width);
145 (
146 Rect::new(
147 area.position.x + area.size.width.saturating_sub(s),
148 area.position.y,
149 s,
150 area.size.height,
151 ),
152 Rect::new(
153 area.position.x,
154 area.position.y,
155 area.size.width.saturating_sub(s),
156 area.size.height,
157 ),
158 )
159 }
160 }
161 }
162}
163
164fn solve_constraints(total: u16, constraints: &[Constraint]) -> Vec<u16> {
166 let n = constraints.len();
167 let mut sizes = vec![0u16; n];
168 let mut remaining = total;
169
170 for (i, c) in constraints.iter().enumerate() {
172 if let Constraint::Fixed(s) = c {
173 let s = (*s).min(remaining);
174 sizes[i] = s;
175 remaining = remaining.saturating_sub(s);
176 }
177 }
178
179 for (i, c) in constraints.iter().enumerate() {
181 if let Constraint::Percentage(p) = c {
182 let s = ((u32::from(total) * u32::from(*p)) / 100) as u16;
183 let s = s.min(remaining);
184 sizes[i] = s;
185 remaining = remaining.saturating_sub(s);
186 }
187 }
188
189 for (i, c) in constraints.iter().enumerate() {
191 if let Constraint::Min(min) = c {
192 let s = (*min).min(remaining);
193 sizes[i] = s;
194 remaining = remaining.saturating_sub(s);
195 }
196 }
197
198 for (i, c) in constraints.iter().enumerate() {
200 if let Constraint::Max(max) = c {
201 let s = (*max).min(remaining);
202 sizes[i] = s;
203 remaining = remaining.saturating_sub(s);
204 }
205 }
206
207 let fill_count = constraints
209 .iter()
210 .filter(|c| matches!(c, Constraint::Fill))
211 .count();
212 if fill_count > 0 {
213 let each = remaining / fill_count as u16;
214 let mut extra = remaining % fill_count as u16;
215 for (i, c) in constraints.iter().enumerate() {
216 if matches!(c, Constraint::Fill) {
217 let bonus = if extra > 0 {
218 extra -= 1;
219 1
220 } else {
221 0
222 };
223 sizes[i] = each + bonus;
224 }
225 }
226 }
227
228 sizes
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use crate::geometry::Rect;
235
236 #[test]
237 fn vertical_split_fixed() {
238 let area = Rect::new(0, 0, 80, 24);
239 let rects = Layout::split(
240 area,
241 Direction::Vertical,
242 &[Constraint::Fixed(3), Constraint::Fixed(5)],
243 );
244 assert_eq!(rects.len(), 2);
245 assert_eq!(rects[0], Rect::new(0, 0, 80, 3));
246 assert_eq!(rects[1], Rect::new(0, 3, 80, 5));
247 }
248
249 #[test]
250 fn horizontal_split_fixed() {
251 let area = Rect::new(0, 0, 80, 24);
252 let rects = Layout::split(
253 area,
254 Direction::Horizontal,
255 &[Constraint::Fixed(20), Constraint::Fixed(30)],
256 );
257 assert_eq!(rects.len(), 2);
258 assert_eq!(rects[0], Rect::new(0, 0, 20, 24));
259 assert_eq!(rects[1], Rect::new(20, 0, 30, 24));
260 }
261
262 #[test]
263 fn vertical_fixed_plus_fill() {
264 let area = Rect::new(0, 0, 80, 24);
265 let rects = Layout::split(
266 area,
267 Direction::Vertical,
268 &[Constraint::Fixed(3), Constraint::Fill],
269 );
270 assert_eq!(rects.len(), 2);
271 assert_eq!(rects[0], Rect::new(0, 0, 80, 3));
272 assert_eq!(rects[1], Rect::new(0, 3, 80, 21));
273 }
274
275 #[test]
276 fn multiple_fills_distribute_equally() {
277 let area = Rect::new(0, 0, 80, 24);
278 let rects = Layout::split(
279 area,
280 Direction::Vertical,
281 &[Constraint::Fill, Constraint::Fill],
282 );
283 assert_eq!(rects.len(), 2);
284 assert_eq!(rects[0].size.height, 12);
285 assert_eq!(rects[1].size.height, 12);
286 }
287
288 #[test]
289 fn percentage_split() {
290 let area = Rect::new(0, 0, 100, 10);
291 let rects = Layout::split(
292 area,
293 Direction::Horizontal,
294 &[Constraint::Percentage(30), Constraint::Percentage(70)],
295 );
296 assert_eq!(rects[0].size.width, 30);
297 assert_eq!(rects[1].size.width, 70);
298 }
299
300 #[test]
301 fn empty_constraints() {
302 let area = Rect::new(0, 0, 80, 24);
303 let rects = Layout::split(area, Direction::Vertical, &[]);
304 assert!(rects.is_empty());
305 }
306
307 #[test]
308 fn dock_top() {
309 let area = Rect::new(0, 0, 80, 24);
310 let (docked, remaining) = Layout::dock(area, Dock::Top, 3);
311 assert_eq!(docked, Rect::new(0, 0, 80, 3));
312 assert_eq!(remaining, Rect::new(0, 3, 80, 21));
313 }
314
315 #[test]
316 fn dock_bottom() {
317 let area = Rect::new(0, 0, 80, 24);
318 let (docked, remaining) = Layout::dock(area, Dock::Bottom, 3);
319 assert_eq!(docked, Rect::new(0, 21, 80, 3));
320 assert_eq!(remaining, Rect::new(0, 0, 80, 21));
321 }
322
323 #[test]
324 fn dock_left() {
325 let area = Rect::new(0, 0, 80, 24);
326 let (docked, remaining) = Layout::dock(area, Dock::Left, 20);
327 assert_eq!(docked, Rect::new(0, 0, 20, 24));
328 assert_eq!(remaining, Rect::new(20, 0, 60, 24));
329 }
330
331 #[test]
332 fn dock_right() {
333 let area = Rect::new(0, 0, 80, 24);
334 let (docked, remaining) = Layout::dock(area, Dock::Right, 20);
335 assert_eq!(docked, Rect::new(60, 0, 20, 24));
336 assert_eq!(remaining, Rect::new(0, 0, 60, 24));
337 }
338
339 #[test]
340 fn dock_larger_than_area() {
341 let area = Rect::new(0, 0, 80, 10);
342 let (docked, remaining) = Layout::dock(area, Dock::Top, 20);
343 assert_eq!(docked, Rect::new(0, 0, 80, 10));
344 assert_eq!(remaining, Rect::new(0, 10, 80, 0));
345 }
346
347 #[test]
348 fn offset_area_split() {
349 let area = Rect::new(5, 10, 40, 20);
350 let rects = Layout::split(
351 area,
352 Direction::Vertical,
353 &[Constraint::Fixed(5), Constraint::Fill],
354 );
355 assert_eq!(rects[0], Rect::new(5, 10, 40, 5));
356 assert_eq!(rects[1], Rect::new(5, 15, 40, 15));
357 }
358}
359
360#[cfg(test)]
361mod integration_tests {
362 use super::engine::LayoutEngine;
363 use super::scroll::ScrollManager;
364 use super::style_converter::computed_to_taffy;
365 use crate::tcss::cascade::CascadeResolver;
366 use crate::tcss::matcher::StyleMatcher;
367 use crate::tcss::parser::parse_stylesheet;
368 use crate::tcss::tree::{WidgetNode, WidgetTree};
369
370 fn layout_from_css(
372 css: &str,
373 tree: &WidgetTree,
374 root_id: u64,
375 width: u16,
376 height: u16,
377 ) -> LayoutEngine {
378 let result = parse_stylesheet(css);
379 assert!(result.is_ok(), "parse failed: {result:?}");
380 let stylesheet = match result {
381 Ok(s) => s,
382 Err(_) => unreachable!(),
383 };
384 let matcher = StyleMatcher::new(&stylesheet);
385 let mut engine = LayoutEngine::new();
386
387 build_engine_nodes(tree, root_id, &matcher, &mut engine);
389
390 engine.set_root(root_id).ok();
391 engine.compute(width, height).ok();
392 engine
393 }
394
395 fn build_engine_nodes(
397 tree: &WidgetTree,
398 widget_id: u64,
399 matcher: &StyleMatcher,
400 engine: &mut LayoutEngine,
401 ) {
402 let node = tree.get(widget_id);
403 assert!(node.is_some(), "widget {widget_id} not found");
404 let node = match node {
405 Some(n) => n,
406 None => unreachable!(),
407 };
408 let children: Vec<u64> = node.children.clone();
409
410 for &child_id in &children {
412 build_engine_nodes(tree, child_id, matcher, engine);
413 }
414
415 let matched = matcher.match_widget(tree, widget_id);
417 let computed = CascadeResolver::resolve(&matched);
418 let taffy_style = computed_to_taffy(&computed);
419
420 if children.is_empty() {
421 engine.add_node(widget_id, taffy_style).ok();
422 } else {
423 engine
424 .add_node_with_children(widget_id, taffy_style, &children)
425 .ok();
426 }
427 }
428
429 #[test]
430 fn integration_parse_to_layout() {
431 let css = r#"
432 #root {
433 display: flex;
434 flex-direction: column;
435 width: 80;
436 height: 24;
437 }
438 Label {
439 flex-grow: 1;
440 }
441 "#;
442 let mut tree = WidgetTree::new();
443 let mut root = WidgetNode::new(1, "Container");
444 root.css_id = Some("root".into());
445 tree.add_node(root);
446 let mut label = WidgetNode::new(2, "Label");
447 label.parent = Some(1);
448 tree.add_node(label);
449
450 let engine = layout_from_css(css, &tree, 1, 80, 24);
451
452 let root_layout = engine.layout(1).unwrap_or_default();
453 assert_eq!(root_layout.width, 80);
454 assert_eq!(root_layout.height, 24);
455
456 let label_layout = engine.layout(2).unwrap_or_default();
457 assert_eq!(label_layout.width, 80);
459 assert_eq!(label_layout.height, 24);
460 }
461
462 #[test]
463 fn integration_flex_sidebar_layout() {
464 let css = r#"
465 #root { display: flex; width: 80; height: 24; }
466 #sidebar { width: 20; }
467 #main { flex-grow: 1; }
468 "#;
469 let mut tree = WidgetTree::new();
470 let mut root = WidgetNode::new(1, "Container");
471 root.css_id = Some("root".into());
472 tree.add_node(root);
473
474 let mut sidebar = WidgetNode::new(2, "Container");
475 sidebar.css_id = Some("sidebar".into());
476 sidebar.parent = Some(1);
477 tree.add_node(sidebar);
478
479 let mut main = WidgetNode::new(3, "Container");
480 main.css_id = Some("main".into());
481 main.parent = Some(1);
482 tree.add_node(main);
483
484 tree.get_mut(1)
485 .iter_mut()
486 .for_each(|n| n.children = vec![2, 3]);
487
488 let engine = layout_from_css(css, &tree, 1, 80, 24);
489
490 let sidebar_layout = engine.layout(2).unwrap_or_default();
491 let main_layout = engine.layout(3).unwrap_or_default();
492 assert_eq!(sidebar_layout.width, 20);
493 assert_eq!(main_layout.width, 60);
494 assert_eq!(main_layout.x, 20);
495 }
496
497 #[test]
498 fn integration_grid_dashboard() {
499 let css = r#"
500 #root {
501 display: grid;
502 grid-template-columns: 1fr 1fr 1fr;
503 grid-template-rows: 1fr 1fr;
504 width: 90;
505 height: 20;
506 }
507 "#;
508 let mut tree = WidgetTree::new();
509 let mut root = WidgetNode::new(1, "Container");
510 root.css_id = Some("root".into());
511 tree.add_node(root);
512
513 let mut child_ids = Vec::new();
514 for i in 2..=7 {
515 let mut child = WidgetNode::new(i, "Panel");
516 child.parent = Some(1);
517 tree.add_node(child);
518 child_ids.push(i);
519 }
520 tree.get_mut(1)
521 .iter_mut()
522 .for_each(|n| n.children = child_ids.clone());
523
524 let engine = layout_from_css(css, &tree, 1, 90, 20);
525
526 let l1 = engine.layout(2).unwrap_or_default();
527 let l2 = engine.layout(3).unwrap_or_default();
528 let l4 = engine.layout(5).unwrap_or_default();
529 assert_eq!(l1.width, 30);
530 assert_eq!(l1.height, 10);
531 assert_eq!(l2.x, 30);
532 assert_eq!(l4.y, 10); }
534
535 #[test]
536 fn integration_nested_flex_grid() {
537 let css = r#"
538 #root { display: flex; width: 80; height: 20; }
539 #left { flex-grow: 1; display: grid; grid-template-columns: 1fr 1fr; }
540 #right { flex-grow: 1; }
541 "#;
542 let mut tree = WidgetTree::new();
543 let mut root = WidgetNode::new(1, "Container");
544 root.css_id = Some("root".into());
545 tree.add_node(root);
546
547 let mut left = WidgetNode::new(2, "Container");
548 left.css_id = Some("left".into());
549 left.parent = Some(1);
550 tree.add_node(left);
551
552 let mut right = WidgetNode::new(3, "Container");
553 right.css_id = Some("right".into());
554 right.parent = Some(1);
555 tree.add_node(right);
556
557 let mut g1 = WidgetNode::new(4, "Panel");
559 g1.parent = Some(2);
560 tree.add_node(g1);
561 let mut g2 = WidgetNode::new(5, "Panel");
562 g2.parent = Some(2);
563 tree.add_node(g2);
564
565 tree.get_mut(1)
566 .iter_mut()
567 .for_each(|n| n.children = vec![2, 3]);
568 tree.get_mut(2)
569 .iter_mut()
570 .for_each(|n| n.children = vec![4, 5]);
571
572 let engine = layout_from_css(css, &tree, 1, 80, 20);
573
574 let left_layout = engine.layout(2).unwrap_or_default();
575 let right_layout = engine.layout(3).unwrap_or_default();
576 assert_eq!(left_layout.width, 40);
577 assert_eq!(right_layout.width, 40);
578
579 let g1_layout = engine.layout(4).unwrap_or_default();
580 let g2_layout = engine.layout(5).unwrap_or_default();
581 assert_eq!(g1_layout.width, 20);
582 assert_eq!(g2_layout.width, 20);
583 }
584
585 #[test]
586 fn integration_box_model_spacing() {
587 let css = r#"
588 #root { display: flex; width: 80; height: 24; padding: 2; }
589 #child { flex-grow: 1; }
590 "#;
591 let mut tree = WidgetTree::new();
592 let mut root = WidgetNode::new(1, "Container");
593 root.css_id = Some("root".into());
594 tree.add_node(root);
595
596 let mut child = WidgetNode::new(2, "Container");
597 child.css_id = Some("child".into());
598 child.parent = Some(1);
599 tree.add_node(child);
600
601 tree.get_mut(1)
602 .iter_mut()
603 .for_each(|n| n.children = vec![2]);
604
605 let engine = layout_from_css(css, &tree, 1, 80, 24);
606
607 let child_layout = engine.layout(2).unwrap_or_default();
608 assert_eq!(child_layout.x, 2);
609 assert_eq!(child_layout.y, 2);
610 assert_eq!(child_layout.width, 76); assert_eq!(child_layout.height, 20); }
613
614 #[test]
615 fn integration_scroll_region_setup() {
616 let css = r#"
617 #root { overflow: scroll; width: 80; height: 24; }
618 "#;
619 let result = parse_stylesheet(css);
620 assert!(result.is_ok());
621 let stylesheet = match result {
622 Ok(s) => s,
623 Err(_) => unreachable!(),
624 };
625 let matcher = StyleMatcher::new(&stylesheet);
626
627 let mut tree = WidgetTree::new();
628 let mut root = WidgetNode::new(1, "Container");
629 root.css_id = Some("root".into());
630 tree.add_node(root);
631
632 let matched = matcher.match_widget(&tree, 1);
633 let computed = CascadeResolver::resolve(&matched);
634
635 let (ox, oy) = super::scroll::extract_overflow(&computed);
636 assert_eq!(ox, super::scroll::OverflowBehavior::Scroll);
637 assert_eq!(oy, super::scroll::OverflowBehavior::Scroll);
638
639 let mut scroll_mgr = ScrollManager::new();
640 scroll_mgr.register(1, 200, 100, 80, 24);
641 assert!(scroll_mgr.can_scroll_x(1));
642 assert!(scroll_mgr.can_scroll_y(1));
643 }
644
645 #[test]
646 fn integration_zero_size_area() {
647 let css = r#"
648 #root { display: flex; width: 0; height: 0; }
649 Label { flex-grow: 1; }
650 "#;
651 let mut tree = WidgetTree::new();
652 let mut root = WidgetNode::new(1, "Container");
653 root.css_id = Some("root".into());
654 tree.add_node(root);
655
656 let mut label = WidgetNode::new(2, "Label");
657 label.parent = Some(1);
658 tree.add_node(label);
659
660 let engine = layout_from_css(css, &tree, 1, 0, 0);
661
662 let root_layout = engine.layout(1).unwrap_or_default();
663 assert_eq!(root_layout.width, 0);
664 assert_eq!(root_layout.height, 0);
665 }
666
667 #[test]
668 fn integration_large_tree() {
669 let css = r#"
670 #root { display: flex; flex-direction: column; width: 100; height: 100; }
671 .item { flex-grow: 1; }
672 "#;
673 let mut tree = WidgetTree::new();
674 let mut root = WidgetNode::new(1, "Container");
675 root.css_id = Some("root".into());
676 tree.add_node(root);
677
678 let mut child_ids = Vec::new();
679 for i in 2..=101 {
680 let mut child = WidgetNode::new(i, "Container");
681 child.classes.push("item".into());
682 child.parent = Some(1);
683 tree.add_node(child);
684 child_ids.push(i);
685 }
686 tree.get_mut(1)
687 .iter_mut()
688 .for_each(|n| n.children = child_ids.clone());
689
690 let engine = layout_from_css(css, &tree, 1, 100, 100);
691
692 let l = engine.layout(2).unwrap_or_default();
694 assert_eq!(l.height, 1);
695 assert_eq!(l.width, 100);
696 }
697
698 #[test]
699 fn integration_theme_affects_layout() {
700 let css = r#"
702 :root { $sidebar-width: 30; }
703 #root { display: flex; width: 80; height: 24; }
704 #sidebar { width: $sidebar-width; }
705 #main { flex-grow: 1; }
706 "#;
707 let result = parse_stylesheet(css);
708 assert!(result.is_ok());
709 let stylesheet = match result {
710 Ok(s) => s,
711 Err(_) => unreachable!(),
712 };
713
714 let globals = crate::tcss::parser::extract_root_variables(&stylesheet);
715 let env = crate::tcss::variable::VariableEnvironment::with_global(globals);
716 let matcher = StyleMatcher::new(&stylesheet);
717
718 let mut tree = WidgetTree::new();
719 let mut root = WidgetNode::new(1, "Container");
720 root.css_id = Some("root".into());
721 tree.add_node(root);
722
723 let mut sidebar = WidgetNode::new(2, "Container");
724 sidebar.css_id = Some("sidebar".into());
725 sidebar.parent = Some(1);
726 tree.add_node(sidebar);
727
728 let mut main = WidgetNode::new(3, "Container");
729 main.css_id = Some("main".into());
730 main.parent = Some(1);
731 tree.add_node(main);
732
733 tree.get_mut(1)
734 .iter_mut()
735 .for_each(|n| n.children = vec![2, 3]);
736
737 let mut engine = LayoutEngine::new();
739
740 for &wid in &[2, 3] {
741 let matched = matcher.match_widget(&tree, wid);
742 let computed = CascadeResolver::resolve_with_variables(&matched, &env);
743 let style = computed_to_taffy(&computed);
744 engine.add_node(wid, style).ok();
745 }
746
747 let matched = matcher.match_widget(&tree, 1);
748 let computed = CascadeResolver::resolve_with_variables(&matched, &env);
749 let style = computed_to_taffy(&computed);
750 engine.add_node_with_children(1, style, &[2, 3]).ok();
751
752 engine.set_root(1).ok();
753 engine.compute(80, 24).ok();
754
755 let sidebar_layout = engine.layout(2).unwrap_or_default();
756 let main_layout = engine.layout(3).unwrap_or_default();
757 assert_eq!(sidebar_layout.width, 30);
758 assert_eq!(main_layout.width, 50);
759 }
760}