use crate::core::{Color, Position, Rect, Style};
use crate::ontology::*;
use crate::runtime::Frame;
use crate::widget::Widget;
#[derive(Debug, Clone)]
pub struct ToolbarItem {
pub id: String,
pub label: String,
pub icon: Option<String>,
pub enabled: bool,
pub toggled: bool,
}
impl ToolbarItem {
#[must_use]
pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
Self {
id: id.into(),
label: label.into(),
icon: None,
enabled: true,
toggled: false,
}
}
pub fn icon(mut self, icon: impl Into<String>) -> Self {
self.icon = Some(icon.into());
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn toggled(mut self, toggled: bool) -> Self {
self.toggled = toggled;
self
}
}
pub struct Toolbar {
items: Vec<ToolbarItem>,
style: Style,
agent_id: String,
}
impl Toolbar {
#[must_use]
pub fn new(items: Vec<ToolbarItem>) -> Self {
Self {
items,
style: Style::default(),
agent_id: String::new(),
}
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn bg(mut self, color: Color) -> Self {
self.style.background = Some(color);
self
}
pub fn fg(mut self, color: Color) -> Self {
self.style.foreground = Some(color);
self
}
pub fn agent_id(mut self, id: impl Into<String>) -> Self {
self.agent_id = id.into();
self
}
}
impl Discoverable for Toolbar {
fn schema(&self) -> WidgetSchema {
let mut schema = WidgetSchema::new(
"Toolbar",
"A horizontal row of action buttons",
SemanticRole::Navigation,
);
schema.usage_hint = Some("Toolbar::new(vec![ToolbarItem::new(\"save\", \"Save\")])".into());
schema.tags = vec!["toolbar".into(), "actions".into(), "buttons".into()];
schema
}
fn capabilities(&self) -> Vec<AgentCapability> {
vec![AgentCapability::Clickable, AgentCapability::Focusable]
}
fn actions(&self) -> Vec<AgentAction> {
vec![
AgentAction::with_params(
"click_item",
"Click a toolbar item by ID",
vec![ActionParam::required(
"item_id",
"ID of the toolbar item to click",
ActionParamType::String,
)],
false,
),
AgentAction::simple("list_items", "List all toolbar items", false),
]
}
fn semantic_role(&self) -> SemanticRole {
SemanticRole::Navigation
}
fn agent_state(&self) -> serde_json::Value {
let items: Vec<_> = self
.items
.iter()
.map(|i| {
serde_json::json!({
"id": i.id,
"label": i.label,
"icon": i.icon,
"enabled": i.enabled,
"toggled": i.toggled,
})
})
.collect();
serde_json::json!({ "items": items })
}
fn execute_action(
&mut self,
action: &str,
params: &serde_json::Value,
) -> Result<serde_json::Value, String> {
match action {
"click_item" => {
let item_id = params
.get("item_id")
.and_then(|v| v.as_str())
.ok_or("Missing item_id")?;
match self.items.iter().find(|i| i.id == item_id) {
Some(item) if item.enabled => Ok(serde_json::json!({ "clicked": item_id })),
Some(_) => Err(format!("Item '{item_id}' is disabled")),
None => Err(format!("Unknown item: {item_id}")),
}
}
"list_items" => Ok(self.agent_state()),
_ => Err(format!("Unknown action: {action}")),
}
}
fn agent_id(&self) -> Option<&str> {
if self.agent_id.is_empty() {
None
} else {
Some(&self.agent_id)
}
}
fn accessibility_label(&self) -> Option<String> {
let labels: Vec<&str> = self.items.iter().map(|i| i.label.as_str()).collect();
Some(format!("Toolbar: {}", labels.join(", ")))
}
}
impl Widget for Toolbar {
fn render(self, area: Rect, frame: &mut Frame<'_>) {
if !self.agent_id.is_empty() {
let mut node = UiNode::new("Toolbar", SemanticRole::Navigation)
.with_id(&self.agent_id)
.with_bounds(area.into());
let items_json: Vec<_> = self
.items
.iter()
.map(|i| {
serde_json::json!({
"id": i.id,
"label": i.label,
"enabled": i.enabled,
"toggled": i.toggled,
})
})
.collect();
node = node.with_property("items", serde_json::json!(items_json));
frame.register_widget(node);
frame.register_hitbox(&self.agent_id, area, 1);
}
let toolbar_bg = self.style.background.unwrap_or(Color::DARK_GRAY);
frame.painter().fill_rect(area, toolbar_bg, 0.0);
let ts = self.style.resolved_text();
let mut disabled_ts = ts.clone();
disabled_ts.color = Color::GRAY;
let mut x = area.x + 4.0;
for item in &self.items {
let style = if item.enabled { &ts } else { &disabled_ts };
let sz = frame.painter().measure_text(&item.label, style);
let btn_w = sz.width + 16.0;
let btn_rect = Rect::new(x, area.y + 2.0, btn_w, area.height - 4.0);
if item.toggled {
frame
.painter()
.fill_rect(btn_rect, Color::from_rgba8(60, 60, 120, 255), 4.0);
}
frame.painter().text(
Position::new(x + 8.0, area.y + (area.height - sz.height) * 0.5),
&item.label,
style,
);
x += btn_w + 4.0;
}
}
}