1use crate::buffer::Buffer;
2use crate::rect::Rect;
3use crate::style::{Align, Border, Color, Constraints, Margin, Padding, Style};
4use unicode_width::UnicodeWidthChar;
5use unicode_width::UnicodeWidthStr;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Direction {
9 Row,
10 Column,
11}
12
13#[derive(Debug, Clone)]
14pub(crate) enum Command {
15 Text {
16 content: String,
17 style: Style,
18 grow: u16,
19 align: Align,
20 wrap: bool,
21 margin: Margin,
22 constraints: Constraints,
23 },
24 BeginContainer {
25 direction: Direction,
26 gap: u32,
27 align: Align,
28 border: Option<Border>,
29 border_style: Style,
30 padding: Padding,
31 margin: Margin,
32 constraints: Constraints,
33 title: Option<(String, Style)>,
34 grow: u16,
35 },
36 BeginScrollable {
37 grow: u16,
38 border: Option<Border>,
39 border_style: Style,
40 padding: Padding,
41 margin: Margin,
42 constraints: Constraints,
43 title: Option<(String, Style)>,
44 scroll_offset: u32,
45 },
46 EndContainer,
47 Spacer {
48 grow: u16,
49 },
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53enum NodeKind {
54 Text,
55 Container(Direction),
56 Spacer,
57}
58
59#[derive(Debug, Clone)]
60pub(crate) struct LayoutNode {
61 kind: NodeKind,
62 content: Option<String>,
63 style: Style,
64 pub grow: u16,
65 align: Align,
66 wrap: bool,
67 gap: u32,
68 border: Option<Border>,
69 border_style: Style,
70 padding: Padding,
71 margin: Margin,
72 constraints: Constraints,
73 title: Option<(String, Style)>,
74 children: Vec<LayoutNode>,
75 pos: (u32, u32),
76 size: (u32, u32),
77 is_scrollable: bool,
78 scroll_offset: u32,
79 content_height: u32,
80 cached_wrapped: Option<Vec<String>>,
81}
82
83#[derive(Debug, Clone)]
84struct ContainerConfig {
85 gap: u32,
86 align: Align,
87 border: Option<Border>,
88 border_style: Style,
89 padding: Padding,
90 margin: Margin,
91 constraints: Constraints,
92 title: Option<(String, Style)>,
93 grow: u16,
94}
95
96impl LayoutNode {
97 fn text(
98 content: String,
99 style: Style,
100 grow: u16,
101 align: Align,
102 wrap: bool,
103 margin: Margin,
104 constraints: Constraints,
105 ) -> Self {
106 let width = UnicodeWidthStr::width(content.as_str()) as u32;
107 Self {
108 kind: NodeKind::Text,
109 content: Some(content),
110 style,
111 grow,
112 align,
113 wrap,
114 gap: 0,
115 border: None,
116 border_style: Style::new(),
117 padding: Padding::default(),
118 margin,
119 constraints,
120 title: None,
121 children: Vec::new(),
122 pos: (0, 0),
123 size: (width, 1),
124 is_scrollable: false,
125 scroll_offset: 0,
126 content_height: 0,
127 cached_wrapped: None,
128 }
129 }
130
131 fn container(direction: Direction, config: ContainerConfig) -> Self {
132 Self {
133 kind: NodeKind::Container(direction),
134 content: None,
135 style: Style::new(),
136 grow: config.grow,
137 align: config.align,
138 wrap: false,
139 gap: config.gap,
140 border: config.border,
141 border_style: config.border_style,
142 padding: config.padding,
143 margin: config.margin,
144 constraints: config.constraints,
145 title: config.title,
146 children: Vec::new(),
147 pos: (0, 0),
148 size: (0, 0),
149 is_scrollable: false,
150 scroll_offset: 0,
151 content_height: 0,
152 cached_wrapped: None,
153 }
154 }
155
156 fn spacer(grow: u16) -> Self {
157 Self {
158 kind: NodeKind::Spacer,
159 content: None,
160 style: Style::new(),
161 grow,
162 align: Align::Start,
163 wrap: false,
164 gap: 0,
165 border: None,
166 border_style: Style::new(),
167 padding: Padding::default(),
168 margin: Margin::default(),
169 constraints: Constraints::default(),
170 title: None,
171 children: Vec::new(),
172 pos: (0, 0),
173 size: (0, 0),
174 is_scrollable: false,
175 scroll_offset: 0,
176 content_height: 0,
177 cached_wrapped: None,
178 }
179 }
180
181 fn border_inset(&self) -> u32 {
182 if self.border.is_some() {
183 1
184 } else {
185 0
186 }
187 }
188
189 fn frame_horizontal(&self) -> u32 {
190 self.padding.horizontal() + self.border_inset() * 2
191 }
192
193 fn frame_vertical(&self) -> u32 {
194 self.padding.vertical() + self.border_inset() * 2
195 }
196
197 fn min_width(&self) -> u32 {
198 let width = match self.kind {
199 NodeKind::Text => self.size.0,
200 NodeKind::Spacer => 0,
201 NodeKind::Container(Direction::Row) => {
202 let gaps = if self.children.is_empty() {
203 0
204 } else {
205 (self.children.len() as u32 - 1) * self.gap
206 };
207 let children_width: u32 = self.children.iter().map(|c| c.min_width()).sum();
208 children_width + gaps + self.frame_horizontal()
209 }
210 NodeKind::Container(Direction::Column) => {
211 self.children
212 .iter()
213 .map(|c| c.min_width())
214 .max()
215 .unwrap_or(0)
216 + self.frame_horizontal()
217 }
218 };
219
220 let width = width.max(self.constraints.min_width.unwrap_or(0));
221 width.saturating_add(self.margin.horizontal())
222 }
223
224 fn min_height(&self) -> u32 {
225 let height = match self.kind {
226 NodeKind::Text => 1,
227 NodeKind::Spacer => 0,
228 NodeKind::Container(Direction::Row) => {
229 self.children
230 .iter()
231 .map(|c| c.min_height())
232 .max()
233 .unwrap_or(0)
234 + self.frame_vertical()
235 }
236 NodeKind::Container(Direction::Column) => {
237 let gaps = if self.children.is_empty() {
238 0
239 } else {
240 (self.children.len() as u32 - 1) * self.gap
241 };
242 let children_height: u32 = self.children.iter().map(|c| c.min_height()).sum();
243 children_height + gaps + self.frame_vertical()
244 }
245 };
246
247 let height = height.max(self.constraints.min_height.unwrap_or(0));
248 height.saturating_add(self.margin.vertical())
249 }
250
251 fn min_height_for_width(&self, available_width: u32) -> u32 {
252 match self.kind {
253 NodeKind::Text if self.wrap => {
254 let text = self.content.as_deref().unwrap_or("");
255 let inner_width = available_width.saturating_sub(self.margin.horizontal());
256 let lines = wrap_lines(text, inner_width).len().max(1) as u32;
257 lines.saturating_add(self.margin.vertical())
258 }
259 _ => self.min_height(),
260 }
261 }
262}
263
264fn wrap_lines(text: &str, max_width: u32) -> Vec<String> {
265 if text.is_empty() {
266 return vec![String::new()];
267 }
268 if max_width == 0 {
269 return vec![text.to_string()];
270 }
271
272 fn split_long_word(word: &str, max_width: u32) -> Vec<(String, u32)> {
273 let mut chunks: Vec<(String, u32)> = Vec::new();
274 let mut chunk = String::new();
275 let mut chunk_width = 0_u32;
276
277 for ch in word.chars() {
278 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
279 if chunk.is_empty() {
280 if ch_width > max_width {
281 chunks.push((ch.to_string(), ch_width));
282 } else {
283 chunk.push(ch);
284 chunk_width = ch_width;
285 }
286 continue;
287 }
288
289 if chunk_width + ch_width > max_width {
290 chunks.push((std::mem::take(&mut chunk), chunk_width));
291 if ch_width > max_width {
292 chunks.push((ch.to_string(), ch_width));
293 chunk_width = 0;
294 } else {
295 chunk.push(ch);
296 chunk_width = ch_width;
297 }
298 } else {
299 chunk.push(ch);
300 chunk_width += ch_width;
301 }
302 }
303
304 if !chunk.is_empty() {
305 chunks.push((chunk, chunk_width));
306 }
307
308 chunks
309 }
310
311 fn push_word_into_line(
312 lines: &mut Vec<String>,
313 current_line: &mut String,
314 current_width: &mut u32,
315 word: &str,
316 word_width: u32,
317 max_width: u32,
318 ) {
319 if word.is_empty() {
320 return;
321 }
322
323 if word_width > max_width {
324 let chunks = split_long_word(word, max_width);
325 for (chunk, chunk_width) in chunks {
326 if current_line.is_empty() {
327 *current_line = chunk;
328 *current_width = chunk_width;
329 } else if *current_width + 1 + chunk_width <= max_width {
330 current_line.push(' ');
331 current_line.push_str(&chunk);
332 *current_width += 1 + chunk_width;
333 } else {
334 lines.push(std::mem::take(current_line));
335 *current_line = chunk;
336 *current_width = chunk_width;
337 }
338 }
339 return;
340 }
341
342 if current_line.is_empty() {
343 *current_line = word.to_string();
344 *current_width = word_width;
345 } else if *current_width + 1 + word_width <= max_width {
346 current_line.push(' ');
347 current_line.push_str(word);
348 *current_width += 1 + word_width;
349 } else {
350 lines.push(std::mem::take(current_line));
351 *current_line = word.to_string();
352 *current_width = word_width;
353 }
354 }
355
356 let mut lines: Vec<String> = Vec::new();
357 let mut current_line = String::new();
358 let mut current_width: u32 = 0;
359 let mut current_word = String::new();
360 let mut word_width: u32 = 0;
361
362 for ch in text.chars() {
363 if ch == ' ' {
364 push_word_into_line(
365 &mut lines,
366 &mut current_line,
367 &mut current_width,
368 ¤t_word,
369 word_width,
370 max_width,
371 );
372 current_word.clear();
373 word_width = 0;
374 continue;
375 }
376
377 current_word.push(ch);
378 word_width += UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
379 }
380
381 push_word_into_line(
382 &mut lines,
383 &mut current_line,
384 &mut current_width,
385 ¤t_word,
386 word_width,
387 max_width,
388 );
389
390 if !current_line.is_empty() {
391 lines.push(current_line);
392 }
393
394 if lines.is_empty() {
395 vec![String::new()]
396 } else {
397 lines
398 }
399}
400
401pub(crate) fn build_tree(commands: &[Command]) -> LayoutNode {
402 let mut root = LayoutNode::container(
403 Direction::Column,
404 ContainerConfig {
405 gap: 0,
406 align: Align::Start,
407 border: None,
408 border_style: Style::new(),
409 padding: Padding::default(),
410 margin: Margin::default(),
411 constraints: Constraints::default(),
412 title: None,
413 grow: 0,
414 },
415 );
416 build_children(&mut root, commands, &mut 0);
417 root
418}
419
420fn build_children(parent: &mut LayoutNode, commands: &[Command], pos: &mut usize) {
421 while *pos < commands.len() {
422 match &commands[*pos] {
423 Command::Text {
424 content,
425 style,
426 grow,
427 align,
428 wrap,
429 margin,
430 constraints,
431 } => {
432 parent.children.push(LayoutNode::text(
433 content.clone(),
434 *style,
435 *grow,
436 *align,
437 *wrap,
438 *margin,
439 *constraints,
440 ));
441 *pos += 1;
442 }
443 Command::BeginContainer {
444 direction,
445 gap,
446 align,
447 border,
448 border_style,
449 padding,
450 margin,
451 constraints,
452 title,
453 grow,
454 } => {
455 let mut node = LayoutNode::container(
456 *direction,
457 ContainerConfig {
458 gap: *gap,
459 align: *align,
460 border: *border,
461 border_style: *border_style,
462 padding: *padding,
463 margin: *margin,
464 constraints: *constraints,
465 title: title.clone(),
466 grow: *grow,
467 },
468 );
469 *pos += 1;
470 build_children(&mut node, commands, pos);
471 parent.children.push(node);
472 }
473 Command::BeginScrollable {
474 grow,
475 border,
476 border_style,
477 padding,
478 margin,
479 constraints,
480 title,
481 scroll_offset,
482 } => {
483 let mut node = LayoutNode::container(
484 Direction::Column,
485 ContainerConfig {
486 gap: 0,
487 align: Align::Start,
488 border: *border,
489 border_style: *border_style,
490 padding: *padding,
491 margin: *margin,
492 constraints: *constraints,
493 title: title.clone(),
494 grow: *grow,
495 },
496 );
497 node.is_scrollable = true;
498 node.scroll_offset = *scroll_offset;
499 *pos += 1;
500 build_children(&mut node, commands, pos);
501 parent.children.push(node);
502 }
503 Command::Spacer { grow } => {
504 parent.children.push(LayoutNode::spacer(*grow));
505 *pos += 1;
506 }
507 Command::EndContainer => {
508 *pos += 1;
509 return;
510 }
511 }
512 }
513}
514
515pub(crate) fn compute(node: &mut LayoutNode, area: Rect) {
516 node.pos = (area.x, area.y);
517 node.size = (
518 area.width.clamp(
519 node.constraints.min_width.unwrap_or(0),
520 node.constraints.max_width.unwrap_or(u32::MAX),
521 ),
522 area.height.clamp(
523 node.constraints.min_height.unwrap_or(0),
524 node.constraints.max_height.unwrap_or(u32::MAX),
525 ),
526 );
527
528 if matches!(node.kind, NodeKind::Text) && node.wrap {
529 let lines = wrap_lines(node.content.as_deref().unwrap_or(""), area.width);
530 node.size = (area.width, lines.len().max(1) as u32);
531 node.cached_wrapped = Some(lines);
532 } else {
533 node.cached_wrapped = None;
534 }
535
536 match node.kind {
537 NodeKind::Text | NodeKind::Spacer => {}
538 NodeKind::Container(Direction::Row) => {
539 layout_row(
540 node,
541 inner_area(
542 node,
543 Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1),
544 ),
545 );
546 node.content_height = 0;
547 }
548 NodeKind::Container(Direction::Column) => {
549 let viewport_area = inner_area(
550 node,
551 Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1),
552 );
553 if node.is_scrollable {
554 let saved_grows: Vec<u16> = node.children.iter().map(|c| c.grow).collect();
555 for child in &mut node.children {
556 child.grow = 0;
557 }
558 let total_gaps = if node.children.is_empty() {
559 0
560 } else {
561 (node.children.len() as u32 - 1) * node.gap
562 };
563 let natural_height: u32 = node
564 .children
565 .iter()
566 .map(|c| c.min_height_for_width(viewport_area.width))
567 .sum::<u32>()
568 + total_gaps;
569
570 if natural_height > viewport_area.height {
571 let virtual_area = Rect::new(
572 viewport_area.x,
573 viewport_area.y,
574 viewport_area.width,
575 natural_height,
576 );
577 layout_column(node, virtual_area);
578 } else {
579 for (child, &grow) in node.children.iter_mut().zip(saved_grows.iter()) {
580 child.grow = grow;
581 }
582 layout_column(node, viewport_area);
583 }
584 node.content_height = scroll_content_height(node, viewport_area.y);
585 } else {
586 layout_column(node, viewport_area);
587 node.content_height = 0;
588 }
589 }
590 }
591}
592
593fn scroll_content_height(node: &LayoutNode, inner_y: u32) -> u32 {
594 let Some(max_bottom) = node
595 .children
596 .iter()
597 .map(|child| {
598 child
599 .pos
600 .1
601 .saturating_add(child.size.1)
602 .saturating_add(child.margin.bottom)
603 })
604 .max()
605 else {
606 return 0;
607 };
608
609 max_bottom.saturating_sub(inner_y)
610}
611
612fn inner_area(node: &LayoutNode, area: Rect) -> Rect {
613 let inset = node.border_inset();
614 let x = area.x + inset + node.padding.left;
615 let y = area.y + inset + node.padding.top;
616 let width = area
617 .width
618 .saturating_sub(inset * 2)
619 .saturating_sub(node.padding.horizontal());
620 let height = area
621 .height
622 .saturating_sub(inset * 2)
623 .saturating_sub(node.padding.vertical());
624
625 Rect::new(x, y, width, height)
626}
627
628fn layout_row(node: &mut LayoutNode, area: Rect) {
629 if node.children.is_empty() {
630 return;
631 }
632
633 let total_gaps = (node.children.len() as u32 - 1) * node.gap;
634 let available = area.width.saturating_sub(total_gaps);
635 let min_widths: Vec<u32> = node
636 .children
637 .iter()
638 .map(|child| child.min_width())
639 .collect();
640
641 let mut total_grow: u32 = 0;
642 let mut fixed_width: u32 = 0;
643 for (child, &min_width) in node.children.iter().zip(min_widths.iter()) {
644 if child.grow > 0 {
645 total_grow += child.grow as u32;
646 } else {
647 fixed_width += min_width;
648 }
649 }
650
651 let mut flex_space = available.saturating_sub(fixed_width);
652 let mut remaining_grow = total_grow;
653 let mut x = area.x;
654
655 for (i, child) in node.children.iter_mut().enumerate() {
656 let w = if child.grow > 0 && total_grow > 0 {
657 let share = if remaining_grow == 0 {
658 0
659 } else {
660 flex_space * child.grow as u32 / remaining_grow
661 };
662 flex_space = flex_space.saturating_sub(share);
663 remaining_grow = remaining_grow.saturating_sub(child.grow as u32);
664 share
665 } else {
666 min_widths[i].min(available)
667 };
668
669 let child_outer_h = match node.align {
670 Align::Start => area.height,
671 _ => child.min_height_for_width(w).min(area.height),
672 };
673 let child_x = x.saturating_add(child.margin.left);
674 let child_y = area.y.saturating_add(child.margin.top);
675 let child_w = w.saturating_sub(child.margin.horizontal());
676 let child_h = child_outer_h.saturating_sub(child.margin.vertical());
677 compute(child, Rect::new(child_x, child_y, child_w, child_h));
678 let child_total_h = child.size.1.saturating_add(child.margin.vertical());
679 let y_offset = match node.align {
680 Align::Start => 0,
681 Align::Center => area.height.saturating_sub(child_total_h) / 2,
682 Align::End => area.height.saturating_sub(child_total_h),
683 };
684 child.pos.1 = child.pos.1.saturating_add(y_offset);
685 x += w + node.gap;
686 }
687}
688
689fn layout_column(node: &mut LayoutNode, area: Rect) {
690 if node.children.is_empty() {
691 return;
692 }
693
694 let total_gaps = (node.children.len() as u32 - 1) * node.gap;
695 let available = area.height.saturating_sub(total_gaps);
696 let min_heights: Vec<u32> = node
697 .children
698 .iter()
699 .map(|child| child.min_height_for_width(area.width))
700 .collect();
701
702 let mut total_grow: u32 = 0;
703 let mut fixed_height: u32 = 0;
704 for (child, &min_height) in node.children.iter().zip(min_heights.iter()) {
705 if child.grow > 0 {
706 total_grow += child.grow as u32;
707 } else {
708 fixed_height += min_height;
709 }
710 }
711
712 let mut flex_space = available.saturating_sub(fixed_height);
713 let mut remaining_grow = total_grow;
714 let mut y = area.y;
715
716 for (i, child) in node.children.iter_mut().enumerate() {
717 let h = if child.grow > 0 && total_grow > 0 {
718 let share = if remaining_grow == 0 {
719 0
720 } else {
721 flex_space * child.grow as u32 / remaining_grow
722 };
723 flex_space = flex_space.saturating_sub(share);
724 remaining_grow = remaining_grow.saturating_sub(child.grow as u32);
725 share
726 } else {
727 min_heights[i].min(available)
728 };
729
730 let child_outer_w = match node.align {
731 Align::Start => area.width,
732 _ => child.min_width().min(area.width),
733 };
734 let child_x = area.x.saturating_add(child.margin.left);
735 let child_y = y.saturating_add(child.margin.top);
736 let child_w = child_outer_w.saturating_sub(child.margin.horizontal());
737 let child_h = h.saturating_sub(child.margin.vertical());
738 compute(child, Rect::new(child_x, child_y, child_w, child_h));
739 let child_total_w = child.size.0.saturating_add(child.margin.horizontal());
740 let x_offset = match node.align {
741 Align::Start => 0,
742 Align::Center => area.width.saturating_sub(child_total_w) / 2,
743 Align::End => area.width.saturating_sub(child_total_w),
744 };
745 child.pos.0 = child.pos.0.saturating_add(x_offset);
746 y += h + node.gap;
747 }
748}
749
750pub(crate) fn render(node: &LayoutNode, buf: &mut Buffer) {
751 render_inner(node, buf, 0);
752}
753
754pub(crate) fn render_debug_overlay(node: &LayoutNode, buf: &mut Buffer) {
755 for child in &node.children {
756 render_debug_overlay_inner(child, buf, 0, 0);
757 }
758}
759
760fn render_debug_overlay_inner(node: &LayoutNode, buf: &mut Buffer, depth: u32, y_offset: u32) {
761 let child_offset = if node.is_scrollable {
762 y_offset.saturating_add(node.scroll_offset)
763 } else {
764 y_offset
765 };
766
767 if let NodeKind::Container(_) = node.kind {
768 let sy = screen_y(node.pos.1, y_offset);
769 if sy + node.size.1 as i64 > 0 {
770 let color = debug_color_for_depth(depth);
771 let style = Style::new().fg(color);
772 let clamped_y = sy.max(0) as u32;
773 draw_debug_border(node.pos.0, clamped_y, node.size.0, node.size.1, buf, style);
774 if sy >= 0 {
775 buf.set_string(node.pos.0, clamped_y, &depth.to_string(), style);
776 }
777 }
778 }
779
780 if node.is_scrollable {
781 if let Some(area) = visible_area(node, y_offset) {
782 let inner = inner_area(node, area);
783 buf.push_clip(inner);
784 for child in &node.children {
785 render_debug_overlay_inner(child, buf, depth.saturating_add(1), child_offset);
786 }
787 buf.pop_clip();
788 }
789 } else {
790 for child in &node.children {
791 render_debug_overlay_inner(child, buf, depth.saturating_add(1), child_offset);
792 }
793 }
794}
795
796fn debug_color_for_depth(depth: u32) -> Color {
797 match depth {
798 0 => Color::Cyan,
799 1 => Color::Yellow,
800 2 => Color::Magenta,
801 _ => Color::Red,
802 }
803}
804
805fn draw_debug_border(x: u32, y: u32, w: u32, h: u32, buf: &mut Buffer, style: Style) {
806 if w == 0 || h == 0 {
807 return;
808 }
809 let right = x + w - 1;
810 let bottom = y + h - 1;
811
812 if w == 1 && h == 1 {
813 buf.set_char(x, y, '┼', style);
814 return;
815 }
816 if h == 1 {
817 for xx in x..=right {
818 buf.set_char(xx, y, '─', style);
819 }
820 return;
821 }
822 if w == 1 {
823 for yy in y..=bottom {
824 buf.set_char(x, yy, '│', style);
825 }
826 return;
827 }
828
829 buf.set_char(x, y, '┌', style);
830 buf.set_char(right, y, '┐', style);
831 buf.set_char(x, bottom, '└', style);
832 buf.set_char(right, bottom, '┘', style);
833
834 for xx in (x + 1)..right {
835 buf.set_char(xx, y, '─', style);
836 buf.set_char(xx, bottom, '─', style);
837 }
838 for yy in (y + 1)..bottom {
839 buf.set_char(x, yy, '│', style);
840 buf.set_char(right, yy, '│', style);
841 }
842}
843
844#[allow(dead_code)]
845fn draw_debug_padding_markers(node: &LayoutNode, y_offset: u32, buf: &mut Buffer, style: Style) {
846 if node.size.0 == 0 || node.size.1 == 0 {
847 return;
848 }
849
850 if node.padding == Padding::default() {
851 return;
852 }
853
854 let Some(area) = visible_area(node, y_offset) else {
855 return;
856 };
857 let inner = inner_area(node, area);
858 if inner.width == 0 || inner.height == 0 {
859 return;
860 }
861
862 let right = inner.right() - 1;
863 let bottom = inner.bottom() - 1;
864 buf.set_char(inner.x, inner.y, 'p', style);
865 buf.set_char(right, inner.y, 'p', style);
866 buf.set_char(inner.x, bottom, 'p', style);
867 buf.set_char(right, bottom, 'p', style);
868}
869
870#[allow(dead_code)]
871fn draw_debug_margin_markers(node: &LayoutNode, y_offset: u32, buf: &mut Buffer, style: Style) {
872 if node.margin == Margin::default() {
873 return;
874 }
875
876 let margin_y_i = node.pos.1 as i64 - node.margin.top as i64 - y_offset as i64;
877 let w = node
878 .size
879 .0
880 .saturating_add(node.margin.horizontal())
881 .max(node.margin.horizontal());
882 let h = node
883 .size
884 .1
885 .saturating_add(node.margin.vertical())
886 .max(node.margin.vertical());
887
888 if w == 0 || h == 0 || margin_y_i + h as i64 <= 0 {
889 return;
890 }
891
892 let x = node.pos.0.saturating_sub(node.margin.left);
893 let y = margin_y_i.max(0) as u32;
894 let bottom_i = margin_y_i + h as i64 - 1;
895 if bottom_i < 0 {
896 return;
897 }
898 let right = x + w - 1;
899 let bottom = bottom_i as u32;
900 if margin_y_i >= 0 {
901 buf.set_char(x, y, 'm', style);
902 buf.set_char(right, y, 'm', style);
903 }
904 buf.set_char(x, bottom, 'm', style);
905 buf.set_char(right, bottom, 'm', style);
906}
907
908fn screen_y(layout_y: u32, y_offset: u32) -> i64 {
909 layout_y as i64 - y_offset as i64
910}
911
912fn visible_area(node: &LayoutNode, y_offset: u32) -> Option<Rect> {
913 let sy = screen_y(node.pos.1, y_offset);
914 let bottom = sy + node.size.1 as i64;
915 if bottom <= 0 || node.size.0 == 0 || node.size.1 == 0 {
916 return None;
917 }
918 let clamped_y = sy.max(0) as u32;
919 let clamped_h = (bottom as u32).saturating_sub(clamped_y);
920 Some(Rect::new(node.pos.0, clamped_y, node.size.0, clamped_h))
921}
922
923fn render_inner(node: &LayoutNode, buf: &mut Buffer, y_offset: u32) {
924 if node.size.0 == 0 || node.size.1 == 0 {
925 return;
926 }
927
928 let sy = screen_y(node.pos.1, y_offset);
929 let sx = i64::from(node.pos.0);
930 let ex = sx.saturating_add(i64::from(node.size.0));
931 let ey = sy.saturating_add(i64::from(node.size.1));
932 let viewport_left = i64::from(buf.area.x);
933 let viewport_top = i64::from(buf.area.y);
934 let viewport_right = viewport_left.saturating_add(i64::from(buf.area.width));
935 let viewport_bottom = viewport_top.saturating_add(i64::from(buf.area.height));
936
937 if ex <= viewport_left || ey <= viewport_top || sx >= viewport_right || sy >= viewport_bottom {
938 return;
939 }
940
941 match node.kind {
942 NodeKind::Text => {
943 if let Some(ref text) = node.content {
944 if node.wrap {
945 let fallback;
946 let lines = if let Some(cached) = &node.cached_wrapped {
947 cached.as_slice()
948 } else {
949 fallback = wrap_lines(text, node.size.0);
950 fallback.as_slice()
951 };
952 for (i, line) in lines.iter().enumerate() {
953 let line_y = sy + i as i64;
954 if line_y < 0 {
955 continue;
956 }
957 let text_width = UnicodeWidthStr::width(line.as_str()) as u32;
958 let x_offset = if text_width < node.size.0 {
959 match node.align {
960 Align::Start => 0,
961 Align::Center => (node.size.0 - text_width) / 2,
962 Align::End => node.size.0 - text_width,
963 }
964 } else {
965 0
966 };
967 buf.set_string(
968 node.pos.0.saturating_add(x_offset),
969 line_y as u32,
970 line,
971 node.style,
972 );
973 }
974 } else {
975 if sy < 0 {
976 return;
977 }
978 let text_width = UnicodeWidthStr::width(text.as_str()) as u32;
979 let x_offset = if text_width < node.size.0 {
980 match node.align {
981 Align::Start => 0,
982 Align::Center => (node.size.0 - text_width) / 2,
983 Align::End => node.size.0 - text_width,
984 }
985 } else {
986 0
987 };
988 buf.set_string(
989 node.pos.0.saturating_add(x_offset),
990 sy as u32,
991 text,
992 node.style,
993 );
994 }
995 }
996 }
997 NodeKind::Spacer => {}
998 NodeKind::Container(_) => {
999 render_container_border(node, buf, y_offset);
1000 if node.is_scrollable {
1001 let Some(area) = visible_area(node, y_offset) else {
1002 return;
1003 };
1004 let inner = inner_area(node, area);
1005 let child_offset = y_offset.saturating_add(node.scroll_offset);
1006 let render_y_start = inner.y as i64;
1007 let render_y_end = inner.bottom() as i64;
1008 buf.push_clip(inner);
1009 for child in &node.children {
1010 let child_top = child.pos.1 as i64 - child_offset as i64;
1011 let child_bottom = child_top + child.size.1 as i64;
1012 if child_bottom <= render_y_start || child_top >= render_y_end {
1013 continue;
1014 }
1015 render_inner(child, buf, child_offset);
1016 }
1017 buf.pop_clip();
1018 render_scroll_indicators(node, inner, buf);
1019 } else {
1020 let Some(area) = visible_area(node, y_offset) else {
1021 return;
1022 };
1023 let clip = inner_area(node, area);
1024 buf.push_clip(clip);
1025 for child in &node.children {
1026 render_inner(child, buf, y_offset);
1027 }
1028 buf.pop_clip();
1029 }
1030 }
1031 }
1032}
1033
1034fn render_container_border(node: &LayoutNode, buf: &mut Buffer, y_offset: u32) {
1035 let Some(border) = node.border else {
1036 return;
1037 };
1038 let chars = border.chars();
1039 let x = node.pos.0;
1040 let w = node.size.0;
1041 let h = node.size.1;
1042 if w == 0 || h == 0 {
1043 return;
1044 }
1045
1046 let top_i = screen_y(node.pos.1, y_offset);
1047 let bottom_i = top_i + h as i64 - 1;
1048 if bottom_i < 0 {
1049 return;
1050 }
1051 let right = x + w - 1;
1052
1053 if w == 1 && h == 1 {
1054 if top_i >= 0 {
1055 buf.set_char(x, top_i as u32, chars.tl, node.border_style);
1056 }
1057 } else if h == 1 {
1058 if top_i >= 0 {
1059 let y = top_i as u32;
1060 for xx in x..=right {
1061 buf.set_char(xx, y, chars.h, node.border_style);
1062 }
1063 }
1064 } else if w == 1 {
1065 let vert_start = (top_i.max(0)) as u32;
1066 let vert_end = bottom_i as u32;
1067 for yy in vert_start..=vert_end {
1068 buf.set_char(x, yy, chars.v, node.border_style);
1069 }
1070 } else {
1071 if top_i >= 0 {
1072 let y = top_i as u32;
1073 buf.set_char(x, y, chars.tl, node.border_style);
1074 buf.set_char(right, y, chars.tr, node.border_style);
1075 for xx in (x + 1)..right {
1076 buf.set_char(xx, y, chars.h, node.border_style);
1077 }
1078 }
1079
1080 let bot = bottom_i as u32;
1081 buf.set_char(x, bot, chars.bl, node.border_style);
1082 buf.set_char(right, bot, chars.br, node.border_style);
1083 for xx in (x + 1)..right {
1084 buf.set_char(xx, bot, chars.h, node.border_style);
1085 }
1086
1087 let vert_start = ((top_i + 1).max(0)) as u32;
1088 let vert_end = bottom_i as u32;
1089 for yy in vert_start..vert_end {
1090 buf.set_char(x, yy, chars.v, node.border_style);
1091 buf.set_char(right, yy, chars.v, node.border_style);
1092 }
1093 }
1094
1095 if top_i >= 0 {
1096 if let Some((title, title_style)) = &node.title {
1097 let y = top_i as u32;
1098 let title_x = x.saturating_add(2);
1099 if title_x <= right {
1100 let max_width = (right - title_x + 1) as usize;
1101 let trimmed: String = title.chars().take(max_width).collect();
1102 buf.set_string(title_x, y, &trimmed, *title_style);
1103 }
1104 }
1105 }
1106}
1107
1108fn render_scroll_indicators(node: &LayoutNode, inner: Rect, buf: &mut Buffer) {
1109 if inner.width == 0 || inner.height == 0 {
1110 return;
1111 }
1112
1113 let indicator_x = inner.right() - 1;
1114 if node.scroll_offset > 0 {
1115 buf.set_char(indicator_x, inner.y, '▲', node.border_style);
1116 }
1117 if node.scroll_offset.saturating_add(inner.height) < node.content_height {
1118 buf.set_char(indicator_x, inner.bottom() - 1, '▼', node.border_style);
1119 }
1120}
1121
1122pub(crate) fn collect_scroll_infos(node: &LayoutNode) -> Vec<(u32, u32)> {
1123 let mut infos = Vec::new();
1124 collect_scroll_infos_inner(node, &mut infos);
1125 infos
1126}
1127
1128pub(crate) fn collect_hit_areas(node: &LayoutNode) -> Vec<Rect> {
1129 let mut areas = Vec::new();
1130 for child in &node.children {
1131 collect_hit_areas_inner(child, &mut areas);
1132 }
1133 areas
1134}
1135
1136fn collect_scroll_infos_inner(node: &LayoutNode, infos: &mut Vec<(u32, u32)>) {
1137 if node.is_scrollable {
1138 let viewport_h = node.size.1.saturating_sub(node.frame_vertical());
1139 infos.push((node.content_height, viewport_h));
1140 }
1141 for child in &node.children {
1142 collect_scroll_infos_inner(child, infos);
1143 }
1144}
1145
1146fn collect_hit_areas_inner(node: &LayoutNode, areas: &mut Vec<Rect>) {
1147 if matches!(node.kind, NodeKind::Container(_)) {
1148 areas.push(Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1));
1149 }
1150 for child in &node.children {
1151 collect_hit_areas_inner(child, areas);
1152 }
1153}
1154
1155#[cfg(test)]
1156mod tests {
1157 use super::wrap_lines;
1158
1159 #[test]
1160 fn wrap_empty() {
1161 assert_eq!(wrap_lines("", 10), vec![""]);
1162 }
1163
1164 #[test]
1165 fn wrap_fits() {
1166 assert_eq!(wrap_lines("hello", 10), vec!["hello"]);
1167 }
1168
1169 #[test]
1170 fn wrap_word_boundary() {
1171 assert_eq!(wrap_lines("hello world", 7), vec!["hello", "world"]);
1172 }
1173
1174 #[test]
1175 fn wrap_multiple_words() {
1176 assert_eq!(
1177 wrap_lines("one two three four", 9),
1178 vec!["one two", "three", "four"]
1179 );
1180 }
1181
1182 #[test]
1183 fn wrap_long_word() {
1184 assert_eq!(wrap_lines("abcdefghij", 4), vec!["abcd", "efgh", "ij"]);
1185 }
1186
1187 #[test]
1188 fn wrap_zero_width() {
1189 assert_eq!(wrap_lines("hello", 0), vec!["hello"]);
1190 }
1191
1192 #[test]
1193 fn diagnostic_demo_layout() {
1194 use super::{compute, ContainerConfig, Direction, LayoutNode};
1195 use crate::rect::Rect;
1196 use crate::style::{Align, Border, Constraints, Margin, Padding, Style};
1197
1198 let mut root = LayoutNode::container(
1211 Direction::Column,
1212 ContainerConfig {
1213 gap: 0,
1214 align: Align::Start,
1215 border: None,
1216 border_style: Style::new(),
1217 padding: Padding::default(),
1218 margin: Margin::default(),
1219 constraints: Constraints::default(),
1220 title: None,
1221 grow: 0,
1222 },
1223 );
1224
1225 let mut outer_container = LayoutNode::container(
1227 Direction::Column,
1228 ContainerConfig {
1229 gap: 0,
1230 align: Align::Start,
1231 border: Some(Border::Rounded),
1232 border_style: Style::new(),
1233 padding: Padding::all(1),
1234 margin: Margin::default(),
1235 constraints: Constraints::default(),
1236 title: None,
1237 grow: 1,
1238 },
1239 );
1240
1241 outer_container.children.push(LayoutNode::text(
1243 "header".to_string(),
1244 Style::new(),
1245 0,
1246 Align::Start,
1247 false,
1248 Margin::default(),
1249 Constraints::default(),
1250 ));
1251
1252 outer_container.children.push(LayoutNode::text(
1254 "separator".to_string(),
1255 Style::new(),
1256 0,
1257 Align::Start,
1258 false,
1259 Margin::default(),
1260 Constraints::default(),
1261 ));
1262
1263 let mut inner_container = LayoutNode::container(
1265 Direction::Column,
1266 ContainerConfig {
1267 gap: 0,
1268 align: Align::Start,
1269 border: None,
1270 border_style: Style::new(),
1271 padding: Padding::default(),
1272 margin: Margin::default(),
1273 constraints: Constraints::default(),
1274 title: None,
1275 grow: 1,
1276 },
1277 );
1278
1279 inner_container.children.push(LayoutNode::text(
1281 "content1".to_string(),
1282 Style::new(),
1283 0,
1284 Align::Start,
1285 false,
1286 Margin::default(),
1287 Constraints::default(),
1288 ));
1289 inner_container.children.push(LayoutNode::text(
1290 "content2".to_string(),
1291 Style::new(),
1292 0,
1293 Align::Start,
1294 false,
1295 Margin::default(),
1296 Constraints::default(),
1297 ));
1298 inner_container.children.push(LayoutNode::text(
1299 "content3".to_string(),
1300 Style::new(),
1301 0,
1302 Align::Start,
1303 false,
1304 Margin::default(),
1305 Constraints::default(),
1306 ));
1307
1308 outer_container.children.push(inner_container);
1309
1310 outer_container.children.push(LayoutNode::text(
1312 "separator2".to_string(),
1313 Style::new(),
1314 0,
1315 Align::Start,
1316 false,
1317 Margin::default(),
1318 Constraints::default(),
1319 ));
1320
1321 outer_container.children.push(LayoutNode::text(
1323 "footer".to_string(),
1324 Style::new(),
1325 0,
1326 Align::Start,
1327 false,
1328 Margin::default(),
1329 Constraints::default(),
1330 ));
1331
1332 root.children.push(outer_container);
1333
1334 compute(&mut root, Rect::new(0, 0, 80, 50));
1336
1337 eprintln!("\n=== DIAGNOSTIC LAYOUT TEST ===");
1339 eprintln!("Root node:");
1340 eprintln!(" pos: {:?}, size: {:?}", root.pos, root.size);
1341
1342 let outer = &root.children[0];
1343 eprintln!("\nOuter bordered container (grow:1):");
1344 eprintln!(" pos: {:?}, size: {:?}", outer.pos, outer.size);
1345
1346 let inner = &outer.children[2];
1347 eprintln!("\nInner container (grow:1, simulates scrollable):");
1348 eprintln!(" pos: {:?}, size: {:?}", inner.pos, inner.size);
1349
1350 eprintln!("\nAll children of outer container:");
1351 for (i, child) in outer.children.iter().enumerate() {
1352 eprintln!(" [{}] pos: {:?}, size: {:?}", i, child.pos, child.size);
1353 }
1354
1355 assert_eq!(
1358 root.size,
1359 (80, 50),
1360 "Root node should fill entire terminal (80x50)"
1361 );
1362
1363 assert_eq!(
1365 outer.size,
1366 (80, 50),
1367 "Outer bordered container should fill entire terminal (80x50)"
1368 );
1369
1370 let expected_inner_height = 50 - 2 - 2 - 4;
1377 assert_eq!(
1378 inner.size.1, expected_inner_height as u32,
1379 "Inner container height should be {} (50 - border(2) - padding(2) - fixed(4))",
1380 expected_inner_height
1381 );
1382
1383 let expected_inner_y = 1 + 1 + 1 + 1;
1385 assert_eq!(
1386 inner.pos.1, expected_inner_y as u32,
1387 "Inner container should start at y={} (border+padding+header+sep)",
1388 expected_inner_y
1389 );
1390
1391 eprintln!("\n✓ All assertions passed!");
1392 eprintln!(" Root size: {:?}", root.size);
1393 eprintln!(" Outer container size: {:?}", outer.size);
1394 eprintln!(" Inner container size: {:?}", inner.size);
1395 eprintln!(" Inner container pos: {:?}", inner.pos);
1396 }
1397}