use std::collections::HashMap;
use std::rc::Rc;
use a2ui_base::catalog::function_api::FunctionImplementation;
use a2ui_base::model::component_context::ComponentContext;
use a2ui_base::model::component_model::ComponentModel;
use a2ui_base::message_processor::MessageProcessor;
use a2ui_base::protocol::common_types::{ChildList, DynamicBoolean, DynamicNumber, DynamicString};
use dioxus::prelude::*;
pub(crate) type Functions = HashMap<String, Box<dyn FunctionImplementation>>;
pub(crate) type OnActivate = Rc<dyn Fn(String)>;
#[component]
pub fn A2uiNode(id: String, base_path: String) -> Element {
let processor: Signal<MessageProcessor> = use_context();
let functions: Rc<Functions> = use_context();
let on_activate: OnActivate = use_context();
let focused: Signal<Option<String>> = use_context();
let p = processor.read();
let Some(surface) = p.model.surfaces().next() else {
return rsx! { span { class: "unknown", "No surface loaded." } };
};
let components = surface.components.borrow();
let data_model = surface.data_model.borrow();
let Some(model) = components.get(&id) else {
return rsx! { span { class: "unknown", "Component not found: {id}" } };
};
let focused_id = focused.read().as_deref().map(str::to_string);
let ctx = ComponentContext::new(
id.clone(),
surface.id.clone(),
&data_model,
&components,
functions.as_ref(),
&base_path,
focused_id,
);
match model.component_type.as_str() {
"Column" | "List" => render_column(model, &ctx),
"Row" => render_row(model, &ctx),
"Card" => render_card(model, &ctx),
"Tabs" => render_tabs(model, &ctx),
"Modal" => render_modal(model, &ctx),
"Text" => render_text(model, &ctx),
"Divider" => render_divider(),
"Icon" => render_icon(model, &ctx),
"DateTimeInput" => render_date_time_input(model, &ctx),
"Image" => render_media("Image", "▣", model, &ctx),
"Video" => render_media("Video", "▷", model, &ctx),
"AudioPlayer" => render_media("Audio", "♪", model, &ctx),
"Button" => render_button(model, &ctx, &on_activate),
"TextField" => render_text_field(model, &ctx, processor),
"CheckBox" => render_checkbox(model, &ctx, processor),
"Slider" => render_slider(model, &ctx, processor),
"ChoicePicker" => render_choice_picker(model, &ctx),
_ => render_unknown(model, &ctx),
}
}
fn build_child_plan(model: &ComponentModel, ctx: &ComponentContext) -> Vec<(String, String)> {
let mut plan = Vec::new();
let base = ctx.data_context.base_path().to_string();
if let Some(child_id) = model.child() {
plan.push((child_id, base.clone()));
}
match model.children() {
Some(ChildList::Static(ids)) => {
for cid in ids {
plan.push((cid.clone(), base.clone()));
}
}
Some(ChildList::Template { component_id, path }) => {
if let Some(serde_json::Value::Array(arr)) = ctx.data_context.get(&path) {
for i in 0..arr.len() {
plan.push((component_id.clone(), format!("{path}/{i}")));
}
}
}
None => {}
}
plan
}
fn render_column(model: &ComponentModel, ctx: &ComponentContext) -> Element {
let plan = build_child_plan(model, ctx);
rsx! {
div { class: "col",
for (cid, base) in plan {
A2uiNode { id: cid, base_path: base }
}
}
}
}
fn render_row(model: &ComponentModel, ctx: &ComponentContext) -> Element {
let plan = build_child_plan(model, ctx);
rsx! {
div { class: "row",
for (cid, base) in plan {
A2uiNode { id: cid, base_path: base }
}
}
}
}
fn render_card(model: &ComponentModel, ctx: &ComponentContext) -> Element {
let plan = build_child_plan(model, ctx);
rsx! {
div { class: "card col",
for (cid, base) in plan {
A2uiNode { id: cid, base_path: base }
}
}
}
}
fn render_modal(model: &ComponentModel, ctx: &ComponentContext) -> Element {
if let Some(trigger_id) = model.get_property::<String>("trigger") {
rsx! { A2uiNode { id: trigger_id, base_path: ctx.data_context.base_path().to_string() } }
} else {
rsx! { span {} }
}
}
fn render_tabs(model: &ComponentModel, ctx: &ComponentContext) -> Element {
let active = model
.get_property::<DynamicNumber>("activeTab")
.as_ref()
.map(|dn| ctx.data_context.resolve_dynamic_number(dn))
.unwrap_or(0.0) as usize;
let plan = build_child_plan(model, ctx);
if let Some((child_id, child_base)) = plan.into_iter().nth(active) {
rsx! { A2uiNode { id: child_id, base_path: child_base } }
} else {
rsx! { span {} }
}
}
fn render_text(model: &ComponentModel, ctx: &ComponentContext) -> Element {
let content = model
.get_property::<DynamicString>("text")
.map(|ds| ctx.data_context.resolve_dynamic_string(&ds))
.unwrap_or_default();
let variant: Option<String> = model.get_property("variant");
let class = match variant.as_deref() {
Some("h1") => "text text--h1",
Some("h2") => "text text--h2",
Some("h3") => "text text--h3",
_ => "text",
};
rsx! { div { class: "{class}", "{content}" } }
}
fn render_divider() -> Element {
rsx! { hr {} }
}
fn render_icon(model: &ComponentModel, ctx: &ComponentContext) -> Element {
let name = model
.get_property::<DynamicString>("name")
.map(|ds| ctx.data_context.resolve_dynamic_string(&ds))
.unwrap_or_default();
chip("◈", &format!("icon · {name}"))
}
fn render_date_time_input(model: &ComponentModel, ctx: &ComponentContext) -> Element {
let label = model
.get_property::<DynamicString>("label")
.map(|ds| ctx.data_context.resolve_dynamic_string(&ds))
.unwrap_or_default();
let value = model
.get_property::<DynamicString>("value")
.map(|ds| ctx.data_context.resolve_dynamic_string(&ds))
.unwrap_or_default();
rsx! {
div { class: "col",
if !label.is_empty() {
span { class: "muted", style: "font-size:12px", "{label}" }
}
div { class: "text", style: "font-size:13px", "{value}" }
}
}
}
fn render_media(kind: &str, glyph: &str, model: &ComponentModel, ctx: &ComponentContext) -> Element {
let url = model
.get_property::<DynamicString>("url")
.map(|ds| ctx.data_context.resolve_dynamic_string(&ds))
.unwrap_or_default();
chip(glyph, &format!("{kind} · {url}"))
}
fn render_button(model: &ComponentModel, ctx: &ComponentContext, on_activate: &OnActivate) -> Element {
let label = resolve_child_text(ctx, model).unwrap_or_else(|| {
model
.accessibility()
.and_then(|a| a.label)
.map(|ds| ctx.data_context.resolve_dynamic_string(&ds))
.unwrap_or_default()
});
let variant: Option<String> = model.get_property("variant");
let disabled = !evaluate_checks(ctx, model);
let class = match variant.as_deref() {
Some("primary") => "btn btn--primary",
Some("borderless") => "btn btn--borderless",
_ => "btn",
};
let cb = on_activate.clone();
let id = ctx.component_id.clone();
rsx! {
button {
class: "{class}",
disabled,
onclick: move |_| cb(id.clone()),
"{label}"
}
}
}
fn render_text_field(model: &ComponentModel, ctx: &ComponentContext, mut processor: Signal<MessageProcessor>) -> Element {
let label = model
.get_property::<DynamicString>("label")
.map(|ds| ctx.data_context.resolve_dynamic_string(&ds))
.unwrap_or_default();
let value_binding = model.get_property::<DynamicString>("value");
let resolved = value_binding
.as_ref()
.map(|ds| ctx.data_context.resolve_dynamic_string(ds))
.unwrap_or_default();
let path: Option<String> = match &value_binding {
Some(DynamicString::Binding(b)) => Some(ctx.data_context.resolve_pointer(&b.path)),
_ => None,
};
rsx! {
label { class: "field",
if !label.is_empty() {
span { class: "label", "{label}" }
}
input {
value: "{resolved}",
placeholder: "{label}",
oninput: move |e| {
if let Some(path) = path.as_ref() {
let v = e.value();
let mut p = processor.write();
if let Some(surface) = p.model.surfaces_mut().next() {
surface.data_model.borrow_mut().set(path, serde_json::Value::String(v));
}
}
},
}
}
}
}
fn render_checkbox(model: &ComponentModel, ctx: &ComponentContext, mut processor: Signal<MessageProcessor>) -> Element {
let label = model
.get_property::<DynamicString>("label")
.map(|ds| ctx.data_context.resolve_dynamic_string(&ds))
.unwrap_or_default();
let value_binding = model.get_property::<DynamicBoolean>("value");
let resolved = value_binding
.as_ref()
.map(|db| ctx.data_context.resolve_dynamic_boolean(db))
.unwrap_or(false);
let path: Option<String> = match &value_binding {
Some(DynamicBoolean::Binding(b)) => Some(ctx.data_context.resolve_pointer(&b.path)),
_ => None,
};
rsx! {
label { class: "check",
input {
r#type: "checkbox",
checked: "{resolved}",
onchange: move |e| {
if let Some(path) = path.as_ref() {
let checked = e.checked();
let mut p = processor.write();
if let Some(surface) = p.model.surfaces_mut().next() {
surface.data_model.borrow_mut().set(path, serde_json::Value::Bool(checked));
}
}
},
}
"{label}"
}
}
}
fn render_slider(model: &ComponentModel, ctx: &ComponentContext, mut processor: Signal<MessageProcessor>) -> Element {
let value_binding = model.get_property::<DynamicNumber>("value");
let resolved = value_binding
.as_ref()
.map(|dn| ctx.data_context.resolve_dynamic_number(dn))
.unwrap_or(0.0) as f32;
let label = model
.get_property::<DynamicString>("label")
.map(|ds| ctx.data_context.resolve_dynamic_string(&ds))
.unwrap_or_default();
let path_opt: Option<String> = match &value_binding {
Some(DynamicNumber::Binding(b)) => Some(ctx.data_context.resolve_pointer(&b.path)),
_ => None,
};
rsx! {
label { class: "field",
if !label.is_empty() {
span { class: "label", "{label}" }
}
input {
class: "range",
r#type: "range",
min: "0",
max: "100",
value: "{resolved}",
oninput: move |e| {
if let Some(path) = path_opt.as_ref() {
let v: f64 = e.value().parse().unwrap_or(0.0);
let mut p = processor.write();
if let Some(surface) = p.model.surfaces_mut().next() {
surface.data_model.borrow_mut().set(path, serde_json::json!(v));
}
}
},
}
}
}
}
fn render_choice_picker(model: &ComponentModel, ctx: &ComponentContext) -> Element {
let label = model
.get_property::<DynamicString>("label")
.map(|ds| ctx.data_context.resolve_dynamic_string(&ds))
.unwrap_or_default();
chip("▾", &format!("select · {label}"))
}
fn render_unknown(model: &ComponentModel, ctx: &ComponentContext) -> Element {
let kind = model.component_type.clone();
let plan = build_child_plan(model, ctx);
let header = chip("?", &format!("{kind} · unknown"))?;
rsx! {
div { class: "col",
{header}
for (cid, base) in plan {
A2uiNode { id: cid, base_path: base }
}
}
}
}
fn chip(glyph: &str, label: &str) -> Element {
rsx! {
span { class: "chip",
span { class: "chip__glyph", "{glyph}" }
span { class: "chip__label", "{label}" }
}
}
}
fn resolve_child_text(ctx: &ComponentContext, model: &ComponentModel) -> Option<String> {
let child_id = model.child()?;
let child = ctx.components.get(&child_id)?;
if child.component_type != "Text" {
return None;
}
child
.get_property::<DynamicString>("text")
.map(|ds| ctx.data_context.resolve_dynamic_string(&ds))
}
fn evaluate_checks(ctx: &ComponentContext, model: &ComponentModel) -> bool {
match model.checks() {
Some(checks) => checks
.iter()
.all(|rule| ctx.data_context.resolve_dynamic_boolean_condition(&rule.condition)),
None => true,
}
}