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