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)]
1094mod tests {
1095 use super::*;
1096
1097 #[test]
1098 fn wrap_empty() {
1099 assert_eq!(wrap_lines("", 10), vec![""]);
1100 }
1101
1102 #[test]
1103 fn wrap_fits() {
1104 assert_eq!(wrap_lines("hello", 10), vec!["hello"]);
1105 }
1106
1107 #[test]
1108 fn wrap_word_boundary() {
1109 assert_eq!(wrap_lines("hello world", 7), vec!["hello", "world"]);
1110 }
1111
1112 #[test]
1113 fn wrap_multiple_words() {
1114 assert_eq!(
1115 wrap_lines("one two three four", 9),
1116 vec!["one two", "three", "four"]
1117 );
1118 }
1119
1120 #[test]
1121 fn wrap_long_word() {
1122 assert_eq!(wrap_lines("abcdefghij", 4), vec!["abcd", "efgh", "ij"]);
1123 }
1124
1125 #[test]
1126 fn wrap_zero_width() {
1127 assert_eq!(wrap_lines("hello", 0), vec!["hello"]);
1128 }
1129
1130 #[test]
1131 fn diagnostic_demo_layout() {
1132 use super::{compute, ContainerConfig, Direction, LayoutNode};
1133 use crate::rect::Rect;
1134 use crate::style::{Align, Border, Constraints, Justify, Margin, Padding, Style};
1135
1136 let mut root = LayoutNode::container(
1149 Direction::Column,
1150 ContainerConfig {
1151 gap: 0,
1152 align: Align::Start,
1153 align_self: None,
1154 justify: Justify::Start,
1155 border: None,
1156 border_sides: BorderSides::all(),
1157 border_style: Style::new(),
1158 bg_color: None,
1159 padding: Padding::default(),
1160 margin: Margin::default(),
1161 constraints: Constraints::default(),
1162 title: None,
1163 grow: 0,
1164 },
1165 );
1166
1167 let mut outer_container = LayoutNode::container(
1169 Direction::Column,
1170 ContainerConfig {
1171 gap: 0,
1172 align: Align::Start,
1173 align_self: None,
1174 justify: Justify::Start,
1175 border: Some(Border::Rounded),
1176 border_sides: BorderSides::all(),
1177 border_style: Style::new(),
1178 bg_color: None,
1179 padding: Padding::all(1),
1180 margin: Margin::default(),
1181 constraints: Constraints::default(),
1182 title: None,
1183 grow: 1,
1184 },
1185 );
1186
1187 outer_container.children.push(LayoutNode::text(
1189 "header".to_string(),
1190 Style::new(),
1191 0,
1192 Align::Start,
1193 (false, false),
1194 Margin::default(),
1195 Constraints::default(),
1196 ));
1197
1198 outer_container.children.push(LayoutNode::text(
1200 "separator".to_string(),
1201 Style::new(),
1202 0,
1203 Align::Start,
1204 (false, false),
1205 Margin::default(),
1206 Constraints::default(),
1207 ));
1208
1209 let mut inner_container = LayoutNode::container(
1211 Direction::Column,
1212 ContainerConfig {
1213 gap: 0,
1214 align: Align::Start,
1215 align_self: None,
1216 justify: Justify::Start,
1217 border: None,
1218 border_sides: BorderSides::all(),
1219 border_style: Style::new(),
1220 bg_color: None,
1221 padding: Padding::default(),
1222 margin: Margin::default(),
1223 constraints: Constraints::default(),
1224 title: None,
1225 grow: 1,
1226 },
1227 );
1228
1229 inner_container.children.push(LayoutNode::text(
1231 "content1".to_string(),
1232 Style::new(),
1233 0,
1234 Align::Start,
1235 (false, false),
1236 Margin::default(),
1237 Constraints::default(),
1238 ));
1239 inner_container.children.push(LayoutNode::text(
1240 "content2".to_string(),
1241 Style::new(),
1242 0,
1243 Align::Start,
1244 (false, false),
1245 Margin::default(),
1246 Constraints::default(),
1247 ));
1248 inner_container.children.push(LayoutNode::text(
1249 "content3".to_string(),
1250 Style::new(),
1251 0,
1252 Align::Start,
1253 (false, false),
1254 Margin::default(),
1255 Constraints::default(),
1256 ));
1257
1258 outer_container.children.push(inner_container);
1259
1260 outer_container.children.push(LayoutNode::text(
1262 "separator2".to_string(),
1263 Style::new(),
1264 0,
1265 Align::Start,
1266 (false, false),
1267 Margin::default(),
1268 Constraints::default(),
1269 ));
1270
1271 outer_container.children.push(LayoutNode::text(
1273 "footer".to_string(),
1274 Style::new(),
1275 0,
1276 Align::Start,
1277 (false, false),
1278 Margin::default(),
1279 Constraints::default(),
1280 ));
1281
1282 root.children.push(outer_container);
1283
1284 compute(&mut root, Rect::new(0, 0, 80, 50));
1286
1287 eprintln!("\n=== DIAGNOSTIC LAYOUT TEST ===");
1289 eprintln!("Root node:");
1290 eprintln!(" pos: {:?}, size: {:?}", root.pos, root.size);
1291
1292 let outer = &root.children[0];
1293 eprintln!("\nOuter bordered container (grow:1):");
1294 eprintln!(" pos: {:?}, size: {:?}", outer.pos, outer.size);
1295
1296 let inner = &outer.children[2];
1297 eprintln!("\nInner container (grow:1, simulates scrollable):");
1298 eprintln!(" pos: {:?}, size: {:?}", inner.pos, inner.size);
1299
1300 eprintln!("\nAll children of outer container:");
1301 for (i, child) in outer.children.iter().enumerate() {
1302 eprintln!(" [{}] pos: {:?}, size: {:?}", i, child.pos, child.size);
1303 }
1304
1305 assert_eq!(
1308 root.size,
1309 (80, 50),
1310 "Root node should fill entire terminal (80x50)"
1311 );
1312
1313 assert_eq!(
1315 outer.size,
1316 (80, 50),
1317 "Outer bordered container should fill entire terminal (80x50)"
1318 );
1319
1320 let expected_inner_height = 50 - 2 - 2 - 4;
1327 assert_eq!(
1328 inner.size.1, expected_inner_height as u32,
1329 "Inner container height should be {} (50 - border(2) - padding(2) - fixed(4))",
1330 expected_inner_height
1331 );
1332
1333 let expected_inner_y = 1 + 1 + 1 + 1;
1335 assert_eq!(
1336 inner.pos.1, expected_inner_y as u32,
1337 "Inner container should start at y={} (border+padding+header+sep)",
1338 expected_inner_y
1339 );
1340
1341 eprintln!("\n✓ All assertions passed!");
1342 eprintln!(" Root size: {:?}", root.size);
1343 eprintln!(" Outer container size: {:?}", outer.size);
1344 eprintln!(" Inner container size: {:?}", inner.size);
1345 eprintln!(" Inner container pos: {:?}", inner.pos);
1346 }
1347
1348 #[test]
1349 fn collect_focus_rects_from_markers() {
1350 use super::*;
1351 use crate::style::Style;
1352
1353 let commands = vec![
1354 Command::FocusMarker(0),
1355 Command::Text {
1356 content: "input1".into(),
1357 style: Style::new(),
1358 grow: 0,
1359 align: Align::Start,
1360 wrap: false,
1361 truncate: false,
1362 margin: Default::default(),
1363 constraints: Default::default(),
1364 },
1365 Command::FocusMarker(1),
1366 Command::Text {
1367 content: "input2".into(),
1368 style: Style::new(),
1369 grow: 0,
1370 align: Align::Start,
1371 wrap: false,
1372 truncate: false,
1373 margin: Default::default(),
1374 constraints: Default::default(),
1375 },
1376 ];
1377
1378 let mut tree = build_tree(&commands);
1379 let area = crate::rect::Rect::new(0, 0, 40, 10);
1380 compute(&mut tree, area);
1381
1382 let fd = collect_all(&tree);
1383 assert_eq!(fd.focus_rects.len(), 2);
1384 assert_eq!(fd.focus_rects[0].0, 0);
1385 assert_eq!(fd.focus_rects[1].0, 1);
1386 assert!(fd.focus_rects[0].1.width > 0);
1387 assert!(fd.focus_rects[1].1.width > 0);
1388 assert_ne!(fd.focus_rects[0].1.y, fd.focus_rects[1].1.y);
1389 }
1390
1391 #[test]
1392 fn focus_marker_tags_container() {
1393 use super::*;
1394 use crate::style::{Border, Style};
1395
1396 let commands = vec![
1397 Command::FocusMarker(0),
1398 Command::BeginContainer {
1399 direction: Direction::Column,
1400 gap: 0,
1401 align: Align::Start,
1402 align_self: None,
1403 justify: Justify::Start,
1404 border: Some(Border::Single),
1405 border_sides: BorderSides::all(),
1406 border_style: Style::new(),
1407 bg_color: None,
1408 padding: Padding::default(),
1409 margin: Default::default(),
1410 constraints: Default::default(),
1411 title: None,
1412 grow: 0,
1413 group_name: None,
1414 },
1415 Command::Text {
1416 content: "inside".into(),
1417 style: Style::new(),
1418 grow: 0,
1419 align: Align::Start,
1420 wrap: false,
1421 truncate: false,
1422 margin: Default::default(),
1423 constraints: Default::default(),
1424 },
1425 Command::EndContainer,
1426 ];
1427
1428 let mut tree = build_tree(&commands);
1429 let area = crate::rect::Rect::new(0, 0, 40, 10);
1430 compute(&mut tree, area);
1431
1432 let fd = collect_all(&tree);
1433 assert_eq!(fd.focus_rects.len(), 1);
1434 assert_eq!(fd.focus_rects[0].0, 0);
1435 assert!(fd.focus_rects[0].1.width >= 8);
1436 assert!(fd.focus_rects[0].1.height >= 3);
1437 }
1438}