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