Skip to main content

agpu/widget/
slider.rs

1//! Slider widget for continuous/discrete value selection.
2
3use crate::core::{Color, Position, Rect};
4use crate::ontology::{
5    AgentAction, AgentCapability, Discoverable, SemanticRole, UiNode, WidgetSchema,
6};
7use crate::paint::Painter;
8use crate::widget::Widget;
9
10/// A horizontal slider for selecting a value within a range.
11pub struct Slider {
12    pub id: String,
13    pub value: f32,
14    pub min: f32,
15    pub max: f32,
16    pub step: Option<f32>,
17    bg_color: Option<Color>,
18    fg_color: Option<Color>,
19    corner_radius: Option<f32>,
20}
21
22impl Slider {
23    #[must_use]
24    pub fn new(id: impl Into<String>, value: f32, min: f32, max: f32) -> Self {
25        Self {
26            id: id.into(),
27            value: value.clamp(min, max),
28            min,
29            max,
30            step: None,
31            bg_color: None,
32            fg_color: None,
33            corner_radius: None,
34        }
35    }
36
37    #[must_use]
38    pub fn step(mut self, step: f32) -> Self {
39        self.step = Some(step);
40        self
41    }
42
43    /// Normalized value (0.0–1.0).
44    pub fn normalized(&self) -> f32 {
45        if (self.max - self.min).abs() < f32::EPSILON {
46            0.0
47        } else {
48            (self.value - self.min) / (self.max - self.min)
49        }
50    }
51
52    #[must_use]
53    pub fn bg(mut self, color: Color) -> Self {
54        self.bg_color = Some(color);
55        self
56    }
57
58    #[must_use]
59    pub fn fg(mut self, color: Color) -> Self {
60        self.fg_color = Some(color);
61        self
62    }
63
64    #[must_use]
65    pub fn rounded(mut self, radius: f32) -> Self {
66        self.corner_radius = Some(radius);
67        self
68    }
69}
70
71impl Widget for Slider {
72    fn draw(&self, painter: &mut dyn Painter, area: Rect) {
73        let track_height = 4.0;
74        let track_y = area.y + (area.height - track_height) * 0.5;
75        let track = Rect::new(area.x, track_y, area.width, track_height);
76        let radius = self.corner_radius.unwrap_or(2.0);
77
78        let track_color = self.bg_color.unwrap_or(Color::rgba(0.3, 0.3, 0.35, 1.0));
79        painter.fill_rect(track, track_color, radius);
80
81        let t = self.normalized();
82        let filled = Rect::new(area.x, track_y, area.width * t, track_height);
83        let fill_color = self.fg_color.unwrap_or(Color::rgba(0.2, 0.5, 1.0, 1.0));
84        painter.fill_rect(filled, fill_color, radius);
85
86        let thumb_radius = 8.0;
87        let thumb_x = area.x + area.width * t;
88        let thumb_center = Position::new(thumb_x, area.y + area.height * 0.5);
89        painter.fill_circle(thumb_center, thumb_radius, Color::WHITE);
90    }
91
92    fn ui_node(&self) -> UiNode {
93        UiNode::new("Slider", SemanticRole::Input).with_id(&self.id)
94    }
95}
96
97impl Discoverable for Slider {
98    fn schema(&self) -> WidgetSchema {
99        WidgetSchema::new(
100            "Slider",
101            "A range slider for value selection",
102            SemanticRole::Input,
103        )
104    }
105
106    fn capabilities(&self) -> Vec<AgentCapability> {
107        vec![
108            AgentCapability::Focusable,
109            AgentCapability::RangeEditable {
110                min: self.min as f64,
111                max: self.max as f64,
112                step: self.step.map(|s| s as f64),
113            },
114        ]
115    }
116
117    fn actions(&self) -> Vec<AgentAction> {
118        vec![AgentAction::simple(
119            "set_value",
120            "Set the slider value",
121            true,
122        )]
123    }
124
125    fn semantic_role(&self) -> SemanticRole {
126        SemanticRole::Input
127    }
128
129    fn agent_state(&self) -> serde_json::Value {
130        serde_json::json!({
131            "value": self.value,
132            "min": self.min,
133            "max": self.max,
134            "normalized": self.normalized(),
135        })
136    }
137
138    fn execute_action(
139        &mut self,
140        action: &str,
141        params: &serde_json::Value,
142    ) -> Result<serde_json::Value, String> {
143        match action {
144            "set_value" => {
145                if let Some(v) = params.get("value").and_then(|v| v.as_f64()) {
146                    self.value = (v as f32).clamp(self.min, self.max);
147                    Ok(serde_json::json!({ "value": self.value }))
148                } else {
149                    Err("Missing 'value' parameter".into())
150                }
151            }
152            _ => Err(format!("Unknown action: {action}")),
153        }
154    }
155
156    fn agent_id(&self) -> Option<&str> {
157        Some(&self.id)
158    }
159}