1use crate::buffer::Buffer;
2use crate::rect::Rect;
3use crate::style::{Align, Border, Color, Constraints, Justify, Margin, Padding, Style};
4use unicode_width::UnicodeWidthChar;
5use unicode_width::UnicodeWidthStr;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Direction {
9 Row,
10 Column,
11}
12
13#[derive(Debug, Clone)]
14pub(crate) enum Command {
15 Text {
16 content: String,
17 style: Style,
18 grow: u16,
19 align: Align,
20 wrap: bool,
21 margin: Margin,
22 constraints: Constraints,
23 },
24 BeginContainer {
25 direction: Direction,
26 gap: u32,
27 align: Align,
28 justify: Justify,
29 border: Option<Border>,
30 border_style: Style,
31 bg_color: Option<Color>,
32 padding: Padding,
33 margin: Margin,
34 constraints: Constraints,
35 title: Option<(String, Style)>,
36 grow: u16,
37 },
38 BeginScrollable {
39 grow: u16,
40 border: Option<Border>,
41 border_style: Style,
42 padding: Padding,
43 margin: Margin,
44 constraints: Constraints,
45 title: Option<(String, Style)>,
46 scroll_offset: u32,
47 },
48 Link {
49 text: String,
50 url: String,
51 style: Style,
52 margin: Margin,
53 constraints: Constraints,
54 },
55 EndContainer,
56 BeginOverlay {
57 modal: bool,
58 },
59 EndOverlay,
60 Spacer {
61 grow: u16,
62 },
63 FocusMarker(usize),
64}
65
66#[derive(Debug, Clone)]
67struct OverlayLayer {
68 node: LayoutNode,
69 modal: bool,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73enum NodeKind {
74 Text,
75 Container(Direction),
76 Spacer,
77}
78
79#[derive(Debug, Clone)]
80pub(crate) struct LayoutNode {
81 kind: NodeKind,
82 content: Option<String>,
83 style: Style,
84 pub grow: u16,
85 align: Align,
86 justify: Justify,
87 wrap: bool,
88 gap: u32,
89 border: Option<Border>,
90 border_style: Style,
91 bg_color: Option<Color>,
92 padding: Padding,
93 margin: Margin,
94 constraints: Constraints,
95 title: Option<(String, Style)>,
96 children: Vec<LayoutNode>,
97 pos: (u32, u32),
98 size: (u32, u32),
99 is_scrollable: bool,
100 scroll_offset: u32,
101 content_height: u32,
102 cached_wrapped: Option<Vec<String>>,
103 pub(crate) focus_id: Option<usize>,
104 link_url: Option<String>,
105 overlays: Vec<OverlayLayer>,
106}
107
108#[derive(Debug, Clone)]
109struct ContainerConfig {
110 gap: u32,
111 align: Align,
112 justify: Justify,
113 border: Option<Border>,
114 border_style: Style,
115 bg_color: Option<Color>,
116 padding: Padding,
117 margin: Margin,
118 constraints: Constraints,
119 title: Option<(String, Style)>,
120 grow: u16,
121}
122
123impl LayoutNode {
124 fn text(
125 content: String,
126 style: Style,
127 grow: u16,
128 align: Align,
129 wrap: bool,
130 margin: Margin,
131 constraints: Constraints,
132 ) -> Self {
133 let width = UnicodeWidthStr::width(content.as_str()) as u32;
134 Self {
135 kind: NodeKind::Text,
136 content: Some(content),
137 style,
138 grow,
139 align,
140 justify: Justify::Start,
141 wrap,
142 gap: 0,
143 border: None,
144 border_style: Style::new(),
145 bg_color: None,
146 padding: Padding::default(),
147 margin,
148 constraints,
149 title: None,
150 children: Vec::new(),
151 pos: (0, 0),
152 size: (width, 1),
153 is_scrollable: false,
154 scroll_offset: 0,
155 content_height: 0,
156 cached_wrapped: None,
157 focus_id: None,
158 link_url: None,
159 overlays: Vec::new(),
160 }
161 }
162
163 fn container(direction: Direction, config: ContainerConfig) -> Self {
164 Self {
165 kind: NodeKind::Container(direction),
166 content: None,
167 style: Style::new(),
168 grow: config.grow,
169 align: config.align,
170 justify: config.justify,
171 wrap: false,
172 gap: config.gap,
173 border: config.border,
174 border_style: config.border_style,
175 bg_color: config.bg_color,
176 padding: config.padding,
177 margin: config.margin,
178 constraints: config.constraints,
179 title: config.title,
180 children: Vec::new(),
181 pos: (0, 0),
182 size: (0, 0),
183 is_scrollable: false,
184 scroll_offset: 0,
185 content_height: 0,
186 cached_wrapped: None,
187 focus_id: None,
188 link_url: None,
189 overlays: Vec::new(),
190 }
191 }
192
193 fn spacer(grow: u16) -> Self {
194 Self {
195 kind: NodeKind::Spacer,
196 content: None,
197 style: Style::new(),
198 grow,
199 align: Align::Start,
200 justify: Justify::Start,
201 wrap: false,
202 gap: 0,
203 border: None,
204 border_style: Style::new(),
205 bg_color: None,
206 padding: Padding::default(),
207 margin: Margin::default(),
208 constraints: Constraints::default(),
209 title: None,
210 children: Vec::new(),
211 pos: (0, 0),
212 size: (0, 0),
213 is_scrollable: false,
214 scroll_offset: 0,
215 content_height: 0,
216 cached_wrapped: None,
217 focus_id: None,
218 link_url: None,
219 overlays: Vec::new(),
220 }
221 }
222
223 fn border_inset(&self) -> u32 {
224 if self.border.is_some() {
225 1
226 } else {
227 0
228 }
229 }
230
231 fn frame_horizontal(&self) -> u32 {
232 self.padding.horizontal() + self.border_inset() * 2
233 }
234
235 fn frame_vertical(&self) -> u32 {
236 self.padding.vertical() + self.border_inset() * 2
237 }
238
239 fn min_width(&self) -> u32 {
240 let width = match self.kind {
241 NodeKind::Text => self.size.0,
242 NodeKind::Spacer => 0,
243 NodeKind::Container(Direction::Row) => {
244 let gaps = if self.children.is_empty() {
245 0
246 } else {
247 (self.children.len() as u32 - 1) * self.gap
248 };
249 let children_width: u32 = self.children.iter().map(|c| c.min_width()).sum();
250 children_width + gaps + self.frame_horizontal()
251 }
252 NodeKind::Container(Direction::Column) => {
253 self.children
254 .iter()
255 .map(|c| c.min_width())
256 .max()
257 .unwrap_or(0)
258 + self.frame_horizontal()
259 }
260 };
261
262 let width = width.max(self.constraints.min_width.unwrap_or(0));
263 let width = match self.constraints.max_width {
264 Some(max_w) => width.min(max_w),
265 None => width,
266 };
267 width.saturating_add(self.margin.horizontal())
268 }
269
270 fn min_height(&self) -> u32 {
271 let height = match self.kind {
272 NodeKind::Text => 1,
273 NodeKind::Spacer => 0,
274 NodeKind::Container(Direction::Row) => {
275 self.children
276 .iter()
277 .map(|c| c.min_height())
278 .max()
279 .unwrap_or(0)
280 + self.frame_vertical()
281 }
282 NodeKind::Container(Direction::Column) => {
283 let gaps = if self.children.is_empty() {
284 0
285 } else {
286 (self.children.len() as u32 - 1) * self.gap
287 };
288 let children_height: u32 = self.children.iter().map(|c| c.min_height()).sum();
289 children_height + gaps + self.frame_vertical()
290 }
291 };
292
293 let height = height.max(self.constraints.min_height.unwrap_or(0));
294 height.saturating_add(self.margin.vertical())
295 }
296
297 fn min_height_for_width(&self, available_width: u32) -> u32 {
298 match self.kind {
299 NodeKind::Text if self.wrap => {
300 let text = self.content.as_deref().unwrap_or("");
301 let inner_width = available_width.saturating_sub(self.margin.horizontal());
302 let lines = wrap_lines(text, inner_width).len().max(1) as u32;
303 lines.saturating_add(self.margin.vertical())
304 }
305 _ => self.min_height(),
306 }
307 }
308}
309
310fn wrap_lines(text: &str, max_width: u32) -> Vec<String> {
311 if text.is_empty() {
312 return vec![String::new()];
313 }
314 if max_width == 0 {
315 return vec![text.to_string()];
316 }
317
318 fn split_long_word(word: &str, max_width: u32) -> Vec<(String, u32)> {
319 let mut chunks: Vec<(String, u32)> = Vec::new();
320 let mut chunk = String::new();
321 let mut chunk_width = 0_u32;
322
323 for ch in word.chars() {
324 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
325 if chunk.is_empty() {
326 if ch_width > max_width {
327 chunks.push((ch.to_string(), ch_width));
328 } else {
329 chunk.push(ch);
330 chunk_width = ch_width;
331 }
332 continue;
333 }
334
335 if chunk_width + ch_width > max_width {
336 chunks.push((std::mem::take(&mut chunk), chunk_width));
337 if ch_width > max_width {
338 chunks.push((ch.to_string(), ch_width));
339 chunk_width = 0;
340 } else {
341 chunk.push(ch);
342 chunk_width = ch_width;
343 }
344 } else {
345 chunk.push(ch);
346 chunk_width += ch_width;
347 }
348 }
349
350 if !chunk.is_empty() {
351 chunks.push((chunk, chunk_width));
352 }
353
354 chunks
355 }
356
357 fn push_word_into_line(
358 lines: &mut Vec<String>,
359 current_line: &mut String,
360 current_width: &mut u32,
361 word: &str,
362 word_width: u32,
363 max_width: u32,
364 ) {
365 if word.is_empty() {
366 return;
367 }
368
369 if word_width > max_width {
370 let chunks = split_long_word(word, max_width);
371 for (chunk, chunk_width) in chunks {
372 if current_line.is_empty() {
373 *current_line = chunk;
374 *current_width = chunk_width;
375 } else if *current_width + 1 + chunk_width <= max_width {
376 current_line.push(' ');
377 current_line.push_str(&chunk);
378 *current_width += 1 + chunk_width;
379 } else {
380 lines.push(std::mem::take(current_line));
381 *current_line = chunk;
382 *current_width = chunk_width;
383 }
384 }
385 return;
386 }
387
388 if current_line.is_empty() {
389 *current_line = word.to_string();
390 *current_width = word_width;
391 } else if *current_width + 1 + word_width <= max_width {
392 current_line.push(' ');
393 current_line.push_str(word);
394 *current_width += 1 + word_width;
395 } else {
396 lines.push(std::mem::take(current_line));
397 *current_line = word.to_string();
398 *current_width = word_width;
399 }
400 }
401
402 let mut lines: Vec<String> = Vec::new();
403 let mut current_line = String::new();
404 let mut current_width: u32 = 0;
405 let mut current_word = String::new();
406 let mut word_width: u32 = 0;
407
408 for ch in text.chars() {
409 if ch == ' ' {
410 push_word_into_line(
411 &mut lines,
412 &mut current_line,
413 &mut current_width,
414 ¤t_word,
415 word_width,
416 max_width,
417 );
418 current_word.clear();
419 word_width = 0;
420 continue;
421 }
422
423 current_word.push(ch);
424 word_width += UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
425 }
426
427 push_word_into_line(
428 &mut lines,
429 &mut current_line,
430 &mut current_width,
431 ¤t_word,
432 word_width,
433 max_width,
434 );
435
436 if !current_line.is_empty() {
437 lines.push(current_line);
438 }
439
440 if lines.is_empty() {
441 vec![String::new()]
442 } else {
443 lines
444 }
445}
446
447pub(crate) fn build_tree(commands: &[Command]) -> LayoutNode {
448 let mut root = LayoutNode::container(Direction::Column, default_container_config());
449 let mut overlays: Vec<OverlayLayer> = Vec::new();
450 build_children(&mut root, commands, &mut 0, &mut overlays, false);
451 root.overlays = overlays;
452 root
453}
454
455fn default_container_config() -> ContainerConfig {
456 ContainerConfig {
457 gap: 0,
458 align: Align::Start,
459 justify: Justify::Start,
460 border: None,
461 border_style: Style::new(),
462 bg_color: None,
463 padding: Padding::default(),
464 margin: Margin::default(),
465 constraints: Constraints::default(),
466 title: None,
467 grow: 0,
468 }
469}
470
471fn build_children(
472 parent: &mut LayoutNode,
473 commands: &[Command],
474 pos: &mut usize,
475 overlays: &mut Vec<OverlayLayer>,
476 stop_on_end_overlay: bool,
477) {
478 let mut pending_focus_id: Option<usize> = None;
479 while *pos < commands.len() {
480 match &commands[*pos] {
481 Command::FocusMarker(id) => {
482 pending_focus_id = Some(*id);
483 *pos += 1;
484 }
485 Command::Text {
486 content,
487 style,
488 grow,
489 align,
490 wrap,
491 margin,
492 constraints,
493 } => {
494 let mut node = LayoutNode::text(
495 content.clone(),
496 *style,
497 *grow,
498 *align,
499 *wrap,
500 *margin,
501 *constraints,
502 );
503 node.focus_id = pending_focus_id.take();
504 parent.children.push(node);
505 *pos += 1;
506 }
507 Command::Link {
508 text,
509 url,
510 style,
511 margin,
512 constraints,
513 } => {
514 let mut node = LayoutNode::text(
515 text.clone(),
516 *style,
517 0,
518 Align::Start,
519 false,
520 *margin,
521 *constraints,
522 );
523 node.link_url = Some(url.clone());
524 node.focus_id = pending_focus_id.take();
525 parent.children.push(node);
526 *pos += 1;
527 }
528 Command::BeginContainer {
529 direction,
530 gap,
531 align,
532 justify,
533 border,
534 border_style,
535 bg_color,
536 padding,
537 margin,
538 constraints,
539 title,
540 grow,
541 } => {
542 let mut node = LayoutNode::container(
543 *direction,
544 ContainerConfig {
545 gap: *gap,
546 align: *align,
547 justify: *justify,
548 border: *border,
549 border_style: *border_style,
550 bg_color: *bg_color,
551 padding: *padding,
552 margin: *margin,
553 constraints: *constraints,
554 title: title.clone(),
555 grow: *grow,
556 },
557 );
558 node.focus_id = pending_focus_id.take();
559 *pos += 1;
560 build_children(&mut node, commands, pos, overlays, false);
561 parent.children.push(node);
562 }
563 Command::BeginScrollable {
564 grow,
565 border,
566 border_style,
567 padding,
568 margin,
569 constraints,
570 title,
571 scroll_offset,
572 } => {
573 let mut node = LayoutNode::container(
574 Direction::Column,
575 ContainerConfig {
576 gap: 0,
577 align: Align::Start,
578 justify: Justify::Start,
579 border: *border,
580 border_style: *border_style,
581 bg_color: None,
582 padding: *padding,
583 margin: *margin,
584 constraints: *constraints,
585 title: title.clone(),
586 grow: *grow,
587 },
588 );
589 node.is_scrollable = true;
590 node.scroll_offset = *scroll_offset;
591 node.focus_id = pending_focus_id.take();
592 *pos += 1;
593 build_children(&mut node, commands, pos, overlays, false);
594 parent.children.push(node);
595 }
596 Command::BeginOverlay { modal } => {
597 *pos += 1;
598 let mut overlay_node =
599 LayoutNode::container(Direction::Column, default_container_config());
600 build_children(&mut overlay_node, commands, pos, overlays, true);
601 overlays.push(OverlayLayer {
602 node: overlay_node,
603 modal: *modal,
604 });
605 }
606 Command::Spacer { grow } => {
607 parent.children.push(LayoutNode::spacer(*grow));
608 *pos += 1;
609 }
610 Command::EndContainer => {
611 *pos += 1;
612 return;
613 }
614 Command::EndOverlay => {
615 *pos += 1;
616 if stop_on_end_overlay {
617 return;
618 }
619 }
620 }
621 }
622}
623
624pub(crate) fn compute(node: &mut LayoutNode, area: Rect) {
625 node.pos = (area.x, area.y);
626 node.size = (
627 area.width.clamp(
628 node.constraints.min_width.unwrap_or(0),
629 node.constraints.max_width.unwrap_or(u32::MAX),
630 ),
631 area.height.clamp(
632 node.constraints.min_height.unwrap_or(0),
633 node.constraints.max_height.unwrap_or(u32::MAX),
634 ),
635 );
636
637 if matches!(node.kind, NodeKind::Text) && node.wrap {
638 let lines = wrap_lines(node.content.as_deref().unwrap_or(""), area.width);
639 node.size = (area.width, lines.len().max(1) as u32);
640 node.cached_wrapped = Some(lines);
641 } else {
642 node.cached_wrapped = None;
643 }
644
645 match node.kind {
646 NodeKind::Text | NodeKind::Spacer => {}
647 NodeKind::Container(Direction::Row) => {
648 layout_row(
649 node,
650 inner_area(
651 node,
652 Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1),
653 ),
654 );
655 node.content_height = 0;
656 }
657 NodeKind::Container(Direction::Column) => {
658 let viewport_area = inner_area(
659 node,
660 Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1),
661 );
662 if node.is_scrollable {
663 let saved_grows: Vec<u16> = node.children.iter().map(|c| c.grow).collect();
664 for child in &mut node.children {
665 child.grow = 0;
666 }
667 let total_gaps = if node.children.is_empty() {
668 0
669 } else {
670 (node.children.len() as u32 - 1) * node.gap
671 };
672 let natural_height: u32 = node
673 .children
674 .iter()
675 .map(|c| c.min_height_for_width(viewport_area.width))
676 .sum::<u32>()
677 + total_gaps;
678
679 if natural_height > viewport_area.height {
680 let virtual_area = Rect::new(
681 viewport_area.x,
682 viewport_area.y,
683 viewport_area.width,
684 natural_height,
685 );
686 layout_column(node, virtual_area);
687 } else {
688 for (child, &grow) in node.children.iter_mut().zip(saved_grows.iter()) {
689 child.grow = grow;
690 }
691 layout_column(node, viewport_area);
692 }
693 node.content_height = scroll_content_height(node, viewport_area.y);
694 } else {
695 layout_column(node, viewport_area);
696 node.content_height = 0;
697 }
698 }
699 }
700
701 for overlay in &mut node.overlays {
702 let width = overlay.node.min_width().min(area.width);
703 let height = overlay.node.min_height_for_width(width).min(area.height);
704 let x = area.x.saturating_add(area.width.saturating_sub(width) / 2);
705 let y = area
706 .y
707 .saturating_add(area.height.saturating_sub(height) / 2);
708 compute(&mut overlay.node, Rect::new(x, y, width, height));
709 }
710}
711
712fn scroll_content_height(node: &LayoutNode, inner_y: u32) -> u32 {
713 let Some(max_bottom) = node
714 .children
715 .iter()
716 .map(|child| {
717 child
718 .pos
719 .1
720 .saturating_add(child.size.1)
721 .saturating_add(child.margin.bottom)
722 })
723 .max()
724 else {
725 return 0;
726 };
727
728 max_bottom.saturating_sub(inner_y)
729}
730
731fn justify_offsets(justify: Justify, remaining: u32, n: u32, gap: u32) -> (u32, u32) {
732 if n <= 1 {
733 let start = match justify {
734 Justify::Center => remaining / 2,
735 Justify::End => remaining,
736 _ => 0,
737 };
738 return (start, gap);
739 }
740
741 match justify {
742 Justify::Start => (0, gap),
743 Justify::Center => (remaining.saturating_sub((n - 1) * gap) / 2, gap),
744 Justify::End => (remaining.saturating_sub((n - 1) * gap), gap),
745 Justify::SpaceBetween => (0, remaining / (n - 1)),
746 Justify::SpaceAround => {
747 let slot = remaining / n;
748 (slot / 2, slot)
749 }
750 Justify::SpaceEvenly => {
751 let slot = remaining / (n + 1);
752 (slot, slot)
753 }
754 }
755}
756
757fn inner_area(node: &LayoutNode, area: Rect) -> Rect {
758 let inset = node.border_inset();
759 let x = area.x + inset + node.padding.left;
760 let y = area.y + inset + node.padding.top;
761 let width = area
762 .width
763 .saturating_sub(inset * 2)
764 .saturating_sub(node.padding.horizontal());
765 let height = area
766 .height
767 .saturating_sub(inset * 2)
768 .saturating_sub(node.padding.vertical());
769
770 Rect::new(x, y, width, height)
771}
772
773fn layout_row(node: &mut LayoutNode, area: Rect) {
774 if node.children.is_empty() {
775 return;
776 }
777
778 let n = node.children.len() as u32;
779 let total_gaps = (n - 1) * node.gap;
780 let available = area.width.saturating_sub(total_gaps);
781 let min_widths: Vec<u32> = node
782 .children
783 .iter()
784 .map(|child| child.min_width())
785 .collect();
786
787 let mut total_grow: u32 = 0;
788 let mut fixed_width: u32 = 0;
789 for (child, &min_width) in node.children.iter().zip(min_widths.iter()) {
790 if child.grow > 0 {
791 total_grow += child.grow as u32;
792 } else {
793 fixed_width += min_width;
794 }
795 }
796
797 let mut flex_space = available.saturating_sub(fixed_width);
798 let mut remaining_grow = total_grow;
799
800 let mut child_widths: Vec<u32> = Vec::with_capacity(node.children.len());
801 for (i, child) in node.children.iter().enumerate() {
802 let w = if child.grow > 0 && total_grow > 0 {
803 let share = if remaining_grow == 0 {
804 0
805 } else {
806 flex_space * child.grow as u32 / remaining_grow
807 };
808 flex_space = flex_space.saturating_sub(share);
809 remaining_grow = remaining_grow.saturating_sub(child.grow as u32);
810 share
811 } else {
812 min_widths[i].min(available)
813 };
814 child_widths.push(w);
815 }
816
817 let total_children_width: u32 = child_widths.iter().sum();
818 let remaining = area.width.saturating_sub(total_children_width);
819 let (start_offset, inter_gap) = justify_offsets(node.justify, remaining, n, node.gap);
820
821 let mut x = area.x + start_offset;
822 for (i, child) in node.children.iter_mut().enumerate() {
823 let w = child_widths[i];
824 let child_outer_h = match node.align {
825 Align::Start => area.height,
826 _ => child.min_height_for_width(w).min(area.height),
827 };
828 let child_x = x.saturating_add(child.margin.left);
829 let child_y = area.y.saturating_add(child.margin.top);
830 let child_w = w.saturating_sub(child.margin.horizontal());
831 let child_h = child_outer_h.saturating_sub(child.margin.vertical());
832 compute(child, Rect::new(child_x, child_y, child_w, child_h));
833 let child_total_h = child.size.1.saturating_add(child.margin.vertical());
834 let y_offset = match node.align {
835 Align::Start => 0,
836 Align::Center => area.height.saturating_sub(child_total_h) / 2,
837 Align::End => area.height.saturating_sub(child_total_h),
838 };
839 child.pos.1 = child.pos.1.saturating_add(y_offset);
840 x += w + inter_gap;
841 }
842}
843
844fn layout_column(node: &mut LayoutNode, area: Rect) {
845 if node.children.is_empty() {
846 return;
847 }
848
849 let n = node.children.len() as u32;
850 let total_gaps = (n - 1) * node.gap;
851 let available = area.height.saturating_sub(total_gaps);
852 let min_heights: Vec<u32> = node
853 .children
854 .iter()
855 .map(|child| child.min_height_for_width(area.width))
856 .collect();
857
858 let mut total_grow: u32 = 0;
859 let mut fixed_height: u32 = 0;
860 for (child, &min_height) in node.children.iter().zip(min_heights.iter()) {
861 if child.grow > 0 {
862 total_grow += child.grow as u32;
863 } else {
864 fixed_height += min_height;
865 }
866 }
867
868 let mut flex_space = available.saturating_sub(fixed_height);
869 let mut remaining_grow = total_grow;
870
871 let mut child_heights: Vec<u32> = Vec::with_capacity(node.children.len());
872 for (i, child) in node.children.iter().enumerate() {
873 let h = if child.grow > 0 && total_grow > 0 {
874 let share = if remaining_grow == 0 {
875 0
876 } else {
877 flex_space * child.grow as u32 / remaining_grow
878 };
879 flex_space = flex_space.saturating_sub(share);
880 remaining_grow = remaining_grow.saturating_sub(child.grow as u32);
881 share
882 } else {
883 min_heights[i].min(available)
884 };
885 child_heights.push(h);
886 }
887
888 let total_children_height: u32 = child_heights.iter().sum();
889 let remaining = area.height.saturating_sub(total_children_height);
890 let (start_offset, inter_gap) = justify_offsets(node.justify, remaining, n, node.gap);
891
892 let mut y = area.y + start_offset;
893 for (i, child) in node.children.iter_mut().enumerate() {
894 let h = child_heights[i];
895 let child_outer_w = match node.align {
896 Align::Start => area.width,
897 _ => child.min_width().min(area.width),
898 };
899 let child_x = area.x.saturating_add(child.margin.left);
900 let child_y = y.saturating_add(child.margin.top);
901 let child_w = child_outer_w.saturating_sub(child.margin.horizontal());
902 let child_h = h.saturating_sub(child.margin.vertical());
903 compute(child, Rect::new(child_x, child_y, child_w, child_h));
904 let child_total_w = child.size.0.saturating_add(child.margin.horizontal());
905 let x_offset = match node.align {
906 Align::Start => 0,
907 Align::Center => area.width.saturating_sub(child_total_w) / 2,
908 Align::End => area.width.saturating_sub(child_total_w),
909 };
910 child.pos.0 = child.pos.0.saturating_add(x_offset);
911 y += h + inter_gap;
912 }
913}
914
915pub(crate) fn render(node: &LayoutNode, buf: &mut Buffer) {
916 render_inner(node, buf, 0, None);
917 buf.clip_stack.clear();
918 for overlay in &node.overlays {
919 if overlay.modal {
920 dim_entire_buffer(buf);
921 }
922 render_inner(&overlay.node, buf, 0, None);
923 }
924}
925
926fn dim_entire_buffer(buf: &mut Buffer) {
927 for y in buf.area.y..buf.area.bottom() {
928 for x in buf.area.x..buf.area.right() {
929 let cell = buf.get_mut(x, y);
930 cell.style.modifiers |= crate::style::Modifiers::DIM;
931 }
932 }
933}
934
935pub(crate) fn render_debug_overlay(node: &LayoutNode, buf: &mut Buffer) {
936 for child in &node.children {
937 render_debug_overlay_inner(child, buf, 0, 0);
938 }
939}
940
941fn render_debug_overlay_inner(node: &LayoutNode, buf: &mut Buffer, depth: u32, y_offset: u32) {
942 let child_offset = if node.is_scrollable {
943 y_offset.saturating_add(node.scroll_offset)
944 } else {
945 y_offset
946 };
947
948 if let NodeKind::Container(_) = node.kind {
949 let sy = screen_y(node.pos.1, y_offset);
950 if sy + node.size.1 as i64 > 0 {
951 let color = debug_color_for_depth(depth);
952 let style = Style::new().fg(color);
953 let clamped_y = sy.max(0) as u32;
954 draw_debug_border(node.pos.0, clamped_y, node.size.0, node.size.1, buf, style);
955 if sy >= 0 {
956 buf.set_string(node.pos.0, clamped_y, &depth.to_string(), style);
957 }
958 }
959 }
960
961 if node.is_scrollable {
962 if let Some(area) = visible_area(node, y_offset) {
963 let inner = inner_area(node, area);
964 buf.push_clip(inner);
965 for child in &node.children {
966 render_debug_overlay_inner(child, buf, depth.saturating_add(1), child_offset);
967 }
968 buf.pop_clip();
969 }
970 } else {
971 for child in &node.children {
972 render_debug_overlay_inner(child, buf, depth.saturating_add(1), child_offset);
973 }
974 }
975}
976
977fn debug_color_for_depth(depth: u32) -> Color {
978 match depth {
979 0 => Color::Cyan,
980 1 => Color::Yellow,
981 2 => Color::Magenta,
982 _ => Color::Red,
983 }
984}
985
986fn draw_debug_border(x: u32, y: u32, w: u32, h: u32, buf: &mut Buffer, style: Style) {
987 if w == 0 || h == 0 {
988 return;
989 }
990 let right = x + w - 1;
991 let bottom = y + h - 1;
992
993 if w == 1 && h == 1 {
994 buf.set_char(x, y, '┼', style);
995 return;
996 }
997 if h == 1 {
998 for xx in x..=right {
999 buf.set_char(xx, y, '─', style);
1000 }
1001 return;
1002 }
1003 if w == 1 {
1004 for yy in y..=bottom {
1005 buf.set_char(x, yy, '│', style);
1006 }
1007 return;
1008 }
1009
1010 buf.set_char(x, y, '┌', style);
1011 buf.set_char(right, y, '┐', style);
1012 buf.set_char(x, bottom, '└', style);
1013 buf.set_char(right, bottom, '┘', style);
1014
1015 for xx in (x + 1)..right {
1016 buf.set_char(xx, y, '─', style);
1017 buf.set_char(xx, bottom, '─', style);
1018 }
1019 for yy in (y + 1)..bottom {
1020 buf.set_char(x, yy, '│', style);
1021 buf.set_char(right, yy, '│', style);
1022 }
1023}
1024
1025#[allow(dead_code)]
1026fn draw_debug_padding_markers(node: &LayoutNode, y_offset: u32, buf: &mut Buffer, style: Style) {
1027 if node.size.0 == 0 || node.size.1 == 0 {
1028 return;
1029 }
1030
1031 if node.padding == Padding::default() {
1032 return;
1033 }
1034
1035 let Some(area) = visible_area(node, y_offset) else {
1036 return;
1037 };
1038 let inner = inner_area(node, area);
1039 if inner.width == 0 || inner.height == 0 {
1040 return;
1041 }
1042
1043 let right = inner.right() - 1;
1044 let bottom = inner.bottom() - 1;
1045 buf.set_char(inner.x, inner.y, 'p', style);
1046 buf.set_char(right, inner.y, 'p', style);
1047 buf.set_char(inner.x, bottom, 'p', style);
1048 buf.set_char(right, bottom, 'p', style);
1049}
1050
1051#[allow(dead_code)]
1052fn draw_debug_margin_markers(node: &LayoutNode, y_offset: u32, buf: &mut Buffer, style: Style) {
1053 if node.margin == Margin::default() {
1054 return;
1055 }
1056
1057 let margin_y_i = node.pos.1 as i64 - node.margin.top as i64 - y_offset as i64;
1058 let w = node
1059 .size
1060 .0
1061 .saturating_add(node.margin.horizontal())
1062 .max(node.margin.horizontal());
1063 let h = node
1064 .size
1065 .1
1066 .saturating_add(node.margin.vertical())
1067 .max(node.margin.vertical());
1068
1069 if w == 0 || h == 0 || margin_y_i + h as i64 <= 0 {
1070 return;
1071 }
1072
1073 let x = node.pos.0.saturating_sub(node.margin.left);
1074 let y = margin_y_i.max(0) as u32;
1075 let bottom_i = margin_y_i + h as i64 - 1;
1076 if bottom_i < 0 {
1077 return;
1078 }
1079 let right = x + w - 1;
1080 let bottom = bottom_i as u32;
1081 if margin_y_i >= 0 {
1082 buf.set_char(x, y, 'm', style);
1083 buf.set_char(right, y, 'm', style);
1084 }
1085 buf.set_char(x, bottom, 'm', style);
1086 buf.set_char(right, bottom, 'm', style);
1087}
1088
1089fn screen_y(layout_y: u32, y_offset: u32) -> i64 {
1090 layout_y as i64 - y_offset as i64
1091}
1092
1093fn visible_area(node: &LayoutNode, y_offset: u32) -> Option<Rect> {
1094 let sy = screen_y(node.pos.1, y_offset);
1095 let bottom = sy + node.size.1 as i64;
1096 if bottom <= 0 || node.size.0 == 0 || node.size.1 == 0 {
1097 return None;
1098 }
1099 let clamped_y = sy.max(0) as u32;
1100 let clamped_h = (bottom as u32).saturating_sub(clamped_y);
1101 Some(Rect::new(node.pos.0, clamped_y, node.size.0, clamped_h))
1102}
1103
1104fn render_inner(node: &LayoutNode, buf: &mut Buffer, y_offset: u32, parent_bg: Option<Color>) {
1105 if node.size.0 == 0 || node.size.1 == 0 {
1106 return;
1107 }
1108
1109 let sy = screen_y(node.pos.1, y_offset);
1110 let sx = i64::from(node.pos.0);
1111 let ex = sx.saturating_add(i64::from(node.size.0));
1112 let ey = sy.saturating_add(i64::from(node.size.1));
1113 let viewport_left = i64::from(buf.area.x);
1114 let viewport_top = i64::from(buf.area.y);
1115 let viewport_right = viewport_left.saturating_add(i64::from(buf.area.width));
1116 let viewport_bottom = viewport_top.saturating_add(i64::from(buf.area.height));
1117
1118 if ex <= viewport_left || ey <= viewport_top || sx >= viewport_right || sy >= viewport_bottom {
1119 return;
1120 }
1121
1122 match node.kind {
1123 NodeKind::Text => {
1124 if let Some(ref text) = node.content {
1125 let mut style = node.style;
1126 if style.bg.is_none() {
1127 style.bg = parent_bg;
1128 }
1129 if node.wrap {
1130 let fallback;
1131 let lines = if let Some(cached) = &node.cached_wrapped {
1132 cached.as_slice()
1133 } else {
1134 fallback = wrap_lines(text, node.size.0);
1135 fallback.as_slice()
1136 };
1137 for (i, line) in lines.iter().enumerate() {
1138 let line_y = sy + i as i64;
1139 if line_y < 0 {
1140 continue;
1141 }
1142 let text_width = UnicodeWidthStr::width(line.as_str()) as u32;
1143 let x_offset = if text_width < node.size.0 {
1144 match node.align {
1145 Align::Start => 0,
1146 Align::Center => (node.size.0 - text_width) / 2,
1147 Align::End => node.size.0 - text_width,
1148 }
1149 } else {
1150 0
1151 };
1152 buf.set_string(
1153 node.pos.0.saturating_add(x_offset),
1154 line_y as u32,
1155 line,
1156 style,
1157 );
1158 }
1159 } else {
1160 if sy < 0 {
1161 return;
1162 }
1163 let text_width = UnicodeWidthStr::width(text.as_str()) as u32;
1164 let x_offset = if text_width < node.size.0 {
1165 match node.align {
1166 Align::Start => 0,
1167 Align::Center => (node.size.0 - text_width) / 2,
1168 Align::End => node.size.0 - text_width,
1169 }
1170 } else {
1171 0
1172 };
1173 let draw_x = node.pos.0.saturating_add(x_offset);
1174 if let Some(ref url) = node.link_url {
1175 buf.set_string_linked(draw_x, sy as u32, text, style, url);
1176 } else {
1177 buf.set_string(draw_x, sy as u32, text, style);
1178 }
1179 }
1180 }
1181 }
1182 NodeKind::Spacer => {}
1183 NodeKind::Container(_) => {
1184 if let Some(color) = node.bg_color {
1185 if let Some(area) = visible_area(node, y_offset) {
1186 let fill_style = Style::new().bg(color);
1187 for y in area.y..area.bottom() {
1188 for x in area.x..area.right() {
1189 buf.set_string(x, y, " ", fill_style);
1190 }
1191 }
1192 }
1193 }
1194 let child_bg = node.bg_color.or(parent_bg);
1195 render_container_border(node, buf, y_offset, child_bg);
1196 if node.is_scrollable {
1197 let Some(area) = visible_area(node, y_offset) else {
1198 return;
1199 };
1200 let inner = inner_area(node, area);
1201 let child_offset = y_offset.saturating_add(node.scroll_offset);
1202 let render_y_start = inner.y as i64;
1203 let render_y_end = inner.bottom() as i64;
1204 buf.push_clip(inner);
1205 for child in &node.children {
1206 let child_top = child.pos.1 as i64 - child_offset as i64;
1207 let child_bottom = child_top + child.size.1 as i64;
1208 if child_bottom <= render_y_start || child_top >= render_y_end {
1209 continue;
1210 }
1211 render_inner(child, buf, child_offset, child_bg);
1212 }
1213 buf.pop_clip();
1214 render_scroll_indicators(node, inner, buf, child_bg);
1215 } else {
1216 let Some(area) = visible_area(node, y_offset) else {
1217 return;
1218 };
1219 let clip = inner_area(node, area);
1220 buf.push_clip(clip);
1221 for child in &node.children {
1222 render_inner(child, buf, y_offset, child_bg);
1223 }
1224 buf.pop_clip();
1225 }
1226 }
1227 }
1228}
1229
1230fn render_container_border(
1231 node: &LayoutNode,
1232 buf: &mut Buffer,
1233 y_offset: u32,
1234 inherit_bg: Option<Color>,
1235) {
1236 let Some(border) = node.border else {
1237 return;
1238 };
1239 let chars = border.chars();
1240 let x = node.pos.0;
1241 let w = node.size.0;
1242 let h = node.size.1;
1243 if w == 0 || h == 0 {
1244 return;
1245 }
1246
1247 let mut style = node.border_style;
1248 if style.bg.is_none() {
1249 style.bg = inherit_bg;
1250 }
1251
1252 let top_i = screen_y(node.pos.1, y_offset);
1253 let bottom_i = top_i + h as i64 - 1;
1254 if bottom_i < 0 {
1255 return;
1256 }
1257 let right = x + w - 1;
1258
1259 if w == 1 && h == 1 {
1260 if top_i >= 0 {
1261 buf.set_char(x, top_i as u32, chars.tl, style);
1262 }
1263 } else if h == 1 {
1264 if top_i >= 0 {
1265 let y = top_i as u32;
1266 for xx in x..=right {
1267 buf.set_char(xx, y, chars.h, style);
1268 }
1269 }
1270 } else if w == 1 {
1271 let vert_start = (top_i.max(0)) as u32;
1272 let vert_end = bottom_i as u32;
1273 for yy in vert_start..=vert_end {
1274 buf.set_char(x, yy, chars.v, style);
1275 }
1276 } else {
1277 if top_i >= 0 {
1278 let y = top_i as u32;
1279 buf.set_char(x, y, chars.tl, style);
1280 buf.set_char(right, y, chars.tr, style);
1281 for xx in (x + 1)..right {
1282 buf.set_char(xx, y, chars.h, style);
1283 }
1284 }
1285
1286 let bot = bottom_i as u32;
1287 buf.set_char(x, bot, chars.bl, style);
1288 buf.set_char(right, bot, chars.br, style);
1289 for xx in (x + 1)..right {
1290 buf.set_char(xx, bot, chars.h, style);
1291 }
1292
1293 let vert_start = ((top_i + 1).max(0)) as u32;
1294 let vert_end = bottom_i as u32;
1295 for yy in vert_start..vert_end {
1296 buf.set_char(x, yy, chars.v, style);
1297 buf.set_char(right, yy, chars.v, style);
1298 }
1299 }
1300
1301 if top_i >= 0 {
1302 if let Some((title, title_style)) = &node.title {
1303 let mut ts = *title_style;
1304 if ts.bg.is_none() {
1305 ts.bg = inherit_bg;
1306 }
1307 let y = top_i as u32;
1308 let title_x = x.saturating_add(2);
1309 if title_x <= right {
1310 let max_width = (right - title_x + 1) as usize;
1311 let trimmed: String = title.chars().take(max_width).collect();
1312 buf.set_string(title_x, y, &trimmed, ts);
1313 }
1314 }
1315 }
1316}
1317
1318fn render_scroll_indicators(
1319 node: &LayoutNode,
1320 inner: Rect,
1321 buf: &mut Buffer,
1322 inherit_bg: Option<Color>,
1323) {
1324 if inner.width == 0 || inner.height == 0 {
1325 return;
1326 }
1327
1328 let mut style = node.border_style;
1329 if style.bg.is_none() {
1330 style.bg = inherit_bg;
1331 }
1332
1333 let indicator_x = inner.right() - 1;
1334 if node.scroll_offset > 0 {
1335 buf.set_char(indicator_x, inner.y, '▲', style);
1336 }
1337 if node.scroll_offset.saturating_add(inner.height) < node.content_height {
1338 buf.set_char(indicator_x, inner.bottom() - 1, '▼', style);
1339 }
1340}
1341
1342pub(crate) fn collect_scroll_infos(node: &LayoutNode) -> Vec<(u32, u32)> {
1343 let mut infos = Vec::new();
1344 collect_scroll_infos_inner(node, &mut infos);
1345 for overlay in &node.overlays {
1346 collect_scroll_infos_inner(&overlay.node, &mut infos);
1347 }
1348 infos
1349}
1350
1351pub(crate) fn collect_hit_areas(node: &LayoutNode) -> Vec<Rect> {
1352 let mut areas = Vec::new();
1353 for child in &node.children {
1354 collect_hit_areas_inner(child, &mut areas);
1355 }
1356 for overlay in &node.overlays {
1357 collect_hit_areas_inner(&overlay.node, &mut areas);
1358 }
1359 areas
1360}
1361
1362fn collect_scroll_infos_inner(node: &LayoutNode, infos: &mut Vec<(u32, u32)>) {
1363 if node.is_scrollable {
1364 let viewport_h = node.size.1.saturating_sub(node.frame_vertical());
1365 infos.push((node.content_height, viewport_h));
1366 }
1367 for child in &node.children {
1368 collect_scroll_infos_inner(child, infos);
1369 }
1370}
1371
1372fn collect_hit_areas_inner(node: &LayoutNode, areas: &mut Vec<Rect>) {
1373 if matches!(node.kind, NodeKind::Container(_)) || node.link_url.is_some() {
1374 areas.push(Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1));
1375 }
1376 for child in &node.children {
1377 collect_hit_areas_inner(child, areas);
1378 }
1379}
1380
1381pub(crate) fn collect_content_areas(node: &LayoutNode) -> Vec<(Rect, Rect)> {
1382 let mut areas = Vec::new();
1383 for child in &node.children {
1384 collect_content_areas_inner(child, &mut areas);
1385 }
1386 for overlay in &node.overlays {
1387 collect_content_areas_inner(&overlay.node, &mut areas);
1388 }
1389 areas
1390}
1391
1392fn collect_content_areas_inner(node: &LayoutNode, areas: &mut Vec<(Rect, Rect)>) {
1393 if matches!(node.kind, NodeKind::Container(_)) {
1394 let full = Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1);
1395 let inset_x = node.padding.left + node.border_inset();
1396 let inset_y = node.padding.top + node.border_inset();
1397 let inner_w = node.size.0.saturating_sub(node.frame_horizontal());
1398 let inner_h = node.size.1.saturating_sub(node.frame_vertical());
1399 let content = Rect::new(node.pos.0 + inset_x, node.pos.1 + inset_y, inner_w, inner_h);
1400 areas.push((full, content));
1401 }
1402 for child in &node.children {
1403 collect_content_areas_inner(child, areas);
1404 }
1405}
1406
1407pub(crate) fn collect_focus_rects(node: &LayoutNode) -> Vec<(usize, Rect)> {
1408 let mut rects = Vec::new();
1409 collect_focus_rects_inner(node, &mut rects);
1410 for overlay in &node.overlays {
1411 collect_focus_rects_inner(&overlay.node, &mut rects);
1412 }
1413 rects
1414}
1415
1416fn collect_focus_rects_inner(node: &LayoutNode, rects: &mut Vec<(usize, Rect)>) {
1417 if let Some(id) = node.focus_id {
1418 rects.push((
1419 id,
1420 Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1),
1421 ));
1422 }
1423 for child in &node.children {
1424 collect_focus_rects_inner(child, rects);
1425 }
1426}
1427
1428#[cfg(test)]
1429mod tests {
1430 use super::wrap_lines;
1431
1432 #[test]
1433 fn wrap_empty() {
1434 assert_eq!(wrap_lines("", 10), vec![""]);
1435 }
1436
1437 #[test]
1438 fn wrap_fits() {
1439 assert_eq!(wrap_lines("hello", 10), vec!["hello"]);
1440 }
1441
1442 #[test]
1443 fn wrap_word_boundary() {
1444 assert_eq!(wrap_lines("hello world", 7), vec!["hello", "world"]);
1445 }
1446
1447 #[test]
1448 fn wrap_multiple_words() {
1449 assert_eq!(
1450 wrap_lines("one two three four", 9),
1451 vec!["one two", "three", "four"]
1452 );
1453 }
1454
1455 #[test]
1456 fn wrap_long_word() {
1457 assert_eq!(wrap_lines("abcdefghij", 4), vec!["abcd", "efgh", "ij"]);
1458 }
1459
1460 #[test]
1461 fn wrap_zero_width() {
1462 assert_eq!(wrap_lines("hello", 0), vec!["hello"]);
1463 }
1464
1465 #[test]
1466 fn diagnostic_demo_layout() {
1467 use super::{compute, ContainerConfig, Direction, LayoutNode};
1468 use crate::rect::Rect;
1469 use crate::style::{Align, Border, Constraints, Justify, Margin, Padding, Style};
1470
1471 let mut root = LayoutNode::container(
1484 Direction::Column,
1485 ContainerConfig {
1486 gap: 0,
1487 align: Align::Start,
1488 justify: Justify::Start,
1489 border: None,
1490 border_style: Style::new(),
1491 bg_color: None,
1492 padding: Padding::default(),
1493 margin: Margin::default(),
1494 constraints: Constraints::default(),
1495 title: None,
1496 grow: 0,
1497 },
1498 );
1499
1500 let mut outer_container = LayoutNode::container(
1502 Direction::Column,
1503 ContainerConfig {
1504 gap: 0,
1505 align: Align::Start,
1506 justify: Justify::Start,
1507 border: Some(Border::Rounded),
1508 border_style: Style::new(),
1509 bg_color: None,
1510 padding: Padding::all(1),
1511 margin: Margin::default(),
1512 constraints: Constraints::default(),
1513 title: None,
1514 grow: 1,
1515 },
1516 );
1517
1518 outer_container.children.push(LayoutNode::text(
1520 "header".to_string(),
1521 Style::new(),
1522 0,
1523 Align::Start,
1524 false,
1525 Margin::default(),
1526 Constraints::default(),
1527 ));
1528
1529 outer_container.children.push(LayoutNode::text(
1531 "separator".to_string(),
1532 Style::new(),
1533 0,
1534 Align::Start,
1535 false,
1536 Margin::default(),
1537 Constraints::default(),
1538 ));
1539
1540 let mut inner_container = LayoutNode::container(
1542 Direction::Column,
1543 ContainerConfig {
1544 gap: 0,
1545 align: Align::Start,
1546 justify: Justify::Start,
1547 border: None,
1548 border_style: Style::new(),
1549 bg_color: None,
1550 padding: Padding::default(),
1551 margin: Margin::default(),
1552 constraints: Constraints::default(),
1553 title: None,
1554 grow: 1,
1555 },
1556 );
1557
1558 inner_container.children.push(LayoutNode::text(
1560 "content1".to_string(),
1561 Style::new(),
1562 0,
1563 Align::Start,
1564 false,
1565 Margin::default(),
1566 Constraints::default(),
1567 ));
1568 inner_container.children.push(LayoutNode::text(
1569 "content2".to_string(),
1570 Style::new(),
1571 0,
1572 Align::Start,
1573 false,
1574 Margin::default(),
1575 Constraints::default(),
1576 ));
1577 inner_container.children.push(LayoutNode::text(
1578 "content3".to_string(),
1579 Style::new(),
1580 0,
1581 Align::Start,
1582 false,
1583 Margin::default(),
1584 Constraints::default(),
1585 ));
1586
1587 outer_container.children.push(inner_container);
1588
1589 outer_container.children.push(LayoutNode::text(
1591 "separator2".to_string(),
1592 Style::new(),
1593 0,
1594 Align::Start,
1595 false,
1596 Margin::default(),
1597 Constraints::default(),
1598 ));
1599
1600 outer_container.children.push(LayoutNode::text(
1602 "footer".to_string(),
1603 Style::new(),
1604 0,
1605 Align::Start,
1606 false,
1607 Margin::default(),
1608 Constraints::default(),
1609 ));
1610
1611 root.children.push(outer_container);
1612
1613 compute(&mut root, Rect::new(0, 0, 80, 50));
1615
1616 eprintln!("\n=== DIAGNOSTIC LAYOUT TEST ===");
1618 eprintln!("Root node:");
1619 eprintln!(" pos: {:?}, size: {:?}", root.pos, root.size);
1620
1621 let outer = &root.children[0];
1622 eprintln!("\nOuter bordered container (grow:1):");
1623 eprintln!(" pos: {:?}, size: {:?}", outer.pos, outer.size);
1624
1625 let inner = &outer.children[2];
1626 eprintln!("\nInner container (grow:1, simulates scrollable):");
1627 eprintln!(" pos: {:?}, size: {:?}", inner.pos, inner.size);
1628
1629 eprintln!("\nAll children of outer container:");
1630 for (i, child) in outer.children.iter().enumerate() {
1631 eprintln!(" [{}] pos: {:?}, size: {:?}", i, child.pos, child.size);
1632 }
1633
1634 assert_eq!(
1637 root.size,
1638 (80, 50),
1639 "Root node should fill entire terminal (80x50)"
1640 );
1641
1642 assert_eq!(
1644 outer.size,
1645 (80, 50),
1646 "Outer bordered container should fill entire terminal (80x50)"
1647 );
1648
1649 let expected_inner_height = 50 - 2 - 2 - 4;
1656 assert_eq!(
1657 inner.size.1, expected_inner_height as u32,
1658 "Inner container height should be {} (50 - border(2) - padding(2) - fixed(4))",
1659 expected_inner_height
1660 );
1661
1662 let expected_inner_y = 1 + 1 + 1 + 1;
1664 assert_eq!(
1665 inner.pos.1, expected_inner_y as u32,
1666 "Inner container should start at y={} (border+padding+header+sep)",
1667 expected_inner_y
1668 );
1669
1670 eprintln!("\n✓ All assertions passed!");
1671 eprintln!(" Root size: {:?}", root.size);
1672 eprintln!(" Outer container size: {:?}", outer.size);
1673 eprintln!(" Inner container size: {:?}", inner.size);
1674 eprintln!(" Inner container pos: {:?}", inner.pos);
1675 }
1676
1677 #[test]
1678 fn collect_focus_rects_from_markers() {
1679 use super::*;
1680 use crate::style::Style;
1681
1682 let commands = vec![
1683 Command::FocusMarker(0),
1684 Command::Text {
1685 content: "input1".into(),
1686 style: Style::new(),
1687 grow: 0,
1688 align: Align::Start,
1689 wrap: false,
1690 margin: Default::default(),
1691 constraints: Default::default(),
1692 },
1693 Command::FocusMarker(1),
1694 Command::Text {
1695 content: "input2".into(),
1696 style: Style::new(),
1697 grow: 0,
1698 align: Align::Start,
1699 wrap: false,
1700 margin: Default::default(),
1701 constraints: Default::default(),
1702 },
1703 ];
1704
1705 let mut tree = build_tree(&commands);
1706 let area = crate::rect::Rect::new(0, 0, 40, 10);
1707 compute(&mut tree, area);
1708
1709 let rects = collect_focus_rects(&tree);
1710 assert_eq!(rects.len(), 2);
1711 assert_eq!(rects[0].0, 0);
1712 assert_eq!(rects[1].0, 1);
1713 assert!(rects[0].1.width > 0);
1714 assert!(rects[1].1.width > 0);
1715 assert_ne!(rects[0].1.y, rects[1].1.y);
1716 }
1717
1718 #[test]
1719 fn focus_marker_tags_container() {
1720 use super::*;
1721 use crate::style::{Border, Style};
1722
1723 let commands = vec![
1724 Command::FocusMarker(0),
1725 Command::BeginContainer {
1726 direction: Direction::Column,
1727 gap: 0,
1728 align: Align::Start,
1729 justify: Justify::Start,
1730 border: Some(Border::Single),
1731 border_style: Style::new(),
1732 bg_color: None,
1733 padding: Padding::default(),
1734 margin: Default::default(),
1735 constraints: Default::default(),
1736 title: None,
1737 grow: 0,
1738 },
1739 Command::Text {
1740 content: "inside".into(),
1741 style: Style::new(),
1742 grow: 0,
1743 align: Align::Start,
1744 wrap: false,
1745 margin: Default::default(),
1746 constraints: Default::default(),
1747 },
1748 Command::EndContainer,
1749 ];
1750
1751 let mut tree = build_tree(&commands);
1752 let area = crate::rect::Rect::new(0, 0, 40, 10);
1753 compute(&mut tree, area);
1754
1755 let rects = collect_focus_rects(&tree);
1756 assert_eq!(rects.len(), 1);
1757 assert_eq!(rects[0].0, 0);
1758 assert!(rects[0].1.width >= 8);
1759 assert!(rects[0].1.height >= 3);
1760 }
1761}