Skip to main content

a2ui_tui/components/
list.rs

1//! List component — renders children in a vertical or horizontal layout.
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::components::row::{render_static_children, render_template_children};
9
10/// List component implementation.
11///
12/// Lays out children vertically (default) or horizontally using weighted splitting.
13/// Invisible container — no margin or padding.
14pub struct ListComponent;
15
16impl TuiComponent for ListComponent {
17    fn name(&self) -> &'static str {
18        "List"
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        // Determine direction: default is "vertical".
40        let dir = list_direction(comp_model);
41
42        let justify = comp_model
43            .get_property::<Justify>("justify")
44            .unwrap_or(Justify::Start);
45        let align = comp_model
46            .get_property::<Align>("align")
47            .unwrap_or(Align::Stretch);
48
49        match children {
50            ChildList::Static(ids) => {
51                render_static_children(
52                    ctx, area, frame, render_child, measure_child,
53                    &ids, justify, align, dir,
54                );
55            }
56            ChildList::Template { component_id, path } => {
57                render_template_children(
58                    ctx, area, frame, render_child, measure_child,
59                    &component_id, &path, justify, align, dir,
60                );
61            }
62        }
63    }
64
65    /// Natural height: vertical → sum of children; horizontal → max of children.
66    fn natural_height(
67        &self,
68        ctx: &ComponentContext,
69        available_width: u16,
70        measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
71    ) -> Option<u16> {
72        let comp_model = ctx.components.get(&ctx.component_id)?;
73        let dir = list_direction(comp_model);
74        let ids = match comp_model.children()? {
75            ChildList::Static(ids) => ids,
76            ChildList::Template { component_id, path } => {
77                let count = match ctx.data_context.get(&path) {
78                    Some(serde_json::Value::Array(arr)) => arr.len(),
79                    _ => return None,
80                };
81                if count == 0 {
82                    return Some(0);
83                }
84                let item_path = format!("{}/{}", path, 0);
85                let one = measure_child(&component_id, &item_path, available_width)?;
86                return match dir {
87                    Direction::Vertical => Some(one.saturating_mul(count as u16)),
88                    Direction::Horizontal => Some(one),
89                };
90            }
91        };
92        if ids.is_empty() {
93            return Some(0);
94        }
95        // Static children inherit this component's base path (matters when this
96        // component is itself a template instance rendered at a nested path).
97        let base = ctx.data_context.base_path();
98        match dir {
99            Direction::Vertical => {
100                let mut sum: u16 = 0;
101                for id in &ids {
102                    sum = sum.saturating_add(measure_child(id, base, available_width)?);
103                }
104                Some(sum)
105            }
106            Direction::Horizontal => {
107                let mut max: u16 = 0;
108                for id in &ids {
109                    max = max.max(measure_child(id, base, available_width)?);
110                }
111                Some(max)
112            }
113        }
114    }
115}
116
117/// Resolve a List's direction property (default vertical).
118fn list_direction(
119    comp_model: &a2ui_base::model::component_model::ComponentModel,
120) -> Direction {
121    let direction: Option<String> = comp_model.get_property("direction");
122    match direction.as_deref() {
123        Some("horizontal") => Direction::Horizontal,
124        _ => Direction::Vertical,
125    }
126}