use super::{AgentCapability, Discoverable, SemanticRole, WidgetSchema};
use crate::core::rect::Rect;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Default)]
pub struct OntologyRegistry {
schemas: HashMap<String, WidgetSchema>,
tree: Option<UiTree>,
}
impl OntologyRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register_schema(&mut self, schema: WidgetSchema) {
self.schemas.insert(schema.name.clone(), schema);
}
pub fn register<W: Discoverable>(&mut self, instance: &W) {
self.register_schema(instance.schema());
}
pub fn list_types(&self) -> Vec<&str> {
self.schemas.keys().map(|s| s.as_str()).collect()
}
pub fn get_schema(&self, name: &str) -> Option<&WidgetSchema> {
self.schemas.get(name)
}
pub fn find_by_role(&self, role: SemanticRole) -> Vec<&WidgetSchema> {
self.schemas
.values()
.filter(|s| s.default_role == role)
.collect()
}
pub fn search(&self, query: &str) -> Vec<&WidgetSchema> {
let query_lower = query.to_lowercase();
self.schemas
.values()
.filter(|s| {
s.name.to_lowercase().contains(&query_lower)
|| s.description.to_lowercase().contains(&query_lower)
|| s.tags
.iter()
.any(|t| t.to_lowercase().contains(&query_lower))
})
.collect()
}
pub fn export_catalog(&self) -> serde_json::Value {
serde_json::to_value(&self.schemas).unwrap_or_default()
}
pub fn validate_action_params(
&self,
widget_type: &str,
action: &str,
params: &serde_json::Value,
) -> Result<(), String> {
let Some(schema) = self.schemas.get(widget_type) else {
return Ok(());
};
let Some(declared) = schema.actions.iter().find(|a| a.name == action) else {
return Ok(());
};
declared.validate_params(params)
}
pub fn set_tree(&mut self, tree: UiTree) {
self.tree = Some(tree);
}
pub fn tree(&self) -> Option<&UiTree> {
self.tree.as_ref()
}
pub fn find_node(&self, agent_id: &str) -> Option<&UiNode> {
self.tree.as_ref().and_then(|t| t.find(agent_id))
}
pub fn export_tree(&self) -> serde_json::Value {
match &self.tree {
Some(tree) => serde_json::to_value(tree).unwrap_or_default(),
None => serde_json::Value::Null,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UiTree {
pub root: UiNode,
}
impl UiTree {
pub fn new(root: UiNode) -> Self {
Self { root }
}
pub fn find(&self, agent_id: &str) -> Option<&UiNode> {
self.root.find(agent_id)
}
pub fn find_by_role(&self, role: SemanticRole) -> Vec<&UiNode> {
let mut results = Vec::new();
self.root.collect_by_role(role, &mut results);
results
}
pub fn focusable_nodes(&self) -> Vec<&UiNode> {
let mut results = Vec::new();
self.root.collect_by_capability("focusable", &mut results);
results
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UiNode {
pub agent_id: Option<String>,
pub widget_type: String,
pub role: SemanticRole,
pub capabilities: Vec<AgentCapability>,
pub state: serde_json::Value,
pub label: Option<String>,
pub bounds: Option<NodeBounds>,
#[serde(default, skip_serializing_if = "Accessibility::is_empty")]
pub accessibility: Accessibility,
pub children: Vec<UiNode>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct NodeBounds {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl From<Rect> for NodeBounds {
fn from(r: Rect) -> Self {
Self {
x: r.x,
y: r.y,
width: r.width,
height: r.height,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Accessibility {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disabled: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value_text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expanded: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub selected: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shortcut: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tab_index: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub live: Option<String>,
}
impl Accessibility {
pub fn is_empty(&self) -> bool {
self.role.is_none()
&& self.description.is_none()
&& self.disabled.is_none()
&& self.value_text.is_none()
&& self.expanded.is_none()
&& self.selected.is_none()
&& self.required.is_none()
&& self.shortcut.is_none()
&& self.tab_index.is_none()
&& self.live.is_none()
}
}
impl UiNode {
#[must_use]
pub fn new(widget_type: impl Into<String>, role: SemanticRole) -> Self {
Self {
agent_id: None,
widget_type: widget_type.into(),
role,
capabilities: Vec::new(),
state: serde_json::Value::Null,
label: None,
bounds: None,
accessibility: Accessibility::default(),
children: Vec::new(),
}
}
#[must_use]
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.agent_id = Some(id.into());
self
}
#[must_use]
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn with_bounds(mut self, bounds: NodeBounds) -> Self {
self.bounds = Some(bounds);
self
}
#[must_use]
pub fn with_state(mut self, state: serde_json::Value) -> Self {
self.state = state;
self
}
#[must_use]
pub fn with_capability(mut self, cap: AgentCapability) -> Self {
self.capabilities.push(cap);
self
}
#[must_use]
pub fn with_child(mut self, child: UiNode) -> Self {
self.children.push(child);
self
}
#[must_use]
pub fn with_accessibility(mut self, acc: Accessibility) -> Self {
self.accessibility = acc;
self
}
#[must_use]
pub fn with_property(mut self, key: &str, value: serde_json::Value) -> Self {
if self.state.is_null() {
self.state = serde_json::json!({});
}
if let Some(obj) = self.state.as_object_mut() {
obj.insert(key.to_string(), value);
}
self
}
pub fn find(&self, agent_id: &str) -> Option<&UiNode> {
if self.agent_id.as_deref() == Some(agent_id) {
return Some(self);
}
for child in &self.children {
if let Some(node) = child.find(agent_id) {
return Some(node);
}
}
None
}
pub fn collect_by_role<'a>(&'a self, role: SemanticRole, results: &mut Vec<&'a UiNode>) {
if self.role == role {
results.push(self);
}
for child in &self.children {
child.collect_by_role(role, results);
}
}
pub fn collect_by_capability<'a>(&'a self, cap_name: &str, results: &mut Vec<&'a UiNode>) {
if self.capabilities.iter().any(|c| c.name() == cap_name) {
results.push(self);
}
for child in &self.children {
child.collect_by_capability(cap_name, results);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry_search() {
let mut reg = OntologyRegistry::new();
reg.register_schema(WidgetSchema {
name: "Button".into(),
description: "A clickable button".into(),
default_role: SemanticRole::Action,
properties: vec![],
actions: vec![],
usage_hint: None,
tags: vec!["button".into(), "action".into()],
});
assert_eq!(reg.search("button").len(), 1);
assert_eq!(reg.search("nonexistent").len(), 0);
}
#[test]
fn ui_tree_find() {
let tree = UiTree::new(
UiNode::new("Panel", SemanticRole::Container)
.with_id("root")
.with_child(
UiNode::new("Button", SemanticRole::Action)
.with_id("btn-1")
.with_capability(AgentCapability::Focusable),
),
);
assert!(tree.find("btn-1").is_some());
assert!(tree.find("missing").is_none());
assert_eq!(tree.focusable_nodes().len(), 1);
}
}