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