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