Skip to main content

a2ui_base/model/
component_context.rs

1//! Component rendering context — passed to component implementations during build.
2
3use std::collections::HashMap;
4
5use super::components_model::SurfaceComponentsModel;
6use super::data_context::DataContext;
7use super::data_model::DataModel;
8use crate::catalog::function_api::FunctionImplementation;
9
10/// Transient context created for each component during rendering.
11///
12/// The caller is responsible for holding the RefCell borrows on DataModel
13/// and SurfaceComponentsModel for the duration of rendering.
14pub struct ComponentContext<'a> {
15    /// The component's ID.
16    pub component_id: String,
17    /// The surface ID this component belongs to.
18    pub surface_id: String,
19    /// Scoped data access for resolving dynamic values.
20    pub data_context: DataContext<'a>,
21    /// The components model (escape hatch for inspecting siblings/children).
22    pub components: &'a SurfaceComponentsModel,
23    /// The ID of the currently focused component, if any.
24    pub focused_id: Option<String>,
25    /// The index of this component within a template iteration, if applicable.
26    pub template_index: Option<usize>,
27}
28
29impl<'a> ComponentContext<'a> {
30    /// Create a component context.
31    ///
32    /// Callers should borrow `surface.data_model` and `surface.components`
33    /// before calling this and pass the references.
34    ///
35    /// The `base_path` scopes data access for this component. When it ends in a
36    /// numeric segment (e.g. `/items/3` — the shape every backend produces when
37    /// expanding a `ChildList::Template`), that segment is taken as the template
38    /// item index and exposes the `@index` system function. Callers needing
39    /// precise control can override it via [`with_template_index`](Self::with_template_index).
40    pub fn new(
41        component_id: String,
42        surface_id: String,
43        data_model: &'a DataModel,
44        components: &'a SurfaceComponentsModel,
45        functions: &'a HashMap<String, Box<dyn FunctionImplementation>>,
46        base_path: &str,
47        focused_id: Option<String>,
48    ) -> Self {
49        let data_context = if base_path.is_empty() {
50            DataContext::new(data_model, functions)
51        } else {
52            DataContext::new(data_model, functions).nested(base_path.trim_start_matches('/'))
53        };
54
55        // Derive the template index from the trailing path segment so the
56        // `@index` system function works without each backend having to thread
57        // the index through explicitly. Template items always render at a path
58        // ending in their array index (`<path>/<i>`); static components never do.
59        let template_index = index_from_base_path(base_path);
60        let data_context = data_context.with_template_index(template_index);
61
62        Self {
63            component_id,
64            surface_id,
65            data_context,
66            components,
67            focused_id,
68            template_index,
69        }
70    }
71
72    /// Override the template index (builder style), propagating it to the data
73    /// context so the `@index` system function resolves correctly.
74    ///
75    /// `Some(i)` sets the index; `None` clears it (disabling `@index`).
76    pub fn with_template_index(mut self, index: Option<usize>) -> Self {
77        self.template_index = index;
78        self.data_context.set_template_index(index);
79        self
80    }
81
82    /// Set the template index in place, propagating it to the data context.
83    pub fn set_template_index(&mut self, index: Option<usize>) {
84        self.template_index = index;
85        self.data_context.set_template_index(index);
86    }
87}
88
89/// Derive a template item index from a (possibly relative) data path.
90///
91/// Returns the trailing segment parsed as a `usize` when it is a plain
92/// non-negative integer (e.g. `"/items/3"` → `3`, `"items/0"` → `0`), and
93/// `None` otherwise (e.g. `""`, `"/"`, `"/user"`, `"/items/3/name"`). This is
94/// exactly the shape every backend emits when expanding a
95/// `ChildList::Template`, so the `@index` system function is resolved without
96/// per-backend plumbing.
97fn index_from_base_path(path: &str) -> Option<usize> {
98    path.rsplit('/').next()?.parse::<usize>().ok()
99}
100
101#[cfg(test)]
102mod path_tests {
103    use super::index_from_base_path;
104
105    #[test]
106    fn absolute_template_path() {
107        assert_eq!(index_from_base_path("/items/0"), Some(0));
108        assert_eq!(index_from_base_path("/items/42"), Some(42));
109    }
110
111    #[test]
112    fn relative_template_path() {
113        assert_eq!(index_from_base_path("items/7"), Some(7));
114    }
115
116    #[test]
117    fn non_template_paths_yield_none() {
118        assert_eq!(index_from_base_path(""), None);
119        assert_eq!(index_from_base_path("/"), None);
120        assert_eq!(index_from_base_path("/user"), None);
121        assert_eq!(index_from_base_path("/items/3/name"), None);
122    }
123}