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)]
pub struct ColorPickerState {
pub color: Color,
}
impl ColorPickerState {
#[must_use]
pub fn new(color: Color) -> Self {
Self { color }
}
}
impl Default for ColorPickerState {
fn default() -> Self {
Self { color: Color::WHITE }
}
}
pub struct ColorPicker {
label: String,
show_alpha: bool,
style: Style,
agent_id: String,
}
impl ColorPicker {
#[must_use]
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
show_alpha: true,
style: Style::default(),
agent_id: String::new(),
}
}
pub fn show_alpha(mut self, show: bool) -> Self {
self.show_alpha = show;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
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 ColorPicker {
fn schema(&self) -> WidgetSchema {
let mut schema = WidgetSchema::new(
"ColorPicker",
"An interactive color selector",
SemanticRole::Input,
);
schema.usage_hint = Some("ColorPicker::new(\"Color\").show_alpha(true)".into());
schema.tags = vec!["color".into(), "picker".into(), "palette".into()];
schema
}
fn capabilities(&self) -> Vec<AgentCapability> {
vec![AgentCapability::Focusable, AgentCapability::Clickable]
}
fn actions(&self) -> Vec<AgentAction> {
vec![
AgentAction::with_params(
"set_color",
"Set the selected color",
vec![
ActionParam::optional(
"r",
"Red (0-255)",
ActionParamType::Integer,
serde_json::json!(0),
),
ActionParam::optional(
"g",
"Green (0-255)",
ActionParamType::Integer,
serde_json::json!(0),
),
ActionParam::optional(
"b",
"Blue (0-255)",
ActionParamType::Integer,
serde_json::json!(0),
),
ActionParam::optional(
"a",
"Alpha (0-255)",
ActionParamType::Integer,
serde_json::json!(255),
),
ActionParam::optional(
"hex",
"Hex color string (#RRGGBB or #RRGGBBAA)",
ActionParamType::String,
serde_json::json!(""),
),
],
true,
),
AgentAction::simple("get_color", "Get the current color", false),
]
}
fn semantic_role(&self) -> SemanticRole {
SemanticRole::Input
}
fn agent_state(&self) -> serde_json::Value {
serde_json::json!({ "label": self.label, "show_alpha": self.show_alpha })
}
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 ColorPicker {
type State = ColorPickerState;
fn render(self, area: Rect, frame: &mut Frame<'_>, state: &mut ColorPickerState) {
if !self.agent_id.is_empty() {
let node = UiNode::new("ColorPicker", SemanticRole::Input)
.with_id(&self.agent_id)
.with_bounds(area.into())
.with_label(&self.label)
.with_property("r", serde_json::json!((state.color.r * 255.0) as u8))
.with_property("g", serde_json::json!((state.color.g * 255.0) as u8))
.with_property("b", serde_json::json!((state.color.b * 255.0) as u8))
.with_property("a", serde_json::json!((state.color.a * 255.0) as u8));
frame.register_widget(node);
frame.register_hitbox(&self.agent_id, area, 1);
}
let swatch_size = area.height.min(area.width).min(32.0);
let swatch = Rect::new(area.x, area.y, swatch_size, swatch_size);
frame.painter().fill_rect(swatch, state.color, 4.0);
frame.painter().stroke_rect(swatch, Color::GRAY, 1.0, 4.0);
if !self.label.is_empty() {
let ts = self.style.resolved_text();
frame.painter().text(
Position::new(area.x + swatch_size + 8.0, area.y + 4.0),
&self.label,
&ts,
);
}
let hex = format!(
"#{:02X}{:02X}{:02X}{:02X}",
(state.color.r * 255.0) as u8,
(state.color.g * 255.0) as u8,
(state.color.b * 255.0) as u8,
(state.color.a * 255.0) as u8,
);
let hex_ts = TextStyle {
font_size: 12.0,
color: Color::GRAY,
..Default::default()
};
frame.painter().text(
Position::new(area.x, area.y + swatch_size + 4.0),
&hex,
&hex_ts,
);
}
}