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