Skip to main content

pepl_ui/components/
list.rs

1//! List & Data component builder — ScrollList.
2//!
3//! ScrollList renders a scrollable list of items using a `render` lambda
4//! and a `key` function for identity. Items come from a list prop, not children.
5
6use crate::accessibility;
7use crate::prop_value::PropValue;
8use crate::surface::SurfaceNode;
9
10// ── ScrollListBuilder ─────────────────────────────────────────────────────────
11
12/// Builder for a ScrollList component.
13///
14/// Required: `items` (List), `render` (Lambda), `key` (Lambda).
15/// Optional: `on_reorder` (Lambda), `dividers` (bool).
16pub struct ScrollListBuilder {
17    items: PropValue,
18    render: PropValue,
19    key: PropValue,
20    on_reorder: Option<PropValue>,
21    dividers: Option<bool>,
22}
23
24impl ScrollListBuilder {
25    /// Create a new ScrollListBuilder with required props.
26    ///
27    /// - `items` must be a `PropValue::List` — the data items to render.
28    /// - `render` must be a `PropValue::Lambda` — called `(item, index) -> Surface`.
29    /// - `key` must be a `PropValue::Lambda` — called `(item) -> string`.
30    pub fn new(items: PropValue, render: PropValue, key: PropValue) -> Self {
31        Self {
32            items,
33            render,
34            key,
35            on_reorder: None,
36            dividers: None,
37        }
38    }
39
40    /// Set the `on_reorder` callback (Lambda).
41    pub fn on_reorder(mut self, on_reorder: PropValue) -> Self {
42        self.on_reorder = Some(on_reorder);
43        self
44    }
45
46    /// Set whether dividers are shown between items.
47    pub fn dividers(mut self, dividers: bool) -> Self {
48        self.dividers = Some(dividers);
49        self
50    }
51
52    pub fn build(self) -> SurfaceNode {
53        let mut node = SurfaceNode::new("ScrollList");
54        node.set_prop("items", self.items);
55        node.set_prop("render", self.render);
56        node.set_prop("key", self.key);
57        if let Some(on_reorder) = self.on_reorder {
58            node.set_prop("on_reorder", on_reorder);
59        }
60        if let Some(dividers) = self.dividers {
61            node.set_prop("dividers", PropValue::Bool(dividers));
62        }
63        accessibility::ensure_accessible(&mut node);
64        node
65    }
66}
67
68// ── Validation ────────────────────────────────────────────────────────────────
69
70/// Validate a list/data component node (ScrollList).
71pub fn validate_list_node(node: &SurfaceNode) -> Vec<String> {
72    match node.component_type.as_str() {
73        "ScrollList" => validate_scroll_list(node),
74        _ => vec![format!("Unknown list component: {}", node.component_type)],
75    }
76}
77
78fn validate_scroll_list(node: &SurfaceNode) -> Vec<String> {
79    let mut errors = Vec::new();
80
81    // Required: items (list)
82    match node.props.get("items") {
83        Some(PropValue::List(_)) => {}
84        Some(other) => errors.push(format!(
85            "ScrollList.items: expected list, got {}",
86            other.type_name()
87        )),
88        None => errors.push("ScrollList.items: required prop missing".to_string()),
89    }
90
91    // Required: render (lambda)
92    match node.props.get("render") {
93        Some(PropValue::Lambda { .. }) => {}
94        Some(other) => errors.push(format!(
95            "ScrollList.render: expected lambda, got {}",
96            other.type_name()
97        )),
98        None => errors.push("ScrollList.render: required prop missing".to_string()),
99    }
100
101    // Required: key (lambda)
102    match node.props.get("key") {
103        Some(PropValue::Lambda { .. }) => {}
104        Some(other) => errors.push(format!(
105            "ScrollList.key: expected lambda, got {}",
106            other.type_name()
107        )),
108        None => errors.push("ScrollList.key: required prop missing".to_string()),
109    }
110
111    // Optional: on_reorder (lambda)
112    if let Some(prop) = node.props.get("on_reorder") {
113        if !matches!(prop, PropValue::Lambda { .. }) {
114            errors.push(format!(
115                "ScrollList.on_reorder: expected lambda, got {}",
116                prop.type_name()
117            ));
118        }
119    }
120
121    // Optional: dividers (bool)
122    if let Some(prop) = node.props.get("dividers") {
123        if !matches!(prop, PropValue::Bool(_)) {
124            errors.push(format!(
125                "ScrollList.dividers: expected bool, got {}",
126                prop.type_name()
127            ));
128        }
129    }
130
131    // No children (items rendered via render lambda)
132    if !node.children.is_empty() {
133        errors.push(format!(
134            "ScrollList: does not accept children, but got {}",
135            node.children.len()
136        ));
137    }
138
139    // Optional: accessible (record)
140    if let Some(prop) = node.props.get("accessible") {
141        errors.extend(accessibility::validate_accessible_prop("ScrollList", prop));
142    }
143
144    // Unknown props
145    for key in node.props.keys() {
146        if !matches!(
147            key.as_str(),
148            "items" | "render" | "key" | "on_reorder" | "dividers" | "accessible"
149        ) {
150            errors.push(format!("ScrollList: unknown prop '{key}'"));
151        }
152    }
153
154    errors
155}