Skip to main content

slt/
layout.rs

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