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