use crate::core::{Color, Position, Rect, TextStyle};
use crate::ontology::{
AgentAction, AgentCapability, Discoverable, SemanticRole, UiNode, WidgetSchema,
};
use crate::paint::Painter;
use crate::widget::Widget;
pub struct Tabs {
pub id: String,
pub labels: Vec<String>,
pub active: usize,
bg_color: Option<Color>,
fg_color: Option<Color>,
corner_radius: Option<f32>,
font_size: Option<f32>,
is_bold: bool,
}
impl Tabs {
#[must_use]
pub fn new(id: impl Into<String>, labels: Vec<String>, active: usize) -> Self {
Self {
id: id.into(),
labels,
active,
bg_color: None,
fg_color: None,
corner_radius: None,
font_size: None,
is_bold: false,
}
}
#[must_use]
pub fn bg(mut self, color: Color) -> Self {
self.bg_color = Some(color);
self
}
#[must_use]
pub fn fg(mut self, color: Color) -> Self {
self.fg_color = Some(color);
self
}
#[must_use]
pub fn rounded(mut self, radius: f32) -> Self {
self.corner_radius = Some(radius);
self
}
#[must_use]
pub fn text_size(mut self, size: f32) -> Self {
self.font_size = Some(size);
self
}
#[must_use]
pub fn bold(mut self) -> Self {
self.is_bold = true;
self
}
}
impl Widget for Tabs {
fn draw(&self, painter: &mut dyn Painter, area: Rect) {
let tab_height = 32.0;
let tab_width = if self.labels.is_empty() {
area.width
} else {
area.width / self.labels.len() as f32
};
let tab_bar = Rect::new(area.x, area.y, area.width, tab_height);
let bar_bg = self.bg_color.unwrap_or(Color::rgba(0.15, 0.15, 0.18, 1.0));
painter.fill_rect(tab_bar, bar_bg, 0.0);
let style = TextStyle {
font_size: self.font_size.unwrap_or(13.0),
color: self.fg_color.unwrap_or(Color::WHITE),
..TextStyle::default()
};
for (i, label) in self.labels.iter().enumerate() {
let tab_rect = Rect::new(area.x + i as f32 * tab_width, area.y, tab_width, tab_height);
if i == self.active {
painter.fill_rect(tab_rect, Color::rgba(0.2, 0.2, 0.25, 1.0), 0.0);
let indicator =
Rect::new(tab_rect.x, tab_rect.y + tab_height - 2.0, tab_width, 2.0);
painter.fill_rect(indicator, Color::rgba(0.3, 0.6, 1.0, 1.0), 0.0);
}
let text_color = if i == self.active {
Color::WHITE
} else {
Color::rgba(0.6, 0.6, 0.7, 1.0)
};
let tab_style = TextStyle {
color: text_color,
..style.clone()
};
let text_size = painter.measure_text(label, &tab_style);
painter.text(
Position::new(
tab_rect.x + (tab_width - text_size.width) * 0.5,
tab_rect.y + (tab_height - text_size.height) * 0.5,
),
label,
&tab_style,
);
}
let content = Rect::new(
area.x,
area.y + tab_height,
area.width,
area.height - tab_height,
);
painter.fill_rect(content, Color::rgba(0.1, 0.1, 0.13, 1.0), 0.0);
}
fn ui_node(&self) -> UiNode {
UiNode::new("Tabs", SemanticRole::Tab).with_id(&self.id)
}
}
impl Discoverable for Tabs {
fn schema(&self) -> WidgetSchema {
WidgetSchema::new("Tabs", "A tabbed container", SemanticRole::Tab)
}
fn capabilities(&self) -> Vec<AgentCapability> {
vec![
AgentCapability::Focusable,
AgentCapability::Selectable {
multi_select: false,
item_count: self.labels.len(),
},
]
}
fn actions(&self) -> Vec<AgentAction> {
vec![AgentAction::simple(
"select_tab",
"Switch to a tab by index",
true,
)]
}
fn semantic_role(&self) -> SemanticRole {
SemanticRole::Tab
}
fn agent_state(&self) -> serde_json::Value {
serde_json::json!({
"active": self.active,
"tab_count": self.labels.len(),
"labels": self.labels,
})
}
fn execute_action(
&mut self,
action: &str,
params: &serde_json::Value,
) -> Result<serde_json::Value, String> {
match action {
"select_tab" => {
if let Some(idx) = params.get("index").and_then(|v| v.as_u64()) {
let idx = idx as usize;
if idx < self.labels.len() {
self.active = idx;
Ok(serde_json::json!({ "active": idx }))
} else {
Err("Tab index out of range".into())
}
} else {
Err("Missing 'index' parameter".into())
}
}
_ => Err(format!("Unknown action: {action}")),
}
}
fn agent_id(&self) -> Option<&str> {
Some(&self.id)
}
}
pub struct Panel {
pub id: String,
pub title: String,
pub collapsed: bool,
bg_color: Option<Color>,
fg_color: Option<Color>,
corner_radius: Option<f32>,
font_size: Option<f32>,
is_bold: bool,
}
impl Panel {
#[must_use]
pub fn new(id: impl Into<String>, title: impl Into<String>) -> Self {
Self {
id: id.into(),
title: title.into(),
collapsed: false,
bg_color: None,
fg_color: None,
corner_radius: None,
font_size: None,
is_bold: false,
}
}
#[must_use]
pub fn collapsed(mut self, collapsed: bool) -> Self {
self.collapsed = collapsed;
self
}
#[must_use]
pub fn bg(mut self, color: Color) -> Self {
self.bg_color = Some(color);
self
}
#[must_use]
pub fn fg(mut self, color: Color) -> Self {
self.fg_color = Some(color);
self
}
#[must_use]
pub fn rounded(mut self, radius: f32) -> Self {
self.corner_radius = Some(radius);
self
}
#[must_use]
pub fn text_size(mut self, size: f32) -> Self {
self.font_size = Some(size);
self
}
#[must_use]
pub fn bold(mut self) -> Self {
self.is_bold = true;
self
}
}
impl Widget for Panel {
fn draw(&self, painter: &mut dyn Painter, area: Rect) {
let header_height = 28.0;
let header = Rect::new(area.x, area.y, area.width, header_height);
let bg = self.bg_color.unwrap_or(Color::rgba(0.18, 0.18, 0.22, 1.0));
let radius = self.corner_radius.unwrap_or(3.0);
painter.fill_rect(header, bg, radius);
let text_color = self.fg_color.unwrap_or(Color::WHITE);
let style = TextStyle {
font_size: self.font_size.unwrap_or(13.0),
color: text_color,
..TextStyle::default()
};
let arrow = if self.collapsed { "▸" } else { "▾" };
painter.text(
Position::new(
area.x + 8.0,
area.y + (header_height - style.font_size) * 0.5,
),
arrow,
&style,
);
painter.text(
Position::new(
area.x + 24.0,
area.y + (header_height - style.font_size) * 0.5,
),
&self.title,
&style,
);
if !self.collapsed {
let content = Rect::new(
area.x,
area.y + header_height,
area.width,
area.height - header_height,
);
painter.fill_rect(content, Color::rgba(0.1, 0.1, 0.13, 1.0), 0.0);
}
}
fn ui_node(&self) -> UiNode {
UiNode::new("Panel", SemanticRole::Container).with_id(&self.id)
}
}
impl Discoverable for Panel {
fn schema(&self) -> WidgetSchema {
WidgetSchema::new(
"Panel",
"A collapsible panel container",
SemanticRole::Container,
)
}
fn capabilities(&self) -> Vec<AgentCapability> {
vec![
AgentCapability::Focusable,
AgentCapability::Expandable {
expanded: !self.collapsed,
},
]
}
fn actions(&self) -> Vec<AgentAction> {
vec![
AgentAction::simple("toggle", "Toggle panel collapse", true),
AgentAction::simple("expand", "Expand the panel", true),
AgentAction::simple("collapse", "Collapse the panel", true),
]
}
fn semantic_role(&self) -> SemanticRole {
SemanticRole::Container
}
fn agent_state(&self) -> serde_json::Value {
serde_json::json!({
"title": self.title,
"collapsed": self.collapsed,
})
}
fn execute_action(
&mut self,
action: &str,
_params: &serde_json::Value,
) -> Result<serde_json::Value, String> {
match action {
"toggle" => {
self.collapsed = !self.collapsed;
Ok(serde_json::json!({ "collapsed": self.collapsed }))
}
"expand" => {
self.collapsed = false;
Ok(serde_json::json!({ "collapsed": false }))
}
"collapse" => {
self.collapsed = true;
Ok(serde_json::json!({ "collapsed": true }))
}
_ => Err(format!("Unknown action: {action}")),
}
}
fn agent_id(&self) -> Option<&str> {
Some(&self.id)
}
}