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 ListState {
pub selected: Option<usize>,
pub offset: usize,
}
impl ListState {
#[must_use]
pub fn new() -> Self {
Self {
selected: None,
offset: 0,
}
}
pub fn with_selected(mut self, index: usize) -> Self {
self.selected = Some(index);
self
}
pub fn select_next(&mut self, len: usize) {
if len == 0 {
return;
}
self.selected = Some(match self.selected {
Some(i) => (i + 1).min(len - 1),
None => 0,
});
}
pub fn select_prev(&mut self) {
self.selected = self.selected.map(|i| i.saturating_sub(1));
}
}
pub struct List {
items: Vec<String>,
style: Style,
agent_id: String,
}
impl List {
#[must_use]
pub fn new(items: Vec<String>) -> 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 List {
fn schema(&self) -> WidgetSchema {
let mut schema = WidgetSchema::new(
"List",
"A vertical list of selectable items",
SemanticRole::Selection,
);
schema.usage_hint = Some("List::new(vec![\"Item 1\".into(), \"Item 2\".into()])".into());
schema.tags = vec!["list".into(), "items".into(), "selection".into()];
schema
}
fn capabilities(&self) -> Vec<AgentCapability> {
vec![
AgentCapability::Selectable {
multi_select: false,
item_count: self.items.len(),
},
AgentCapability::Scrollable {
vertical: true,
horizontal: false,
},
AgentCapability::Focusable,
]
}
fn actions(&self) -> Vec<AgentAction> {
vec![AgentAction::with_params(
"select",
"Select an item by index",
vec![ActionParam::required(
"index",
"The index to select",
ActionParamType::Index,
)],
true,
)]
}
fn semantic_role(&self) -> SemanticRole {
SemanticRole::Selection
}
fn agent_state(&self) -> serde_json::Value {
serde_json::json!({ "items": self.items, "count": self.items.len() })
}
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(format!("List ({} items)", self.items.len()))
}
}
impl StatefulWidget for List {
type State = ListState;
fn render(self, area: Rect, frame: &mut Frame<'_>, state: &mut ListState) {
if !self.agent_id.is_empty() {
let node = UiNode::new("List", SemanticRole::Selection)
.with_id(&self.agent_id)
.with_bounds(area.into())
.with_property("items", serde_json::json!(self.items))
.with_property("selected", serde_json::json!(state.selected));
frame.register_widget(node);
frame.register_hitbox(&self.agent_id, area, 1);
}
frame.painter().push_clip(area);
let item_h = 24.0;
let ts = self.style.resolved_text();
for (i, item) in self.items.iter().enumerate() {
let y = area.y + i as f32 * item_h;
let row = Rect::new(area.x, y, area.width, item_h);
if state.selected == Some(i) {
frame
.painter()
.fill_rect(row, Color::BLUE.with_alpha(0.3), 0.0);
}
frame
.painter()
.text(Position::new(area.x + 4.0, y + 4.0), item, &ts);
}
frame.painter().pop_clip();
}
}