use crate::core::style::TextStyle;
use crate::core::{Color, Position, Rect, Style};
use crate::ontology::*;
use crate::runtime::Frame;
use crate::widget::StatefulWidget;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SelectState {
pub selected: usize,
}
impl SelectState {
#[must_use]
pub fn new() -> Self {
Self { selected: 0 }
}
}
pub struct Select {
options: Vec<String>,
label: String,
style: Style,
agent_id: String,
}
impl Select {
#[must_use]
pub fn new(label: impl Into<String>, options: Vec<String>) -> Self {
Self {
options,
label: label.into(),
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 rounded(mut self, radius: f32) -> Self {
self.style.border_radius = Some(radius);
self
}
pub fn agent_id(mut self, id: impl Into<String>) -> Self {
self.agent_id = id.into();
self
}
}
impl Discoverable for Select {
fn schema(&self) -> WidgetSchema {
let mut schema = WidgetSchema::new("Select", "A dropdown select", SemanticRole::Selection);
schema.usage_hint =
Some("Select::new(\"Color\", vec![\"Red\".into(), \"Blue\".into()])".into());
schema.tags = vec!["select".into(), "dropdown".into(), "combo".into()];
schema
}
fn capabilities(&self) -> Vec<AgentCapability> {
vec![
AgentCapability::Selectable {
multi_select: false,
item_count: self.options.len(),
},
AgentCapability::Focusable,
]
}
fn actions(&self) -> Vec<AgentAction> {
vec![AgentAction::with_params(
"select",
"Select an option by index",
vec![ActionParam::required(
"index",
"Option index",
ActionParamType::Index,
)],
true,
)]
}
fn semantic_role(&self) -> SemanticRole {
SemanticRole::Selection
}
fn agent_state(&self) -> serde_json::Value {
serde_json::json!({ "options": self.options, "label": self.label })
}
fn execute_action(
&mut self,
_action: &str,
_params: &serde_json::Value,
) -> Result<serde_json::Value, String> {
Err("Use StatefulWidget for state mutations".to_string())
}
fn agent_id(&self) -> Option<&str> {
if self.agent_id.is_empty() {
None
} else {
Some(&self.agent_id)
}
}
fn accessibility_label(&self) -> Option<String> {
Some(self.label.clone())
}
}
impl StatefulWidget for Select {
type State = SelectState;
fn render(self, area: Rect, frame: &mut Frame<'_>, state: &mut SelectState) {
if !self.agent_id.is_empty() {
let selected_text = self
.options
.get(state.selected)
.cloned()
.unwrap_or_default();
let node = UiNode::new("Select", SemanticRole::Selection)
.with_id(&self.agent_id)
.with_bounds(area.into())
.with_property("options", serde_json::json!(self.options))
.with_property("selected", serde_json::json!(state.selected))
.with_property("selected_text", serde_json::json!(selected_text));
frame.register_widget(node);
frame.register_hitbox(&self.agent_id, area, 1);
}
let bg = self.style.background.unwrap_or(Color::DARK_GRAY);
let radius = self.style.border_radius.unwrap_or(4.0);
frame.painter().fill_rect(area, bg, radius);
frame.painter().stroke_rect(area, Color::GRAY, 1.0, radius);
let current = self
.options
.get(state.selected)
.cloned()
.unwrap_or_default();
let ts = self.style.resolved_text();
if !self.label.is_empty() {
let label_ts = TextStyle {
font_size: 12.0,
color: Color::GRAY,
..Default::default()
};
frame
.painter()
.text(Position::new(area.x, area.y - 16.0), &self.label, &label_ts);
}
frame
.painter()
.text(Position::new(area.x + 4.0, area.y + 4.0), ¤t, &ts);
let arrow_x = area.x + area.width - 16.0;
let arrow_y = area.y + area.height * 0.5;
frame
.painter()
.text(Position::new(arrow_x, arrow_y - 7.0), "\u{25BC}", &ts);
}
}