1use crate::buffer::Buffer;
5use crate::rect::Rect;
6use crate::style::{
7 Align, Border, BorderSides, Color, Constraints, Justify, Margin, Padding, Style,
8};
9use unicode_width::UnicodeWidthChar;
10use unicode_width::UnicodeWidthStr;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Direction {
15 Row,
17 Column,
19}
20
21#[derive(Debug, Clone)]
22pub(crate) enum Command {
23 Text {
24 content: String,
25 style: Style,
26 grow: u16,
27 align: Align,
28 wrap: bool,
29 margin: Margin,
30 constraints: Constraints,
31 },
32 BeginContainer {
33 direction: Direction,
34 gap: u32,
35 align: Align,
36 justify: Justify,
37 border: Option<Border>,
38 border_sides: BorderSides,
39 border_style: Style,
40 bg_color: Option<Color>,
41 padding: Padding,
42 margin: Margin,
43 constraints: Constraints,
44 title: Option<(String, Style)>,
45 grow: u16,
46 },
47 BeginScrollable {
48 grow: u16,
49 border: Option<Border>,
50 border_sides: BorderSides,
51 border_style: Style,
52 padding: Padding,
53 margin: Margin,
54 constraints: Constraints,
55 title: Option<(String, Style)>,
56 scroll_offset: u32,
57 },
58 Link {
59 text: String,
60 url: String,
61 style: Style,
62 margin: Margin,
63 constraints: Constraints,
64 },
65 EndContainer,
66 BeginOverlay {
67 modal: bool,
68 },
69 EndOverlay,
70 Spacer {
71 grow: u16,
72 },
73 FocusMarker(usize),
74}
75
76#[derive(Debug, Clone)]
77struct OverlayLayer {
78 node: LayoutNode,
79 modal: bool,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83enum NodeKind {
84 Text,
85 Container(Direction),
86 Spacer,
87}
88
89#[derive(Debug, Clone)]
90pub(crate) struct LayoutNode {
91 kind: NodeKind,
92 content: Option<String>,
93 style: Style,
94 pub grow: u16,
95 align: Align,
96 justify: Justify,
97 wrap: bool,
98 gap: u32,
99 border: Option<Border>,
100 border_sides: BorderSides,
101 border_style: Style,
102 bg_color: Option<Color>,
103 padding: Padding,
104 margin: Margin,
105 constraints: Constraints,
106 title: Option<(String, Style)>,
107 children: Vec<LayoutNode>,
108 pos: (u32, u32),
109 size: (u32, u32),
110 is_scrollable: bool,
111 scroll_offset: u32,
112 content_height: u32,
113 cached_wrapped: Option<Vec<String>>,
114 pub(crate) focus_id: Option<usize>,
115 link_url: Option<String>,
116 overlays: Vec<OverlayLayer>,
117}
118
119#[derive(Debug, Clone)]
120struct ContainerConfig {
121 gap: u32,
122 align: Align,
123 justify: Justify,
124 border: Option<Border>,
125 border_sides: BorderSides,
126 border_style: Style,
127 bg_color: Option<Color>,
128 padding: Padding,
129 margin: Margin,
130 constraints: Constraints,
131 title: Option<(String, Style)>,
132 grow: u16,
133}
134
135impl LayoutNode {
136 fn text(
137 content: String,
138 style: Style,
139 grow: u16,
140 align: Align,
141 wrap: bool,
142 margin: Margin,
143 constraints: Constraints,
144 ) -> Self {
145 let width = UnicodeWidthStr::width(content.as_str()) as u32;
146 Self {
147 kind: NodeKind::Text,
148 content: Some(content),
149 style,
150 grow,
151 align,
152 justify: Justify::Start,
153 wrap,
154 gap: 0,
155 border: None,
156 border_sides: BorderSides::all(),
157 border_style: Style::new(),
158 bg_color: None,
159 padding: Padding::default(),
160 margin,
161 constraints,
162 title: None,
163 children: Vec::new(),
164 pos: (0, 0),
165 size: (width, 1),
166 is_scrollable: false,
167 scroll_offset: 0,
168 content_height: 0,
169 cached_wrapped: None,
170 focus_id: None,
171 link_url: None,
172 overlays: Vec::new(),
173 }
174 }
175
176 fn container(direction: Direction, config: ContainerConfig) -> Self {
177 Self {
178 kind: NodeKind::Container(direction),
179 content: None,
180 style: Style::new(),
181 grow: config.grow,
182 align: config.align,
183 justify: config.justify,
184 wrap: false,
185 gap: config.gap,
186 border: config.border,
187 border_sides: config.border_sides,
188 border_style: config.border_style,
189 bg_color: config.bg_color,
190 padding: config.padding,
191 margin: config.margin,
192 constraints: config.constraints,
193 title: config.title,
194 children: Vec::new(),
195 pos: (0, 0),
196 size: (0, 0),
197 is_scrollable: false,
198 scroll_offset: 0,
199 content_height: 0,
200 cached_wrapped: None,
201 focus_id: None,
202 link_url: None,
203 overlays: Vec::new(),
204 }
205 }
206
207 fn spacer(grow: u16) -> Self {
208 Self {
209 kind: NodeKind::Spacer,
210 content: None,
211 style: Style::new(),
212 grow,
213 align: Align::Start,
214 justify: Justify::Start,
215 wrap: false,
216 gap: 0,
217 border: None,
218 border_sides: BorderSides::all(),
219 border_style: Style::new(),
220 bg_color: None,
221 padding: Padding::default(),
222 margin: Margin::default(),
223 constraints: Constraints::default(),
224 title: None,
225 children: Vec::new(),
226 pos: (0, 0),
227 size: (0, 0),
228 is_scrollable: false,
229 scroll_offset: 0,
230 content_height: 0,
231 cached_wrapped: None,
232 focus_id: None,
233 link_url: None,
234 overlays: Vec::new(),
235 }
236 }
237
238 fn border_inset(&self) -> u32 {
239 if self.border.is_some() {
240 1
241 } else {
242 0
243 }
244 }
245
246 fn border_left_inset(&self) -> u32 {
247 if self.border.is_some() && self.border_sides.left {
248 1
249 } else {
250 0
251 }
252 }
253
254 fn border_right_inset(&self) -> u32 {
255 if self.border.is_some() && self.border_sides.right {
256 1
257 } else {
258 0
259 }
260 }
261
262 fn border_top_inset(&self) -> u32 {
263 if self.border.is_some() && self.border_sides.top {
264 1
265 } else {
266 0
267 }
268 }
269
270 fn border_bottom_inset(&self) -> u32 {
271 if self.border.is_some() && self.border_sides.bottom {
272 1
273 } else {
274 0
275 }
276 }
277
278 fn frame_horizontal(&self) -> u32 {
279 self.padding.horizontal() + self.border_left_inset() + self.border_right_inset()
280 }
281
282 fn frame_vertical(&self) -> u32 {
283 self.padding.vertical() + self.border_top_inset() + self.border_bottom_inset()
284 }
285
286 fn min_width(&self) -> u32 {
287 let width = match self.kind {
288 NodeKind::Text => self.size.0,
289 NodeKind::Spacer => 0,
290 NodeKind::Container(Direction::Row) => {
291 let gaps = if self.children.is_empty() {
292 0
293 } else {
294 (self.children.len() as u32 - 1) * self.gap
295 };
296 let children_width: u32 = self.children.iter().map(|c| c.min_width()).sum();
297 children_width + gaps + self.frame_horizontal()
298 }
299 NodeKind::Container(Direction::Column) => {
300 self.children
301 .iter()
302 .map(|c| c.min_width())
303 .max()
304 .unwrap_or(0)
305 + self.frame_horizontal()
306 }
307 };
308
309 let width = width.max(self.constraints.min_width.unwrap_or(0));
310 let width = match self.constraints.max_width {
311 Some(max_w) => width.min(max_w),
312 None => width,
313 };
314 width.saturating_add(self.margin.horizontal())
315 }
316
317 fn min_height(&self) -> u32 {
318 let height = match self.kind {
319 NodeKind::Text => 1,
320 NodeKind::Spacer => 0,
321 NodeKind::Container(Direction::Row) => {
322 self.children
323 .iter()
324 .map(|c| c.min_height())
325 .max()
326 .unwrap_or(0)
327 + self.frame_vertical()
328 }
329 NodeKind::Container(Direction::Column) => {
330 let gaps = if self.children.is_empty() {
331 0
332 } else {
333 (self.children.len() as u32 - 1) * self.gap
334 };
335 let children_height: u32 = self.children.iter().map(|c| c.min_height()).sum();
336 children_height + gaps + self.frame_vertical()
337 }
338 };
339
340 let height = height.max(self.constraints.min_height.unwrap_or(0));
341 height.saturating_add(self.margin.vertical())
342 }
343
344 fn min_height_for_width(&self, available_width: u32) -> u32 {
345 match self.kind {
346 NodeKind::Text if self.wrap => {
347 let text = self.content.as_deref().unwrap_or("");
348 let inner_width = available_width.saturating_sub(self.margin.horizontal());
349 let lines = wrap_lines(text, inner_width).len().max(1) as u32;
350 lines.saturating_add(self.margin.vertical())
351 }
352 _ => self.min_height(),
353 }
354 }
355}
356
357fn wrap_lines(text: &str, max_width: u32) -> Vec<String> {
358 if text.is_empty() {
359 return vec![String::new()];
360 }
361 if max_width == 0 {
362 return vec![text.to_string()];
363 }
364
365 fn split_long_word(word: &str, max_width: u32) -> Vec<(String, u32)> {
366 let mut chunks: Vec<(String, u32)> = Vec::new();
367 let mut chunk = String::new();
368 let mut chunk_width = 0_u32;
369
370 for ch in word.chars() {
371 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
372 if chunk.is_empty() {
373 if ch_width > max_width {
374 chunks.push((ch.to_string(), ch_width));
375 } else {
376 chunk.push(ch);
377 chunk_width = ch_width;
378 }
379 continue;
380 }
381
382 if chunk_width + ch_width > max_width {
383 chunks.push((std::mem::take(&mut chunk), chunk_width));
384 if ch_width > max_width {
385 chunks.push((ch.to_string(), ch_width));
386 chunk_width = 0;
387 } else {
388 chunk.push(ch);
389 chunk_width = ch_width;
390 }
391 } else {
392 chunk.push(ch);
393 chunk_width += ch_width;
394 }
395 }
396
397 if !chunk.is_empty() {
398 chunks.push((chunk, chunk_width));
399 }
400
401 chunks
402 }
403
404 fn push_word_into_line(
405 lines: &mut Vec<String>,
406 current_line: &mut String,
407 current_width: &mut u32,
408 word: &str,
409 word_width: u32,
410 max_width: u32,
411 ) {
412 if word.is_empty() {
413 return;
414 }
415
416 if word_width > max_width {
417 let chunks = split_long_word(word, max_width);
418 for (chunk, chunk_width) in chunks {
419 if current_line.is_empty() {
420 *current_line = chunk;
421 *current_width = chunk_width;
422 } else if *current_width + 1 + chunk_width <= max_width {
423 current_line.push(' ');
424 current_line.push_str(&chunk);
425 *current_width += 1 + chunk_width;
426 } else {
427 lines.push(std::mem::take(current_line));
428 *current_line = chunk;
429 *current_width = chunk_width;
430 }
431 }
432 return;
433 }
434
435 if current_line.is_empty() {
436 *current_line = word.to_string();
437 *current_width = word_width;
438 } else if *current_width + 1 + word_width <= max_width {
439 current_line.push(' ');
440 current_line.push_str(word);
441 *current_width += 1 + word_width;
442 } else {
443 lines.push(std::mem::take(current_line));
444 *current_line = word.to_string();
445 *current_width = word_width;
446 }
447 }
448
449 let mut lines: Vec<String> = Vec::new();
450 let mut current_line = String::new();
451 let mut current_width: u32 = 0;
452 let mut current_word = String::new();
453 let mut word_width: u32 = 0;
454
455 for ch in text.chars() {
456 if ch == ' ' {
457 push_word_into_line(
458 &mut lines,
459 &mut current_line,
460 &mut current_width,
461 ¤t_word,
462 word_width,
463 max_width,
464 );
465 current_word.clear();
466 word_width = 0;
467 continue;
468 }
469
470 current_word.push(ch);
471 word_width += UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
472 }
473
474 push_word_into_line(
475 &mut lines,
476 &mut current_line,
477 &mut current_width,
478 ¤t_word,
479 word_width,
480 max_width,
481 );
482
483 if !current_line.is_empty() {
484 lines.push(current_line);
485 }
486
487 if lines.is_empty() {
488 vec![String::new()]
489 } else {
490 lines
491 }
492}
493
494pub(crate) fn build_tree(commands: &[Command]) -> LayoutNode {
495 let mut root = LayoutNode::container(Direction::Column, default_container_config());
496 let mut overlays: Vec<OverlayLayer> = Vec::new();
497 build_children(&mut root, commands, &mut 0, &mut overlays, false);
498 root.overlays = overlays;
499 root
500}
501
502fn default_container_config() -> ContainerConfig {
503 ContainerConfig {
504 gap: 0,
505 align: Align::Start,
506 justify: Justify::Start,
507 border: None,
508 border_sides: BorderSides::all(),
509 border_style: Style::new(),
510 bg_color: None,
511 padding: Padding::default(),
512 margin: Margin::default(),
513 constraints: Constraints::default(),
514 title: None,
515 grow: 0,
516 }
517}
518
519fn build_children(
520 parent: &mut LayoutNode,
521 commands: &[Command],
522 pos: &mut usize,
523 overlays: &mut Vec<OverlayLayer>,
524 stop_on_end_overlay: bool,
525) {
526 let mut pending_focus_id: Option<usize> = None;
527 while *pos < commands.len() {
528 match &commands[*pos] {
529 Command::FocusMarker(id) => {
530 pending_focus_id = Some(*id);
531 *pos += 1;
532 }
533 Command::Text {
534 content,
535 style,
536 grow,
537 align,
538 wrap,
539 margin,
540 constraints,
541 } => {
542 let mut node = LayoutNode::text(
543 content.clone(),
544 *style,
545 *grow,
546 *align,
547 *wrap,
548 *margin,
549 *constraints,
550 );
551 node.focus_id = pending_focus_id.take();
552 parent.children.push(node);
553 *pos += 1;
554 }
555 Command::Link {
556 text,
557 url,
558 style,
559 margin,
560 constraints,
561 } => {
562 let mut node = LayoutNode::text(
563 text.clone(),
564 *style,
565 0,
566 Align::Start,
567 false,
568 *margin,
569 *constraints,
570 );
571 node.link_url = Some(url.clone());
572 node.focus_id = pending_focus_id.take();
573 parent.children.push(node);
574 *pos += 1;
575 }
576 Command::BeginContainer {
577 direction,
578 gap,
579 align,
580 justify,
581 border,
582 border_sides,
583 border_style,
584 bg_color,
585 padding,
586 margin,
587 constraints,
588 title,
589 grow,
590 } => {
591 let mut node = LayoutNode::container(
592 *direction,
593 ContainerConfig {
594 gap: *gap,
595 align: *align,
596 justify: *justify,
597 border: *border,
598 border_sides: *border_sides,
599 border_style: *border_style,
600 bg_color: *bg_color,
601 padding: *padding,
602 margin: *margin,
603 constraints: *constraints,
604 title: title.clone(),
605 grow: *grow,
606 },
607 );
608 node.focus_id = pending_focus_id.take();
609 *pos += 1;
610 build_children(&mut node, commands, pos, overlays, false);
611 parent.children.push(node);
612 }
613 Command::BeginScrollable {
614 grow,
615 border,
616 border_sides,
617 border_style,
618 padding,
619 margin,
620 constraints,
621 title,
622 scroll_offset,
623 } => {
624 let mut node = LayoutNode::container(
625 Direction::Column,
626 ContainerConfig {
627 gap: 0,
628 align: Align::Start,
629 justify: Justify::Start,
630 border: *border,
631 border_sides: *border_sides,
632 border_style: *border_style,
633 bg_color: None,
634 padding: *padding,
635 margin: *margin,
636 constraints: *constraints,
637 title: title.clone(),
638 grow: *grow,
639 },
640 );
641 node.is_scrollable = true;
642 node.scroll_offset = *scroll_offset;
643 node.focus_id = pending_focus_id.take();
644 *pos += 1;
645 build_children(&mut node, commands, pos, overlays, false);
646 parent.children.push(node);
647 }
648 Command::BeginOverlay { modal } => {
649 *pos += 1;
650 let mut overlay_node =
651 LayoutNode::container(Direction::Column, default_container_config());
652 build_children(&mut overlay_node, commands, pos, overlays, true);
653 overlays.push(OverlayLayer {
654 node: overlay_node,
655 modal: *modal,
656 });
657 }
658 Command::Spacer { grow } => {
659 parent.children.push(LayoutNode::spacer(*grow));
660 *pos += 1;
661 }
662 Command::EndContainer => {
663 *pos += 1;
664 return;
665 }
666 Command::EndOverlay => {
667 *pos += 1;
668 if stop_on_end_overlay {
669 return;
670 }
671 }
672 }
673 }
674}
675
676pub(crate) fn compute(node: &mut LayoutNode, area: Rect) {
677 if let Some(pct) = node.constraints.width_pct {
678 let resolved = (area.width as u64 * pct.min(100) as u64 / 100) as u32;
679 node.constraints.min_width = Some(resolved);
680 node.constraints.max_width = Some(resolved);
681 node.constraints.width_pct = None;
682 }
683 if let Some(pct) = node.constraints.height_pct {
684 let resolved = (area.height as u64 * pct.min(100) as u64 / 100) as u32;
685 node.constraints.min_height = Some(resolved);
686 node.constraints.max_height = Some(resolved);
687 node.constraints.height_pct = None;
688 }
689
690 node.pos = (area.x, area.y);
691 node.size = (
692 area.width.clamp(
693 node.constraints.min_width.unwrap_or(0),
694 node.constraints.max_width.unwrap_or(u32::MAX),
695 ),
696 area.height.clamp(
697 node.constraints.min_height.unwrap_or(0),
698 node.constraints.max_height.unwrap_or(u32::MAX),
699 ),
700 );
701
702 if matches!(node.kind, NodeKind::Text) && node.wrap {
703 let lines = wrap_lines(node.content.as_deref().unwrap_or(""), area.width);
704 node.size = (area.width, lines.len().max(1) as u32);
705 node.cached_wrapped = Some(lines);
706 } else {
707 node.cached_wrapped = None;
708 }
709
710 match node.kind {
711 NodeKind::Text | NodeKind::Spacer => {}
712 NodeKind::Container(Direction::Row) => {
713 layout_row(
714 node,
715 inner_area(
716 node,
717 Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1),
718 ),
719 );
720 node.content_height = 0;
721 }
722 NodeKind::Container(Direction::Column) => {
723 let viewport_area = inner_area(
724 node,
725 Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1),
726 );
727 if node.is_scrollable {
728 let saved_grows: Vec<u16> = node.children.iter().map(|c| c.grow).collect();
729 for child in &mut node.children {
730 child.grow = 0;
731 }
732 let total_gaps = if node.children.is_empty() {
733 0
734 } else {
735 (node.children.len() as u32 - 1) * node.gap
736 };
737 let natural_height: u32 = node
738 .children
739 .iter()
740 .map(|c| c.min_height_for_width(viewport_area.width))
741 .sum::<u32>()
742 + total_gaps;
743
744 if natural_height > viewport_area.height {
745 let virtual_area = Rect::new(
746 viewport_area.x,
747 viewport_area.y,
748 viewport_area.width,
749 natural_height,
750 );
751 layout_column(node, virtual_area);
752 } else {
753 for (child, &grow) in node.children.iter_mut().zip(saved_grows.iter()) {
754 child.grow = grow;
755 }
756 layout_column(node, viewport_area);
757 }
758 node.content_height = scroll_content_height(node, viewport_area.y);
759 } else {
760 layout_column(node, viewport_area);
761 node.content_height = 0;
762 }
763 }
764 }
765
766 for overlay in &mut node.overlays {
767 let width = overlay.node.min_width().min(area.width);
768 let height = overlay.node.min_height_for_width(width).min(area.height);
769 let x = area.x.saturating_add(area.width.saturating_sub(width) / 2);
770 let y = area
771 .y
772 .saturating_add(area.height.saturating_sub(height) / 2);
773 compute(&mut overlay.node, Rect::new(x, y, width, height));
774 }
775}
776
777fn scroll_content_height(node: &LayoutNode, inner_y: u32) -> u32 {
778 let Some(max_bottom) = node
779 .children
780 .iter()
781 .map(|child| {
782 child
783 .pos
784 .1
785 .saturating_add(child.size.1)
786 .saturating_add(child.margin.bottom)
787 })
788 .max()
789 else {
790 return 0;
791 };
792
793 max_bottom.saturating_sub(inner_y)
794}
795
796fn justify_offsets(justify: Justify, remaining: u32, n: u32, gap: u32) -> (u32, u32) {
797 if n <= 1 {
798 let start = match justify {
799 Justify::Center => remaining / 2,
800 Justify::End => remaining,
801 _ => 0,
802 };
803 return (start, gap);
804 }
805
806 match justify {
807 Justify::Start => (0, gap),
808 Justify::Center => (remaining.saturating_sub((n - 1) * gap) / 2, gap),
809 Justify::End => (remaining.saturating_sub((n - 1) * gap), gap),
810 Justify::SpaceBetween => (0, remaining / (n - 1)),
811 Justify::SpaceAround => {
812 let slot = remaining / n;
813 (slot / 2, slot)
814 }
815 Justify::SpaceEvenly => {
816 let slot = remaining / (n + 1);
817 (slot, slot)
818 }
819 }
820}
821
822fn inner_area(node: &LayoutNode, area: Rect) -> Rect {
823 let x = area.x + node.border_left_inset() + node.padding.left;
824 let y = area.y + node.border_top_inset() + node.padding.top;
825 let width = area
826 .width
827 .saturating_sub(node.border_left_inset() + node.border_right_inset())
828 .saturating_sub(node.padding.horizontal());
829 let height = area
830 .height
831 .saturating_sub(node.border_top_inset() + node.border_bottom_inset())
832 .saturating_sub(node.padding.vertical());
833
834 Rect::new(x, y, width, height)
835}
836
837fn layout_row(node: &mut LayoutNode, area: Rect) {
838 if node.children.is_empty() {
839 return;
840 }
841
842 for child in &mut node.children {
843 if let Some(pct) = child.constraints.width_pct {
844 let resolved = (area.width as u64 * pct.min(100) as u64 / 100) as u32;
845 child.constraints.min_width = Some(resolved);
846 child.constraints.max_width = Some(resolved);
847 child.constraints.width_pct = None;
848 }
849 if let Some(pct) = child.constraints.height_pct {
850 let resolved = (area.height as u64 * pct.min(100) as u64 / 100) as u32;
851 child.constraints.min_height = Some(resolved);
852 child.constraints.max_height = Some(resolved);
853 child.constraints.height_pct = None;
854 }
855 }
856
857 let n = node.children.len() as u32;
858 let total_gaps = (n - 1) * node.gap;
859 let available = area.width.saturating_sub(total_gaps);
860 let min_widths: Vec<u32> = node
861 .children
862 .iter()
863 .map(|child| child.min_width())
864 .collect();
865
866 let mut total_grow: u32 = 0;
867 let mut fixed_width: u32 = 0;
868 for (child, &min_width) in node.children.iter().zip(min_widths.iter()) {
869 if child.grow > 0 {
870 total_grow += child.grow as u32;
871 } else {
872 fixed_width += min_width;
873 }
874 }
875
876 let mut flex_space = available.saturating_sub(fixed_width);
877 let mut remaining_grow = total_grow;
878
879 let mut child_widths: Vec<u32> = Vec::with_capacity(node.children.len());
880 for (i, child) in node.children.iter().enumerate() {
881 let w = if child.grow > 0 && total_grow > 0 {
882 let share = if remaining_grow == 0 {
883 0
884 } else {
885 flex_space * child.grow as u32 / remaining_grow
886 };
887 flex_space = flex_space.saturating_sub(share);
888 remaining_grow = remaining_grow.saturating_sub(child.grow as u32);
889 share
890 } else {
891 min_widths[i].min(available)
892 };
893 child_widths.push(w);
894 }
895
896 let total_children_width: u32 = child_widths.iter().sum();
897 let remaining = area.width.saturating_sub(total_children_width);
898 let (start_offset, inter_gap) = justify_offsets(node.justify, remaining, n, node.gap);
899
900 let mut x = area.x + start_offset;
901 for (i, child) in node.children.iter_mut().enumerate() {
902 let w = child_widths[i];
903 let child_outer_h = match node.align {
904 Align::Start => area.height,
905 _ => child.min_height_for_width(w).min(area.height),
906 };
907 let child_x = x.saturating_add(child.margin.left);
908 let child_y = area.y.saturating_add(child.margin.top);
909 let child_w = w.saturating_sub(child.margin.horizontal());
910 let child_h = child_outer_h.saturating_sub(child.margin.vertical());
911 compute(child, Rect::new(child_x, child_y, child_w, child_h));
912 let child_total_h = child.size.1.saturating_add(child.margin.vertical());
913 let y_offset = match node.align {
914 Align::Start => 0,
915 Align::Center => area.height.saturating_sub(child_total_h) / 2,
916 Align::End => area.height.saturating_sub(child_total_h),
917 };
918 child.pos.1 = child.pos.1.saturating_add(y_offset);
919 x += w + inter_gap;
920 }
921}
922
923fn layout_column(node: &mut LayoutNode, area: Rect) {
924 if node.children.is_empty() {
925 return;
926 }
927
928 for child in &mut node.children {
929 if let Some(pct) = child.constraints.width_pct {
930 let resolved = (area.width as u64 * pct.min(100) as u64 / 100) as u32;
931 child.constraints.min_width = Some(resolved);
932 child.constraints.max_width = Some(resolved);
933 child.constraints.width_pct = None;
934 }
935 if let Some(pct) = child.constraints.height_pct {
936 let resolved = (area.height as u64 * pct.min(100) as u64 / 100) as u32;
937 child.constraints.min_height = Some(resolved);
938 child.constraints.max_height = Some(resolved);
939 child.constraints.height_pct = None;
940 }
941 }
942
943 let n = node.children.len() as u32;
944 let total_gaps = (n - 1) * node.gap;
945 let available = area.height.saturating_sub(total_gaps);
946 let min_heights: Vec<u32> = node
947 .children
948 .iter()
949 .map(|child| child.min_height_for_width(area.width))
950 .collect();
951
952 let mut total_grow: u32 = 0;
953 let mut fixed_height: u32 = 0;
954 for (child, &min_height) in node.children.iter().zip(min_heights.iter()) {
955 if child.grow > 0 {
956 total_grow += child.grow as u32;
957 } else {
958 fixed_height += min_height;
959 }
960 }
961
962 let mut flex_space = available.saturating_sub(fixed_height);
963 let mut remaining_grow = total_grow;
964
965 let mut child_heights: Vec<u32> = Vec::with_capacity(node.children.len());
966 for (i, child) in node.children.iter().enumerate() {
967 let h = if child.grow > 0 && total_grow > 0 {
968 let share = if remaining_grow == 0 {
969 0
970 } else {
971 flex_space * child.grow as u32 / remaining_grow
972 };
973 flex_space = flex_space.saturating_sub(share);
974 remaining_grow = remaining_grow.saturating_sub(child.grow as u32);
975 share
976 } else {
977 min_heights[i].min(available)
978 };
979 child_heights.push(h);
980 }
981
982 let total_children_height: u32 = child_heights.iter().sum();
983 let remaining = area.height.saturating_sub(total_children_height);
984 let (start_offset, inter_gap) = justify_offsets(node.justify, remaining, n, node.gap);
985
986 let mut y = area.y + start_offset;
987 for (i, child) in node.children.iter_mut().enumerate() {
988 let h = child_heights[i];
989 let child_outer_w = match node.align {
990 Align::Start => area.width,
991 _ => child.min_width().min(area.width),
992 };
993 let child_x = area.x.saturating_add(child.margin.left);
994 let child_y = y.saturating_add(child.margin.top);
995 let child_w = child_outer_w.saturating_sub(child.margin.horizontal());
996 let child_h = h.saturating_sub(child.margin.vertical());
997 compute(child, Rect::new(child_x, child_y, child_w, child_h));
998 let child_total_w = child.size.0.saturating_add(child.margin.horizontal());
999 let x_offset = match node.align {
1000 Align::Start => 0,
1001 Align::Center => area.width.saturating_sub(child_total_w) / 2,
1002 Align::End => area.width.saturating_sub(child_total_w),
1003 };
1004 child.pos.0 = child.pos.0.saturating_add(x_offset);
1005 y += h + inter_gap;
1006 }
1007}
1008
1009pub(crate) fn render(node: &LayoutNode, buf: &mut Buffer) {
1010 render_inner(node, buf, 0, None);
1011 buf.clip_stack.clear();
1012 for overlay in &node.overlays {
1013 if overlay.modal {
1014 dim_entire_buffer(buf);
1015 }
1016 render_inner(&overlay.node, buf, 0, None);
1017 }
1018}
1019
1020fn dim_entire_buffer(buf: &mut Buffer) {
1021 for y in buf.area.y..buf.area.bottom() {
1022 for x in buf.area.x..buf.area.right() {
1023 let cell = buf.get_mut(x, y);
1024 cell.style.modifiers |= crate::style::Modifiers::DIM;
1025 }
1026 }
1027}
1028
1029pub(crate) fn render_debug_overlay(node: &LayoutNode, buf: &mut Buffer) {
1030 for child in &node.children {
1031 render_debug_overlay_inner(child, buf, 0, 0);
1032 }
1033}
1034
1035fn render_debug_overlay_inner(node: &LayoutNode, buf: &mut Buffer, depth: u32, y_offset: u32) {
1036 let child_offset = if node.is_scrollable {
1037 y_offset.saturating_add(node.scroll_offset)
1038 } else {
1039 y_offset
1040 };
1041
1042 if let NodeKind::Container(_) = node.kind {
1043 let sy = screen_y(node.pos.1, y_offset);
1044 if sy + node.size.1 as i64 > 0 {
1045 let color = debug_color_for_depth(depth);
1046 let style = Style::new().fg(color);
1047 let clamped_y = sy.max(0) as u32;
1048 draw_debug_border(node.pos.0, clamped_y, node.size.0, node.size.1, buf, style);
1049 if sy >= 0 {
1050 buf.set_string(node.pos.0, clamped_y, &depth.to_string(), style);
1051 }
1052 }
1053 }
1054
1055 if node.is_scrollable {
1056 if let Some(area) = visible_area(node, y_offset) {
1057 let inner = inner_area(node, area);
1058 buf.push_clip(inner);
1059 for child in &node.children {
1060 render_debug_overlay_inner(child, buf, depth.saturating_add(1), child_offset);
1061 }
1062 buf.pop_clip();
1063 }
1064 } else {
1065 for child in &node.children {
1066 render_debug_overlay_inner(child, buf, depth.saturating_add(1), child_offset);
1067 }
1068 }
1069}
1070
1071fn debug_color_for_depth(depth: u32) -> Color {
1072 match depth {
1073 0 => Color::Cyan,
1074 1 => Color::Yellow,
1075 2 => Color::Magenta,
1076 _ => Color::Red,
1077 }
1078}
1079
1080fn draw_debug_border(x: u32, y: u32, w: u32, h: u32, buf: &mut Buffer, style: Style) {
1081 if w == 0 || h == 0 {
1082 return;
1083 }
1084 let right = x + w - 1;
1085 let bottom = y + h - 1;
1086
1087 if w == 1 && h == 1 {
1088 buf.set_char(x, y, '┼', style);
1089 return;
1090 }
1091 if h == 1 {
1092 for xx in x..=right {
1093 buf.set_char(xx, y, '─', style);
1094 }
1095 return;
1096 }
1097 if w == 1 {
1098 for yy in y..=bottom {
1099 buf.set_char(x, yy, '│', style);
1100 }
1101 return;
1102 }
1103
1104 buf.set_char(x, y, '┌', style);
1105 buf.set_char(right, y, '┐', style);
1106 buf.set_char(x, bottom, '└', style);
1107 buf.set_char(right, bottom, '┘', style);
1108
1109 for xx in (x + 1)..right {
1110 buf.set_char(xx, y, '─', style);
1111 buf.set_char(xx, bottom, '─', style);
1112 }
1113 for yy in (y + 1)..bottom {
1114 buf.set_char(x, yy, '│', style);
1115 buf.set_char(right, yy, '│', style);
1116 }
1117}
1118
1119#[allow(dead_code)]
1120fn draw_debug_padding_markers(node: &LayoutNode, y_offset: u32, buf: &mut Buffer, style: Style) {
1121 if node.size.0 == 0 || node.size.1 == 0 {
1122 return;
1123 }
1124
1125 if node.padding == Padding::default() {
1126 return;
1127 }
1128
1129 let Some(area) = visible_area(node, y_offset) else {
1130 return;
1131 };
1132 let inner = inner_area(node, area);
1133 if inner.width == 0 || inner.height == 0 {
1134 return;
1135 }
1136
1137 let right = inner.right() - 1;
1138 let bottom = inner.bottom() - 1;
1139 buf.set_char(inner.x, inner.y, 'p', style);
1140 buf.set_char(right, inner.y, 'p', style);
1141 buf.set_char(inner.x, bottom, 'p', style);
1142 buf.set_char(right, bottom, 'p', style);
1143}
1144
1145#[allow(dead_code)]
1146fn draw_debug_margin_markers(node: &LayoutNode, y_offset: u32, buf: &mut Buffer, style: Style) {
1147 if node.margin == Margin::default() {
1148 return;
1149 }
1150
1151 let margin_y_i = node.pos.1 as i64 - node.margin.top as i64 - y_offset as i64;
1152 let w = node
1153 .size
1154 .0
1155 .saturating_add(node.margin.horizontal())
1156 .max(node.margin.horizontal());
1157 let h = node
1158 .size
1159 .1
1160 .saturating_add(node.margin.vertical())
1161 .max(node.margin.vertical());
1162
1163 if w == 0 || h == 0 || margin_y_i + h as i64 <= 0 {
1164 return;
1165 }
1166
1167 let x = node.pos.0.saturating_sub(node.margin.left);
1168 let y = margin_y_i.max(0) as u32;
1169 let bottom_i = margin_y_i + h as i64 - 1;
1170 if bottom_i < 0 {
1171 return;
1172 }
1173 let right = x + w - 1;
1174 let bottom = bottom_i as u32;
1175 if margin_y_i >= 0 {
1176 buf.set_char(x, y, 'm', style);
1177 buf.set_char(right, y, 'm', style);
1178 }
1179 buf.set_char(x, bottom, 'm', style);
1180 buf.set_char(right, bottom, 'm', style);
1181}
1182
1183fn screen_y(layout_y: u32, y_offset: u32) -> i64 {
1184 layout_y as i64 - y_offset as i64
1185}
1186
1187fn visible_area(node: &LayoutNode, y_offset: u32) -> Option<Rect> {
1188 let sy = screen_y(node.pos.1, y_offset);
1189 let bottom = sy + node.size.1 as i64;
1190 if bottom <= 0 || node.size.0 == 0 || node.size.1 == 0 {
1191 return None;
1192 }
1193 let clamped_y = sy.max(0) as u32;
1194 let clamped_h = (bottom as u32).saturating_sub(clamped_y);
1195 Some(Rect::new(node.pos.0, clamped_y, node.size.0, clamped_h))
1196}
1197
1198fn render_inner(node: &LayoutNode, buf: &mut Buffer, y_offset: u32, parent_bg: Option<Color>) {
1199 if node.size.0 == 0 || node.size.1 == 0 {
1200 return;
1201 }
1202
1203 let sy = screen_y(node.pos.1, y_offset);
1204 let sx = i64::from(node.pos.0);
1205 let ex = sx.saturating_add(i64::from(node.size.0));
1206 let ey = sy.saturating_add(i64::from(node.size.1));
1207 let viewport_left = i64::from(buf.area.x);
1208 let viewport_top = i64::from(buf.area.y);
1209 let viewport_right = viewport_left.saturating_add(i64::from(buf.area.width));
1210 let viewport_bottom = viewport_top.saturating_add(i64::from(buf.area.height));
1211
1212 if ex <= viewport_left || ey <= viewport_top || sx >= viewport_right || sy >= viewport_bottom {
1213 return;
1214 }
1215
1216 match node.kind {
1217 NodeKind::Text => {
1218 if let Some(ref text) = node.content {
1219 let mut style = node.style;
1220 if style.bg.is_none() {
1221 style.bg = parent_bg;
1222 }
1223 if node.wrap {
1224 let fallback;
1225 let lines = if let Some(cached) = &node.cached_wrapped {
1226 cached.as_slice()
1227 } else {
1228 fallback = wrap_lines(text, node.size.0);
1229 fallback.as_slice()
1230 };
1231 for (i, line) in lines.iter().enumerate() {
1232 let line_y = sy + i as i64;
1233 if line_y < 0 {
1234 continue;
1235 }
1236 let text_width = UnicodeWidthStr::width(line.as_str()) as u32;
1237 let x_offset = if text_width < node.size.0 {
1238 match node.align {
1239 Align::Start => 0,
1240 Align::Center => (node.size.0 - text_width) / 2,
1241 Align::End => node.size.0 - text_width,
1242 }
1243 } else {
1244 0
1245 };
1246 buf.set_string(
1247 node.pos.0.saturating_add(x_offset),
1248 line_y as u32,
1249 line,
1250 style,
1251 );
1252 }
1253 } else {
1254 if sy < 0 {
1255 return;
1256 }
1257 let text_width = UnicodeWidthStr::width(text.as_str()) as u32;
1258 let x_offset = if text_width < node.size.0 {
1259 match node.align {
1260 Align::Start => 0,
1261 Align::Center => (node.size.0 - text_width) / 2,
1262 Align::End => node.size.0 - text_width,
1263 }
1264 } else {
1265 0
1266 };
1267 let draw_x = node.pos.0.saturating_add(x_offset);
1268 if let Some(ref url) = node.link_url {
1269 buf.set_string_linked(draw_x, sy as u32, text, style, url);
1270 } else {
1271 buf.set_string(draw_x, sy as u32, text, style);
1272 }
1273 }
1274 }
1275 }
1276 NodeKind::Spacer => {}
1277 NodeKind::Container(_) => {
1278 if let Some(color) = node.bg_color {
1279 if let Some(area) = visible_area(node, y_offset) {
1280 let fill_style = Style::new().bg(color);
1281 for y in area.y..area.bottom() {
1282 for x in area.x..area.right() {
1283 buf.set_string(x, y, " ", fill_style);
1284 }
1285 }
1286 }
1287 }
1288 let child_bg = node.bg_color.or(parent_bg);
1289 render_container_border(node, buf, y_offset, child_bg);
1290 if node.is_scrollable {
1291 let Some(area) = visible_area(node, y_offset) else {
1292 return;
1293 };
1294 let inner = inner_area(node, area);
1295 let child_offset = y_offset.saturating_add(node.scroll_offset);
1296 let render_y_start = inner.y as i64;
1297 let render_y_end = inner.bottom() as i64;
1298 buf.push_clip(inner);
1299 for child in &node.children {
1300 let child_top = child.pos.1 as i64 - child_offset as i64;
1301 let child_bottom = child_top + child.size.1 as i64;
1302 if child_bottom <= render_y_start || child_top >= render_y_end {
1303 continue;
1304 }
1305 render_inner(child, buf, child_offset, child_bg);
1306 }
1307 buf.pop_clip();
1308 render_scroll_indicators(node, inner, buf, child_bg);
1309 } else {
1310 let Some(area) = visible_area(node, y_offset) else {
1311 return;
1312 };
1313 let clip = inner_area(node, area);
1314 buf.push_clip(clip);
1315 for child in &node.children {
1316 render_inner(child, buf, y_offset, child_bg);
1317 }
1318 buf.pop_clip();
1319 }
1320 }
1321 }
1322}
1323
1324fn render_container_border(
1325 node: &LayoutNode,
1326 buf: &mut Buffer,
1327 y_offset: u32,
1328 inherit_bg: Option<Color>,
1329) {
1330 if node.border_inset() == 0 {
1331 return;
1332 }
1333 let Some(border) = node.border else {
1334 return;
1335 };
1336 let sides = node.border_sides;
1337 let chars = border.chars();
1338 let x = node.pos.0;
1339 let w = node.size.0;
1340 let h = node.size.1;
1341 if w == 0 || h == 0 {
1342 return;
1343 }
1344
1345 let mut style = node.border_style;
1346 if style.bg.is_none() {
1347 style.bg = inherit_bg;
1348 }
1349
1350 let top_i = screen_y(node.pos.1, y_offset);
1351 let bottom_i = top_i + h as i64 - 1;
1352 if bottom_i < 0 {
1353 return;
1354 }
1355 let right = x + w - 1;
1356
1357 if sides.top && top_i >= 0 {
1358 let y = top_i as u32;
1359 for xx in x..=right {
1360 buf.set_char(xx, y, chars.h, style);
1361 }
1362 }
1363 if sides.bottom {
1364 let y = bottom_i as u32;
1365 for xx in x..=right {
1366 buf.set_char(xx, y, chars.h, style);
1367 }
1368 }
1369 if sides.left {
1370 let vert_start = top_i.max(0) as u32;
1371 let vert_end = bottom_i as u32;
1372 for yy in vert_start..=vert_end {
1373 buf.set_char(x, yy, chars.v, style);
1374 }
1375 }
1376 if sides.right {
1377 let vert_start = top_i.max(0) as u32;
1378 let vert_end = bottom_i as u32;
1379 for yy in vert_start..=vert_end {
1380 buf.set_char(right, yy, chars.v, style);
1381 }
1382 }
1383
1384 if top_i >= 0 {
1385 let y = top_i as u32;
1386 let tl = match (sides.top, sides.left) {
1387 (true, true) => Some(chars.tl),
1388 (true, false) => Some(chars.h),
1389 (false, true) => Some(chars.v),
1390 (false, false) => None,
1391 };
1392 if let Some(ch) = tl {
1393 buf.set_char(x, y, ch, style);
1394 }
1395
1396 let tr = match (sides.top, sides.right) {
1397 (true, true) => Some(chars.tr),
1398 (true, false) => Some(chars.h),
1399 (false, true) => Some(chars.v),
1400 (false, false) => None,
1401 };
1402 if let Some(ch) = tr {
1403 buf.set_char(right, y, ch, style);
1404 }
1405 }
1406
1407 let y = bottom_i as u32;
1408 let bl = match (sides.bottom, sides.left) {
1409 (true, true) => Some(chars.bl),
1410 (true, false) => Some(chars.h),
1411 (false, true) => Some(chars.v),
1412 (false, false) => None,
1413 };
1414 if let Some(ch) = bl {
1415 buf.set_char(x, y, ch, style);
1416 }
1417
1418 let br = match (sides.bottom, sides.right) {
1419 (true, true) => Some(chars.br),
1420 (true, false) => Some(chars.h),
1421 (false, true) => Some(chars.v),
1422 (false, false) => None,
1423 };
1424 if let Some(ch) = br {
1425 buf.set_char(right, y, ch, style);
1426 }
1427
1428 if sides.top && top_i >= 0 {
1429 if let Some((title, title_style)) = &node.title {
1430 let mut ts = *title_style;
1431 if ts.bg.is_none() {
1432 ts.bg = inherit_bg;
1433 }
1434 let y = top_i as u32;
1435 let title_x = x.saturating_add(2);
1436 if title_x <= right {
1437 let max_width = (right - title_x + 1) as usize;
1438 let trimmed: String = title.chars().take(max_width).collect();
1439 buf.set_string(title_x, y, &trimmed, ts);
1440 }
1441 }
1442 }
1443}
1444
1445fn render_scroll_indicators(
1446 node: &LayoutNode,
1447 inner: Rect,
1448 buf: &mut Buffer,
1449 inherit_bg: Option<Color>,
1450) {
1451 if inner.width == 0 || inner.height == 0 {
1452 return;
1453 }
1454
1455 let mut style = node.border_style;
1456 if style.bg.is_none() {
1457 style.bg = inherit_bg;
1458 }
1459
1460 let indicator_x = inner.right() - 1;
1461 if node.scroll_offset > 0 {
1462 buf.set_char(indicator_x, inner.y, '▲', style);
1463 }
1464 if node.scroll_offset.saturating_add(inner.height) < node.content_height {
1465 buf.set_char(indicator_x, inner.bottom() - 1, '▼', style);
1466 }
1467}
1468
1469pub(crate) fn collect_scroll_infos(node: &LayoutNode) -> Vec<(u32, u32)> {
1470 let mut infos = Vec::new();
1471 collect_scroll_infos_inner(node, &mut infos);
1472 for overlay in &node.overlays {
1473 collect_scroll_infos_inner(&overlay.node, &mut infos);
1474 }
1475 infos
1476}
1477
1478pub(crate) fn collect_hit_areas(node: &LayoutNode) -> Vec<Rect> {
1479 let mut areas = Vec::new();
1480 for child in &node.children {
1481 collect_hit_areas_inner(child, &mut areas);
1482 }
1483 for overlay in &node.overlays {
1484 collect_hit_areas_inner(&overlay.node, &mut areas);
1485 }
1486 areas
1487}
1488
1489fn collect_scroll_infos_inner(node: &LayoutNode, infos: &mut Vec<(u32, u32)>) {
1490 if node.is_scrollable {
1491 let viewport_h = node.size.1.saturating_sub(node.frame_vertical());
1492 infos.push((node.content_height, viewport_h));
1493 }
1494 for child in &node.children {
1495 collect_scroll_infos_inner(child, infos);
1496 }
1497}
1498
1499fn collect_hit_areas_inner(node: &LayoutNode, areas: &mut Vec<Rect>) {
1500 if matches!(node.kind, NodeKind::Container(_)) || node.link_url.is_some() {
1501 areas.push(Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1));
1502 }
1503 for child in &node.children {
1504 collect_hit_areas_inner(child, areas);
1505 }
1506}
1507
1508pub(crate) fn collect_content_areas(node: &LayoutNode) -> Vec<(Rect, Rect)> {
1509 let mut areas = Vec::new();
1510 for child in &node.children {
1511 collect_content_areas_inner(child, &mut areas);
1512 }
1513 for overlay in &node.overlays {
1514 collect_content_areas_inner(&overlay.node, &mut areas);
1515 }
1516 areas
1517}
1518
1519fn collect_content_areas_inner(node: &LayoutNode, areas: &mut Vec<(Rect, Rect)>) {
1520 if matches!(node.kind, NodeKind::Container(_)) {
1521 let full = Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1);
1522 let inset_x = node.padding.left + node.border_left_inset();
1523 let inset_y = node.padding.top + node.border_top_inset();
1524 let inner_w = node.size.0.saturating_sub(node.frame_horizontal());
1525 let inner_h = node.size.1.saturating_sub(node.frame_vertical());
1526 let content = Rect::new(node.pos.0 + inset_x, node.pos.1 + inset_y, inner_w, inner_h);
1527 areas.push((full, content));
1528 }
1529 for child in &node.children {
1530 collect_content_areas_inner(child, areas);
1531 }
1532}
1533
1534pub(crate) fn collect_focus_rects(node: &LayoutNode) -> Vec<(usize, Rect)> {
1535 let mut rects = Vec::new();
1536 collect_focus_rects_inner(node, &mut rects);
1537 for overlay in &node.overlays {
1538 collect_focus_rects_inner(&overlay.node, &mut rects);
1539 }
1540 rects
1541}
1542
1543fn collect_focus_rects_inner(node: &LayoutNode, rects: &mut Vec<(usize, Rect)>) {
1544 if let Some(id) = node.focus_id {
1545 rects.push((
1546 id,
1547 Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1),
1548 ));
1549 }
1550 for child in &node.children {
1551 collect_focus_rects_inner(child, rects);
1552 }
1553}
1554
1555#[cfg(test)]
1556mod tests {
1557 use super::*;
1558
1559 #[test]
1560 fn wrap_empty() {
1561 assert_eq!(wrap_lines("", 10), vec![""]);
1562 }
1563
1564 #[test]
1565 fn wrap_fits() {
1566 assert_eq!(wrap_lines("hello", 10), vec!["hello"]);
1567 }
1568
1569 #[test]
1570 fn wrap_word_boundary() {
1571 assert_eq!(wrap_lines("hello world", 7), vec!["hello", "world"]);
1572 }
1573
1574 #[test]
1575 fn wrap_multiple_words() {
1576 assert_eq!(
1577 wrap_lines("one two three four", 9),
1578 vec!["one two", "three", "four"]
1579 );
1580 }
1581
1582 #[test]
1583 fn wrap_long_word() {
1584 assert_eq!(wrap_lines("abcdefghij", 4), vec!["abcd", "efgh", "ij"]);
1585 }
1586
1587 #[test]
1588 fn wrap_zero_width() {
1589 assert_eq!(wrap_lines("hello", 0), vec!["hello"]);
1590 }
1591
1592 #[test]
1593 fn diagnostic_demo_layout() {
1594 use super::{compute, ContainerConfig, Direction, LayoutNode};
1595 use crate::rect::Rect;
1596 use crate::style::{Align, Border, Constraints, Justify, Margin, Padding, Style};
1597
1598 let mut root = LayoutNode::container(
1611 Direction::Column,
1612 ContainerConfig {
1613 gap: 0,
1614 align: Align::Start,
1615 justify: Justify::Start,
1616 border: None,
1617 border_sides: BorderSides::all(),
1618 border_style: Style::new(),
1619 bg_color: None,
1620 padding: Padding::default(),
1621 margin: Margin::default(),
1622 constraints: Constraints::default(),
1623 title: None,
1624 grow: 0,
1625 },
1626 );
1627
1628 let mut outer_container = LayoutNode::container(
1630 Direction::Column,
1631 ContainerConfig {
1632 gap: 0,
1633 align: Align::Start,
1634 justify: Justify::Start,
1635 border: Some(Border::Rounded),
1636 border_sides: BorderSides::all(),
1637 border_style: Style::new(),
1638 bg_color: None,
1639 padding: Padding::all(1),
1640 margin: Margin::default(),
1641 constraints: Constraints::default(),
1642 title: None,
1643 grow: 1,
1644 },
1645 );
1646
1647 outer_container.children.push(LayoutNode::text(
1649 "header".to_string(),
1650 Style::new(),
1651 0,
1652 Align::Start,
1653 false,
1654 Margin::default(),
1655 Constraints::default(),
1656 ));
1657
1658 outer_container.children.push(LayoutNode::text(
1660 "separator".to_string(),
1661 Style::new(),
1662 0,
1663 Align::Start,
1664 false,
1665 Margin::default(),
1666 Constraints::default(),
1667 ));
1668
1669 let mut inner_container = LayoutNode::container(
1671 Direction::Column,
1672 ContainerConfig {
1673 gap: 0,
1674 align: Align::Start,
1675 justify: Justify::Start,
1676 border: None,
1677 border_sides: BorderSides::all(),
1678 border_style: Style::new(),
1679 bg_color: None,
1680 padding: Padding::default(),
1681 margin: Margin::default(),
1682 constraints: Constraints::default(),
1683 title: None,
1684 grow: 1,
1685 },
1686 );
1687
1688 inner_container.children.push(LayoutNode::text(
1690 "content1".to_string(),
1691 Style::new(),
1692 0,
1693 Align::Start,
1694 false,
1695 Margin::default(),
1696 Constraints::default(),
1697 ));
1698 inner_container.children.push(LayoutNode::text(
1699 "content2".to_string(),
1700 Style::new(),
1701 0,
1702 Align::Start,
1703 false,
1704 Margin::default(),
1705 Constraints::default(),
1706 ));
1707 inner_container.children.push(LayoutNode::text(
1708 "content3".to_string(),
1709 Style::new(),
1710 0,
1711 Align::Start,
1712 false,
1713 Margin::default(),
1714 Constraints::default(),
1715 ));
1716
1717 outer_container.children.push(inner_container);
1718
1719 outer_container.children.push(LayoutNode::text(
1721 "separator2".to_string(),
1722 Style::new(),
1723 0,
1724 Align::Start,
1725 false,
1726 Margin::default(),
1727 Constraints::default(),
1728 ));
1729
1730 outer_container.children.push(LayoutNode::text(
1732 "footer".to_string(),
1733 Style::new(),
1734 0,
1735 Align::Start,
1736 false,
1737 Margin::default(),
1738 Constraints::default(),
1739 ));
1740
1741 root.children.push(outer_container);
1742
1743 compute(&mut root, Rect::new(0, 0, 80, 50));
1745
1746 eprintln!("\n=== DIAGNOSTIC LAYOUT TEST ===");
1748 eprintln!("Root node:");
1749 eprintln!(" pos: {:?}, size: {:?}", root.pos, root.size);
1750
1751 let outer = &root.children[0];
1752 eprintln!("\nOuter bordered container (grow:1):");
1753 eprintln!(" pos: {:?}, size: {:?}", outer.pos, outer.size);
1754
1755 let inner = &outer.children[2];
1756 eprintln!("\nInner container (grow:1, simulates scrollable):");
1757 eprintln!(" pos: {:?}, size: {:?}", inner.pos, inner.size);
1758
1759 eprintln!("\nAll children of outer container:");
1760 for (i, child) in outer.children.iter().enumerate() {
1761 eprintln!(" [{}] pos: {:?}, size: {:?}", i, child.pos, child.size);
1762 }
1763
1764 assert_eq!(
1767 root.size,
1768 (80, 50),
1769 "Root node should fill entire terminal (80x50)"
1770 );
1771
1772 assert_eq!(
1774 outer.size,
1775 (80, 50),
1776 "Outer bordered container should fill entire terminal (80x50)"
1777 );
1778
1779 let expected_inner_height = 50 - 2 - 2 - 4;
1786 assert_eq!(
1787 inner.size.1, expected_inner_height as u32,
1788 "Inner container height should be {} (50 - border(2) - padding(2) - fixed(4))",
1789 expected_inner_height
1790 );
1791
1792 let expected_inner_y = 1 + 1 + 1 + 1;
1794 assert_eq!(
1795 inner.pos.1, expected_inner_y as u32,
1796 "Inner container should start at y={} (border+padding+header+sep)",
1797 expected_inner_y
1798 );
1799
1800 eprintln!("\n✓ All assertions passed!");
1801 eprintln!(" Root size: {:?}", root.size);
1802 eprintln!(" Outer container size: {:?}", outer.size);
1803 eprintln!(" Inner container size: {:?}", inner.size);
1804 eprintln!(" Inner container pos: {:?}", inner.pos);
1805 }
1806
1807 #[test]
1808 fn collect_focus_rects_from_markers() {
1809 use super::*;
1810 use crate::style::Style;
1811
1812 let commands = vec![
1813 Command::FocusMarker(0),
1814 Command::Text {
1815 content: "input1".into(),
1816 style: Style::new(),
1817 grow: 0,
1818 align: Align::Start,
1819 wrap: false,
1820 margin: Default::default(),
1821 constraints: Default::default(),
1822 },
1823 Command::FocusMarker(1),
1824 Command::Text {
1825 content: "input2".into(),
1826 style: Style::new(),
1827 grow: 0,
1828 align: Align::Start,
1829 wrap: false,
1830 margin: Default::default(),
1831 constraints: Default::default(),
1832 },
1833 ];
1834
1835 let mut tree = build_tree(&commands);
1836 let area = crate::rect::Rect::new(0, 0, 40, 10);
1837 compute(&mut tree, area);
1838
1839 let rects = collect_focus_rects(&tree);
1840 assert_eq!(rects.len(), 2);
1841 assert_eq!(rects[0].0, 0);
1842 assert_eq!(rects[1].0, 1);
1843 assert!(rects[0].1.width > 0);
1844 assert!(rects[1].1.width > 0);
1845 assert_ne!(rects[0].1.y, rects[1].1.y);
1846 }
1847
1848 #[test]
1849 fn focus_marker_tags_container() {
1850 use super::*;
1851 use crate::style::{Border, Style};
1852
1853 let commands = vec![
1854 Command::FocusMarker(0),
1855 Command::BeginContainer {
1856 direction: Direction::Column,
1857 gap: 0,
1858 align: Align::Start,
1859 justify: Justify::Start,
1860 border: Some(Border::Single),
1861 border_sides: BorderSides::all(),
1862 border_style: Style::new(),
1863 bg_color: None,
1864 padding: Padding::default(),
1865 margin: Default::default(),
1866 constraints: Default::default(),
1867 title: None,
1868 grow: 0,
1869 },
1870 Command::Text {
1871 content: "inside".into(),
1872 style: Style::new(),
1873 grow: 0,
1874 align: Align::Start,
1875 wrap: false,
1876 margin: Default::default(),
1877 constraints: Default::default(),
1878 },
1879 Command::EndContainer,
1880 ];
1881
1882 let mut tree = build_tree(&commands);
1883 let area = crate::rect::Rect::new(0, 0, 40, 10);
1884 compute(&mut tree, area);
1885
1886 let rects = collect_focus_rects(&tree);
1887 assert_eq!(rects.len(), 1);
1888 assert_eq!(rects[0].0, 0);
1889 assert!(rects[0].1.width >= 8);
1890 assert!(rects[0].1.height >= 3);
1891 }
1892}