agpu/widget/
text_input.rs1use 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
10pub 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}