use ratatui::{
Frame,
layout::{Alignment, Rect},
style::{Color, Style},
text::{Line, Span},
widgets::{Block, Paragraph},
};
use a2ui_base::model::component_context::ComponentContext;
use a2ui_base::protocol::common_types::{ChildList, DynamicString};
use crate::component_impl::TuiComponent;
pub struct GenericComponent;
impl TuiComponent for GenericComponent {
fn name(&self) -> &'static str {
"Generic"
}
fn render(
&self,
ctx: &ComponentContext,
area: Rect,
frame: &mut Frame,
render_child: &mut dyn FnMut(&str, Rect, &mut Frame, &str),
_measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
) {
let comp_model = match ctx.components.get(&ctx.component_id) {
Some(m) => m,
None => return,
};
let title = format!("{} (unknown)", comp_model.component_type);
let block = Block::bordered()
.title(title)
.border_style(Style::default().fg(Color::Yellow))
.style(Style::default().fg(Color::Gray));
let inner = block.inner(area);
frame.render_widget(block, area);
if inner.width == 0 || inner.height == 0 {
return;
}
let mut lines: Vec<Line> = Vec::new();
for (key, val) in comp_model.properties.iter() {
let resolved = resolve_value_for_display(val, ctx);
let line = Line::from(vec![
Span::styled(format!("{key}: "), Style::default().fg(Color::Cyan)),
Span::raw(resolved),
]);
lines.push(line);
}
if lines.is_empty() {
lines.push(Line::from("(no properties)").alignment(Alignment::Center));
}
let child_ids = collect_child_ids(comp_model);
let child_row_count = if child_ids.is_empty() { 0 } else { 1 };
let prop_area = Rect {
x: inner.x,
y: inner.y,
width: inner.width,
height: inner.height.saturating_sub(child_row_count),
};
if prop_area.height > 0 {
frame.render_widget(Paragraph::new(lines), prop_area);
}
if !child_ids.is_empty() && child_row_count > 0 && inner.height > child_row_count {
let child_area = Rect {
x: inner.x,
y: inner.y + prop_area.height,
width: inner.width,
height: child_row_count as u16,
};
let count = child_ids.len() as u16;
let slice_w = child_area.width / count.max(1);
for (i, cid) in child_ids.iter().enumerate() {
let ca = Rect {
x: child_area.x + (slice_w * i as u16),
y: child_area.y,
width: slice_w,
height: child_area.height,
};
if ca.width > 0 && ca.height > 0 {
render_child(cid, ca, frame, "");
}
}
}
}
fn natural_height(
&self,
ctx: &ComponentContext,
_available_width: u16,
_measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
) -> Option<u16> {
let comp_model = ctx.components.get(&ctx.component_id)?;
let prop_count = comp_model.properties.len().max(1) as u16;
let has_children = comp_model.child().is_some()
|| matches!(comp_model.children(), Some(a2ui_base::protocol::common_types::ChildList::Static(v)) if !v.is_empty());
let mut h = prop_count.saturating_add(2);
if has_children {
h = h.saturating_add(1);
}
Some(h)
}
}
fn resolve_value_for_display(
val: &serde_json::Value,
ctx: &ComponentContext,
) -> String {
if let serde_json::Value::String(_) = val {
if let Ok(ds) = serde_json::from_value::<DynamicString>(val.clone()) {
return ctx.data_context.resolve_dynamic_string(&ds);
}
}
val.to_string()
}
fn collect_child_ids(
comp_model: &a2ui_base::model::component_model::ComponentModel,
) -> Vec<String> {
let mut ids = Vec::new();
if let Some(single) = comp_model.child() {
ids.push(single);
}
if let Some(list) = comp_model.children() {
match list {
ChildList::Static(v) => ids.extend(v),
ChildList::Template { .. } => {}
}
}
ids
}
#[cfg(test)]
mod tests {
use super::*;
use a2ui_base::model::component_model::ComponentModel;
use serde_json::json;
#[test]
fn collect_child_ids_single() {
let cm = ComponentModel::from_json(&json!({
"id": "x",
"component": "Mystery",
"child": "label"
}))
.unwrap();
assert_eq!(collect_child_ids(&cm), vec!["label".to_string()]);
}
#[test]
fn collect_child_ids_list() {
let cm = ComponentModel::from_json(&json!({
"id": "x",
"component": "Mystery",
"children": ["a", "b"]
}))
.unwrap();
assert_eq!(collect_child_ids(&cm), vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn collect_child_ids_empty() {
let cm = ComponentModel::from_json(&json!({
"id": "x",
"component": "Mystery"
}))
.unwrap();
assert!(collect_child_ids(&cm).is_empty());
}
#[test]
fn name_is_generic() {
assert_eq!(GenericComponent.name(), "Generic");
}
}