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