Skip to main content

agpu/widget/
text_input.rs

1//! Single-line text input widget.
2
3use crate::core::{Color, Position, Rect, TextStyle};
4use crate::ontology::{
5    AgentAction, AgentCapability, Discoverable, SemanticRole, UiNode, WidgetSchema,
6};
7use crate::paint::Painter;
8use crate::widget::Widget;
9
10/// A single-line editable text field.
11pub struct TextInput {
12    pub id: String,
13    pub value: String,
14    pub placeholder: String,
15    pub cursor: usize,
16    pub selection: Option<(usize, usize)>,
17    bg_color: Option<Color>,
18    fg_color: Option<Color>,
19    corner_radius: Option<f32>,
20    font_size: Option<f32>,
21    is_bold: bool,
22}
23
24impl TextInput {
25    #[must_use]
26    pub fn new(id: impl Into<String>) -> Self {
27        Self {
28            id: id.into(),
29            value: String::new(),
30            placeholder: String::new(),
31            cursor: 0,
32            selection: None,
33            bg_color: None,
34            fg_color: None,
35            corner_radius: None,
36            font_size: None,
37            is_bold: false,
38        }
39    }
40
41    #[must_use]
42    pub fn value(mut self, value: impl Into<String>) -> Self {
43        let v = value.into();
44        self.cursor = v.len();
45        self.value = v;
46        self
47    }
48
49    #[must_use]
50    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
51        self.placeholder = placeholder.into();
52        self
53    }
54
55    pub fn insert_char(&mut self, ch: char) {
56        self.value.insert(self.cursor, ch);
57        self.cursor += ch.len_utf8();
58    }
59
60    pub fn delete_back(&mut self) {
61        if self.cursor > 0 {
62            let prev = self.value[..self.cursor]
63                .char_indices()
64                .last()
65                .map(|(i, _)| i)
66                .unwrap_or(0);
67            self.value.drain(prev..self.cursor);
68            self.cursor = prev;
69        }
70    }
71
72    #[must_use]
73    pub fn bg(mut self, color: Color) -> Self {
74        self.bg_color = Some(color);
75        self
76    }
77
78    #[must_use]
79    pub fn fg(mut self, color: Color) -> Self {
80        self.fg_color = Some(color);
81        self
82    }
83
84    #[must_use]
85    pub fn rounded(mut self, radius: f32) -> Self {
86        self.corner_radius = Some(radius);
87        self
88    }
89
90    #[must_use]
91    pub fn text_size(mut self, size: f32) -> Self {
92        self.font_size = Some(size);
93        self
94    }
95
96    #[must_use]
97    pub fn bold(mut self) -> Self {
98        self.is_bold = true;
99        self
100    }
101}
102
103impl Widget for TextInput {
104    fn draw(&self, painter: &mut dyn Painter, area: Rect) {
105        let bg = self.bg_color.unwrap_or(Color::rgba(0.15, 0.15, 0.18, 1.0));
106        let radius = self.corner_radius.unwrap_or(3.0);
107        painter.fill_rect(area, bg, radius);
108        painter.stroke_rect(area, Color::rgba(0.4, 0.4, 0.5, 1.0), 1.0, radius);
109
110        let style = TextStyle {
111            font_size: self.font_size.unwrap_or(14.0),
112            color: self.fg_color.unwrap_or(Color::WHITE),
113            ..TextStyle::default()
114        };
115
116        let display_text = if self.value.is_empty() {
117            &self.placeholder
118        } else {
119            &self.value
120        };
121        let text_color = if self.value.is_empty() {
122            Color::rgba(0.5, 0.5, 0.5, 1.0)
123        } else {
124            Color::WHITE
125        };
126
127        let text_style = TextStyle {
128            color: text_color,
129            ..style
130        };
131
132        let padding = 6.0;
133        painter.text(
134            Position::new(
135                area.x + padding,
136                area.y + (area.height - text_style.font_size) * 0.5,
137            ),
138            display_text,
139            &text_style,
140        );
141    }
142
143    fn ui_node(&self) -> UiNode {
144        UiNode::new("TextInput", SemanticRole::Input).with_id(&self.id)
145    }
146}
147
148impl Discoverable for TextInput {
149    fn schema(&self) -> WidgetSchema {
150        WidgetSchema::new("TextInput", "A single-line text field", SemanticRole::Input)
151    }
152
153    fn capabilities(&self) -> Vec<AgentCapability> {
154        vec![
155            AgentCapability::Focusable,
156            AgentCapability::TextInput {
157                multiline: false,
158                max_length: None,
159            },
160        ]
161    }
162
163    fn actions(&self) -> Vec<AgentAction> {
164        vec![
165            AgentAction::simple("set_value", "Set the text value", true),
166            AgentAction::simple("clear", "Clear the text value", true),
167        ]
168    }
169
170    fn semantic_role(&self) -> SemanticRole {
171        SemanticRole::Input
172    }
173
174    fn agent_state(&self) -> serde_json::Value {
175        serde_json::json!({
176            "value": self.value,
177            "placeholder": self.placeholder,
178            "cursor": self.cursor,
179        })
180    }
181
182    fn execute_action(
183        &mut self,
184        action: &str,
185        params: &serde_json::Value,
186    ) -> Result<serde_json::Value, String> {
187        match action {
188            "set_value" => {
189                if let Some(v) = params.get("value").and_then(|v| v.as_str()) {
190                    self.value = v.to_string();
191                    self.cursor = self.value.len();
192                    Ok(serde_json::json!({ "value": self.value }))
193                } else {
194                    Err("Missing 'value' parameter".into())
195                }
196            }
197            "clear" => {
198                self.value.clear();
199                self.cursor = 0;
200                Ok(serde_json::json!({ "value": "" }))
201            }
202            _ => Err(format!("Unknown action: {action}")),
203        }
204    }
205
206    fn agent_id(&self) -> Option<&str> {
207        Some(&self.id)
208    }
209}