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