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