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}