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