a2ui_tui/components/
generic.rs1use ratatui::{
14 Frame,
15 layout::{Alignment, Rect},
16 style::{Color, Style},
17 text::{Line, Span},
18 widgets::{Block, Paragraph},
19};
20
21use a2ui_base::model::component_context::ComponentContext;
22use a2ui_base::protocol::common_types::{ChildList, DynamicString};
23use crate::component_impl::TuiComponent;
24
25pub struct GenericComponent;
27
28impl TuiComponent for GenericComponent {
29 fn name(&self) -> &'static str {
30 "Generic"
31 }
32
33 fn render(
34 &self,
35 ctx: &ComponentContext,
36 area: Rect,
37 frame: &mut Frame,
38 render_child: &mut dyn FnMut(&str, Rect, &mut Frame, &str),
39 _measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
40 ) {
41 let comp_model = match ctx.components.get(&ctx.component_id) {
42 Some(m) => m,
43 None => return,
44 };
45
46 let title = format!("{} (unknown)", comp_model.component_type);
48 let block = Block::bordered()
49 .title(title)
50 .border_style(Style::default().fg(Color::Yellow))
51 .style(Style::default().fg(Color::Gray));
52
53 let inner = block.inner(area);
54 frame.render_widget(block, area);
56 if inner.width == 0 || inner.height == 0 {
57 return;
58 }
59
60 let mut lines: Vec<Line> = Vec::new();
63 for (key, val) in comp_model.properties.iter() {
64 let resolved = resolve_value_for_display(val, ctx);
65 let line = Line::from(vec![
66 Span::styled(format!("{key}: "), Style::default().fg(Color::Cyan)),
67 Span::raw(resolved),
68 ]);
69 lines.push(line);
70 }
71
72 if lines.is_empty() {
73 lines.push(Line::from("(no properties)").alignment(Alignment::Center));
74 }
75
76 let child_ids = collect_child_ids(comp_model);
78 let child_row_count = if child_ids.is_empty() { 0 } else { 1 };
79
80 let prop_area = Rect {
81 x: inner.x,
82 y: inner.y,
83 width: inner.width,
84 height: inner.height.saturating_sub(child_row_count),
85 };
86
87 if prop_area.height > 0 {
88 frame.render_widget(Paragraph::new(lines), prop_area);
89 }
90
91 if !child_ids.is_empty() && child_row_count > 0 && inner.height > child_row_count {
93 let child_area = Rect {
94 x: inner.x,
95 y: inner.y + prop_area.height,
96 width: inner.width,
97 height: child_row_count as u16,
98 };
99 let count = child_ids.len() as u16;
101 let slice_w = child_area.width / count.max(1);
102 for (i, cid) in child_ids.iter().enumerate() {
103 let ca = Rect {
104 x: child_area.x + (slice_w * i as u16),
105 y: child_area.y,
106 width: slice_w,
107 height: child_area.height,
108 };
109 if ca.width > 0 && ca.height > 0 {
110 render_child(cid, ca, frame, "");
111 }
112 }
113 }
114 }
115
116 fn natural_height(
117 &self,
118 ctx: &ComponentContext,
119 _available_width: u16,
120 _measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
121 ) -> Option<u16> {
122 let comp_model = ctx.components.get(&ctx.component_id)?;
123 let prop_count = comp_model.properties.len().max(1) as u16;
124 let has_children = comp_model.child().is_some()
125 || matches!(comp_model.children(), Some(a2ui_base::protocol::common_types::ChildList::Static(v)) if !v.is_empty());
126 let mut h = prop_count.saturating_add(2);
127 if has_children {
128 h = h.saturating_add(1);
129 }
130 Some(h)
131 }
132}
133
134fn resolve_value_for_display(
138 val: &serde_json::Value,
139 ctx: &ComponentContext,
140) -> String {
141 if let serde_json::Value::String(_) = val {
143 if let Ok(ds) = serde_json::from_value::<DynamicString>(val.clone()) {
144 return ctx.data_context.resolve_dynamic_string(&ds);
145 }
146 }
147 val.to_string()
149}
150
151fn collect_child_ids(
154 comp_model: &a2ui_base::model::component_model::ComponentModel,
155) -> Vec<String> {
156 let mut ids = Vec::new();
157 if let Some(single) = comp_model.child() {
158 ids.push(single);
159 }
160 if let Some(list) = comp_model.children() {
161 match list {
162 ChildList::Static(v) => ids.extend(v),
163 ChildList::Template { .. } => {}
165 }
166 }
167 ids
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use a2ui_base::model::component_model::ComponentModel;
174 use serde_json::json;
175
176 #[test]
177 fn collect_child_ids_single() {
178 let cm = ComponentModel::from_json(&json!({
179 "id": "x",
180 "component": "Mystery",
181 "child": "label"
182 }))
183 .unwrap();
184 assert_eq!(collect_child_ids(&cm), vec!["label".to_string()]);
185 }
186
187 #[test]
188 fn collect_child_ids_list() {
189 let cm = ComponentModel::from_json(&json!({
190 "id": "x",
191 "component": "Mystery",
192 "children": ["a", "b"]
193 }))
194 .unwrap();
195 assert_eq!(collect_child_ids(&cm), vec!["a".to_string(), "b".to_string()]);
196 }
197
198 #[test]
199 fn collect_child_ids_empty() {
200 let cm = ComponentModel::from_json(&json!({
201 "id": "x",
202 "component": "Mystery"
203 }))
204 .unwrap();
205 assert!(collect_child_ids(&cm).is_empty());
206 }
207
208 #[test]
209 fn name_is_generic() {
210 assert_eq!(GenericComponent.name(), "Generic");
211 }
212}