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