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