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) fn collect_raw_draw_rects(node: &LayoutNode) -> Vec<(usize, Rect)> {
1099 let mut rects = Vec::new();
1100 collect_raw_draw_rects_inner(node, &mut rects, 0);
1101 for overlay in &node.overlays {
1102 collect_raw_draw_rects_inner(&overlay.node, &mut rects, 0);
1103 }
1104 rects
1105}
1106
1107fn collect_raw_draw_rects_inner(node: &LayoutNode, rects: &mut Vec<(usize, Rect)>, y_offset: u32) {
1108 if let NodeKind::RawDraw(draw_id) = node.kind {
1109 let adj_y = node.pos.1.saturating_sub(y_offset);
1110 rects.push((
1111 draw_id,
1112 Rect::new(node.pos.0, adj_y, node.size.0, node.size.1),
1113 ));
1114 }
1115 let child_offset = if node.is_scrollable {
1116 y_offset.saturating_add(node.scroll_offset)
1117 } else {
1118 y_offset
1119 };
1120 for child in &node.children {
1121 collect_raw_draw_rects_inner(child, rects, child_offset);
1122 }
1123}
1124
1125#[cfg(test)]
1126#[allow(clippy::print_stderr)]
1127mod tests {
1128 use super::*;
1129
1130 #[test]
1131 fn wrap_empty() {
1132 assert_eq!(wrap_lines("", 10), vec![""]);
1133 }
1134
1135 #[test]
1136 fn wrap_fits() {
1137 assert_eq!(wrap_lines("hello", 10), vec!["hello"]);
1138 }
1139
1140 #[test]
1141 fn wrap_word_boundary() {
1142 assert_eq!(wrap_lines("hello world", 7), vec!["hello", "world"]);
1143 }
1144
1145 #[test]
1146 fn wrap_multiple_words() {
1147 assert_eq!(
1148 wrap_lines("one two three four", 9),
1149 vec!["one two", "three", "four"]
1150 );
1151 }
1152
1153 #[test]
1154 fn wrap_long_word() {
1155 assert_eq!(wrap_lines("abcdefghij", 4), vec!["abcd", "efgh", "ij"]);
1156 }
1157
1158 #[test]
1159 fn wrap_zero_width() {
1160 assert_eq!(wrap_lines("hello", 0), vec!["hello"]);
1161 }
1162
1163 #[test]
1164 fn diagnostic_demo_layout() {
1165 use super::{compute, ContainerConfig, Direction, LayoutNode};
1166 use crate::rect::Rect;
1167 use crate::style::{Align, Border, Constraints, Justify, Margin, Padding, Style};
1168
1169 let mut root = LayoutNode::container(
1182 Direction::Column,
1183 ContainerConfig {
1184 gap: 0,
1185 align: Align::Start,
1186 align_self: None,
1187 justify: Justify::Start,
1188 border: None,
1189 border_sides: BorderSides::all(),
1190 border_style: Style::new(),
1191 bg_color: None,
1192 padding: Padding::default(),
1193 margin: Margin::default(),
1194 constraints: Constraints::default(),
1195 title: None,
1196 grow: 0,
1197 },
1198 );
1199
1200 let mut outer_container = LayoutNode::container(
1202 Direction::Column,
1203 ContainerConfig {
1204 gap: 0,
1205 align: Align::Start,
1206 align_self: None,
1207 justify: Justify::Start,
1208 border: Some(Border::Rounded),
1209 border_sides: BorderSides::all(),
1210 border_style: Style::new(),
1211 bg_color: None,
1212 padding: Padding::all(1),
1213 margin: Margin::default(),
1214 constraints: Constraints::default(),
1215 title: None,
1216 grow: 1,
1217 },
1218 );
1219
1220 outer_container.children.push(LayoutNode::text(
1222 "header".to_string(),
1223 Style::new(),
1224 0,
1225 Align::Start,
1226 (false, false),
1227 Margin::default(),
1228 Constraints::default(),
1229 ));
1230
1231 outer_container.children.push(LayoutNode::text(
1233 "separator".to_string(),
1234 Style::new(),
1235 0,
1236 Align::Start,
1237 (false, false),
1238 Margin::default(),
1239 Constraints::default(),
1240 ));
1241
1242 let mut inner_container = LayoutNode::container(
1244 Direction::Column,
1245 ContainerConfig {
1246 gap: 0,
1247 align: Align::Start,
1248 align_self: None,
1249 justify: Justify::Start,
1250 border: None,
1251 border_sides: BorderSides::all(),
1252 border_style: Style::new(),
1253 bg_color: None,
1254 padding: Padding::default(),
1255 margin: Margin::default(),
1256 constraints: Constraints::default(),
1257 title: None,
1258 grow: 1,
1259 },
1260 );
1261
1262 inner_container.children.push(LayoutNode::text(
1264 "content1".to_string(),
1265 Style::new(),
1266 0,
1267 Align::Start,
1268 (false, false),
1269 Margin::default(),
1270 Constraints::default(),
1271 ));
1272 inner_container.children.push(LayoutNode::text(
1273 "content2".to_string(),
1274 Style::new(),
1275 0,
1276 Align::Start,
1277 (false, false),
1278 Margin::default(),
1279 Constraints::default(),
1280 ));
1281 inner_container.children.push(LayoutNode::text(
1282 "content3".to_string(),
1283 Style::new(),
1284 0,
1285 Align::Start,
1286 (false, false),
1287 Margin::default(),
1288 Constraints::default(),
1289 ));
1290
1291 outer_container.children.push(inner_container);
1292
1293 outer_container.children.push(LayoutNode::text(
1295 "separator2".to_string(),
1296 Style::new(),
1297 0,
1298 Align::Start,
1299 (false, false),
1300 Margin::default(),
1301 Constraints::default(),
1302 ));
1303
1304 outer_container.children.push(LayoutNode::text(
1306 "footer".to_string(),
1307 Style::new(),
1308 0,
1309 Align::Start,
1310 (false, false),
1311 Margin::default(),
1312 Constraints::default(),
1313 ));
1314
1315 root.children.push(outer_container);
1316
1317 compute(&mut root, Rect::new(0, 0, 80, 50));
1319
1320 eprintln!("\n=== DIAGNOSTIC LAYOUT TEST ===");
1322 eprintln!("Root node:");
1323 eprintln!(" pos: {:?}, size: {:?}", root.pos, root.size);
1324
1325 let outer = &root.children[0];
1326 eprintln!("\nOuter bordered container (grow:1):");
1327 eprintln!(" pos: {:?}, size: {:?}", outer.pos, outer.size);
1328
1329 let inner = &outer.children[2];
1330 eprintln!("\nInner container (grow:1, simulates scrollable):");
1331 eprintln!(" pos: {:?}, size: {:?}", inner.pos, inner.size);
1332
1333 eprintln!("\nAll children of outer container:");
1334 for (i, child) in outer.children.iter().enumerate() {
1335 eprintln!(" [{}] pos: {:?}, size: {:?}", i, child.pos, child.size);
1336 }
1337
1338 assert_eq!(
1341 root.size,
1342 (80, 50),
1343 "Root node should fill entire terminal (80x50)"
1344 );
1345
1346 assert_eq!(
1348 outer.size,
1349 (80, 50),
1350 "Outer bordered container should fill entire terminal (80x50)"
1351 );
1352
1353 let expected_inner_height = 50 - 2 - 2 - 4;
1360 assert_eq!(
1361 inner.size.1, expected_inner_height as u32,
1362 "Inner container height should be {} (50 - border(2) - padding(2) - fixed(4))",
1363 expected_inner_height
1364 );
1365
1366 let expected_inner_y = 1 + 1 + 1 + 1;
1368 assert_eq!(
1369 inner.pos.1, expected_inner_y as u32,
1370 "Inner container should start at y={} (border+padding+header+sep)",
1371 expected_inner_y
1372 );
1373
1374 eprintln!("\n✓ All assertions passed!");
1375 eprintln!(" Root size: {:?}", root.size);
1376 eprintln!(" Outer container size: {:?}", outer.size);
1377 eprintln!(" Inner container size: {:?}", inner.size);
1378 eprintln!(" Inner container pos: {:?}", inner.pos);
1379 }
1380
1381 #[test]
1382 fn collect_focus_rects_from_markers() {
1383 use super::*;
1384 use crate::style::Style;
1385
1386 let commands = vec![
1387 Command::FocusMarker(0),
1388 Command::Text {
1389 content: "input1".into(),
1390 style: Style::new(),
1391 grow: 0,
1392 align: Align::Start,
1393 wrap: false,
1394 truncate: false,
1395 margin: Default::default(),
1396 constraints: Default::default(),
1397 },
1398 Command::FocusMarker(1),
1399 Command::Text {
1400 content: "input2".into(),
1401 style: Style::new(),
1402 grow: 0,
1403 align: Align::Start,
1404 wrap: false,
1405 truncate: false,
1406 margin: Default::default(),
1407 constraints: Default::default(),
1408 },
1409 ];
1410
1411 let mut tree = build_tree(&commands);
1412 let area = crate::rect::Rect::new(0, 0, 40, 10);
1413 compute(&mut tree, area);
1414
1415 let fd = collect_all(&tree);
1416 assert_eq!(fd.focus_rects.len(), 2);
1417 assert_eq!(fd.focus_rects[0].0, 0);
1418 assert_eq!(fd.focus_rects[1].0, 1);
1419 assert!(fd.focus_rects[0].1.width > 0);
1420 assert!(fd.focus_rects[1].1.width > 0);
1421 assert_ne!(fd.focus_rects[0].1.y, fd.focus_rects[1].1.y);
1422 }
1423
1424 #[test]
1425 fn focus_marker_tags_container() {
1426 use super::*;
1427 use crate::style::{Border, Style};
1428
1429 let commands = vec![
1430 Command::FocusMarker(0),
1431 Command::BeginContainer {
1432 direction: Direction::Column,
1433 gap: 0,
1434 align: Align::Start,
1435 align_self: None,
1436 justify: Justify::Start,
1437 border: Some(Border::Single),
1438 border_sides: BorderSides::all(),
1439 border_style: Style::new(),
1440 bg_color: None,
1441 padding: Padding::default(),
1442 margin: Default::default(),
1443 constraints: Default::default(),
1444 title: None,
1445 grow: 0,
1446 group_name: None,
1447 },
1448 Command::Text {
1449 content: "inside".into(),
1450 style: Style::new(),
1451 grow: 0,
1452 align: Align::Start,
1453 wrap: false,
1454 truncate: false,
1455 margin: Default::default(),
1456 constraints: Default::default(),
1457 },
1458 Command::EndContainer,
1459 ];
1460
1461 let mut tree = build_tree(&commands);
1462 let area = crate::rect::Rect::new(0, 0, 40, 10);
1463 compute(&mut tree, area);
1464
1465 let fd = collect_all(&tree);
1466 assert_eq!(fd.focus_rects.len(), 1);
1467 assert_eq!(fd.focus_rects[0].0, 0);
1468 assert!(fd.focus_rects[0].1.width >= 8);
1469 assert!(fd.focus_rects[0].1.height >= 3);
1470 }
1471}