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 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 parent.children.push(node);
877 *pos += 1;
878 }
879 Command::EndContainer => {
880 *pos += 1;
881 return;
882 }
883 Command::EndOverlay => {
884 *pos += 1;
885 if stop_on_end_overlay {
886 return;
887 }
888 }
889 }
890 }
891}
892
893mod flexbox;
894mod render;
895
896pub(crate) use flexbox::compute;
897pub(crate) use render::{render, render_debug_overlay};
898
899#[derive(Default)]
900pub(crate) struct FrameData {
901 pub scroll_infos: Vec<(u32, u32)>,
902 pub scroll_rects: Vec<Rect>,
903 pub hit_areas: Vec<Rect>,
904 pub group_rects: Vec<(String, Rect)>,
905 pub content_areas: Vec<(Rect, Rect)>,
906 pub focus_rects: Vec<(usize, Rect)>,
907 pub focus_groups: Vec<Option<String>>,
908}
909
910pub(crate) fn collect_all(node: &LayoutNode) -> FrameData {
915 let mut data = FrameData::default();
916
917 if node.is_scrollable {
920 let viewport_h = node.size.1.saturating_sub(node.frame_vertical());
921 data.scroll_infos.push((node.content_height, viewport_h));
922 data.scroll_rects
923 .push(Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1));
924 }
925 if let Some(id) = node.focus_id {
926 if node.pos.1 + node.size.1 > 0 {
927 data.focus_rects.push((
928 id,
929 Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1),
930 ));
931 }
932 }
933
934 let child_offset = if node.is_scrollable {
935 node.scroll_offset
936 } else {
937 0
938 };
939 for child in &node.children {
940 collect_all_inner(child, &mut data, child_offset, None);
941 }
942
943 for overlay in &node.overlays {
944 collect_all_inner(&overlay.node, &mut data, 0, None);
945 }
946
947 data
948}
949
950fn collect_all_inner(
951 node: &LayoutNode,
952 data: &mut FrameData,
953 y_offset: u32,
954 active_group: Option<&str>,
955) {
956 if node.is_scrollable {
958 let viewport_h = node.size.1.saturating_sub(node.frame_vertical());
959 data.scroll_infos.push((node.content_height, viewport_h));
960 }
961
962 if node.is_scrollable {
964 let adj_y = node.pos.1.saturating_sub(y_offset);
965 data.scroll_rects
966 .push(Rect::new(node.pos.0, adj_y, node.size.0, node.size.1));
967 }
968
969 if matches!(node.kind, NodeKind::Container(_)) || node.link_url.is_some() {
971 if node.pos.1 + node.size.1 > y_offset {
972 data.hit_areas.push(Rect::new(
973 node.pos.0,
974 node.pos.1.saturating_sub(y_offset),
975 node.size.0,
976 node.size.1,
977 ));
978 } else {
979 data.hit_areas.push(Rect::new(0, 0, 0, 0));
980 }
981 }
982
983 if let Some(name) = &node.group_name {
985 if node.pos.1 + node.size.1 > y_offset {
986 data.group_rects.push((
987 name.clone(),
988 Rect::new(
989 node.pos.0,
990 node.pos.1.saturating_sub(y_offset),
991 node.size.0,
992 node.size.1,
993 ),
994 ));
995 }
996 }
997
998 if matches!(node.kind, NodeKind::Container(_)) {
1000 let adj_y = node.pos.1.saturating_sub(y_offset);
1001 let full = Rect::new(node.pos.0, adj_y, node.size.0, node.size.1);
1002 let inset_x = node.padding.left + node.border_left_inset();
1003 let inset_y = node.padding.top + node.border_top_inset();
1004 let inner_w = node.size.0.saturating_sub(node.frame_horizontal());
1005 let inner_h = node.size.1.saturating_sub(node.frame_vertical());
1006 let content = Rect::new(node.pos.0 + inset_x, adj_y + inset_y, inner_w, inner_h);
1007 data.content_areas.push((full, content));
1008 }
1009
1010 if let Some(id) = node.focus_id {
1012 if node.pos.1 + node.size.1 > y_offset {
1013 data.focus_rects.push((
1014 id,
1015 Rect::new(
1016 node.pos.0,
1017 node.pos.1.saturating_sub(y_offset),
1018 node.size.0,
1019 node.size.1,
1020 ),
1021 ));
1022 }
1023 }
1024
1025 let current_group = node.group_name.as_deref().or(active_group);
1027 if let Some(id) = node.focus_id {
1028 if id >= data.focus_groups.len() {
1029 data.focus_groups.resize(id + 1, None);
1030 }
1031 data.focus_groups[id] = current_group.map(ToString::to_string);
1032 }
1033
1034 let child_offset = if node.is_scrollable {
1036 y_offset.saturating_add(node.scroll_offset)
1037 } else {
1038 y_offset
1039 };
1040 for child in &node.children {
1041 collect_all_inner(child, data, child_offset, current_group);
1042 }
1043}
1044
1045pub(crate) fn collect_raw_draw_rects(node: &LayoutNode) -> Vec<(usize, Rect)> {
1046 let mut rects = Vec::new();
1047 collect_raw_draw_rects_inner(node, &mut rects, 0);
1048 for overlay in &node.overlays {
1049 collect_raw_draw_rects_inner(&overlay.node, &mut rects, 0);
1050 }
1051 rects
1052}
1053
1054fn collect_raw_draw_rects_inner(node: &LayoutNode, rects: &mut Vec<(usize, Rect)>, y_offset: u32) {
1055 if let NodeKind::RawDraw(draw_id) = node.kind {
1056 let adj_y = node.pos.1.saturating_sub(y_offset);
1057 rects.push((
1058 draw_id,
1059 Rect::new(node.pos.0, adj_y, node.size.0, node.size.1),
1060 ));
1061 }
1062 let child_offset = if node.is_scrollable {
1063 y_offset.saturating_add(node.scroll_offset)
1064 } else {
1065 y_offset
1066 };
1067 for child in &node.children {
1068 collect_raw_draw_rects_inner(child, rects, child_offset);
1069 }
1070}
1071
1072#[cfg(test)]
1073mod tests {
1074 use super::*;
1075
1076 #[test]
1077 fn wrap_empty() {
1078 assert_eq!(wrap_lines("", 10), vec![""]);
1079 }
1080
1081 #[test]
1082 fn wrap_fits() {
1083 assert_eq!(wrap_lines("hello", 10), vec!["hello"]);
1084 }
1085
1086 #[test]
1087 fn wrap_word_boundary() {
1088 assert_eq!(wrap_lines("hello world", 7), vec!["hello", "world"]);
1089 }
1090
1091 #[test]
1092 fn wrap_multiple_words() {
1093 assert_eq!(
1094 wrap_lines("one two three four", 9),
1095 vec!["one two", "three", "four"]
1096 );
1097 }
1098
1099 #[test]
1100 fn wrap_long_word() {
1101 assert_eq!(wrap_lines("abcdefghij", 4), vec!["abcd", "efgh", "ij"]);
1102 }
1103
1104 #[test]
1105 fn wrap_zero_width() {
1106 assert_eq!(wrap_lines("hello", 0), vec!["hello"]);
1107 }
1108
1109 #[test]
1110 fn diagnostic_demo_layout() {
1111 use super::{compute, ContainerConfig, Direction, LayoutNode};
1112 use crate::rect::Rect;
1113 use crate::style::{Align, Border, Constraints, Justify, Margin, Padding, Style};
1114
1115 let mut root = LayoutNode::container(
1128 Direction::Column,
1129 ContainerConfig {
1130 gap: 0,
1131 align: Align::Start,
1132 justify: Justify::Start,
1133 border: None,
1134 border_sides: BorderSides::all(),
1135 border_style: Style::new(),
1136 bg_color: None,
1137 padding: Padding::default(),
1138 margin: Margin::default(),
1139 constraints: Constraints::default(),
1140 title: None,
1141 grow: 0,
1142 },
1143 );
1144
1145 let mut outer_container = LayoutNode::container(
1147 Direction::Column,
1148 ContainerConfig {
1149 gap: 0,
1150 align: Align::Start,
1151 justify: Justify::Start,
1152 border: Some(Border::Rounded),
1153 border_sides: BorderSides::all(),
1154 border_style: Style::new(),
1155 bg_color: None,
1156 padding: Padding::all(1),
1157 margin: Margin::default(),
1158 constraints: Constraints::default(),
1159 title: None,
1160 grow: 1,
1161 },
1162 );
1163
1164 outer_container.children.push(LayoutNode::text(
1166 "header".to_string(),
1167 Style::new(),
1168 0,
1169 Align::Start,
1170 false,
1171 Margin::default(),
1172 Constraints::default(),
1173 ));
1174
1175 outer_container.children.push(LayoutNode::text(
1177 "separator".to_string(),
1178 Style::new(),
1179 0,
1180 Align::Start,
1181 false,
1182 Margin::default(),
1183 Constraints::default(),
1184 ));
1185
1186 let mut inner_container = LayoutNode::container(
1188 Direction::Column,
1189 ContainerConfig {
1190 gap: 0,
1191 align: Align::Start,
1192 justify: Justify::Start,
1193 border: None,
1194 border_sides: BorderSides::all(),
1195 border_style: Style::new(),
1196 bg_color: None,
1197 padding: Padding::default(),
1198 margin: Margin::default(),
1199 constraints: Constraints::default(),
1200 title: None,
1201 grow: 1,
1202 },
1203 );
1204
1205 inner_container.children.push(LayoutNode::text(
1207 "content1".to_string(),
1208 Style::new(),
1209 0,
1210 Align::Start,
1211 false,
1212 Margin::default(),
1213 Constraints::default(),
1214 ));
1215 inner_container.children.push(LayoutNode::text(
1216 "content2".to_string(),
1217 Style::new(),
1218 0,
1219 Align::Start,
1220 false,
1221 Margin::default(),
1222 Constraints::default(),
1223 ));
1224 inner_container.children.push(LayoutNode::text(
1225 "content3".to_string(),
1226 Style::new(),
1227 0,
1228 Align::Start,
1229 false,
1230 Margin::default(),
1231 Constraints::default(),
1232 ));
1233
1234 outer_container.children.push(inner_container);
1235
1236 outer_container.children.push(LayoutNode::text(
1238 "separator2".to_string(),
1239 Style::new(),
1240 0,
1241 Align::Start,
1242 false,
1243 Margin::default(),
1244 Constraints::default(),
1245 ));
1246
1247 outer_container.children.push(LayoutNode::text(
1249 "footer".to_string(),
1250 Style::new(),
1251 0,
1252 Align::Start,
1253 false,
1254 Margin::default(),
1255 Constraints::default(),
1256 ));
1257
1258 root.children.push(outer_container);
1259
1260 compute(&mut root, Rect::new(0, 0, 80, 50));
1262
1263 eprintln!("\n=== DIAGNOSTIC LAYOUT TEST ===");
1265 eprintln!("Root node:");
1266 eprintln!(" pos: {:?}, size: {:?}", root.pos, root.size);
1267
1268 let outer = &root.children[0];
1269 eprintln!("\nOuter bordered container (grow:1):");
1270 eprintln!(" pos: {:?}, size: {:?}", outer.pos, outer.size);
1271
1272 let inner = &outer.children[2];
1273 eprintln!("\nInner container (grow:1, simulates scrollable):");
1274 eprintln!(" pos: {:?}, size: {:?}", inner.pos, inner.size);
1275
1276 eprintln!("\nAll children of outer container:");
1277 for (i, child) in outer.children.iter().enumerate() {
1278 eprintln!(" [{}] pos: {:?}, size: {:?}", i, child.pos, child.size);
1279 }
1280
1281 assert_eq!(
1284 root.size,
1285 (80, 50),
1286 "Root node should fill entire terminal (80x50)"
1287 );
1288
1289 assert_eq!(
1291 outer.size,
1292 (80, 50),
1293 "Outer bordered container should fill entire terminal (80x50)"
1294 );
1295
1296 let expected_inner_height = 50 - 2 - 2 - 4;
1303 assert_eq!(
1304 inner.size.1, expected_inner_height as u32,
1305 "Inner container height should be {} (50 - border(2) - padding(2) - fixed(4))",
1306 expected_inner_height
1307 );
1308
1309 let expected_inner_y = 1 + 1 + 1 + 1;
1311 assert_eq!(
1312 inner.pos.1, expected_inner_y as u32,
1313 "Inner container should start at y={} (border+padding+header+sep)",
1314 expected_inner_y
1315 );
1316
1317 eprintln!("\n✓ All assertions passed!");
1318 eprintln!(" Root size: {:?}", root.size);
1319 eprintln!(" Outer container size: {:?}", outer.size);
1320 eprintln!(" Inner container size: {:?}", inner.size);
1321 eprintln!(" Inner container pos: {:?}", inner.pos);
1322 }
1323
1324 #[test]
1325 fn collect_focus_rects_from_markers() {
1326 use super::*;
1327 use crate::style::Style;
1328
1329 let commands = vec![
1330 Command::FocusMarker(0),
1331 Command::Text {
1332 content: "input1".into(),
1333 style: Style::new(),
1334 grow: 0,
1335 align: Align::Start,
1336 wrap: false,
1337 margin: Default::default(),
1338 constraints: Default::default(),
1339 },
1340 Command::FocusMarker(1),
1341 Command::Text {
1342 content: "input2".into(),
1343 style: Style::new(),
1344 grow: 0,
1345 align: Align::Start,
1346 wrap: false,
1347 margin: Default::default(),
1348 constraints: Default::default(),
1349 },
1350 ];
1351
1352 let mut tree = build_tree(&commands);
1353 let area = crate::rect::Rect::new(0, 0, 40, 10);
1354 compute(&mut tree, area);
1355
1356 let fd = collect_all(&tree);
1357 assert_eq!(fd.focus_rects.len(), 2);
1358 assert_eq!(fd.focus_rects[0].0, 0);
1359 assert_eq!(fd.focus_rects[1].0, 1);
1360 assert!(fd.focus_rects[0].1.width > 0);
1361 assert!(fd.focus_rects[1].1.width > 0);
1362 assert_ne!(fd.focus_rects[0].1.y, fd.focus_rects[1].1.y);
1363 }
1364
1365 #[test]
1366 fn focus_marker_tags_container() {
1367 use super::*;
1368 use crate::style::{Border, Style};
1369
1370 let commands = vec![
1371 Command::FocusMarker(0),
1372 Command::BeginContainer {
1373 direction: Direction::Column,
1374 gap: 0,
1375 align: Align::Start,
1376 justify: Justify::Start,
1377 border: Some(Border::Single),
1378 border_sides: BorderSides::all(),
1379 border_style: Style::new(),
1380 bg_color: None,
1381 padding: Padding::default(),
1382 margin: Default::default(),
1383 constraints: Default::default(),
1384 title: None,
1385 grow: 0,
1386 group_name: None,
1387 },
1388 Command::Text {
1389 content: "inside".into(),
1390 style: Style::new(),
1391 grow: 0,
1392 align: Align::Start,
1393 wrap: false,
1394 margin: Default::default(),
1395 constraints: Default::default(),
1396 },
1397 Command::EndContainer,
1398 ];
1399
1400 let mut tree = build_tree(&commands);
1401 let area = crate::rect::Rect::new(0, 0, 40, 10);
1402 compute(&mut tree, area);
1403
1404 let fd = collect_all(&tree);
1405 assert_eq!(fd.focus_rects.len(), 1);
1406 assert_eq!(fd.focus_rects[0].0, 0);
1407 assert!(fd.focus_rects[0].1.width >= 8);
1408 assert!(fd.focus_rects[0].1.height >= 3);
1409 }
1410}