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 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                parent.children.push(node);
877                *pos += 1;
878            }
879            Command::EndContainer => {
880                *pos += 1;
881                return;
882            }
883            Command::EndOverlay => {
884                *pos += 1;
885                if stop_on_end_overlay {
886                    return;
887                }
888            }
889        }
890    }
891}
892
893mod flexbox;
894mod render;
895
896pub(crate) use flexbox::compute;
897pub(crate) use render::{render, render_debug_overlay};
898
899#[derive(Default)]
900pub(crate) struct FrameData {
901    pub scroll_infos: Vec<(u32, u32)>,
902    pub scroll_rects: Vec<Rect>,
903    pub hit_areas: Vec<Rect>,
904    pub group_rects: Vec<(String, Rect)>,
905    pub content_areas: Vec<(Rect, Rect)>,
906    pub focus_rects: Vec<(usize, Rect)>,
907    pub focus_groups: Vec<Option<String>>,
908}
909
910/// Collect all per-frame data from a laid-out tree in a single DFS pass.
911///
912/// Replaces the 7 individual `collect_*` functions that each traversed the
913/// tree independently, reducing per-frame traversals from 7× to 1×.
914pub(crate) fn collect_all(node: &LayoutNode) -> FrameData {
915    let mut data = FrameData::default();
916
917    // scroll_infos, scroll_rects, focus_rects process the root node itself.
918    // hit_areas, group_rects, content_areas, focus_groups skip the root.
919    if node.is_scrollable {
920        let viewport_h = node.size.1.saturating_sub(node.frame_vertical());
921        data.scroll_infos.push((node.content_height, viewport_h));
922        data.scroll_rects
923            .push(Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1));
924    }
925    if let Some(id) = node.focus_id {
926        if node.pos.1 + node.size.1 > 0 {
927            data.focus_rects.push((
928                id,
929                Rect::new(node.pos.0, node.pos.1, node.size.0, node.size.1),
930            ));
931        }
932    }
933
934    let child_offset = if node.is_scrollable {
935        node.scroll_offset
936    } else {
937        0
938    };
939    for child in &node.children {
940        collect_all_inner(child, &mut data, child_offset, None);
941    }
942
943    for overlay in &node.overlays {
944        collect_all_inner(&overlay.node, &mut data, 0, None);
945    }
946
947    data
948}
949
950fn collect_all_inner(
951    node: &LayoutNode,
952    data: &mut FrameData,
953    y_offset: u32,
954    active_group: Option<&str>,
955) {
956    // --- scroll_infos (no y_offset dependency) ---
957    if node.is_scrollable {
958        let viewport_h = node.size.1.saturating_sub(node.frame_vertical());
959        data.scroll_infos.push((node.content_height, viewport_h));
960    }
961
962    // --- scroll_rects (uses y_offset) ---
963    if node.is_scrollable {
964        let adj_y = node.pos.1.saturating_sub(y_offset);
965        data.scroll_rects
966            .push(Rect::new(node.pos.0, adj_y, node.size.0, node.size.1));
967    }
968
969    // --- hit_areas (container or link) ---
970    if matches!(node.kind, NodeKind::Container(_)) || node.link_url.is_some() {
971        if node.pos.1 + node.size.1 > y_offset {
972            data.hit_areas.push(Rect::new(
973                node.pos.0,
974                node.pos.1.saturating_sub(y_offset),
975                node.size.0,
976                node.size.1,
977            ));
978        } else {
979            data.hit_areas.push(Rect::new(0, 0, 0, 0));
980        }
981    }
982
983    // --- group_rects ---
984    if let Some(name) = &node.group_name {
985        if node.pos.1 + node.size.1 > y_offset {
986            data.group_rects.push((
987                name.clone(),
988                Rect::new(
989                    node.pos.0,
990                    node.pos.1.saturating_sub(y_offset),
991                    node.size.0,
992                    node.size.1,
993                ),
994            ));
995        }
996    }
997
998    // --- content_areas ---
999    if matches!(node.kind, NodeKind::Container(_)) {
1000        let adj_y = node.pos.1.saturating_sub(y_offset);
1001        let full = Rect::new(node.pos.0, adj_y, node.size.0, node.size.1);
1002        let inset_x = node.padding.left + node.border_left_inset();
1003        let inset_y = node.padding.top + node.border_top_inset();
1004        let inner_w = node.size.0.saturating_sub(node.frame_horizontal());
1005        let inner_h = node.size.1.saturating_sub(node.frame_vertical());
1006        let content = Rect::new(node.pos.0 + inset_x, adj_y + inset_y, inner_w, inner_h);
1007        data.content_areas.push((full, content));
1008    }
1009
1010    // --- focus_rects ---
1011    if let Some(id) = node.focus_id {
1012        if node.pos.1 + node.size.1 > y_offset {
1013            data.focus_rects.push((
1014                id,
1015                Rect::new(
1016                    node.pos.0,
1017                    node.pos.1.saturating_sub(y_offset),
1018                    node.size.0,
1019                    node.size.1,
1020                ),
1021            ));
1022        }
1023    }
1024
1025    // --- focus_groups ---
1026    let current_group = node.group_name.as_deref().or(active_group);
1027    if let Some(id) = node.focus_id {
1028        if id >= data.focus_groups.len() {
1029            data.focus_groups.resize(id + 1, None);
1030        }
1031        data.focus_groups[id] = current_group.map(ToString::to_string);
1032    }
1033
1034    // --- Recurse into children ---
1035    let child_offset = if node.is_scrollable {
1036        y_offset.saturating_add(node.scroll_offset)
1037    } else {
1038        y_offset
1039    };
1040    for child in &node.children {
1041        collect_all_inner(child, data, child_offset, current_group);
1042    }
1043}
1044
1045pub(crate) fn collect_raw_draw_rects(node: &LayoutNode) -> Vec<(usize, Rect)> {
1046    let mut rects = Vec::new();
1047    collect_raw_draw_rects_inner(node, &mut rects, 0);
1048    for overlay in &node.overlays {
1049        collect_raw_draw_rects_inner(&overlay.node, &mut rects, 0);
1050    }
1051    rects
1052}
1053
1054fn collect_raw_draw_rects_inner(node: &LayoutNode, rects: &mut Vec<(usize, Rect)>, y_offset: u32) {
1055    if let NodeKind::RawDraw(draw_id) = node.kind {
1056        let adj_y = node.pos.1.saturating_sub(y_offset);
1057        rects.push((
1058            draw_id,
1059            Rect::new(node.pos.0, adj_y, node.size.0, node.size.1),
1060        ));
1061    }
1062    let child_offset = if node.is_scrollable {
1063        y_offset.saturating_add(node.scroll_offset)
1064    } else {
1065        y_offset
1066    };
1067    for child in &node.children {
1068        collect_raw_draw_rects_inner(child, rects, child_offset);
1069    }
1070}
1071
1072#[cfg(test)]
1073mod tests {
1074    use super::*;
1075
1076    #[test]
1077    fn wrap_empty() {
1078        assert_eq!(wrap_lines("", 10), vec![""]);
1079    }
1080
1081    #[test]
1082    fn wrap_fits() {
1083        assert_eq!(wrap_lines("hello", 10), vec!["hello"]);
1084    }
1085
1086    #[test]
1087    fn wrap_word_boundary() {
1088        assert_eq!(wrap_lines("hello world", 7), vec!["hello", "world"]);
1089    }
1090
1091    #[test]
1092    fn wrap_multiple_words() {
1093        assert_eq!(
1094            wrap_lines("one two three four", 9),
1095            vec!["one two", "three", "four"]
1096        );
1097    }
1098
1099    #[test]
1100    fn wrap_long_word() {
1101        assert_eq!(wrap_lines("abcdefghij", 4), vec!["abcd", "efgh", "ij"]);
1102    }
1103
1104    #[test]
1105    fn wrap_zero_width() {
1106        assert_eq!(wrap_lines("hello", 0), vec!["hello"]);
1107    }
1108
1109    #[test]
1110    fn diagnostic_demo_layout() {
1111        use super::{compute, ContainerConfig, Direction, LayoutNode};
1112        use crate::rect::Rect;
1113        use crate::style::{Align, Border, Constraints, Justify, Margin, Padding, Style};
1114
1115        // Build the tree structure matching demo.rs:
1116        // Root (Column, grow:0)
1117        //   └─ Container (Column, grow:1, border:Rounded, padding:all(1))
1118        //        ├─ Text "header" (grow:0)
1119        //        ├─ Text "separator" (grow:0)
1120        //        ├─ Container (Column, grow:1)  ← simulates scrollable
1121        //        │    ├─ Text "content1" (grow:0)
1122        //        │    ├─ Text "content2" (grow:0)
1123        //        │    └─ Text "content3" (grow:0)
1124        //        ├─ Text "separator2" (grow:0)
1125        //        └─ Text "footer" (grow:0)
1126
1127        let mut root = LayoutNode::container(
1128            Direction::Column,
1129            ContainerConfig {
1130                gap: 0,
1131                align: Align::Start,
1132                justify: Justify::Start,
1133                border: None,
1134                border_sides: BorderSides::all(),
1135                border_style: Style::new(),
1136                bg_color: None,
1137                padding: Padding::default(),
1138                margin: Margin::default(),
1139                constraints: Constraints::default(),
1140                title: None,
1141                grow: 0,
1142            },
1143        );
1144
1145        // Outer bordered container with grow:1
1146        let mut outer_container = LayoutNode::container(
1147            Direction::Column,
1148            ContainerConfig {
1149                gap: 0,
1150                align: Align::Start,
1151                justify: Justify::Start,
1152                border: Some(Border::Rounded),
1153                border_sides: BorderSides::all(),
1154                border_style: Style::new(),
1155                bg_color: None,
1156                padding: Padding::all(1),
1157                margin: Margin::default(),
1158                constraints: Constraints::default(),
1159                title: None,
1160                grow: 1,
1161            },
1162        );
1163
1164        // Header text
1165        outer_container.children.push(LayoutNode::text(
1166            "header".to_string(),
1167            Style::new(),
1168            0,
1169            Align::Start,
1170            false,
1171            Margin::default(),
1172            Constraints::default(),
1173        ));
1174
1175        // Separator 1
1176        outer_container.children.push(LayoutNode::text(
1177            "separator".to_string(),
1178            Style::new(),
1179            0,
1180            Align::Start,
1181            false,
1182            Margin::default(),
1183            Constraints::default(),
1184        ));
1185
1186        // Inner scrollable-like container with grow:1
1187        let mut inner_container = LayoutNode::container(
1188            Direction::Column,
1189            ContainerConfig {
1190                gap: 0,
1191                align: Align::Start,
1192                justify: Justify::Start,
1193                border: None,
1194                border_sides: BorderSides::all(),
1195                border_style: Style::new(),
1196                bg_color: None,
1197                padding: Padding::default(),
1198                margin: Margin::default(),
1199                constraints: Constraints::default(),
1200                title: None,
1201                grow: 1,
1202            },
1203        );
1204
1205        // Content items
1206        inner_container.children.push(LayoutNode::text(
1207            "content1".to_string(),
1208            Style::new(),
1209            0,
1210            Align::Start,
1211            false,
1212            Margin::default(),
1213            Constraints::default(),
1214        ));
1215        inner_container.children.push(LayoutNode::text(
1216            "content2".to_string(),
1217            Style::new(),
1218            0,
1219            Align::Start,
1220            false,
1221            Margin::default(),
1222            Constraints::default(),
1223        ));
1224        inner_container.children.push(LayoutNode::text(
1225            "content3".to_string(),
1226            Style::new(),
1227            0,
1228            Align::Start,
1229            false,
1230            Margin::default(),
1231            Constraints::default(),
1232        ));
1233
1234        outer_container.children.push(inner_container);
1235
1236        // Separator 2
1237        outer_container.children.push(LayoutNode::text(
1238            "separator2".to_string(),
1239            Style::new(),
1240            0,
1241            Align::Start,
1242            false,
1243            Margin::default(),
1244            Constraints::default(),
1245        ));
1246
1247        // Footer
1248        outer_container.children.push(LayoutNode::text(
1249            "footer".to_string(),
1250            Style::new(),
1251            0,
1252            Align::Start,
1253            false,
1254            Margin::default(),
1255            Constraints::default(),
1256        ));
1257
1258        root.children.push(outer_container);
1259
1260        // Compute layout with 80x50 terminal
1261        compute(&mut root, Rect::new(0, 0, 80, 50));
1262
1263        // Debug output
1264        eprintln!("\n=== DIAGNOSTIC LAYOUT TEST ===");
1265        eprintln!("Root node:");
1266        eprintln!("  pos: {:?}, size: {:?}", root.pos, root.size);
1267
1268        let outer = &root.children[0];
1269        eprintln!("\nOuter bordered container (grow:1):");
1270        eprintln!("  pos: {:?}, size: {:?}", outer.pos, outer.size);
1271
1272        let inner = &outer.children[2];
1273        eprintln!("\nInner container (grow:1, simulates scrollable):");
1274        eprintln!("  pos: {:?}, size: {:?}", inner.pos, inner.size);
1275
1276        eprintln!("\nAll children of outer container:");
1277        for (i, child) in outer.children.iter().enumerate() {
1278            eprintln!("  [{}] pos: {:?}, size: {:?}", i, child.pos, child.size);
1279        }
1280
1281        // Assertions
1282        // Root should fill the entire 80x50 area
1283        assert_eq!(
1284            root.size,
1285            (80, 50),
1286            "Root node should fill entire terminal (80x50)"
1287        );
1288
1289        // Outer container should also be 80x50 (full height due to grow:1)
1290        assert_eq!(
1291            outer.size,
1292            (80, 50),
1293            "Outer bordered container should fill entire terminal (80x50)"
1294        );
1295
1296        // Calculate expected inner container height:
1297        // Available height = 50 (total)
1298        // Border inset = 1 (top) + 1 (bottom) = 2
1299        // Padding = 1 (top) + 1 (bottom) = 2
1300        // Fixed children heights: header(1) + sep(1) + sep2(1) + footer(1) = 4
1301        // Expected inner height = 50 - 2 - 2 - 4 = 42
1302        let expected_inner_height = 50 - 2 - 2 - 4;
1303        assert_eq!(
1304            inner.size.1, expected_inner_height as u32,
1305            "Inner container height should be {} (50 - border(2) - padding(2) - fixed(4))",
1306            expected_inner_height
1307        );
1308
1309        // Inner container should start at y = border(1) + padding(1) + header(1) + sep(1) = 4
1310        let expected_inner_y = 1 + 1 + 1 + 1;
1311        assert_eq!(
1312            inner.pos.1, expected_inner_y as u32,
1313            "Inner container should start at y={} (border+padding+header+sep)",
1314            expected_inner_y
1315        );
1316
1317        eprintln!("\n✓ All assertions passed!");
1318        eprintln!("  Root size: {:?}", root.size);
1319        eprintln!("  Outer container size: {:?}", outer.size);
1320        eprintln!("  Inner container size: {:?}", inner.size);
1321        eprintln!("  Inner container pos: {:?}", inner.pos);
1322    }
1323
1324    #[test]
1325    fn collect_focus_rects_from_markers() {
1326        use super::*;
1327        use crate::style::Style;
1328
1329        let commands = vec![
1330            Command::FocusMarker(0),
1331            Command::Text {
1332                content: "input1".into(),
1333                style: Style::new(),
1334                grow: 0,
1335                align: Align::Start,
1336                wrap: false,
1337                margin: Default::default(),
1338                constraints: Default::default(),
1339            },
1340            Command::FocusMarker(1),
1341            Command::Text {
1342                content: "input2".into(),
1343                style: Style::new(),
1344                grow: 0,
1345                align: Align::Start,
1346                wrap: false,
1347                margin: Default::default(),
1348                constraints: Default::default(),
1349            },
1350        ];
1351
1352        let mut tree = build_tree(&commands);
1353        let area = crate::rect::Rect::new(0, 0, 40, 10);
1354        compute(&mut tree, area);
1355
1356        let fd = collect_all(&tree);
1357        assert_eq!(fd.focus_rects.len(), 2);
1358        assert_eq!(fd.focus_rects[0].0, 0);
1359        assert_eq!(fd.focus_rects[1].0, 1);
1360        assert!(fd.focus_rects[0].1.width > 0);
1361        assert!(fd.focus_rects[1].1.width > 0);
1362        assert_ne!(fd.focus_rects[0].1.y, fd.focus_rects[1].1.y);
1363    }
1364
1365    #[test]
1366    fn focus_marker_tags_container() {
1367        use super::*;
1368        use crate::style::{Border, Style};
1369
1370        let commands = vec![
1371            Command::FocusMarker(0),
1372            Command::BeginContainer {
1373                direction: Direction::Column,
1374                gap: 0,
1375                align: Align::Start,
1376                justify: Justify::Start,
1377                border: Some(Border::Single),
1378                border_sides: BorderSides::all(),
1379                border_style: Style::new(),
1380                bg_color: None,
1381                padding: Padding::default(),
1382                margin: Default::default(),
1383                constraints: Default::default(),
1384                title: None,
1385                grow: 0,
1386                group_name: None,
1387            },
1388            Command::Text {
1389                content: "inside".into(),
1390                style: Style::new(),
1391                grow: 0,
1392                align: Align::Start,
1393                wrap: false,
1394                margin: Default::default(),
1395                constraints: Default::default(),
1396            },
1397            Command::EndContainer,
1398        ];
1399
1400        let mut tree = build_tree(&commands);
1401        let area = crate::rect::Rect::new(0, 0, 40, 10);
1402        compute(&mut tree, area);
1403
1404        let fd = collect_all(&tree);
1405        assert_eq!(fd.focus_rects.len(), 1);
1406        assert_eq!(fd.focus_rects[0].0, 0);
1407        assert!(fd.focus_rects[0].1.width >= 8);
1408        assert!(fd.focus_rects[0].1.height >= 3);
1409    }
1410}