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