1use 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 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}