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 Select {
pub id: String,
pub options: Vec<String>,
pub selected: Option<usize>,
pub open: bool,
pub searchable: bool,
pub search_query: String,
bg_color: Option<Color>,
fg_color: Option<Color>,
corner_radius: Option<f32>,
font_size: Option<f32>,
is_bold: bool,
}
impl Select {
#[must_use]
pub fn new(id: impl Into<String>, options: Vec<String>) -> Self {
Self {
id: id.into(),
options,
selected: None,
open: false,
searchable: false,
search_query: String::new(),
bg_color: None,
fg_color: None,
corner_radius: None,
font_size: None,
is_bold: false,
}
}
#[must_use]
pub fn selected(mut self, index: usize) -> Self {
self.selected = Some(index);
self
}
#[must_use]
pub fn searchable(mut self, searchable: bool) -> Self {
self.searchable = searchable;
self
}
pub fn selected_label(&self) -> &str {
self.selected
.and_then(|i| self.options.get(i))
.map(|s| s.as_str())
.unwrap_or("Select...")
}
#[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 Select {
fn draw(&self, painter: &mut dyn Painter, area: Rect) {
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(area, bg, radius);
painter.stroke_rect(area, Color::rgba(0.4, 0.4, 0.5, 1.0), 1.0, radius);
let style = TextStyle {
font_size: self.font_size.unwrap_or(14.0),
color: self.fg_color.unwrap_or(Color::WHITE),
..TextStyle::default()
};
let padding = 8.0;
painter.text(
Position::new(
area.x + padding,
area.y + (area.height - style.font_size) * 0.5,
),
self.selected_label(),
&style,
);
let arrow_x = area.x + area.width - 20.0;
let arrow_y = area.y + area.height * 0.5;
painter.line(
Position::new(arrow_x, arrow_y - 3.0),
Position::new(arrow_x + 6.0, arrow_y + 3.0),
Color::WHITE,
1.5,
);
painter.line(
Position::new(arrow_x + 6.0, arrow_y + 3.0),
Position::new(arrow_x + 12.0, arrow_y - 3.0),
Color::WHITE,
1.5,
);
}
fn ui_node(&self) -> UiNode {
UiNode::new("Select", SemanticRole::Selection).with_id(&self.id)
}
}
impl Discoverable for Select {
fn schema(&self) -> WidgetSchema {
WidgetSchema::new("Select", "A dropdown selection", SemanticRole::Selection)
}
fn capabilities(&self) -> Vec<AgentCapability> {
vec![
AgentCapability::Focusable,
AgentCapability::Selectable {
multi_select: false,
item_count: self.options.len(),
},
]
}
fn actions(&self) -> Vec<AgentAction> {
vec![
AgentAction::simple("open", "Open the dropdown", true),
AgentAction::simple("close", "Close the dropdown", true),
AgentAction::simple("select", "Select an option by index", true),
]
}
fn semantic_role(&self) -> SemanticRole {
SemanticRole::Selection
}
fn agent_state(&self) -> serde_json::Value {
serde_json::json!({
"selected": self.selected,
"selected_label": self.selected_label(),
"option_count": self.options.len(),
"open": self.open,
})
}
fn execute_action(
&mut self,
action: &str,
params: &serde_json::Value,
) -> Result<serde_json::Value, String> {
match action {
"open" => {
self.open = true;
Ok(serde_json::json!({ "open": true }))
}
"close" => {
self.open = false;
Ok(serde_json::json!({ "open": false }))
}
"select" => {
if let Some(idx) = params.get("index").and_then(|v| v.as_u64()) {
let idx = idx as usize;
if idx < self.options.len() {
self.selected = Some(idx);
self.open = false;
Ok(serde_json::json!({ "selected": idx, "label": self.options[idx] }))
} else {
Err("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)
}
}