Skip to main content

slt/
layout.rs

1//! Flexbox layout engine: builds a tree from commands, computes positions,
2//! and renders to a [`Buffer`].
3
4use crate::buffer::Buffer;
5use crate::rect::Rect;
6use crate::style::{
7    Align, Border, BorderSides, Color, Constraints, Justify, Margin, Padding, Style,
8};
9use unicode_width::UnicodeWidthChar;
10use unicode_width::UnicodeWidthStr;
11
12/// Main axis direction for a container's children.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Direction {
15    /// Lay out children horizontally (left to right).
16    Row,
17    /// Lay out children vertically (top to bottom).
18    Column,
19}
20
21#[derive(Debug, Clone)]
22pub(crate) enum Command {
23    Text {
24        content: String,
25        style: Style,
26        grow: u16,
27        align: Align,
28        wrap: bool,
29        margin: Margin,
30        constraints: Constraints,
31    },
32    BeginContainer {
33        direction: Direction,
34        gap: u32,
35        align: Align,
36        justify: Justify,
37        border: Option<Border>,
38        border_sides: BorderSides,
39        border_style: Style,
40        bg_color: Option<Color>,
41        padding: Padding,
42        margin: Margin,
43        constraints: Constraints,
44        title: Option<(String, Style)>,
45        grow: u16,
46        group_name: Option<String>,
47    },
48    BeginScrollable {
49        grow: u16,
50        border: Option<Border>,
51        border_sides: BorderSides,
52        border_style: Style,
53        padding: Padding,
54        margin: Margin,
55        constraints: Constraints,
56        title: Option<(String, Style)>,
57        scroll_offset: u32,
58    },
59    Link {
60        text: String,
61        url: String,
62        style: Style,
63        margin: Margin,
64        constraints: Constraints,
65    },
66    RichText {
67        segments: Vec<(String, Style)>,
68        wrap: bool,
69        align: Align,
70        margin: Margin,
71        constraints: Constraints,
72    },
73    EndContainer,
74    BeginOverlay {
75        modal: bool,
76    },
77    EndOverlay,
78    Spacer {
79        grow: u16,
80    },
81    FocusMarker(usize),
82}
83
84#[derive(Debug, Clone)]
85struct OverlayLayer {
86    node: LayoutNode,
87    modal: bool,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91enum NodeKind {
92    Text,
93    Container(Direction),
94    Spacer,
95}
96
97#[derive(Debug, Clone)]
98pub(crate) struct LayoutNode {
99    kind: NodeKind,
100    content: Option<String>,
101    style: Style,
102    pub grow: u16,
103    align: Align,
104    justify: Justify,
105    wrap: bool,
106    gap: u32,
107    border: Option<Border>,
108    border_sides: BorderSides,
109    border_style: Style,
110    bg_color: Option<Color>,
111    padding: Padding,
112    margin: Margin,
113    constraints: Constraints,
114    title: Option<(String, Style)>,
115    children: Vec<LayoutNode>,
116    pos: (u32, u32),
117    size: (u32, u32),
118    is_scrollable: bool,
119    scroll_offset: u32,
120    content_height: u32,
121    cached_wrapped: Option<Vec<String>>,
122    segments: Option<Vec<(String, Style)>>,
123    cached_wrapped_segments: Option<Vec<Vec<(String, Style)>>>,
124    pub(crate) focus_id: Option<usize>,
125    link_url: Option<String>,
126    group_name: Option<String>,
127    overlays: Vec<OverlayLayer>,
128}
129
130#[derive(Debug, Clone)]
131struct ContainerConfig {
132    gap: u32,
133    align: Align,
134    justify: Justify,
135    border: Option<Border>,
136    border_sides: BorderSides,
137    border_style: Style,
138    bg_color: Option<Color>,
139    padding: Padding,
140    margin: Margin,
141    constraints: Constraints,
142    title: Option<(String, Style)>,
143    grow: u16,
144}
145
146impl LayoutNode {
147    fn text(
148        content: String,
149        style: Style,
150        grow: u16,
151        align: Align,
152        wrap: bool,
153        margin: Margin,
154        constraints: Constraints,
155    ) -> Self {
156        let width = UnicodeWidthStr::width(content.as_str()) as u32;
157        Self {
158            kind: NodeKind::Text,
159            content: Some(content),
160            style,
161            grow,
162            align,
163            justify: Justify::Start,
164            wrap,
165            gap: 0,
166            border: None,
167            border_sides: BorderSides::all(),
168            border_style: Style::new(),
169            bg_color: None,
170            padding: Padding::default(),
171            margin,
172            constraints,
173            title: None,
174            children: Vec::new(),
175            pos: (0, 0),
176            size: (width, 1),
177            is_scrollable: false,
178            scroll_offset: 0,
179            content_height: 0,
180            cached_wrapped: None,
181            segments: None,
182            cached_wrapped_segments: None,
183            focus_id: None,
184            link_url: None,
185            group_name: None,
186            overlays: Vec::new(),
187        }
188    }
189
190    fn rich_text(
191        segments: Vec<(String, Style)>,
192        wrap: bool,
193        align: Align,
194        margin: Margin,
195        constraints: Constraints,
196    ) -> Self {
197        let width: u32 = segments
198            .iter()
199            .map(|(s, _)| UnicodeWidthStr::width(s.as_str()) as u32)
200            .sum();
201        Self {
202            kind: NodeKind::Text,
203            content: None,
204            style: Style::new(),
205            grow: 0,
206            align,
207            justify: Justify::Start,
208            wrap,
209            gap: 0,
210            border: None,
211            border_sides: BorderSides::all(),
212            border_style: Style::new(),
213            bg_color: None,
214            padding: Padding::default(),
215            margin,
216            constraints,
217            title: None,
218            children: Vec::new(),
219            pos: (0, 0),
220            size: (width, 1),
221            is_scrollable: false,
222            scroll_offset: 0,
223            content_height: 0,
224            cached_wrapped: None,
225            segments: Some(segments),
226            cached_wrapped_segments: None,
227            focus_id: None,
228            link_url: None,
229            group_name: None,
230            overlays: Vec::new(),
231        }
232    }
233
234    fn container(direction: Direction, config: ContainerConfig) -> Self {
235        Self {
236            kind: NodeKind::Container(direction),
237            content: None,
238            style: Style::new(),
239            grow: config.grow,
240            align: config.align,
241            justify: config.justify,
242            wrap: false,
243            gap: config.gap,
244            border: config.border,
245            border_sides: config.border_sides,
246            border_style: config.border_style,
247            bg_color: config.bg_color,
248            padding: config.padding,
249            margin: config.margin,
250            constraints: config.constraints,
251            title: config.title,
252            children: Vec::new(),
253            pos: (0, 0),
254            size: (0, 0),
255            is_scrollable: false,
256            scroll_offset: 0,
257            content_height: 0,
258            cached_wrapped: None,
259            segments: None,
260            cached_wrapped_segments: None,
261            focus_id: None,
262            link_url: None,
263            group_name: None,
264            overlays: Vec::new(),
265        }
266    }
267
268    fn spacer(grow: u16) -> Self {
269        Self {
270            kind: NodeKind::Spacer,
271            content: None,
272            style: Style::new(),
273            grow,
274            align: Align::Start,
275            justify: Justify::Start,
276            wrap: false,
277            gap: 0,
278            border: None,
279            border_sides: BorderSides::all(),
280            border_style: Style::new(),
281            bg_color: None,
282            padding: Padding::default(),
283            margin: Margin::default(),
284            constraints: Constraints::default(),
285            title: None,
286            children: Vec::new(),
287            pos: (0, 0),
288            size: (0, 0),
289            is_scrollable: false,
290            scroll_offset: 0,
291            content_height: 0,
292            cached_wrapped: None,
293            segments: None,
294            cached_wrapped_segments: None,
295            focus_id: None,
296            link_url: None,
297            group_name: None,
298            overlays: Vec::new(),
299        }
300    }
301
302    fn border_inset(&self) -> u32 {
303        if self.border.is_some() {
304            1
305        } else {
306            0
307        }
308    }
309
310    fn border_left_inset(&self) -> u32 {
311        if self.border.is_some() && self.border_sides.left {
312            1
313        } else {
314            0
315        }
316    }
317
318    fn border_right_inset(&self) -> u32 {
319        if self.border.is_some() && self.border_sides.right {
320            1
321        } else {
322            0
323        }
324    }
325
326    fn border_top_inset(&self) -> u32 {
327        if self.border.is_some() && self.border_sides.top {
328            1
329        } else {
330            0
331        }
332    }
333
334    fn border_bottom_inset(&self) -> u32 {
335        if self.border.is_some() && self.border_sides.bottom {
336            1
337        } else {
338            0
339        }
340    }
341
342    fn frame_horizontal(&self) -> u32 {
343        self.padding.horizontal() + self.border_left_inset() + self.border_right_inset()
344    }
345
346    fn frame_vertical(&self) -> u32 {
347        self.padding.vertical() + self.border_top_inset() + self.border_bottom_inset()
348    }
349
350    fn min_width(&self) -> u32 {
351        let width = match self.kind {
352            NodeKind::Text => self.size.0,
353            NodeKind::Spacer => 0,
354            NodeKind::Container(Direction::Row) => {
355                let gaps = if self.children.is_empty() {
356                    0
357                } else {
358                    (self.children.len() as u32 - 1) * self.gap
359                };
360                let children_width: u32 = self.children.iter().map(|c| c.min_width()).sum();
361                children_width + gaps + self.frame_horizontal()
362            }
363            NodeKind::Container(Direction::Column) => {
364                self.children
365                    .iter()
366                    .map(|c| c.min_width())
367                    .max()
368                    .unwrap_or(0)
369                    + self.frame_horizontal()
370            }
371        };
372
373        let width = width.max(self.constraints.min_width.unwrap_or(0));
374        let width = match self.constraints.max_width {
375            Some(max_w) => width.min(max_w),
376            None => width,
377        };
378        width.saturating_add(self.margin.horizontal())
379    }
380
381    fn min_height(&self) -> u32 {
382        let height = match self.kind {
383            NodeKind::Text => 1,
384            NodeKind::Spacer => 0,
385            NodeKind::Container(Direction::Row) => {
386                self.children
387                    .iter()
388                    .map(|c| c.min_height())
389                    .max()
390                    .unwrap_or(0)
391                    + self.frame_vertical()
392            }
393            NodeKind::Container(Direction::Column) => {
394                let gaps = if self.children.is_empty() {
395                    0
396                } else {
397                    (self.children.len() as u32 - 1) * self.gap
398                };
399                let children_height: u32 = self.children.iter().map(|c| c.min_height()).sum();
400                children_height + gaps + self.frame_vertical()
401            }
402        };
403
404        let height = height.max(self.constraints.min_height.unwrap_or(0));
405        height.saturating_add(self.margin.vertical())
406    }
407
408    fn min_height_for_width(&self, available_width: u32) -> u32 {
409        match self.kind {
410            NodeKind::Text if self.wrap => {
411                let inner_width = available_width.saturating_sub(self.margin.horizontal());
412                let lines = if let Some(ref segs) = self.segments {
413                    wrap_segments(segs, inner_width).len().max(1) as u32
414                } else {
415                    let text = self.content.as_deref().unwrap_or("");
416                    wrap_lines(text, inner_width).len().max(1) as u32
417                };
418                lines.saturating_add(self.margin.vertical())
419            }
420            _ => self.min_height(),
421        }
422    }
423}
424
425fn wrap_lines(text: &str, max_width: u32) -> Vec<String> {
426    if text.is_empty() {
427        return vec![String::new()];
428    }
429    if max_width == 0 {
430        return vec![text.to_string()];
431    }
432
433    fn split_long_word(word: &str, max_width: u32) -> Vec<(String, u32)> {
434        let mut chunks: Vec<(String, u32)> = Vec::new();
435        let mut chunk = String::new();
436        let mut chunk_width = 0_u32;
437
438        for ch in word.chars() {
439            let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
440            if chunk.is_empty() {
441                if ch_width > max_width {
442                    chunks.push((ch.to_string(), ch_width));
443                } else {
444                    chunk.push(ch);
445                    chunk_width = ch_width;
446                }
447                continue;
448            }
449
450            if chunk_width + ch_width > max_width {
451                chunks.push((std::mem::take(&mut chunk), chunk_width));
452                if ch_width > max_width {
453                    chunks.push((ch.to_string(), ch_width));
454                    chunk_width = 0;
455                } else {
456                    chunk.push(ch);
457                    chunk_width = ch_width;
458                }
459            } else {
460                chunk.push(ch);
461                chunk_width += ch_width;
462            }
463        }
464
465        if !chunk.is_empty() {
466            chunks.push((chunk, chunk_width));
467        }
468
469        chunks
470    }
471
472    fn push_word_into_line(
473        lines: &mut Vec<String>,
474        current_line: &mut String,
475        current_width: &mut u32,
476        word: &str,
477        word_width: u32,
478        max_width: u32,
479    ) {
480        if word.is_empty() {
481            return;
482        }
483
484        if word_width > max_width {
485            let chunks = split_long_word(word, max_width);
486            for (chunk, chunk_width) in chunks {
487                if current_line.is_empty() {
488                    *current_line = chunk;
489                    *current_width = chunk_width;
490                } else if *current_width + 1 + chunk_width <= max_width {
491                    current_line.push(' ');
492                    current_line.push_str(&chunk);
493                    *current_width += 1 + chunk_width;
494                } else {
495                    lines.push(std::mem::take(current_line));
496                    *current_line = chunk;
497                    *current_width = chunk_width;
498                }
499            }
500            return;
501        }
502
503        if current_line.is_empty() {
504            *current_line = word.to_string();
505            *current_width = word_width;
506        } else if *current_width + 1 + word_width <= max_width {
507            current_line.push(' ');
508            current_line.push_str(word);
509            *current_width += 1 + word_width;
510        } else {
511            lines.push(std::mem::take(current_line));
512            *current_line = word.to_string();
513            *current_width = word_width;
514        }
515    }
516
517    let mut lines: Vec<String> = Vec::new();
518    let mut current_line = String::new();
519    let mut current_width: u32 = 0;
520    let mut current_word = String::new();
521    let mut word_width: u32 = 0;
522
523    for ch in text.chars() {
524        if ch == ' ' {
525            push_word_into_line(
526                &mut lines,
527                &mut current_line,
528                &mut current_width,
529                &current_word,
530                word_width,
531                max_width,
532            );
533            current_word.clear();
534            word_width = 0;
535            continue;
536        }
537
538        current_word.push(ch);
539        word_width += UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
540    }
541
542    push_word_into_line(
543        &mut lines,
544        &mut current_line,
545        &mut current_width,
546        &current_word,
547        word_width,
548        max_width,
549    );
550
551    if !current_line.is_empty() {
552        lines.push(current_line);
553    }
554
555    if lines.is_empty() {
556        vec![String::new()]
557    } else {
558        lines
559    }
560}
561
562fn wrap_segments(segments: &[(String, Style)], max_width: u32) -> Vec<Vec<(String, Style)>> {
563    if max_width == 0 || segments.is_empty() {
564        return vec![vec![]];
565    }
566    let mut chars: Vec<(char, Style)> = Vec::new();
567    for (text, style) in segments {
568        for ch in text.chars() {
569            chars.push((ch, *style));
570        }
571    }
572    if chars.is_empty() {
573        return vec![vec![]];
574    }
575
576    let mut lines: Vec<Vec<(String, Style)>> = Vec::new();
577    let mut i = 0;
578    while i < chars.len() {
579        let mut line_chars: Vec<(char, Style)> = Vec::new();
580        let mut line_width: u32 = 0;
581
582        if !lines.is_empty() {
583            while i < chars.len() && chars[i].0 == ' ' {
584                i += 1;
585            }
586        }
587
588        while i < chars.len() {
589            let (ch, st) = chars[i];
590            let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
591            if line_width + ch_width > max_width && line_width > 0 {
592                if let Some(bp) = line_chars.iter().rposition(|(c, _)| *c == ' ') {
593                    let rewind = line_chars.len() - bp - 1;
594                    i -= rewind;
595                    line_chars.truncate(bp);
596                }
597                break;
598            }
599            line_chars.push((ch, st));
600            line_width += ch_width;
601            i += 1;
602        }
603
604        let mut line_segs: Vec<(String, Style)> = Vec::new();
605        let mut cur = String::new();
606        let mut cur_style: Option<Style> = None;
607        for (ch, st) in &line_chars {
608            if cur_style == Some(*st) {
609                cur.push(*ch);
610            } else {
611                if let Some(s) = cur_style {
612                    if !cur.is_empty() {
613                        line_segs.push((std::mem::take(&mut cur), s));
614                    }
615                }
616                cur_style = Some(*st);
617                cur.push(*ch);
618            }
619        }
620        if let Some(s) = cur_style {
621            if !cur.is_empty() {
622                let trimmed = cur.trim_end().to_string();
623                if !trimmed.is_empty() {
624                    line_segs.push((trimmed, s));
625                } else if !line_segs.is_empty() {
626                    if let Some(last) = line_segs.last_mut() {
627                        let t = last.0.trim_end().to_string();
628                        if t.is_empty() {
629                            line_segs.pop();
630                        } else {
631                            last.0 = t;
632                        }
633                    }
634                }
635            }
636        }
637        lines.push(line_segs);
638    }
639    if lines.is_empty() {
640        vec![vec![]]
641    } else {
642        lines
643    }
644}
645
646pub(crate) fn build_tree(commands: &[Command]) -> LayoutNode {
647    let mut root = LayoutNode::container(Direction::Column, default_container_config());
648    let mut overlays: Vec<OverlayLayer> = Vec::new();
649    build_children(&mut root, commands, &mut 0, &mut overlays, false);
650    root.overlays = overlays;
651    root
652}
653
654fn default_container_config() -> ContainerConfig {
655    ContainerConfig {
656        gap: 0,
657        align: Align::Start,
658        justify: Justify::Start,
659        border: None,
660        border_sides: BorderSides::all(),
661        border_style: Style::new(),
662        bg_color: None,
663        padding: Padding::default(),
664        margin: Margin::default(),
665        constraints: Constraints::default(),
666        title: None,
667        grow: 0,
668    }
669}
670
671fn build_children(
672    parent: &mut LayoutNode,
673    commands: &[Command],
674    pos: &mut usize,
675    overlays: &mut Vec<OverlayLayer>,
676    stop_on_end_overlay: bool,
677) {
678    let mut pending_focus_id: Option<usize> = None;
679    while *pos < commands.len() {
680        match &commands[*pos] {
681            Command::FocusMarker(id) => {
682                pending_focus_id = Some(*id);
683                *pos += 1;
684            }
685            Command::Text {
686                content,
687                style,
688                grow,
689                align,
690                wrap,
691                margin,
692                constraints,
693            } => {
694                let mut node = LayoutNode::text(
695                    content.clone(),
696                    *style,
697                    *grow,
698                    *align,
699                    *wrap,
700                    *margin,
701                    *constraints,
702                );
703                node.focus_id = pending_focus_id.take();
704                parent.children.push(node);
705                *pos += 1;
706            }
707            Command::RichText {
708                segments,
709                wrap,
710                align,
711                margin,
712                constraints,
713            } => {
714                let mut node =
715                    LayoutNode::rich_text(segments.clone(), *wrap, *align, *margin, *constraints);
716                node.focus_id = pending_focus_id.take();
717                parent.children.push(node);
718                *pos += 1;
719            }
720            Command::Link {
721                text,
722                url,
723                style,
724                margin,
725                constraints,
726            } => {
727                let mut node = LayoutNode::text(
728                    text.clone(),
729                    *style,
730                    0,
731                    Align::Start,
732                    false,
733                    *margin,
734                    *constraints,
735                );
736                node.link_url = Some(url.clone());
737                node.focus_id = pending_focus_id.take();
738                parent.children.push(node);
739                *pos += 1;
740            }
741            Command::BeginContainer {
742                direction,
743                gap,
744                align,
745                justify,
746                border,
747                border_sides,
748                border_style,
749                bg_color,
750                padding,
751                margin,
752                constraints,
753                title,
754                grow,
755                group_name,
756            } => {
757                let mut node = LayoutNode::container(
758                    *direction,
759                    ContainerConfig {
760                        gap: *gap,
761                        align: *align,
762                        justify: *justify,
763                        border: *border,
764                        border_sides: *border_sides,
765                        border_style: *border_style,
766                        bg_color: *bg_color,
767                        padding: *padding,
768                        margin: *margin,
769                        constraints: *constraints,
770                        title: title.clone(),
771                        grow: *grow,
772                    },
773                );
774                node.focus_id = pending_focus_id.take();
775                node.group_name = group_name.clone();
776                *pos += 1;
777                build_children(&mut node, commands, pos, overlays, false);
778                parent.children.push(node);
779            }
780            Command::BeginScrollable {
781                grow,
782                border,
783                border_sides,
784                border_style,
785                padding,
786                margin,
787                constraints,
788                title,
789                scroll_offset,
790            } => {
791                let mut node = LayoutNode::container(
792                    Direction::Column,
793                    ContainerConfig {
794                        gap: 0,
795                        align: Align::Start,
796                        justify: Justify::Start,
797                        border: *border,
798                        border_sides: *border_sides,
799                        border_style: *border_style,
800                        bg_color: None,
801                        padding: *padding,
802                        margin: *margin,
803                        constraints: *constraints,
804                        title: title.clone(),
805                        grow: *grow,
806                    },
807                );
808                node.is_scrollable = true;
809                node.scroll_offset = *scroll_offset;
810                node.focus_id = pending_focus_id.take();
811                *pos += 1;
812                build_children(&mut node, commands, pos, overlays, false);
813                parent.children.push(node);
814            }
815            Command::BeginOverlay { modal } => {
816                *pos += 1;
817                let mut overlay_node =
818                    LayoutNode::container(Direction::Column, default_container_config());
819                build_children(&mut overlay_node, commands, pos, overlays, true);
820                overlays.push(OverlayLayer {
821                    node: overlay_node,
822                    modal: *modal,
823                });
824            }
825            Command::Spacer { grow } => {
826                parent.children.push(LayoutNode::spacer(*grow));
827                *pos += 1;
828            }
829            Command::EndContainer => {
830                *pos += 1;
831                return;
832            }
833            Command::EndOverlay => {
834                *pos += 1;
835                if stop_on_end_overlay {
836                    return;
837                }
838            }
839        }
840    }
841}
842
843pub(crate) fn compute(node: &mut LayoutNode, area: Rect) {
844    if let Some(pct) = node.constraints.width_pct {
845        let resolved = (area.width as u64 * pct.min(100) as u64 / 100) as u32;
846        node.constraints.min_width = Some(resolved);
847        node.constraints.max_width = Some(resolved);
848        node.constraints.width_pct = None;
849    }
850    if let Some(pct) = node.constraints.height_pct {
851        let resolved = (area.height as u64 * pct.min(100) as u64 / 100) as u32;
852        node.constraints.min_height = Some(resolved);
853        node.constraints.max_height = Some(resolved);
854        node.constraints.height_pct = None;
855    }
856
857    node.pos = (area.x, area.y);
858    node.size = (
859        area.width.clamp(
860            node.constraints.min_width.unwrap_or(0),
861            node.constraints.max_width.unwrap_or(u32::MAX),
862        ),
863        area.height.clamp(
864            node.constraints.min_height.unwrap_or(0),
865            node.constraints.max_height.unwrap_or(u32::MAX),
866        ),
867    );
868
869    if matches!(node.kind, NodeKind::Text) && node.wrap {
870        if let Some(ref segs) = node.segments {
871            let wrapped = wrap_segments(segs, area.width);
872            node.size = (area.width, wrapped.len().max(1) as u32);
873            node.cached_wrapped_segments = Some(wrapped);
874            node.cached_wrapped = None;
875        } else {
876            let lines = wrap_lines(node.content.as_deref().unwrap_or(""), area.width);
877            node.size = (area.width, lines.len().max(1) as u32);
878            node.cached_wrapped = Some(lines);
879            node.cached_wrapped_segments = None;
880        }
881    } else {
882        node.cached_wrapped = None;
883        node.cached_wrapped_segments = None;
884    }
885
886    match node.kind {
887        NodeKind::Text | NodeKind::Spacer => {}
888        NodeKind::Container(Direction::Row) => {
889            layout_row(
890                node,
891                inner_area(
892                    node,
893                    Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1),
894                ),
895            );
896            node.content_height = 0;
897        }
898        NodeKind::Container(Direction::Column) => {
899            let viewport_area = inner_area(
900                node,
901                Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1),
902            );
903            if node.is_scrollable {
904                let saved_grows: Vec<u16> = node.children.iter().map(|c| c.grow).collect();
905                for child in &mut node.children {
906                    child.grow = 0;
907                }
908                let total_gaps = if node.children.is_empty() {
909                    0
910                } else {
911                    (node.children.len() as u32 - 1) * node.gap
912                };
913                let natural_height: u32 = node
914                    .children
915                    .iter()
916                    .map(|c| c.min_height_for_width(viewport_area.width))
917                    .sum::<u32>()
918                    + total_gaps;
919
920                if natural_height > viewport_area.height {
921                    let virtual_area = Rect::new(
922                        viewport_area.x,
923                        viewport_area.y,
924                        viewport_area.width,
925                        natural_height,
926                    );
927                    layout_column(node, virtual_area);
928                } else {
929                    for (child, &grow) in node.children.iter_mut().zip(saved_grows.iter()) {
930                        child.grow = grow;
931                    }
932                    layout_column(node, viewport_area);
933                }
934                node.content_height = scroll_content_height(node, viewport_area.y);
935            } else {
936                layout_column(node, viewport_area);
937                node.content_height = 0;
938            }
939        }
940    }
941
942    for overlay in &mut node.overlays {
943        let width = overlay.node.min_width().min(area.width);
944        let height = overlay.node.min_height_for_width(width).min(area.height);
945        let x = area.x.saturating_add(area.width.saturating_sub(width) / 2);
946        let y = area
947            .y
948            .saturating_add(area.height.saturating_sub(height) / 2);
949        compute(&mut overlay.node, Rect::new(x, y, width, height));
950    }
951}
952
953fn scroll_content_height(node: &LayoutNode, inner_y: u32) -> u32 {
954    let Some(max_bottom) = node
955        .children
956        .iter()
957        .map(|child| {
958            child
959                .pos
960                .1
961                .saturating_add(child.size.1)
962                .saturating_add(child.margin.bottom)
963        })
964        .max()
965    else {
966        return 0;
967    };
968
969    max_bottom.saturating_sub(inner_y)
970}
971
972fn justify_offsets(justify: Justify, remaining: u32, n: u32, gap: u32) -> (u32, u32) {
973    if n <= 1 {
974        let start = match justify {
975            Justify::Center => remaining / 2,
976            Justify::End => remaining,
977            _ => 0,
978        };
979        return (start, gap);
980    }
981
982    match justify {
983        Justify::Start => (0, gap),
984        Justify::Center => (remaining.saturating_sub((n - 1) * gap) / 2, gap),
985        Justify::End => (remaining.saturating_sub((n - 1) * gap), gap),
986        Justify::SpaceBetween => (0, remaining / (n - 1)),
987        Justify::SpaceAround => {
988            let slot = remaining / n;
989            (slot / 2, slot)
990        }
991        Justify::SpaceEvenly => {
992            let slot = remaining / (n + 1);
993            (slot, slot)
994        }
995    }
996}
997
998fn inner_area(node: &LayoutNode, area: Rect) -> Rect {
999    let x = area.x + node.border_left_inset() + node.padding.left;
1000    let y = area.y + node.border_top_inset() + node.padding.top;
1001    let width = area
1002        .width
1003        .saturating_sub(node.border_left_inset() + node.border_right_inset())
1004        .saturating_sub(node.padding.horizontal());
1005    let height = area
1006        .height
1007        .saturating_sub(node.border_top_inset() + node.border_bottom_inset())
1008        .saturating_sub(node.padding.vertical());
1009
1010    Rect::new(x, y, width, height)
1011}
1012
1013fn layout_row(node: &mut LayoutNode, area: Rect) {
1014    if node.children.is_empty() {
1015        return;
1016    }
1017
1018    for child in &mut node.children {
1019        if let Some(pct) = child.constraints.width_pct {
1020            let resolved = (area.width as u64 * pct.min(100) as u64 / 100) as u32;
1021            child.constraints.min_width = Some(resolved);
1022            child.constraints.max_width = Some(resolved);
1023            child.constraints.width_pct = None;
1024        }
1025        if let Some(pct) = child.constraints.height_pct {
1026            let resolved = (area.height as u64 * pct.min(100) as u64 / 100) as u32;
1027            child.constraints.min_height = Some(resolved);
1028            child.constraints.max_height = Some(resolved);
1029            child.constraints.height_pct = None;
1030        }
1031    }
1032
1033    let n = node.children.len() as u32;
1034    let total_gaps = (n - 1) * node.gap;
1035    let available = area.width.saturating_sub(total_gaps);
1036    let min_widths: Vec<u32> = node
1037        .children
1038        .iter()
1039        .map(|child| child.min_width())
1040        .collect();
1041
1042    let mut total_grow: u32 = 0;
1043    let mut fixed_width: u32 = 0;
1044    for (child, &min_width) in node.children.iter().zip(min_widths.iter()) {
1045        if child.grow > 0 {
1046            total_grow += child.grow as u32;
1047        } else {
1048            fixed_width += min_width;
1049        }
1050    }
1051
1052    let mut flex_space = available.saturating_sub(fixed_width);
1053    let mut remaining_grow = total_grow;
1054
1055    let mut child_widths: Vec<u32> = Vec::with_capacity(node.children.len());
1056    for (i, child) in node.children.iter().enumerate() {
1057        let w = if child.grow > 0 && total_grow > 0 {
1058            let share = if remaining_grow == 0 {
1059                0
1060            } else {
1061                flex_space * child.grow as u32 / remaining_grow
1062            };
1063            flex_space = flex_space.saturating_sub(share);
1064            remaining_grow = remaining_grow.saturating_sub(child.grow as u32);
1065            share
1066        } else {
1067            min_widths[i].min(available)
1068        };
1069        child_widths.push(w);
1070    }
1071
1072    let total_children_width: u32 = child_widths.iter().sum();
1073    let remaining = area.width.saturating_sub(total_children_width);
1074    let (start_offset, inter_gap) = justify_offsets(node.justify, remaining, n, node.gap);
1075
1076    let mut x = area.x + start_offset;
1077    for (i, child) in node.children.iter_mut().enumerate() {
1078        let w = child_widths[i];
1079        let child_outer_h = match node.align {
1080            Align::Start => area.height,
1081            _ => child.min_height_for_width(w).min(area.height),
1082        };
1083        let child_x = x.saturating_add(child.margin.left);
1084        let child_y = area.y.saturating_add(child.margin.top);
1085        let child_w = w.saturating_sub(child.margin.horizontal());
1086        let child_h = child_outer_h.saturating_sub(child.margin.vertical());
1087        compute(child, Rect::new(child_x, child_y, child_w, child_h));
1088        let child_total_h = child.size.1.saturating_add(child.margin.vertical());
1089        let y_offset = match node.align {
1090            Align::Start => 0,
1091            Align::Center => area.height.saturating_sub(child_total_h) / 2,
1092            Align::End => area.height.saturating_sub(child_total_h),
1093        };
1094        child.pos.1 = child.pos.1.saturating_add(y_offset);
1095        x += w + inter_gap;
1096    }
1097}
1098
1099fn layout_column(node: &mut LayoutNode, area: Rect) {
1100    if node.children.is_empty() {
1101        return;
1102    }
1103
1104    for child in &mut node.children {
1105        if let Some(pct) = child.constraints.width_pct {
1106            let resolved = (area.width as u64 * pct.min(100) as u64 / 100) as u32;
1107            child.constraints.min_width = Some(resolved);
1108            child.constraints.max_width = Some(resolved);
1109            child.constraints.width_pct = None;
1110        }
1111        if let Some(pct) = child.constraints.height_pct {
1112            let resolved = (area.height as u64 * pct.min(100) as u64 / 100) as u32;
1113            child.constraints.min_height = Some(resolved);
1114            child.constraints.max_height = Some(resolved);
1115            child.constraints.height_pct = None;
1116        }
1117    }
1118
1119    let n = node.children.len() as u32;
1120    let total_gaps = (n - 1) * node.gap;
1121    let available = area.height.saturating_sub(total_gaps);
1122    let min_heights: Vec<u32> = node
1123        .children
1124        .iter()
1125        .map(|child| child.min_height_for_width(area.width))
1126        .collect();
1127
1128    let mut total_grow: u32 = 0;
1129    let mut fixed_height: u32 = 0;
1130    for (child, &min_height) in node.children.iter().zip(min_heights.iter()) {
1131        if child.grow > 0 {
1132            total_grow += child.grow as u32;
1133        } else {
1134            fixed_height += min_height;
1135        }
1136    }
1137
1138    let mut flex_space = available.saturating_sub(fixed_height);
1139    let mut remaining_grow = total_grow;
1140
1141    let mut child_heights: Vec<u32> = Vec::with_capacity(node.children.len());
1142    for (i, child) in node.children.iter().enumerate() {
1143        let h = if child.grow > 0 && total_grow > 0 {
1144            let share = if remaining_grow == 0 {
1145                0
1146            } else {
1147                flex_space * child.grow as u32 / remaining_grow
1148            };
1149            flex_space = flex_space.saturating_sub(share);
1150            remaining_grow = remaining_grow.saturating_sub(child.grow as u32);
1151            share
1152        } else {
1153            min_heights[i].min(available)
1154        };
1155        child_heights.push(h);
1156    }
1157
1158    let total_children_height: u32 = child_heights.iter().sum();
1159    let remaining = area.height.saturating_sub(total_children_height);
1160    let (start_offset, inter_gap) = justify_offsets(node.justify, remaining, n, node.gap);
1161
1162    let mut y = area.y + start_offset;
1163    for (i, child) in node.children.iter_mut().enumerate() {
1164        let h = child_heights[i];
1165        let child_outer_w = match node.align {
1166            Align::Start => area.width,
1167            _ => child.min_width().min(area.width),
1168        };
1169        let child_x = area.x.saturating_add(child.margin.left);
1170        let child_y = y.saturating_add(child.margin.top);
1171        let child_w = child_outer_w.saturating_sub(child.margin.horizontal());
1172        let child_h = h.saturating_sub(child.margin.vertical());
1173        compute(child, Rect::new(child_x, child_y, child_w, child_h));
1174        let child_total_w = child.size.0.saturating_add(child.margin.horizontal());
1175        let x_offset = match node.align {
1176            Align::Start => 0,
1177            Align::Center => area.width.saturating_sub(child_total_w) / 2,
1178            Align::End => area.width.saturating_sub(child_total_w),
1179        };
1180        child.pos.0 = child.pos.0.saturating_add(x_offset);
1181        y += h + inter_gap;
1182    }
1183}
1184
1185pub(crate) fn render(node: &LayoutNode, buf: &mut Buffer) {
1186    render_inner(node, buf, 0, None);
1187    buf.clip_stack.clear();
1188    for overlay in &node.overlays {
1189        if overlay.modal {
1190            dim_entire_buffer(buf);
1191        }
1192        render_inner(&overlay.node, buf, 0, None);
1193    }
1194}
1195
1196fn dim_entire_buffer(buf: &mut Buffer) {
1197    for y in buf.area.y..buf.area.bottom() {
1198        for x in buf.area.x..buf.area.right() {
1199            let cell = buf.get_mut(x, y);
1200            cell.style.modifiers |= crate::style::Modifiers::DIM;
1201        }
1202    }
1203}
1204
1205pub(crate) fn render_debug_overlay(
1206    node: &LayoutNode,
1207    buf: &mut Buffer,
1208    frame_time_us: u64,
1209    fps: f32,
1210) {
1211    for child in &node.children {
1212        render_debug_overlay_inner(child, buf, 0, 0);
1213    }
1214    render_debug_status_bar(node, buf, frame_time_us, fps);
1215}
1216
1217fn render_debug_status_bar(node: &LayoutNode, buf: &mut Buffer, frame_time_us: u64, fps: f32) {
1218    if buf.area.height == 0 || buf.area.width == 0 {
1219        return;
1220    }
1221
1222    let widgets: u32 = node.children.iter().map(count_leaf_widgets).sum();
1223    let width = buf.area.width;
1224    let height = buf.area.height;
1225    let y = buf.area.bottom() - 1;
1226    let style = Style::new().fg(Color::Black).bg(Color::Yellow).bold();
1227
1228    let status = format!(
1229        "[SLT Debug] {}x{} | {} widgets | {:.1}ms | {:.0}fps",
1230        width,
1231        height,
1232        widgets,
1233        frame_time_us as f64 / 1_000.0,
1234        fps.max(0.0)
1235    );
1236
1237    let row_fill = " ".repeat(width as usize);
1238    buf.set_string(buf.area.x, y, &row_fill, style);
1239    buf.set_string(buf.area.x, y, &status, style);
1240}
1241
1242fn count_leaf_widgets(node: &LayoutNode) -> u32 {
1243    let mut total = if node.children.is_empty() {
1244        match node.kind {
1245            NodeKind::Spacer => 0,
1246            _ => 1,
1247        }
1248    } else {
1249        node.children.iter().map(count_leaf_widgets).sum()
1250    };
1251
1252    for overlay in &node.overlays {
1253        total = total.saturating_add(count_leaf_widgets(&overlay.node));
1254    }
1255
1256    total
1257}
1258
1259fn render_debug_overlay_inner(node: &LayoutNode, buf: &mut Buffer, depth: u32, y_offset: u32) {
1260    let child_offset = if node.is_scrollable {
1261        y_offset.saturating_add(node.scroll_offset)
1262    } else {
1263        y_offset
1264    };
1265
1266    if let NodeKind::Container(_) = node.kind {
1267        let sy = screen_y(node.pos.1, y_offset);
1268        if sy + node.size.1 as i64 > 0 {
1269            let color = debug_color_for_depth(depth);
1270            let style = Style::new().fg(color);
1271            let clamped_y = sy.max(0) as u32;
1272            draw_debug_border(node.pos.0, clamped_y, node.size.0, node.size.1, buf, style);
1273            if sy >= 0 {
1274                buf.set_string(node.pos.0, clamped_y, &depth.to_string(), style);
1275            }
1276        }
1277    }
1278
1279    if node.is_scrollable {
1280        if let Some(area) = visible_area(node, y_offset) {
1281            let inner = inner_area(node, area);
1282            buf.push_clip(inner);
1283            for child in &node.children {
1284                render_debug_overlay_inner(child, buf, depth.saturating_add(1), child_offset);
1285            }
1286            buf.pop_clip();
1287        }
1288    } else {
1289        for child in &node.children {
1290            render_debug_overlay_inner(child, buf, depth.saturating_add(1), child_offset);
1291        }
1292    }
1293}
1294
1295fn debug_color_for_depth(depth: u32) -> Color {
1296    match depth {
1297        0 => Color::Cyan,
1298        1 => Color::Yellow,
1299        2 => Color::Magenta,
1300        _ => Color::Red,
1301    }
1302}
1303
1304fn draw_debug_border(x: u32, y: u32, w: u32, h: u32, buf: &mut Buffer, style: Style) {
1305    if w == 0 || h == 0 {
1306        return;
1307    }
1308    let right = x + w - 1;
1309    let bottom = y + h - 1;
1310
1311    if w == 1 && h == 1 {
1312        buf.set_char(x, y, '┼', style);
1313        return;
1314    }
1315    if h == 1 {
1316        for xx in x..=right {
1317            buf.set_char(xx, y, '─', style);
1318        }
1319        return;
1320    }
1321    if w == 1 {
1322        for yy in y..=bottom {
1323            buf.set_char(x, yy, '│', style);
1324        }
1325        return;
1326    }
1327
1328    buf.set_char(x, y, '┌', style);
1329    buf.set_char(right, y, '┐', style);
1330    buf.set_char(x, bottom, '└', style);
1331    buf.set_char(right, bottom, '┘', style);
1332
1333    for xx in (x + 1)..right {
1334        buf.set_char(xx, y, '─', style);
1335        buf.set_char(xx, bottom, '─', style);
1336    }
1337    for yy in (y + 1)..bottom {
1338        buf.set_char(x, yy, '│', style);
1339        buf.set_char(right, yy, '│', style);
1340    }
1341}
1342
1343#[allow(dead_code)]
1344fn draw_debug_padding_markers(node: &LayoutNode, y_offset: u32, buf: &mut Buffer, style: Style) {
1345    if node.size.0 == 0 || node.size.1 == 0 {
1346        return;
1347    }
1348
1349    if node.padding == Padding::default() {
1350        return;
1351    }
1352
1353    let Some(area) = visible_area(node, y_offset) else {
1354        return;
1355    };
1356    let inner = inner_area(node, area);
1357    if inner.width == 0 || inner.height == 0 {
1358        return;
1359    }
1360
1361    let right = inner.right() - 1;
1362    let bottom = inner.bottom() - 1;
1363    buf.set_char(inner.x, inner.y, 'p', style);
1364    buf.set_char(right, inner.y, 'p', style);
1365    buf.set_char(inner.x, bottom, 'p', style);
1366    buf.set_char(right, bottom, 'p', style);
1367}
1368
1369#[allow(dead_code)]
1370fn draw_debug_margin_markers(node: &LayoutNode, y_offset: u32, buf: &mut Buffer, style: Style) {
1371    if node.margin == Margin::default() {
1372        return;
1373    }
1374
1375    let margin_y_i = node.pos.1 as i64 - node.margin.top as i64 - y_offset as i64;
1376    let w = node
1377        .size
1378        .0
1379        .saturating_add(node.margin.horizontal())
1380        .max(node.margin.horizontal());
1381    let h = node
1382        .size
1383        .1
1384        .saturating_add(node.margin.vertical())
1385        .max(node.margin.vertical());
1386
1387    if w == 0 || h == 0 || margin_y_i + h as i64 <= 0 {
1388        return;
1389    }
1390
1391    let x = node.pos.0.saturating_sub(node.margin.left);
1392    let y = margin_y_i.max(0) as u32;
1393    let bottom_i = margin_y_i + h as i64 - 1;
1394    if bottom_i < 0 {
1395        return;
1396    }
1397    let right = x + w - 1;
1398    let bottom = bottom_i as u32;
1399    if margin_y_i >= 0 {
1400        buf.set_char(x, y, 'm', style);
1401        buf.set_char(right, y, 'm', style);
1402    }
1403    buf.set_char(x, bottom, 'm', style);
1404    buf.set_char(right, bottom, 'm', style);
1405}
1406
1407fn screen_y(layout_y: u32, y_offset: u32) -> i64 {
1408    layout_y as i64 - y_offset as i64
1409}
1410
1411fn visible_area(node: &LayoutNode, y_offset: u32) -> Option<Rect> {
1412    let sy = screen_y(node.pos.1, y_offset);
1413    let bottom = sy + node.size.1 as i64;
1414    if bottom <= 0 || node.size.0 == 0 || node.size.1 == 0 {
1415        return None;
1416    }
1417    let clamped_y = sy.max(0) as u32;
1418    let clamped_h = (bottom as u32).saturating_sub(clamped_y);
1419    Some(Rect::new(node.pos.0, clamped_y, node.size.0, clamped_h))
1420}
1421
1422fn render_inner(node: &LayoutNode, buf: &mut Buffer, y_offset: u32, parent_bg: Option<Color>) {
1423    if node.size.0 == 0 || node.size.1 == 0 {
1424        return;
1425    }
1426
1427    let sy = screen_y(node.pos.1, y_offset);
1428    let sx = i64::from(node.pos.0);
1429    let ex = sx.saturating_add(i64::from(node.size.0));
1430    let ey = sy.saturating_add(i64::from(node.size.1));
1431    let viewport_left = i64::from(buf.area.x);
1432    let viewport_top = i64::from(buf.area.y);
1433    let viewport_right = viewport_left.saturating_add(i64::from(buf.area.width));
1434    let viewport_bottom = viewport_top.saturating_add(i64::from(buf.area.height));
1435
1436    if ex <= viewport_left || ey <= viewport_top || sx >= viewport_right || sy >= viewport_bottom {
1437        return;
1438    }
1439
1440    match node.kind {
1441        NodeKind::Text => {
1442            if let Some(ref segs) = node.segments {
1443                if node.wrap {
1444                    let fallback;
1445                    let wrapped = if let Some(cached) = &node.cached_wrapped_segments {
1446                        cached.as_slice()
1447                    } else {
1448                        fallback = wrap_segments(segs, node.size.0);
1449                        &fallback
1450                    };
1451                    for (i, line_segs) in wrapped.iter().enumerate() {
1452                        let line_y = sy + i as i64;
1453                        if line_y < 0 {
1454                            continue;
1455                        }
1456                        let mut x = node.pos.0;
1457                        for (text, style) in line_segs {
1458                            let mut s = *style;
1459                            if s.bg.is_none() {
1460                                s.bg = parent_bg;
1461                            }
1462                            buf.set_string(x, line_y as u32, text, s);
1463                            x += UnicodeWidthStr::width(text.as_str()) as u32;
1464                        }
1465                    }
1466                } else {
1467                    if sy < 0 {
1468                        return;
1469                    }
1470                    let mut x = node.pos.0;
1471                    for (text, style) in segs {
1472                        let mut s = *style;
1473                        if s.bg.is_none() {
1474                            s.bg = parent_bg;
1475                        }
1476                        buf.set_string(x, sy as u32, text, s);
1477                        x += UnicodeWidthStr::width(text.as_str()) as u32;
1478                    }
1479                }
1480            } else if let Some(ref text) = node.content {
1481                let mut style = node.style;
1482                if style.bg.is_none() {
1483                    style.bg = parent_bg;
1484                }
1485                if node.wrap {
1486                    let fallback;
1487                    let lines = if let Some(cached) = &node.cached_wrapped {
1488                        cached.as_slice()
1489                    } else {
1490                        fallback = wrap_lines(text, node.size.0);
1491                        fallback.as_slice()
1492                    };
1493                    for (i, line) in lines.iter().enumerate() {
1494                        let line_y = sy + i as i64;
1495                        if line_y < 0 {
1496                            continue;
1497                        }
1498                        let text_width = UnicodeWidthStr::width(line.as_str()) as u32;
1499                        let x_offset = if text_width < node.size.0 {
1500                            match node.align {
1501                                Align::Start => 0,
1502                                Align::Center => (node.size.0 - text_width) / 2,
1503                                Align::End => node.size.0 - text_width,
1504                            }
1505                        } else {
1506                            0
1507                        };
1508                        buf.set_string(
1509                            node.pos.0.saturating_add(x_offset),
1510                            line_y as u32,
1511                            line,
1512                            style,
1513                        );
1514                    }
1515                } else {
1516                    if sy < 0 {
1517                        return;
1518                    }
1519                    let text_width = UnicodeWidthStr::width(text.as_str()) as u32;
1520                    let x_offset = if text_width < node.size.0 {
1521                        match node.align {
1522                            Align::Start => 0,
1523                            Align::Center => (node.size.0 - text_width) / 2,
1524                            Align::End => node.size.0 - text_width,
1525                        }
1526                    } else {
1527                        0
1528                    };
1529                    let draw_x = node.pos.0.saturating_add(x_offset);
1530                    if let Some(ref url) = node.link_url {
1531                        buf.set_string_linked(draw_x, sy as u32, text, style, url);
1532                    } else {
1533                        buf.set_string(draw_x, sy as u32, text, style);
1534                    }
1535                }
1536            }
1537        }
1538        NodeKind::Spacer => {}
1539        NodeKind::Container(_) => {
1540            if let Some(color) = node.bg_color {
1541                if let Some(area) = visible_area(node, y_offset) {
1542                    let fill_style = Style::new().bg(color);
1543                    for y in area.y..area.bottom() {
1544                        for x in area.x..area.right() {
1545                            buf.set_string(x, y, " ", fill_style);
1546                        }
1547                    }
1548                }
1549            }
1550            let child_bg = node.bg_color.or(parent_bg);
1551            render_container_border(node, buf, y_offset, child_bg);
1552            if node.is_scrollable {
1553                let Some(area) = visible_area(node, y_offset) else {
1554                    return;
1555                };
1556                let inner = inner_area(node, area);
1557                let child_offset = y_offset.saturating_add(node.scroll_offset);
1558                let render_y_start = inner.y as i64;
1559                let render_y_end = inner.bottom() as i64;
1560                buf.push_clip(inner);
1561                for child in &node.children {
1562                    let child_top = child.pos.1 as i64 - child_offset as i64;
1563                    let child_bottom = child_top + child.size.1 as i64;
1564                    if child_bottom <= render_y_start || child_top >= render_y_end {
1565                        continue;
1566                    }
1567                    render_inner(child, buf, child_offset, child_bg);
1568                }
1569                buf.pop_clip();
1570                render_scroll_indicators(node, inner, buf, child_bg);
1571            } else {
1572                let Some(area) = visible_area(node, y_offset) else {
1573                    return;
1574                };
1575                let clip = inner_area(node, area);
1576                buf.push_clip(clip);
1577                for child in &node.children {
1578                    render_inner(child, buf, y_offset, child_bg);
1579                }
1580                buf.pop_clip();
1581            }
1582        }
1583    }
1584}
1585
1586fn render_container_border(
1587    node: &LayoutNode,
1588    buf: &mut Buffer,
1589    y_offset: u32,
1590    inherit_bg: Option<Color>,
1591) {
1592    if node.border_inset() == 0 {
1593        return;
1594    }
1595    let Some(border) = node.border else {
1596        return;
1597    };
1598    let sides = node.border_sides;
1599    let chars = border.chars();
1600    let x = node.pos.0;
1601    let w = node.size.0;
1602    let h = node.size.1;
1603    if w == 0 || h == 0 {
1604        return;
1605    }
1606
1607    let mut style = node.border_style;
1608    if style.bg.is_none() {
1609        style.bg = inherit_bg;
1610    }
1611
1612    let top_i = screen_y(node.pos.1, y_offset);
1613    let bottom_i = top_i + h as i64 - 1;
1614    if bottom_i < 0 {
1615        return;
1616    }
1617    let right = x + w - 1;
1618
1619    if sides.top && top_i >= 0 {
1620        let y = top_i as u32;
1621        for xx in x..=right {
1622            buf.set_char(xx, y, chars.h, style);
1623        }
1624    }
1625    if sides.bottom {
1626        let y = bottom_i as u32;
1627        for xx in x..=right {
1628            buf.set_char(xx, y, chars.h, style);
1629        }
1630    }
1631    if sides.left {
1632        let vert_start = top_i.max(0) as u32;
1633        let vert_end = bottom_i as u32;
1634        for yy in vert_start..=vert_end {
1635            buf.set_char(x, yy, chars.v, style);
1636        }
1637    }
1638    if sides.right {
1639        let vert_start = top_i.max(0) as u32;
1640        let vert_end = bottom_i as u32;
1641        for yy in vert_start..=vert_end {
1642            buf.set_char(right, yy, chars.v, style);
1643        }
1644    }
1645
1646    if top_i >= 0 {
1647        let y = top_i as u32;
1648        let tl = match (sides.top, sides.left) {
1649            (true, true) => Some(chars.tl),
1650            (true, false) => Some(chars.h),
1651            (false, true) => Some(chars.v),
1652            (false, false) => None,
1653        };
1654        if let Some(ch) = tl {
1655            buf.set_char(x, y, ch, style);
1656        }
1657
1658        let tr = match (sides.top, sides.right) {
1659            (true, true) => Some(chars.tr),
1660            (true, false) => Some(chars.h),
1661            (false, true) => Some(chars.v),
1662            (false, false) => None,
1663        };
1664        if let Some(ch) = tr {
1665            buf.set_char(right, y, ch, style);
1666        }
1667    }
1668
1669    let y = bottom_i as u32;
1670    let bl = match (sides.bottom, sides.left) {
1671        (true, true) => Some(chars.bl),
1672        (true, false) => Some(chars.h),
1673        (false, true) => Some(chars.v),
1674        (false, false) => None,
1675    };
1676    if let Some(ch) = bl {
1677        buf.set_char(x, y, ch, style);
1678    }
1679
1680    let br = match (sides.bottom, sides.right) {
1681        (true, true) => Some(chars.br),
1682        (true, false) => Some(chars.h),
1683        (false, true) => Some(chars.v),
1684        (false, false) => None,
1685    };
1686    if let Some(ch) = br {
1687        buf.set_char(right, y, ch, style);
1688    }
1689
1690    if sides.top && top_i >= 0 {
1691        if let Some((title, title_style)) = &node.title {
1692            let mut ts = *title_style;
1693            if ts.bg.is_none() {
1694                ts.bg = inherit_bg;
1695            }
1696            let y = top_i as u32;
1697            let title_x = x.saturating_add(2);
1698            if title_x <= right {
1699                let max_width = (right - title_x + 1) as usize;
1700                let trimmed: String = title.chars().take(max_width).collect();
1701                buf.set_string(title_x, y, &trimmed, ts);
1702            }
1703        }
1704    }
1705}
1706
1707fn render_scroll_indicators(
1708    node: &LayoutNode,
1709    inner: Rect,
1710    buf: &mut Buffer,
1711    inherit_bg: Option<Color>,
1712) {
1713    if inner.width == 0 || inner.height == 0 {
1714        return;
1715    }
1716
1717    let mut style = node.border_style;
1718    if style.bg.is_none() {
1719        style.bg = inherit_bg;
1720    }
1721
1722    let indicator_x = inner.right() - 1;
1723    if node.scroll_offset > 0 {
1724        buf.set_char(indicator_x, inner.y, '▲', style);
1725    }
1726    if node.scroll_offset.saturating_add(inner.height) < node.content_height {
1727        buf.set_char(indicator_x, inner.bottom() - 1, '▼', style);
1728    }
1729}
1730
1731pub(crate) fn collect_scroll_infos(node: &LayoutNode) -> Vec<(u32, u32)> {
1732    let mut infos = Vec::new();
1733    collect_scroll_infos_inner(node, &mut infos);
1734    for overlay in &node.overlays {
1735        collect_scroll_infos_inner(&overlay.node, &mut infos);
1736    }
1737    infos
1738}
1739
1740pub(crate) fn collect_scroll_rects(node: &LayoutNode) -> Vec<Rect> {
1741    let mut rects = Vec::new();
1742    collect_scroll_rects_inner(node, &mut rects, 0);
1743    for overlay in &node.overlays {
1744        collect_scroll_rects_inner(&overlay.node, &mut rects, 0);
1745    }
1746    rects
1747}
1748
1749fn collect_scroll_rects_inner(node: &LayoutNode, rects: &mut Vec<Rect>, y_offset: u32) {
1750    if node.is_scrollable {
1751        let adj_y = node.pos.1.saturating_sub(y_offset);
1752        rects.push(Rect::new(node.pos.0, adj_y, node.size.0, node.size.1));
1753    }
1754    let child_offset = if node.is_scrollable {
1755        y_offset.saturating_add(node.scroll_offset)
1756    } else {
1757        y_offset
1758    };
1759    for child in &node.children {
1760        collect_scroll_rects_inner(child, rects, child_offset);
1761    }
1762}
1763
1764pub(crate) fn collect_hit_areas(node: &LayoutNode) -> Vec<Rect> {
1765    let mut areas = Vec::new();
1766    for child in &node.children {
1767        collect_hit_areas_inner(child, &mut areas, 0);
1768    }
1769    for overlay in &node.overlays {
1770        collect_hit_areas_inner(&overlay.node, &mut areas, 0);
1771    }
1772    areas
1773}
1774
1775pub(crate) fn collect_group_rects(node: &LayoutNode) -> Vec<(String, Rect)> {
1776    let mut rects = Vec::new();
1777    for child in &node.children {
1778        collect_group_rects_inner(child, &mut rects, 0);
1779    }
1780    for overlay in &node.overlays {
1781        collect_group_rects_inner(&overlay.node, &mut rects, 0);
1782    }
1783    rects
1784}
1785
1786fn collect_scroll_infos_inner(node: &LayoutNode, infos: &mut Vec<(u32, u32)>) {
1787    if node.is_scrollable {
1788        let viewport_h = node.size.1.saturating_sub(node.frame_vertical());
1789        infos.push((node.content_height, viewport_h));
1790    }
1791    for child in &node.children {
1792        collect_scroll_infos_inner(child, infos);
1793    }
1794}
1795
1796fn collect_hit_areas_inner(node: &LayoutNode, areas: &mut Vec<Rect>, y_offset: u32) {
1797    if matches!(node.kind, NodeKind::Container(_)) || node.link_url.is_some() {
1798        if node.pos.1 + node.size.1 > y_offset {
1799            areas.push(Rect::new(
1800                node.pos.0,
1801                node.pos.1.saturating_sub(y_offset),
1802                node.size.0,
1803                node.size.1,
1804            ));
1805        } else {
1806            areas.push(Rect::new(0, 0, 0, 0));
1807        }
1808    }
1809    let child_offset = if node.is_scrollable {
1810        y_offset.saturating_add(node.scroll_offset)
1811    } else {
1812        y_offset
1813    };
1814    for child in &node.children {
1815        collect_hit_areas_inner(child, areas, child_offset);
1816    }
1817}
1818
1819fn collect_group_rects_inner(node: &LayoutNode, rects: &mut Vec<(String, Rect)>, y_offset: u32) {
1820    if let Some(name) = &node.group_name {
1821        if node.pos.1 + node.size.1 > y_offset {
1822            rects.push((
1823                name.clone(),
1824                Rect::new(
1825                    node.pos.0,
1826                    node.pos.1.saturating_sub(y_offset),
1827                    node.size.0,
1828                    node.size.1,
1829                ),
1830            ));
1831        }
1832    }
1833    let child_offset = if node.is_scrollable {
1834        y_offset.saturating_add(node.scroll_offset)
1835    } else {
1836        y_offset
1837    };
1838    for child in &node.children {
1839        collect_group_rects_inner(child, rects, child_offset);
1840    }
1841}
1842
1843pub(crate) fn collect_content_areas(node: &LayoutNode) -> Vec<(Rect, Rect)> {
1844    let mut areas = Vec::new();
1845    for child in &node.children {
1846        collect_content_areas_inner(child, &mut areas, 0);
1847    }
1848    for overlay in &node.overlays {
1849        collect_content_areas_inner(&overlay.node, &mut areas, 0);
1850    }
1851    areas
1852}
1853
1854fn collect_content_areas_inner(node: &LayoutNode, areas: &mut Vec<(Rect, Rect)>, y_offset: u32) {
1855    if matches!(node.kind, NodeKind::Container(_)) {
1856        let adj_y = node.pos.1.saturating_sub(y_offset);
1857        let full = Rect::new(node.pos.0, adj_y, node.size.0, node.size.1);
1858        let inset_x = node.padding.left + node.border_left_inset();
1859        let inset_y = node.padding.top + node.border_top_inset();
1860        let inner_w = node.size.0.saturating_sub(node.frame_horizontal());
1861        let inner_h = node.size.1.saturating_sub(node.frame_vertical());
1862        let content = Rect::new(node.pos.0 + inset_x, adj_y + inset_y, inner_w, inner_h);
1863        areas.push((full, content));
1864    }
1865    let child_offset = if node.is_scrollable {
1866        y_offset.saturating_add(node.scroll_offset)
1867    } else {
1868        y_offset
1869    };
1870    for child in &node.children {
1871        collect_content_areas_inner(child, areas, child_offset);
1872    }
1873}
1874
1875pub(crate) fn collect_focus_rects(node: &LayoutNode) -> Vec<(usize, Rect)> {
1876    let mut rects = Vec::new();
1877    collect_focus_rects_inner(node, &mut rects, 0);
1878    for overlay in &node.overlays {
1879        collect_focus_rects_inner(&overlay.node, &mut rects, 0);
1880    }
1881    rects
1882}
1883
1884pub(crate) fn collect_focus_groups(node: &LayoutNode) -> Vec<Option<String>> {
1885    let mut groups = Vec::new();
1886    for child in &node.children {
1887        collect_focus_groups_inner(child, &mut groups, None);
1888    }
1889    for overlay in &node.overlays {
1890        collect_focus_groups_inner(&overlay.node, &mut groups, None);
1891    }
1892    groups
1893}
1894
1895fn collect_focus_rects_inner(node: &LayoutNode, rects: &mut Vec<(usize, Rect)>, y_offset: u32) {
1896    if let Some(id) = node.focus_id {
1897        if node.pos.1 + node.size.1 > y_offset {
1898            rects.push((
1899                id,
1900                Rect::new(
1901                    node.pos.0,
1902                    node.pos.1.saturating_sub(y_offset),
1903                    node.size.0,
1904                    node.size.1,
1905                ),
1906            ));
1907        }
1908    }
1909    let child_offset = if node.is_scrollable {
1910        y_offset.saturating_add(node.scroll_offset)
1911    } else {
1912        y_offset
1913    };
1914    for child in &node.children {
1915        collect_focus_rects_inner(child, rects, child_offset);
1916    }
1917}
1918
1919fn collect_focus_groups_inner(
1920    node: &LayoutNode,
1921    groups: &mut Vec<Option<String>>,
1922    active_group: Option<&str>,
1923) {
1924    let current_group = node.group_name.as_deref().or(active_group);
1925    if let Some(id) = node.focus_id {
1926        if id >= groups.len() {
1927            groups.resize(id + 1, None);
1928        }
1929        groups[id] = current_group.map(ToString::to_string);
1930    }
1931    for child in &node.children {
1932        collect_focus_groups_inner(child, groups, current_group);
1933    }
1934}
1935
1936#[cfg(test)]
1937mod tests {
1938    use super::*;
1939
1940    #[test]
1941    fn wrap_empty() {
1942        assert_eq!(wrap_lines("", 10), vec![""]);
1943    }
1944
1945    #[test]
1946    fn wrap_fits() {
1947        assert_eq!(wrap_lines("hello", 10), vec!["hello"]);
1948    }
1949
1950    #[test]
1951    fn wrap_word_boundary() {
1952        assert_eq!(wrap_lines("hello world", 7), vec!["hello", "world"]);
1953    }
1954
1955    #[test]
1956    fn wrap_multiple_words() {
1957        assert_eq!(
1958            wrap_lines("one two three four", 9),
1959            vec!["one two", "three", "four"]
1960        );
1961    }
1962
1963    #[test]
1964    fn wrap_long_word() {
1965        assert_eq!(wrap_lines("abcdefghij", 4), vec!["abcd", "efgh", "ij"]);
1966    }
1967
1968    #[test]
1969    fn wrap_zero_width() {
1970        assert_eq!(wrap_lines("hello", 0), vec!["hello"]);
1971    }
1972
1973    #[test]
1974    fn diagnostic_demo_layout() {
1975        use super::{compute, ContainerConfig, Direction, LayoutNode};
1976        use crate::rect::Rect;
1977        use crate::style::{Align, Border, Constraints, Justify, Margin, Padding, Style};
1978
1979        // Build the tree structure matching demo.rs:
1980        // Root (Column, grow:0)
1981        //   └─ Container (Column, grow:1, border:Rounded, padding:all(1))
1982        //        ├─ Text "header" (grow:0)
1983        //        ├─ Text "separator" (grow:0)
1984        //        ├─ Container (Column, grow:1)  ← simulates scrollable
1985        //        │    ├─ Text "content1" (grow:0)
1986        //        │    ├─ Text "content2" (grow:0)
1987        //        │    └─ Text "content3" (grow:0)
1988        //        ├─ Text "separator2" (grow:0)
1989        //        └─ Text "footer" (grow:0)
1990
1991        let mut root = LayoutNode::container(
1992            Direction::Column,
1993            ContainerConfig {
1994                gap: 0,
1995                align: Align::Start,
1996                justify: Justify::Start,
1997                border: None,
1998                border_sides: BorderSides::all(),
1999                border_style: Style::new(),
2000                bg_color: None,
2001                padding: Padding::default(),
2002                margin: Margin::default(),
2003                constraints: Constraints::default(),
2004                title: None,
2005                grow: 0,
2006            },
2007        );
2008
2009        // Outer bordered container with grow:1
2010        let mut outer_container = LayoutNode::container(
2011            Direction::Column,
2012            ContainerConfig {
2013                gap: 0,
2014                align: Align::Start,
2015                justify: Justify::Start,
2016                border: Some(Border::Rounded),
2017                border_sides: BorderSides::all(),
2018                border_style: Style::new(),
2019                bg_color: None,
2020                padding: Padding::all(1),
2021                margin: Margin::default(),
2022                constraints: Constraints::default(),
2023                title: None,
2024                grow: 1,
2025            },
2026        );
2027
2028        // Header text
2029        outer_container.children.push(LayoutNode::text(
2030            "header".to_string(),
2031            Style::new(),
2032            0,
2033            Align::Start,
2034            false,
2035            Margin::default(),
2036            Constraints::default(),
2037        ));
2038
2039        // Separator 1
2040        outer_container.children.push(LayoutNode::text(
2041            "separator".to_string(),
2042            Style::new(),
2043            0,
2044            Align::Start,
2045            false,
2046            Margin::default(),
2047            Constraints::default(),
2048        ));
2049
2050        // Inner scrollable-like container with grow:1
2051        let mut inner_container = LayoutNode::container(
2052            Direction::Column,
2053            ContainerConfig {
2054                gap: 0,
2055                align: Align::Start,
2056                justify: Justify::Start,
2057                border: None,
2058                border_sides: BorderSides::all(),
2059                border_style: Style::new(),
2060                bg_color: None,
2061                padding: Padding::default(),
2062                margin: Margin::default(),
2063                constraints: Constraints::default(),
2064                title: None,
2065                grow: 1,
2066            },
2067        );
2068
2069        // Content items
2070        inner_container.children.push(LayoutNode::text(
2071            "content1".to_string(),
2072            Style::new(),
2073            0,
2074            Align::Start,
2075            false,
2076            Margin::default(),
2077            Constraints::default(),
2078        ));
2079        inner_container.children.push(LayoutNode::text(
2080            "content2".to_string(),
2081            Style::new(),
2082            0,
2083            Align::Start,
2084            false,
2085            Margin::default(),
2086            Constraints::default(),
2087        ));
2088        inner_container.children.push(LayoutNode::text(
2089            "content3".to_string(),
2090            Style::new(),
2091            0,
2092            Align::Start,
2093            false,
2094            Margin::default(),
2095            Constraints::default(),
2096        ));
2097
2098        outer_container.children.push(inner_container);
2099
2100        // Separator 2
2101        outer_container.children.push(LayoutNode::text(
2102            "separator2".to_string(),
2103            Style::new(),
2104            0,
2105            Align::Start,
2106            false,
2107            Margin::default(),
2108            Constraints::default(),
2109        ));
2110
2111        // Footer
2112        outer_container.children.push(LayoutNode::text(
2113            "footer".to_string(),
2114            Style::new(),
2115            0,
2116            Align::Start,
2117            false,
2118            Margin::default(),
2119            Constraints::default(),
2120        ));
2121
2122        root.children.push(outer_container);
2123
2124        // Compute layout with 80x50 terminal
2125        compute(&mut root, Rect::new(0, 0, 80, 50));
2126
2127        // Debug output
2128        eprintln!("\n=== DIAGNOSTIC LAYOUT TEST ===");
2129        eprintln!("Root node:");
2130        eprintln!("  pos: {:?}, size: {:?}", root.pos, root.size);
2131
2132        let outer = &root.children[0];
2133        eprintln!("\nOuter bordered container (grow:1):");
2134        eprintln!("  pos: {:?}, size: {:?}", outer.pos, outer.size);
2135
2136        let inner = &outer.children[2];
2137        eprintln!("\nInner container (grow:1, simulates scrollable):");
2138        eprintln!("  pos: {:?}, size: {:?}", inner.pos, inner.size);
2139
2140        eprintln!("\nAll children of outer container:");
2141        for (i, child) in outer.children.iter().enumerate() {
2142            eprintln!("  [{}] pos: {:?}, size: {:?}", i, child.pos, child.size);
2143        }
2144
2145        // Assertions
2146        // Root should fill the entire 80x50 area
2147        assert_eq!(
2148            root.size,
2149            (80, 50),
2150            "Root node should fill entire terminal (80x50)"
2151        );
2152
2153        // Outer container should also be 80x50 (full height due to grow:1)
2154        assert_eq!(
2155            outer.size,
2156            (80, 50),
2157            "Outer bordered container should fill entire terminal (80x50)"
2158        );
2159
2160        // Calculate expected inner container height:
2161        // Available height = 50 (total)
2162        // Border inset = 1 (top) + 1 (bottom) = 2
2163        // Padding = 1 (top) + 1 (bottom) = 2
2164        // Fixed children heights: header(1) + sep(1) + sep2(1) + footer(1) = 4
2165        // Expected inner height = 50 - 2 - 2 - 4 = 42
2166        let expected_inner_height = 50 - 2 - 2 - 4;
2167        assert_eq!(
2168            inner.size.1, expected_inner_height as u32,
2169            "Inner container height should be {} (50 - border(2) - padding(2) - fixed(4))",
2170            expected_inner_height
2171        );
2172
2173        // Inner container should start at y = border(1) + padding(1) + header(1) + sep(1) = 4
2174        let expected_inner_y = 1 + 1 + 1 + 1;
2175        assert_eq!(
2176            inner.pos.1, expected_inner_y as u32,
2177            "Inner container should start at y={} (border+padding+header+sep)",
2178            expected_inner_y
2179        );
2180
2181        eprintln!("\n✓ All assertions passed!");
2182        eprintln!("  Root size: {:?}", root.size);
2183        eprintln!("  Outer container size: {:?}", outer.size);
2184        eprintln!("  Inner container size: {:?}", inner.size);
2185        eprintln!("  Inner container pos: {:?}", inner.pos);
2186    }
2187
2188    #[test]
2189    fn collect_focus_rects_from_markers() {
2190        use super::*;
2191        use crate::style::Style;
2192
2193        let commands = vec![
2194            Command::FocusMarker(0),
2195            Command::Text {
2196                content: "input1".into(),
2197                style: Style::new(),
2198                grow: 0,
2199                align: Align::Start,
2200                wrap: false,
2201                margin: Default::default(),
2202                constraints: Default::default(),
2203            },
2204            Command::FocusMarker(1),
2205            Command::Text {
2206                content: "input2".into(),
2207                style: Style::new(),
2208                grow: 0,
2209                align: Align::Start,
2210                wrap: false,
2211                margin: Default::default(),
2212                constraints: Default::default(),
2213            },
2214        ];
2215
2216        let mut tree = build_tree(&commands);
2217        let area = crate::rect::Rect::new(0, 0, 40, 10);
2218        compute(&mut tree, area);
2219
2220        let rects = collect_focus_rects(&tree);
2221        assert_eq!(rects.len(), 2);
2222        assert_eq!(rects[0].0, 0);
2223        assert_eq!(rects[1].0, 1);
2224        assert!(rects[0].1.width > 0);
2225        assert!(rects[1].1.width > 0);
2226        assert_ne!(rects[0].1.y, rects[1].1.y);
2227    }
2228
2229    #[test]
2230    fn focus_marker_tags_container() {
2231        use super::*;
2232        use crate::style::{Border, Style};
2233
2234        let commands = vec![
2235            Command::FocusMarker(0),
2236            Command::BeginContainer {
2237                direction: Direction::Column,
2238                gap: 0,
2239                align: Align::Start,
2240                justify: Justify::Start,
2241                border: Some(Border::Single),
2242                border_sides: BorderSides::all(),
2243                border_style: Style::new(),
2244                bg_color: None,
2245                padding: Padding::default(),
2246                margin: Default::default(),
2247                constraints: Default::default(),
2248                title: None,
2249                grow: 0,
2250                group_name: None,
2251            },
2252            Command::Text {
2253                content: "inside".into(),
2254                style: Style::new(),
2255                grow: 0,
2256                align: Align::Start,
2257                wrap: false,
2258                margin: Default::default(),
2259                constraints: Default::default(),
2260            },
2261            Command::EndContainer,
2262        ];
2263
2264        let mut tree = build_tree(&commands);
2265        let area = crate::rect::Rect::new(0, 0, 40, 10);
2266        compute(&mut tree, area);
2267
2268        let rects = collect_focus_rects(&tree);
2269        assert_eq!(rects.len(), 1);
2270        assert_eq!(rects[0].0, 0);
2271        assert!(rects[0].1.width >= 8);
2272        assert!(rects[0].1.height >= 3);
2273    }
2274}