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