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