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