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