Skip to main content

slt/
layout.rs

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                &current_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        &current_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        // Build the tree structure matching demo.rs:
1254        // Root (Column, grow:0)
1255        //   └─ Container (Column, grow:1, border:Rounded, padding:all(1))
1256        //        ├─ Text "header" (grow:0)
1257        //        ├─ Text "separator" (grow:0)
1258        //        ├─ Container (Column, grow:1)  ← simulates scrollable
1259        //        │    ├─ Text "content1" (grow:0)
1260        //        │    ├─ Text "content2" (grow:0)
1261        //        │    └─ Text "content3" (grow:0)
1262        //        ├─ Text "separator2" (grow:0)
1263        //        └─ Text "footer" (grow:0)
1264
1265        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        // Outer bordered container with grow:1
1281        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        // Header text
1297        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        // Separator 1
1308        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        // Inner scrollable-like container with grow:1
1319        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        // Content items
1335        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        // Separator 2
1366        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        // Footer
1377        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 layout with 80x50 terminal
1390        compute(&mut root, Rect::new(0, 0, 80, 50));
1391
1392        // Debug output
1393        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        // Assertions
1411        // Root should fill the entire 80x50 area
1412        assert_eq!(
1413            root.size,
1414            (80, 50),
1415            "Root node should fill entire terminal (80x50)"
1416        );
1417
1418        // Outer container should also be 80x50 (full height due to grow:1)
1419        assert_eq!(
1420            outer.size,
1421            (80, 50),
1422            "Outer bordered container should fill entire terminal (80x50)"
1423        );
1424
1425        // Calculate expected inner container height:
1426        // Available height = 50 (total)
1427        // Border inset = 1 (top) + 1 (bottom) = 2
1428        // Padding = 1 (top) + 1 (bottom) = 2
1429        // Fixed children heights: header(1) + sep(1) + sep2(1) + footer(1) = 4
1430        // Expected inner height = 50 - 2 - 2 - 4 = 42
1431        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        // Inner container should start at y = border(1) + padding(1) + header(1) + sep(1) = 4
1439        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}