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 truncate: bool,
30 margin: Margin,
31 constraints: Constraints,
32 },
33 BeginContainer {
34 direction: Direction,
35 gap: u32,
36 align: Align,
37 align_self: Option<Align>,
38 justify: Justify,
39 border: Option<Border>,
40 border_sides: BorderSides,
41 border_style: Style,
42 bg_color: Option<Color>,
43 padding: Padding,
44 margin: Margin,
45 constraints: Constraints,
46 title: Option<(String, Style)>,
47 grow: u16,
48 group_name: Option<String>,
49 },
50 BeginScrollable {
51 grow: u16,
52 border: Option<Border>,
53 border_sides: BorderSides,
54 border_style: Style,
55 padding: Padding,
56 margin: Margin,
57 constraints: Constraints,
58 title: Option<(String, Style)>,
59 scroll_offset: u32,
60 },
61 Link {
62 text: String,
63 url: String,
64 style: Style,
65 margin: Margin,
66 constraints: Constraints,
67 },
68 RichText {
69 segments: Vec<(String, Style)>,
70 wrap: bool,
71 align: Align,
72 margin: Margin,
73 constraints: Constraints,
74 },
75 EndContainer,
76 BeginOverlay {
77 modal: bool,
78 },
79 EndOverlay,
80 Spacer {
81 grow: u16,
82 },
83 FocusMarker(usize),
84 RawDraw {
85 draw_id: usize,
86 constraints: Constraints,
87 grow: u16,
88 margin: Margin,
89 },
90}
91
92#[derive(Debug, Clone)]
93struct OverlayLayer {
94 node: LayoutNode,
95 modal: bool,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99enum NodeKind {
100 Text,
101 Container(Direction),
102 Spacer,
103 RawDraw(usize),
104}
105
106#[derive(Debug, Clone)]
107pub(crate) struct LayoutNode {
108 kind: NodeKind,
109 content: Option<String>,
110 style: Style,
111 pub grow: u16,
112 align: Align,
113 pub(crate) align_self: Option<Align>,
114 justify: Justify,
115 wrap: bool,
116 truncate: bool,
117 gap: u32,
118 border: Option<Border>,
119 border_sides: BorderSides,
120 border_style: Style,
121 bg_color: Option<Color>,
122 padding: Padding,
123 margin: Margin,
124 constraints: Constraints,
125 title: Option<(String, Style)>,
126 children: Vec<LayoutNode>,
127 pos: (u32, u32),
128 size: (u32, u32),
129 is_scrollable: bool,
130 scroll_offset: u32,
131 content_height: u32,
132 cached_wrapped: Option<Vec<String>>,
133 segments: Option<Vec<(String, Style)>>,
134 cached_wrapped_segments: Option<Vec<Vec<(String, Style)>>>,
135 pub(crate) focus_id: Option<usize>,
136 link_url: Option<String>,
137 group_name: Option<String>,
138 overlays: Vec<OverlayLayer>,
139}
140
141#[derive(Debug, Clone)]
142struct ContainerConfig {
143 gap: u32,
144 align: Align,
145 align_self: Option<Align>,
146 justify: Justify,
147 border: Option<Border>,
148 border_sides: BorderSides,
149 border_style: Style,
150 bg_color: Option<Color>,
151 padding: Padding,
152 margin: Margin,
153 constraints: Constraints,
154 title: Option<(String, Style)>,
155 grow: u16,
156}
157
158impl LayoutNode {
159 fn text(
160 content: String,
161 style: Style,
162 grow: u16,
163 align: Align,
164 text_flags: (bool, bool),
165 margin: Margin,
166 constraints: Constraints,
167 ) -> Self {
168 let (wrap, truncate) = text_flags;
169 let width = UnicodeWidthStr::width(content.as_str()) as u32;
170 Self {
171 kind: NodeKind::Text,
172 content: Some(content),
173 style,
174 grow,
175 align,
176 align_self: None,
177 justify: Justify::Start,
178 wrap,
179 truncate,
180 gap: 0,
181 border: None,
182 border_sides: BorderSides::all(),
183 border_style: Style::new(),
184 bg_color: None,
185 padding: Padding::default(),
186 margin,
187 constraints,
188 title: None,
189 children: Vec::new(),
190 pos: (0, 0),
191 size: (width, 1),
192 is_scrollable: false,
193 scroll_offset: 0,
194 content_height: 0,
195 cached_wrapped: None,
196 segments: None,
197 cached_wrapped_segments: None,
198 focus_id: None,
199 link_url: None,
200 group_name: None,
201 overlays: Vec::new(),
202 }
203 }
204
205 fn rich_text(
206 segments: Vec<(String, Style)>,
207 wrap: bool,
208 align: Align,
209 margin: Margin,
210 constraints: Constraints,
211 ) -> Self {
212 let width: u32 = segments
213 .iter()
214 .map(|(s, _)| UnicodeWidthStr::width(s.as_str()) as u32)
215 .sum();
216 Self {
217 kind: NodeKind::Text,
218 content: None,
219 style: Style::new(),
220 grow: 0,
221 align,
222 align_self: None,
223 justify: Justify::Start,
224 wrap,
225 truncate: false,
226 gap: 0,
227 border: None,
228 border_sides: BorderSides::all(),
229 border_style: Style::new(),
230 bg_color: None,
231 padding: Padding::default(),
232 margin,
233 constraints,
234 title: None,
235 children: Vec::new(),
236 pos: (0, 0),
237 size: (width, 1),
238 is_scrollable: false,
239 scroll_offset: 0,
240 content_height: 0,
241 cached_wrapped: None,
242 segments: Some(segments),
243 cached_wrapped_segments: None,
244 focus_id: None,
245 link_url: None,
246 group_name: None,
247 overlays: Vec::new(),
248 }
249 }
250
251 fn container(direction: Direction, config: ContainerConfig) -> Self {
252 Self {
253 kind: NodeKind::Container(direction),
254 content: None,
255 style: Style::new(),
256 grow: config.grow,
257 align: config.align,
258 align_self: config.align_self,
259 justify: config.justify,
260 wrap: false,
261 truncate: false,
262 gap: config.gap,
263 border: config.border,
264 border_sides: config.border_sides,
265 border_style: config.border_style,
266 bg_color: config.bg_color,
267 padding: config.padding,
268 margin: config.margin,
269 constraints: config.constraints,
270 title: config.title,
271 children: Vec::new(),
272 pos: (0, 0),
273 size: (0, 0),
274 is_scrollable: false,
275 scroll_offset: 0,
276 content_height: 0,
277 cached_wrapped: None,
278 segments: None,
279 cached_wrapped_segments: None,
280 focus_id: None,
281 link_url: None,
282 group_name: None,
283 overlays: Vec::new(),
284 }
285 }
286
287 fn spacer(grow: u16) -> Self {
288 Self {
289 kind: NodeKind::Spacer,
290 content: None,
291 style: Style::new(),
292 grow,
293 align: Align::Start,
294 align_self: None,
295 justify: Justify::Start,
296 wrap: false,
297 truncate: false,
298 gap: 0,
299 border: None,
300 border_sides: BorderSides::all(),
301 border_style: Style::new(),
302 bg_color: None,
303 padding: Padding::default(),
304 margin: Margin::default(),
305 constraints: Constraints::default(),
306 title: None,
307 children: Vec::new(),
308 pos: (0, 0),
309 size: (0, 0),
310 is_scrollable: false,
311 scroll_offset: 0,
312 content_height: 0,
313 cached_wrapped: None,
314 segments: None,
315 cached_wrapped_segments: None,
316 focus_id: None,
317 link_url: None,
318 group_name: None,
319 overlays: Vec::new(),
320 }
321 }
322
323 fn border_inset(&self) -> u32 {
324 if self.border.is_some() {
325 1
326 } else {
327 0
328 }
329 }
330
331 fn border_left_inset(&self) -> u32 {
332 if self.border.is_some() && self.border_sides.left {
333 1
334 } else {
335 0
336 }
337 }
338
339 fn border_right_inset(&self) -> u32 {
340 if self.border.is_some() && self.border_sides.right {
341 1
342 } else {
343 0
344 }
345 }
346
347 fn border_top_inset(&self) -> u32 {
348 if self.border.is_some() && self.border_sides.top {
349 1
350 } else {
351 0
352 }
353 }
354
355 fn border_bottom_inset(&self) -> u32 {
356 if self.border.is_some() && self.border_sides.bottom {
357 1
358 } else {
359 0
360 }
361 }
362
363 fn frame_horizontal(&self) -> u32 {
364 self.padding.horizontal() + self.border_left_inset() + self.border_right_inset()
365 }
366
367 fn frame_vertical(&self) -> u32 {
368 self.padding.vertical() + self.border_top_inset() + self.border_bottom_inset()
369 }
370
371 fn min_width(&self) -> u32 {
372 let width = match self.kind {
373 NodeKind::Text => self.size.0,
374 NodeKind::Spacer | NodeKind::RawDraw(_) => 0,
375 NodeKind::Container(Direction::Row) => {
376 let gaps = if self.children.is_empty() {
377 0
378 } else {
379 (self.children.len() as u32 - 1) * self.gap
380 };
381 let children_width: u32 = self.children.iter().map(|c| c.min_width()).sum();
382 children_width + gaps + self.frame_horizontal()
383 }
384 NodeKind::Container(Direction::Column) => {
385 self.children
386 .iter()
387 .map(|c| c.min_width())
388 .max()
389 .unwrap_or(0)
390 + self.frame_horizontal()
391 }
392 };
393
394 let width = width.max(self.constraints.min_width.unwrap_or(0));
395 let width = match self.constraints.max_width {
396 Some(max_w) => width.min(max_w),
397 None => width,
398 };
399 width.saturating_add(self.margin.horizontal())
400 }
401
402 fn min_height(&self) -> u32 {
403 let height = match self.kind {
404 NodeKind::Text => 1,
405 NodeKind::Spacer | NodeKind::RawDraw(_) => 0,
406 NodeKind::Container(Direction::Row) => {
407 self.children
408 .iter()
409 .map(|c| c.min_height())
410 .max()
411 .unwrap_or(0)
412 + self.frame_vertical()
413 }
414 NodeKind::Container(Direction::Column) => {
415 let gaps = if self.children.is_empty() {
416 0
417 } else {
418 (self.children.len() as u32 - 1) * self.gap
419 };
420 let children_height: u32 = self.children.iter().map(|c| c.min_height()).sum();
421 children_height + gaps + self.frame_vertical()
422 }
423 };
424
425 let height = height.max(self.constraints.min_height.unwrap_or(0));
426 height.saturating_add(self.margin.vertical())
427 }
428
429 fn min_height_for_width(&self, available_width: u32) -> u32 {
430 match self.kind {
431 NodeKind::Text if self.wrap => {
432 let inner_width = available_width.saturating_sub(self.margin.horizontal());
433 let lines = if let Some(ref segs) = self.segments {
434 wrap_segments(segs, inner_width).len().max(1) as u32
435 } else {
436 let text = self.content.as_deref().unwrap_or("");
437 wrap_lines(text, inner_width).len().max(1) as u32
438 };
439 lines.saturating_add(self.margin.vertical())
440 }
441 _ => self.min_height(),
442 }
443 }
444}
445
446fn wrap_lines(text: &str, max_width: u32) -> Vec<String> {
447 if text.is_empty() {
448 return vec![String::new()];
449 }
450 if max_width == 0 {
451 return vec![text.to_string()];
452 }
453
454 fn split_long_word(word: &str, max_width: u32) -> Vec<(String, u32)> {
455 let mut chunks: Vec<(String, u32)> = Vec::new();
456 let mut chunk = String::new();
457 let mut chunk_width = 0_u32;
458
459 for ch in word.chars() {
460 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
461 if chunk.is_empty() {
462 if ch_width > max_width {
463 chunks.push((ch.to_string(), ch_width));
464 } else {
465 chunk.push(ch);
466 chunk_width = ch_width;
467 }
468 continue;
469 }
470
471 if chunk_width + ch_width > max_width {
472 chunks.push((std::mem::take(&mut chunk), chunk_width));
473 if ch_width > max_width {
474 chunks.push((ch.to_string(), ch_width));
475 chunk_width = 0;
476 } else {
477 chunk.push(ch);
478 chunk_width = ch_width;
479 }
480 } else {
481 chunk.push(ch);
482 chunk_width += ch_width;
483 }
484 }
485
486 if !chunk.is_empty() {
487 chunks.push((chunk, chunk_width));
488 }
489
490 chunks
491 }
492
493 fn push_word_into_line(
494 lines: &mut Vec<String>,
495 current_line: &mut String,
496 current_width: &mut u32,
497 word: &str,
498 word_width: u32,
499 max_width: u32,
500 ) {
501 if word.is_empty() {
502 return;
503 }
504
505 if word_width > max_width {
506 let chunks = split_long_word(word, max_width);
507 for (chunk, chunk_width) in chunks {
508 if current_line.is_empty() {
509 *current_line = chunk;
510 *current_width = chunk_width;
511 } else if *current_width + 1 + chunk_width <= max_width {
512 current_line.push(' ');
513 current_line.push_str(&chunk);
514 *current_width += 1 + chunk_width;
515 } else {
516 lines.push(std::mem::take(current_line));
517 *current_line = chunk;
518 *current_width = chunk_width;
519 }
520 }
521 return;
522 }
523
524 if current_line.is_empty() {
525 *current_line = word.to_string();
526 *current_width = word_width;
527 } else if *current_width + 1 + word_width <= max_width {
528 current_line.push(' ');
529 current_line.push_str(word);
530 *current_width += 1 + word_width;
531 } else {
532 lines.push(std::mem::take(current_line));
533 *current_line = word.to_string();
534 *current_width = word_width;
535 }
536 }
537
538 let mut lines: Vec<String> = Vec::new();
539 let mut current_line = String::new();
540 let mut current_width: u32 = 0;
541 let mut current_word = String::new();
542 let mut word_width: u32 = 0;
543
544 for ch in text.chars() {
545 if ch == ' ' {
546 push_word_into_line(
547 &mut lines,
548 &mut current_line,
549 &mut current_width,
550 ¤t_word,
551 word_width,
552 max_width,
553 );
554 current_word.clear();
555 word_width = 0;
556 continue;
557 }
558
559 current_word.push(ch);
560 word_width += UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
561 }
562
563 push_word_into_line(
564 &mut lines,
565 &mut current_line,
566 &mut current_width,
567 ¤t_word,
568 word_width,
569 max_width,
570 );
571
572 if !current_line.is_empty() {
573 lines.push(current_line);
574 }
575
576 if lines.is_empty() {
577 vec![String::new()]
578 } else {
579 lines
580 }
581}
582
583fn wrap_segments(segments: &[(String, Style)], max_width: u32) -> Vec<Vec<(String, Style)>> {
584 if max_width == 0 || segments.is_empty() {
585 return vec![vec![]];
586 }
587 let mut chars: Vec<(char, Style)> = Vec::new();
588 for (text, style) in segments {
589 for ch in text.chars() {
590 chars.push((ch, *style));
591 }
592 }
593 if chars.is_empty() {
594 return vec![vec![]];
595 }
596
597 let mut lines: Vec<Vec<(String, Style)>> = Vec::new();
598 let mut i = 0;
599 while i < chars.len() {
600 let mut line_chars: Vec<(char, Style)> = Vec::new();
601 let mut line_width: u32 = 0;
602
603 if !lines.is_empty() {
604 while i < chars.len() && chars[i].0 == ' ' {
605 i += 1;
606 }
607 }
608
609 while i < chars.len() {
610 let (ch, st) = chars[i];
611 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
612 if line_width + ch_width > max_width && line_width > 0 {
613 if let Some(bp) = line_chars.iter().rposition(|(c, _)| *c == ' ') {
614 let rewind = line_chars.len() - bp - 1;
615 i -= rewind;
616 line_chars.truncate(bp);
617 }
618 break;
619 }
620 line_chars.push((ch, st));
621 line_width += ch_width;
622 i += 1;
623 }
624
625 let mut line_segs: Vec<(String, Style)> = Vec::new();
626 let mut cur = String::new();
627 let mut cur_style: Option<Style> = None;
628 for (ch, st) in &line_chars {
629 if cur_style == Some(*st) {
630 cur.push(*ch);
631 } else {
632 if let Some(s) = cur_style {
633 if !cur.is_empty() {
634 line_segs.push((std::mem::take(&mut cur), s));
635 }
636 }
637 cur_style = Some(*st);
638 cur.push(*ch);
639 }
640 }
641 if let Some(s) = cur_style {
642 if !cur.is_empty() {
643 let trimmed = cur.trim_end().to_string();
644 if !trimmed.is_empty() {
645 line_segs.push((trimmed, s));
646 } else if !line_segs.is_empty() {
647 if let Some(last) = line_segs.last_mut() {
648 let t = last.0.trim_end().to_string();
649 if t.is_empty() {
650 line_segs.pop();
651 } else {
652 last.0 = t;
653 }
654 }
655 }
656 }
657 }
658 lines.push(line_segs);
659 }
660 if lines.is_empty() {
661 vec![vec![]]
662 } else {
663 lines
664 }
665}
666
667pub(crate) fn build_tree(commands: &[Command]) -> LayoutNode {
668 let mut root = LayoutNode::container(Direction::Column, default_container_config());
669 let mut overlays: Vec<OverlayLayer> = Vec::new();
670 build_children(&mut root, commands, &mut 0, &mut overlays, false);
671 root.overlays = overlays;
672 root
673}
674
675fn default_container_config() -> ContainerConfig {
676 ContainerConfig {
677 gap: 0,
678 align: Align::Start,
679 align_self: None,
680 justify: Justify::Start,
681 border: None,
682 border_sides: BorderSides::all(),
683 border_style: Style::new(),
684 bg_color: None,
685 padding: Padding::default(),
686 margin: Margin::default(),
687 constraints: Constraints::default(),
688 title: None,
689 grow: 0,
690 }
691}
692
693fn build_children(
694 parent: &mut LayoutNode,
695 commands: &[Command],
696 pos: &mut usize,
697 overlays: &mut Vec<OverlayLayer>,
698 stop_on_end_overlay: bool,
699) {
700 let mut pending_focus_id: Option<usize> = None;
701 while *pos < commands.len() {
702 match &commands[*pos] {
703 Command::FocusMarker(id) => {
704 pending_focus_id = Some(*id);
705 *pos += 1;
706 }
707 Command::Text {
708 content,
709 style,
710 grow,
711 align,
712 wrap,
713 truncate,
714 margin,
715 constraints,
716 } => {
717 let mut node = LayoutNode::text(
718 content.clone(),
719 *style,
720 *grow,
721 *align,
722 (*wrap, *truncate),
723 *margin,
724 *constraints,
725 );
726 node.focus_id = pending_focus_id.take();
727 parent.children.push(node);
728 *pos += 1;
729 }
730 Command::RichText {
731 segments,
732 wrap,
733 align,
734 margin,
735 constraints,
736 } => {
737 let mut node =
738 LayoutNode::rich_text(segments.clone(), *wrap, *align, *margin, *constraints);
739 node.focus_id = pending_focus_id.take();
740 parent.children.push(node);
741 *pos += 1;
742 }
743 Command::Link {
744 text,
745 url,
746 style,
747 margin,
748 constraints,
749 } => {
750 let mut node = LayoutNode::text(
751 text.clone(),
752 *style,
753 0,
754 Align::Start,
755 (false, false),
756 *margin,
757 *constraints,
758 );
759 node.link_url = Some(url.clone());
760 node.focus_id = pending_focus_id.take();
761 parent.children.push(node);
762 *pos += 1;
763 }
764 Command::BeginContainer {
765 direction,
766 gap,
767 align,
768 align_self,
769 justify,
770 border,
771 border_sides,
772 border_style,
773 bg_color,
774 padding,
775 margin,
776 constraints,
777 title,
778 grow,
779 group_name,
780 } => {
781 let mut node = LayoutNode::container(
782 *direction,
783 ContainerConfig {
784 gap: *gap,
785 align: *align,
786 align_self: *align_self,
787 justify: *justify,
788 border: *border,
789 border_sides: *border_sides,
790 border_style: *border_style,
791 bg_color: *bg_color,
792 padding: *padding,
793 margin: *margin,
794 constraints: *constraints,
795 title: title.clone(),
796 grow: *grow,
797 },
798 );
799 node.focus_id = pending_focus_id.take();
800 node.group_name = group_name.clone();
801 *pos += 1;
802 build_children(&mut node, commands, pos, overlays, false);
803 parent.children.push(node);
804 }
805 Command::BeginScrollable {
806 grow,
807 border,
808 border_sides,
809 border_style,
810 padding,
811 margin,
812 constraints,
813 title,
814 scroll_offset,
815 } => {
816 let mut node = LayoutNode::container(
817 Direction::Column,
818 ContainerConfig {
819 gap: 0,
820 align: Align::Start,
821 align_self: None,
822 justify: Justify::Start,
823 border: *border,
824 border_sides: *border_sides,
825 border_style: *border_style,
826 bg_color: None,
827 padding: *padding,
828 margin: *margin,
829 constraints: *constraints,
830 title: title.clone(),
831 grow: *grow,
832 },
833 );
834 node.is_scrollable = true;
835 node.scroll_offset = *scroll_offset;
836 node.focus_id = pending_focus_id.take();
837 *pos += 1;
838 build_children(&mut node, commands, pos, overlays, false);
839 parent.children.push(node);
840 }
841 Command::BeginOverlay { modal } => {
842 *pos += 1;
843 let mut overlay_node =
844 LayoutNode::container(Direction::Column, default_container_config());
845 build_children(&mut overlay_node, commands, pos, overlays, true);
846 overlays.push(OverlayLayer {
847 node: overlay_node,
848 modal: *modal,
849 });
850 }
851 Command::Spacer { grow } => {
852 parent.children.push(LayoutNode::spacer(*grow));
853 *pos += 1;
854 }
855 Command::RawDraw {
856 draw_id,
857 constraints,
858 grow,
859 margin,
860 } => {
861 let node = LayoutNode {
862 kind: NodeKind::RawDraw(*draw_id),
863 content: None,
864 style: Style::new(),
865 grow: *grow,
866 align: Align::Start,
867 align_self: None,
868 justify: Justify::Start,
869 wrap: false,
870 truncate: false,
871 gap: 0,
872 border: None,
873 border_sides: BorderSides::all(),
874 border_style: Style::new(),
875 bg_color: None,
876 padding: Padding::default(),
877 margin: *margin,
878 constraints: *constraints,
879 title: None,
880 children: Vec::new(),
881 pos: (0, 0),
882 size: (
883 constraints.min_width.unwrap_or(0),
884 constraints.min_height.unwrap_or(0),
885 ),
886 is_scrollable: false,
887 scroll_offset: 0,
888 content_height: 0,
889 cached_wrapped: None,
890 segments: None,
891 cached_wrapped_segments: None,
892 focus_id: pending_focus_id.take(),
893 link_url: None,
894 group_name: None,
895 overlays: Vec::new(),
896 };
897 parent.children.push(node);
898 *pos += 1;
899 }
900 Command::EndContainer => {
901 *pos += 1;
902 return;
903 }
904 Command::EndOverlay => {
905 *pos += 1;
906 if stop_on_end_overlay {
907 return;
908 }
909 }
910 }
911 }
912}
913
914mod flexbox;
915mod render;
916
917pub(crate) use flexbox::compute;
918pub(crate) use render::{render, render_debug_overlay};
919
920#[derive(Default)]
921pub(crate) struct FrameData {
922 pub scroll_infos: Vec<(u32, u32)>,
923 pub scroll_rects: Vec<Rect>,
924 pub hit_areas: Vec<Rect>,
925 pub group_rects: Vec<(String, Rect)>,
926 pub content_areas: Vec<(Rect, Rect)>,
927 pub focus_rects: Vec<(usize, Rect)>,
928 pub focus_groups: Vec<Option<String>>,
929}
930
931pub(crate) fn collect_all(node: &LayoutNode) -> FrameData {
936 let mut data = FrameData::default();
937
938 if node.is_scrollable {
941 let viewport_h = node.size.1.saturating_sub(node.frame_vertical());
942 data.scroll_infos.push((node.content_height, viewport_h));
943 data.scroll_rects
944 .push(Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1));
945 }
946 if let Some(id) = node.focus_id {
947 if node.pos.1 + node.size.1 > 0 {
948 data.focus_rects.push((
949 id,
950 Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1),
951 ));
952 }
953 }
954
955 let child_offset = if node.is_scrollable {
956 node.scroll_offset
957 } else {
958 0
959 };
960 for child in &node.children {
961 collect_all_inner(child, &mut data, child_offset, None);
962 }
963
964 for overlay in &node.overlays {
965 collect_all_inner(&overlay.node, &mut data, 0, None);
966 }
967
968 data
969}
970
971fn collect_all_inner(
972 node: &LayoutNode,
973 data: &mut FrameData,
974 y_offset: u32,
975 active_group: Option<&str>,
976) {
977 if node.is_scrollable {
979 let viewport_h = node.size.1.saturating_sub(node.frame_vertical());
980 data.scroll_infos.push((node.content_height, viewport_h));
981 }
982
983 if node.is_scrollable {
985 let adj_y = node.pos.1.saturating_sub(y_offset);
986 data.scroll_rects
987 .push(Rect::new(node.pos.0, adj_y, node.size.0, node.size.1));
988 }
989
990 if matches!(node.kind, NodeKind::Container(_)) || node.link_url.is_some() {
992 if node.pos.1 + node.size.1 > y_offset {
993 data.hit_areas.push(Rect::new(
994 node.pos.0,
995 node.pos.1.saturating_sub(y_offset),
996 node.size.0,
997 node.size.1,
998 ));
999 } else {
1000 data.hit_areas.push(Rect::new(0, 0, 0, 0));
1001 }
1002 }
1003
1004 if let Some(name) = &node.group_name {
1006 if node.pos.1 + node.size.1 > y_offset {
1007 data.group_rects.push((
1008 name.clone(),
1009 Rect::new(
1010 node.pos.0,
1011 node.pos.1.saturating_sub(y_offset),
1012 node.size.0,
1013 node.size.1,
1014 ),
1015 ));
1016 }
1017 }
1018
1019 if matches!(node.kind, NodeKind::Container(_)) {
1021 let adj_y = node.pos.1.saturating_sub(y_offset);
1022 let full = Rect::new(node.pos.0, adj_y, node.size.0, node.size.1);
1023 let inset_x = node.padding.left + node.border_left_inset();
1024 let inset_y = node.padding.top + node.border_top_inset();
1025 let inner_w = node.size.0.saturating_sub(node.frame_horizontal());
1026 let inner_h = node.size.1.saturating_sub(node.frame_vertical());
1027 let content = Rect::new(node.pos.0 + inset_x, adj_y + inset_y, inner_w, inner_h);
1028 data.content_areas.push((full, content));
1029 }
1030
1031 if let Some(id) = node.focus_id {
1033 if node.pos.1 + node.size.1 > y_offset {
1034 data.focus_rects.push((
1035 id,
1036 Rect::new(
1037 node.pos.0,
1038 node.pos.1.saturating_sub(y_offset),
1039 node.size.0,
1040 node.size.1,
1041 ),
1042 ));
1043 }
1044 }
1045
1046 let current_group = node.group_name.as_deref().or(active_group);
1048 if let Some(id) = node.focus_id {
1049 if id >= data.focus_groups.len() {
1050 data.focus_groups.resize(id + 1, None);
1051 }
1052 data.focus_groups[id] = current_group.map(ToString::to_string);
1053 }
1054
1055 let child_offset = if node.is_scrollable {
1057 y_offset.saturating_add(node.scroll_offset)
1058 } else {
1059 y_offset
1060 };
1061 for child in &node.children {
1062 collect_all_inner(child, data, child_offset, current_group);
1063 }
1064}
1065
1066pub(crate) fn collect_raw_draw_rects(node: &LayoutNode) -> Vec<(usize, Rect)> {
1067 let mut rects = Vec::new();
1068 collect_raw_draw_rects_inner(node, &mut rects, 0);
1069 for overlay in &node.overlays {
1070 collect_raw_draw_rects_inner(&overlay.node, &mut rects, 0);
1071 }
1072 rects
1073}
1074
1075fn collect_raw_draw_rects_inner(node: &LayoutNode, rects: &mut Vec<(usize, Rect)>, y_offset: u32) {
1076 if let NodeKind::RawDraw(draw_id) = node.kind {
1077 let adj_y = node.pos.1.saturating_sub(y_offset);
1078 rects.push((
1079 draw_id,
1080 Rect::new(node.pos.0, adj_y, node.size.0, node.size.1),
1081 ));
1082 }
1083 let child_offset = if node.is_scrollable {
1084 y_offset.saturating_add(node.scroll_offset)
1085 } else {
1086 y_offset
1087 };
1088 for child in &node.children {
1089 collect_raw_draw_rects_inner(child, rects, child_offset);
1090 }
1091}
1092
1093#[cfg(test)]
1094#[allow(clippy::print_stderr)]
1095mod tests {
1096 use super::*;
1097
1098 #[test]
1099 fn wrap_empty() {
1100 assert_eq!(wrap_lines("", 10), vec![""]);
1101 }
1102
1103 #[test]
1104 fn wrap_fits() {
1105 assert_eq!(wrap_lines("hello", 10), vec!["hello"]);
1106 }
1107
1108 #[test]
1109 fn wrap_word_boundary() {
1110 assert_eq!(wrap_lines("hello world", 7), vec!["hello", "world"]);
1111 }
1112
1113 #[test]
1114 fn wrap_multiple_words() {
1115 assert_eq!(
1116 wrap_lines("one two three four", 9),
1117 vec!["one two", "three", "four"]
1118 );
1119 }
1120
1121 #[test]
1122 fn wrap_long_word() {
1123 assert_eq!(wrap_lines("abcdefghij", 4), vec!["abcd", "efgh", "ij"]);
1124 }
1125
1126 #[test]
1127 fn wrap_zero_width() {
1128 assert_eq!(wrap_lines("hello", 0), vec!["hello"]);
1129 }
1130
1131 #[test]
1132 fn diagnostic_demo_layout() {
1133 use super::{compute, ContainerConfig, Direction, LayoutNode};
1134 use crate::rect::Rect;
1135 use crate::style::{Align, Border, Constraints, Justify, Margin, Padding, Style};
1136
1137 let mut root = LayoutNode::container(
1150 Direction::Column,
1151 ContainerConfig {
1152 gap: 0,
1153 align: Align::Start,
1154 align_self: None,
1155 justify: Justify::Start,
1156 border: None,
1157 border_sides: BorderSides::all(),
1158 border_style: Style::new(),
1159 bg_color: None,
1160 padding: Padding::default(),
1161 margin: Margin::default(),
1162 constraints: Constraints::default(),
1163 title: None,
1164 grow: 0,
1165 },
1166 );
1167
1168 let mut outer_container = LayoutNode::container(
1170 Direction::Column,
1171 ContainerConfig {
1172 gap: 0,
1173 align: Align::Start,
1174 align_self: None,
1175 justify: Justify::Start,
1176 border: Some(Border::Rounded),
1177 border_sides: BorderSides::all(),
1178 border_style: Style::new(),
1179 bg_color: None,
1180 padding: Padding::all(1),
1181 margin: Margin::default(),
1182 constraints: Constraints::default(),
1183 title: None,
1184 grow: 1,
1185 },
1186 );
1187
1188 outer_container.children.push(LayoutNode::text(
1190 "header".to_string(),
1191 Style::new(),
1192 0,
1193 Align::Start,
1194 (false, false),
1195 Margin::default(),
1196 Constraints::default(),
1197 ));
1198
1199 outer_container.children.push(LayoutNode::text(
1201 "separator".to_string(),
1202 Style::new(),
1203 0,
1204 Align::Start,
1205 (false, false),
1206 Margin::default(),
1207 Constraints::default(),
1208 ));
1209
1210 let mut inner_container = LayoutNode::container(
1212 Direction::Column,
1213 ContainerConfig {
1214 gap: 0,
1215 align: Align::Start,
1216 align_self: None,
1217 justify: Justify::Start,
1218 border: None,
1219 border_sides: BorderSides::all(),
1220 border_style: Style::new(),
1221 bg_color: None,
1222 padding: Padding::default(),
1223 margin: Margin::default(),
1224 constraints: Constraints::default(),
1225 title: None,
1226 grow: 1,
1227 },
1228 );
1229
1230 inner_container.children.push(LayoutNode::text(
1232 "content1".to_string(),
1233 Style::new(),
1234 0,
1235 Align::Start,
1236 (false, false),
1237 Margin::default(),
1238 Constraints::default(),
1239 ));
1240 inner_container.children.push(LayoutNode::text(
1241 "content2".to_string(),
1242 Style::new(),
1243 0,
1244 Align::Start,
1245 (false, false),
1246 Margin::default(),
1247 Constraints::default(),
1248 ));
1249 inner_container.children.push(LayoutNode::text(
1250 "content3".to_string(),
1251 Style::new(),
1252 0,
1253 Align::Start,
1254 (false, false),
1255 Margin::default(),
1256 Constraints::default(),
1257 ));
1258
1259 outer_container.children.push(inner_container);
1260
1261 outer_container.children.push(LayoutNode::text(
1263 "separator2".to_string(),
1264 Style::new(),
1265 0,
1266 Align::Start,
1267 (false, false),
1268 Margin::default(),
1269 Constraints::default(),
1270 ));
1271
1272 outer_container.children.push(LayoutNode::text(
1274 "footer".to_string(),
1275 Style::new(),
1276 0,
1277 Align::Start,
1278 (false, false),
1279 Margin::default(),
1280 Constraints::default(),
1281 ));
1282
1283 root.children.push(outer_container);
1284
1285 compute(&mut root, Rect::new(0, 0, 80, 50));
1287
1288 eprintln!("\n=== DIAGNOSTIC LAYOUT TEST ===");
1290 eprintln!("Root node:");
1291 eprintln!(" pos: {:?}, size: {:?}", root.pos, root.size);
1292
1293 let outer = &root.children[0];
1294 eprintln!("\nOuter bordered container (grow:1):");
1295 eprintln!(" pos: {:?}, size: {:?}", outer.pos, outer.size);
1296
1297 let inner = &outer.children[2];
1298 eprintln!("\nInner container (grow:1, simulates scrollable):");
1299 eprintln!(" pos: {:?}, size: {:?}", inner.pos, inner.size);
1300
1301 eprintln!("\nAll children of outer container:");
1302 for (i, child) in outer.children.iter().enumerate() {
1303 eprintln!(" [{}] pos: {:?}, size: {:?}", i, child.pos, child.size);
1304 }
1305
1306 assert_eq!(
1309 root.size,
1310 (80, 50),
1311 "Root node should fill entire terminal (80x50)"
1312 );
1313
1314 assert_eq!(
1316 outer.size,
1317 (80, 50),
1318 "Outer bordered container should fill entire terminal (80x50)"
1319 );
1320
1321 let expected_inner_height = 50 - 2 - 2 - 4;
1328 assert_eq!(
1329 inner.size.1, expected_inner_height as u32,
1330 "Inner container height should be {} (50 - border(2) - padding(2) - fixed(4))",
1331 expected_inner_height
1332 );
1333
1334 let expected_inner_y = 1 + 1 + 1 + 1;
1336 assert_eq!(
1337 inner.pos.1, expected_inner_y as u32,
1338 "Inner container should start at y={} (border+padding+header+sep)",
1339 expected_inner_y
1340 );
1341
1342 eprintln!("\n✓ All assertions passed!");
1343 eprintln!(" Root size: {:?}", root.size);
1344 eprintln!(" Outer container size: {:?}", outer.size);
1345 eprintln!(" Inner container size: {:?}", inner.size);
1346 eprintln!(" Inner container pos: {:?}", inner.pos);
1347 }
1348
1349 #[test]
1350 fn collect_focus_rects_from_markers() {
1351 use super::*;
1352 use crate::style::Style;
1353
1354 let commands = vec![
1355 Command::FocusMarker(0),
1356 Command::Text {
1357 content: "input1".into(),
1358 style: Style::new(),
1359 grow: 0,
1360 align: Align::Start,
1361 wrap: false,
1362 truncate: false,
1363 margin: Default::default(),
1364 constraints: Default::default(),
1365 },
1366 Command::FocusMarker(1),
1367 Command::Text {
1368 content: "input2".into(),
1369 style: Style::new(),
1370 grow: 0,
1371 align: Align::Start,
1372 wrap: false,
1373 truncate: false,
1374 margin: Default::default(),
1375 constraints: Default::default(),
1376 },
1377 ];
1378
1379 let mut tree = build_tree(&commands);
1380 let area = crate::rect::Rect::new(0, 0, 40, 10);
1381 compute(&mut tree, area);
1382
1383 let fd = collect_all(&tree);
1384 assert_eq!(fd.focus_rects.len(), 2);
1385 assert_eq!(fd.focus_rects[0].0, 0);
1386 assert_eq!(fd.focus_rects[1].0, 1);
1387 assert!(fd.focus_rects[0].1.width > 0);
1388 assert!(fd.focus_rects[1].1.width > 0);
1389 assert_ne!(fd.focus_rects[0].1.y, fd.focus_rects[1].1.y);
1390 }
1391
1392 #[test]
1393 fn focus_marker_tags_container() {
1394 use super::*;
1395 use crate::style::{Border, Style};
1396
1397 let commands = vec![
1398 Command::FocusMarker(0),
1399 Command::BeginContainer {
1400 direction: Direction::Column,
1401 gap: 0,
1402 align: Align::Start,
1403 align_self: None,
1404 justify: Justify::Start,
1405 border: Some(Border::Single),
1406 border_sides: BorderSides::all(),
1407 border_style: Style::new(),
1408 bg_color: None,
1409 padding: Padding::default(),
1410 margin: Default::default(),
1411 constraints: Default::default(),
1412 title: None,
1413 grow: 0,
1414 group_name: None,
1415 },
1416 Command::Text {
1417 content: "inside".into(),
1418 style: Style::new(),
1419 grow: 0,
1420 align: Align::Start,
1421 wrap: false,
1422 truncate: false,
1423 margin: Default::default(),
1424 constraints: Default::default(),
1425 },
1426 Command::EndContainer,
1427 ];
1428
1429 let mut tree = build_tree(&commands);
1430 let area = crate::rect::Rect::new(0, 0, 40, 10);
1431 compute(&mut tree, area);
1432
1433 let fd = collect_all(&tree);
1434 assert_eq!(fd.focus_rects.len(), 1);
1435 assert_eq!(fd.focus_rects[0].0, 0);
1436 assert!(fd.focus_rects[0].1.width >= 8);
1437 assert!(fd.focus_rects[0].1.height >= 3);
1438 }
1439}