Skip to main content

agpu/widget/
text_area.rs

1//! Multi-line text area 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 multi-line editable text area with scrolling.
11pub struct TextArea {
12    pub id: String,
13    pub value: String,
14    pub scroll_offset: f32,
15    bg_color: Option<Color>,
16    fg_color: Option<Color>,
17    corner_radius: Option<f32>,
18    font_size: Option<f32>,
19    is_bold: bool,
20}
21
22impl TextArea {
23    #[must_use]
24    pub fn new(id: impl Into<String>) -> Self {
25        Self {
26            id: id.into(),
27            value: String::new(),
28            scroll_offset: 0.0,
29            bg_color: None,
30            fg_color: None,
31            corner_radius: None,
32            font_size: None,
33            is_bold: false,
34        }
35    }
36
37    #[must_use]
38    pub fn value(mut self, value: impl Into<String>) -> Self {
39        self.value = value.into();
40        self
41    }
42
43    #[must_use]
44    pub fn bg(mut self, color: Color) -> Self {
45        self.bg_color = Some(color);
46        self
47    }
48
49    #[must_use]
50    pub fn fg(mut self, color: Color) -> Self {
51        self.fg_color = Some(color);
52        self
53    }
54
55    #[must_use]
56    pub fn rounded(mut self, radius: f32) -> Self {
57        self.corner_radius = Some(radius);
58        self
59    }
60
61    #[must_use]
62    pub fn text_size(mut self, size: f32) -> Self {
63        self.font_size = Some(size);
64        self
65    }
66
67    #[must_use]
68    pub fn bold(mut self) -> Self {
69        self.is_bold = true;
70        self
71    }
72}
73
74impl Widget for TextArea {
75    fn draw(&self, painter: &mut dyn Painter, area: Rect) {
76        let bg = self.bg_color.unwrap_or(Color::rgba(0.12, 0.12, 0.15, 1.0));
77        let radius = self.corner_radius.unwrap_or(3.0);
78        painter.fill_rect(area, bg, radius);
79        painter.stroke_rect(area, Color::rgba(0.4, 0.4, 0.5, 1.0), 1.0, radius);
80
81        let fs = self.font_size.unwrap_or(13.0);
82        let style = TextStyle {
83            font_size: fs,
84            color: self.fg_color.unwrap_or(Color::WHITE),
85            ..TextStyle::default()
86        };
87
88        let padding = 6.0;
89        let line_height = style.font_size * 1.4;
90        let max_lines = ((area.height - padding * 2.0) / line_height).floor() as usize;
91
92        for (i, line) in self.value.lines().enumerate().take(max_lines) {
93            let y = area.y + padding + i as f32 * line_height - self.scroll_offset;
94            if y >= area.y && y + line_height <= area.y + area.height {
95                painter.text(Position::new(area.x + padding, y), line, &style);
96            }
97        }
98    }
99
100    fn ui_node(&self) -> UiNode {
101        UiNode::new("TextArea", SemanticRole::Input).with_id(&self.id)
102    }
103}
104
105impl Discoverable for TextArea {
106    fn schema(&self) -> WidgetSchema {
107        WidgetSchema::new("TextArea", "A multi-line text editor", SemanticRole::Input)
108    }
109
110    fn capabilities(&self) -> Vec<AgentCapability> {
111        vec![
112            AgentCapability::Focusable,
113            AgentCapability::TextInput {
114                multiline: true,
115                max_length: None,
116            },
117            AgentCapability::Scrollable {
118                vertical: true,
119                horizontal: false,
120            },
121        ]
122    }
123
124    fn actions(&self) -> Vec<AgentAction> {
125        vec![
126            AgentAction::simple("set_value", "Set the text content", true),
127            AgentAction::simple("clear", "Clear all text", true),
128        ]
129    }
130
131    fn semantic_role(&self) -> SemanticRole {
132        SemanticRole::Input
133    }
134
135    fn agent_state(&self) -> serde_json::Value {
136        serde_json::json!({
137            "value": self.value,
138            "line_count": self.value.lines().count(),
139        })
140    }
141
142    fn execute_action(
143        &mut self,
144        action: &str,
145        params: &serde_json::Value,
146    ) -> Result<serde_json::Value, String> {
147        match action {
148            "set_value" => {
149                if let Some(v) = params.get("value").and_then(|v| v.as_str()) {
150                    self.value = v.to_string();
151                    Ok(serde_json::json!({ "value": self.value }))
152                } else {
153                    Err("Missing 'value' parameter".into())
154                }
155            }
156            "clear" => {
157                self.value.clear();
158                Ok(serde_json::json!({ "value": "" }))
159            }
160            _ => Err(format!("Unknown action: {action}")),
161        }
162    }
163
164    fn agent_id(&self) -> Option<&str> {
165        Some(&self.id)
166    }
167}