Skip to main content

slt/
layout.rs

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