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