Skip to main content

a2ui_tui/components/
row.rs

1//! Row component — horizontal layout container.
2
3use ratatui::{Frame, layout::{Direction, Rect}};
4
5use a2ui_base::model::component_context::ComponentContext;
6use a2ui_base::protocol::common_types::{Align, ChildList, Justify};
7use crate::component_impl::TuiComponent;
8use crate::layout_engine::{apply_align, flex_layout};
9
10/// Row component implementation.
11///
12/// Lays out children horizontally using weighted splitting.
13/// Invisible container — no margin or padding.
14pub struct RowComponent;
15
16impl TuiComponent for RowComponent {
17    fn name(&self) -> &'static str {
18        "Row"
19    }
20
21    fn render(
22        &self,
23        ctx: &ComponentContext,
24        area: Rect,
25        frame: &mut Frame,
26        render_child: &mut dyn FnMut(&str, Rect, &mut Frame, &str),
27        measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
28    ) {
29        let comp_model = match ctx.components.get(&ctx.component_id) {
30            Some(m) => m,
31            None => return,
32        };
33
34        let children = match comp_model.children() {
35            Some(c) => c,
36            None => return,
37        };
38
39        let justify = comp_model.get_property::<Justify>("justify").unwrap_or(Justify::Start);
40        let align = comp_model.get_property::<Align>("align").unwrap_or(Align::Start);
41
42        match children {
43            ChildList::Static(ids) => {
44                render_static_children(
45                    ctx, area, frame, render_child, measure_child,
46                    &ids, justify, align, Direction::Horizontal,
47                );
48            }
49            ChildList::Template { component_id, path } => {
50                render_template_children(
51                    ctx, area, frame, render_child, measure_child,
52                    &component_id, &path, justify, align, Direction::Horizontal,
53                );
54            }
55        }
56    }
57
58    /// A Row's height = the tallest child's natural height (cross axis).
59    fn natural_height(
60        &self,
61        ctx: &ComponentContext,
62        available_width: u16,
63        measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
64    ) -> Option<u16> {
65        let comp_model = ctx.components.get(&ctx.component_id)?;
66        let ids = match comp_model.children()? {
67            ChildList::Static(ids) => ids,
68            ChildList::Template { component_id, path } => {
69                let count = match ctx.data_context.get(&path) {
70                    Some(serde_json::Value::Array(arr)) => arr.len(),
71                    _ => return None,
72                };
73                if count == 0 {
74                    return Some(0);
75                }
76                let item_path = format!("{}/{}", path, 0);
77                return measure_child(&component_id, &item_path, available_width);
78            }
79        };
80        if ids.is_empty() {
81            return Some(0);
82        }
83        // Static children inherit this component's base path (matters when this
84        // component is itself a template instance rendered at a nested path).
85        let base = ctx.data_context.base_path();
86        let mut max: u16 = 0;
87        for id in &ids {
88            max = max.max(measure_child(id, base, available_width)?);
89        }
90        Some(max)
91    }
92}
93
94/// Render a static list of children with flexbox layout (shared with Column/Row/List).
95///
96/// On the **vertical** main axis, each child is measured for its natural height and the
97/// axis is distributed by natural size + flex-grow (`weight`); leftover space is placed
98/// per `justify`. On the **horizontal** main axis, natural width is not measured, so
99/// children are distributed by weight (legacy behavior); their cross-axis (height) is
100/// then handled by `align`.
101pub(crate) fn render_static_children(
102    ctx: &ComponentContext,
103    area: Rect,
104    frame: &mut Frame,
105    render_child: &mut dyn FnMut(&str, Rect, &mut Frame, &str),
106    measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
107    ids: &[String],
108    justify: Justify,
109    align: Align,
110    direction: Direction,
111) {
112    if ids.is_empty() {
113        return;
114    }
115
116    // Static children inherit the parent's current base path so a static list
117    // nested inside a template instance still resolves bindings against the item's
118    // data scope (e.g. a template card rendered at /items/0 → [title, subtitle]).
119    let base = ctx.data_context.base_path().to_string();
120
121    // Build (natural_main_size, weight) per child. Only the vertical main axis has a
122    // measured natural size (height); horizontal relies on weight distribution.
123    let items: Vec<(Option<u16>, Option<f64>)> = ids
124        .iter()
125        .map(|id| {
126            let weight = ctx.components.get(id).and_then(|m| m.weight());
127            let natural = match direction {
128                Direction::Vertical => measure_child(id, &base, area.width),
129                Direction::Horizontal => None,
130            };
131            (natural, weight)
132        })
133        .collect();
134
135    let rects = flex_layout(direction, area, &items, justify);
136
137    // Apply cross-axis alignment and render each child.
138    for (i, child_id) in ids.iter().enumerate() {
139        let child_area = apply_align(align, rects[i], area, direction);
140        render_child(child_id, child_area, frame, &base);
141    }
142}
143
144/// Render template children by iterating over a data-bound array (shared with Column/Row/List).
145pub(crate) fn render_template_children(
146    ctx: &ComponentContext,
147    area: Rect,
148    frame: &mut Frame,
149    render_child: &mut dyn FnMut(&str, Rect, &mut Frame, &str),
150    measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
151    component_id: &str,
152    path: &str,
153    justify: Justify,
154    align: Align,
155    direction: Direction,
156) {
157    // Resolve the data array at the given path.
158    let array = match ctx.data_context.get(path) {
159        Some(serde_json::Value::Array(arr)) => arr,
160        _ => return,
161    };
162
163    let count = array.len();
164    if count == 0 {
165        return;
166    }
167
168    // Measure each instance at its own item path so data-dependent heights (e.g. option
169    // counts) resolve correctly. Horizontal main axis has no measured natural width.
170    let items: Vec<(Option<u16>, Option<f64>)> = (0..count)
171        .map(|i| {
172            let item_path = format!("{}/{}", path, i);
173            let natural = match direction {
174                Direction::Vertical => measure_child(component_id, &item_path, area.width),
175                Direction::Horizontal => None,
176            };
177            // Template instances carry no explicit weight → equal share / legacy fill.
178            (natural, None)
179        })
180        .collect();
181
182    let rects = flex_layout(direction, area, &items, justify);
183
184    for i in 0..count {
185        let child_area = apply_align(align, rects[i], area, direction);
186        // Per-item nested path so each template instance resolves its own array element.
187        let item_path = format!("{}/{}", path, i);
188        render_child(component_id, child_area, frame, &item_path);
189    }
190}