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