1use gpui::prelude::*;
8use gpui::{
9 div, px, Context, EventEmitter, FocusHandle, IntoElement, KeyDownEvent, MouseButton,
10 SharedString, Window,
11};
12
13use super::{control_metrics, Field, TextEdit};
14use crate::theme::{theme, ColorName, Size};
15
16#[derive(Debug, Clone)]
18pub struct TextAreaEvent(pub String);
19
20pub struct TextArea {
22 edit: TextEdit,
23 focus: FocusHandle,
24 placeholder: SharedString,
25 label: Option<SharedString>,
26 description: Option<SharedString>,
27 error: Option<SharedString>,
28 rows: usize,
29 size: Size,
30 disabled: bool,
31}
32
33impl EventEmitter<TextAreaEvent> for TextArea {}
34
35fn line(text: &str) -> SharedString {
37 if text.is_empty() {
38 SharedString::new_static(" ")
39 } else {
40 SharedString::from(text.to_string())
41 }
42}
43
44impl TextArea {
45 pub fn new(cx: &mut Context<Self>) -> Self {
46 TextArea {
47 edit: TextEdit::new(""),
48 focus: cx.focus_handle(),
49 placeholder: SharedString::default(),
50 label: None,
51 description: None,
52 error: None,
53 rows: 3,
54 size: Size::Sm,
55 disabled: false,
56 }
57 }
58
59 pub fn value(mut self, value: &str) -> Self {
60 self.edit = TextEdit::new(value);
61 self
62 }
63
64 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
65 self.placeholder = placeholder.into();
66 self
67 }
68
69 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
70 self.label = Some(label.into());
71 self
72 }
73
74 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
75 self.description = Some(description.into());
76 self
77 }
78
79 pub fn error(mut self, error: impl Into<SharedString>) -> Self {
80 self.error = Some(error.into());
81 self
82 }
83
84 pub fn rows(mut self, rows: usize) -> Self {
86 self.rows = rows.max(1);
87 self
88 }
89
90 pub fn size(mut self, size: Size) -> Self {
91 self.size = size;
92 self
93 }
94
95 pub fn disabled(mut self, disabled: bool) -> Self {
96 self.disabled = disabled;
97 self
98 }
99
100 pub fn focus_handle(&self) -> FocusHandle {
102 self.focus.clone()
103 }
104
105 pub fn text(&self) -> String {
106 self.edit.text()
107 }
108
109 pub fn set_text(&mut self, value: &str, cx: &mut Context<Self>) {
110 self.edit = TextEdit::new(value);
111 cx.notify();
112 }
113
114 fn on_key(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
115 if self.disabled {
116 return;
117 }
118 let ks = &event.keystroke;
119 let m = &ks.modifiers;
120 if matches!(ks.key.as_str(), "escape" | "tab") {
123 return;
124 }
125 let edited = match ks.key.as_str() {
126 "enter" => {
127 self.edit.insert("\n");
128 true
129 }
130 "left" => {
131 if m.platform {
132 self.edit.home();
133 } else if m.alt {
134 self.edit.word_left();
135 } else {
136 self.edit.left();
137 }
138 true
139 }
140 "right" => {
141 if m.platform {
142 self.edit.end();
143 } else if m.alt {
144 self.edit.word_right();
145 } else {
146 self.edit.right();
147 }
148 true
149 }
150 "up" => {
151 self.edit.up();
152 true
153 }
154 "down" => {
155 self.edit.down();
156 true
157 }
158 "home" => {
159 self.edit.home();
160 true
161 }
162 "end" => {
163 self.edit.end();
164 true
165 }
166 "backspace" => {
167 if m.platform {
168 self.edit.delete_to_start();
169 } else if m.alt {
170 self.edit.delete_word_back();
171 } else {
172 self.edit.backspace();
173 }
174 true
175 }
176 "delete" => {
177 if m.platform {
178 self.edit.delete_to_end();
179 } else if m.alt {
180 self.edit.delete_word_forward();
181 } else {
182 self.edit.delete();
183 }
184 true
185 }
186 "k" if m.control => {
187 self.edit.delete_to_end();
188 true
189 }
190 "a" if m.control => {
191 self.edit.home();
192 true
193 }
194 "e" if m.control => {
195 self.edit.end();
196 true
197 }
198 _ => {
199 if !m.platform && !m.control {
200 if let Some(text) = ks.key_char.as_deref().filter(|t| !t.is_empty()) {
201 self.edit.insert(text);
202 true
203 } else {
204 false
205 }
206 } else {
207 false
208 }
209 }
210 };
211 if edited {
212 cx.emit(TextAreaEvent(self.edit.text()));
213 cx.notify();
214 cx.stop_propagation();
215 }
216 }
217}
218
219impl Render for TextArea {
220 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
221 let t = theme(cx);
222 let (_, pad_x, font) = control_metrics(self.size);
223 let radius = t.radius(t.default_radius);
224 let focused = self.focus.is_focused(window) && !self.disabled;
225 let line_h = font * 1.5;
226 let pad_y = 8.0;
227 let min_h = self.rows as f32 * line_h + pad_y * 2.0;
228
229 let border = if self.error.is_some() {
230 t.color(ColorName::Red, 6)
231 } else if focused {
232 t.primary()
233 } else {
234 t.border()
235 }
236 .hsla();
237 let text_color = t.text().hsla();
238 let dimmed = t.dimmed().hsla();
239 let surface = t.surface().hsla();
240 let caret = t.primary().hsla();
241
242 let mut body = div().flex().flex_col().text_color(text_color);
243 if focused {
244 let (before, after) = self.edit.split();
245 let before_lines: Vec<&str> = before.split('\n').collect();
246 let after_lines: Vec<&str> = after.split('\n').collect();
247 let last = before_lines.len() - 1;
248 for l in &before_lines[..last] {
249 body = body.child(div().h(px(line_h)).child(line(l)));
250 }
251 body = body.child(
253 div()
254 .flex()
255 .items_center()
256 .h(px(line_h))
257 .child(SharedString::from(before_lines[last].to_string()))
258 .child(div().w(px(1.0)).h(px(font * 1.15)).bg(caret))
259 .child(SharedString::from(after_lines[0].to_string())),
260 );
261 for l in &after_lines[1..] {
262 body = body.child(div().h(px(line_h)).child(line(l)));
263 }
264 } else if self.edit.is_empty() {
265 body = body
266 .text_color(dimmed)
267 .child(div().h(px(line_h)).child(self.placeholder.clone()));
268 } else {
269 for l in self.edit.text().split('\n') {
270 body = body.child(div().h(px(line_h)).child(line(l)));
271 }
272 }
273
274 let field = div()
275 .id("guise-textarea")
276 .track_focus(&self.focus)
277 .on_key_down(cx.listener(Self::on_key))
278 .on_mouse_down(
279 MouseButton::Left,
280 cx.listener(|this, _ev, window, cx| {
281 window.focus(&this.focus);
282 cx.notify();
283 }),
284 )
285 .flex()
286 .items_start()
287 .min_h(px(min_h))
288 .w_full()
289 .px(px(pad_x))
290 .py(px(pad_y))
291 .rounded(px(radius))
292 .border_1()
293 .border_color(border)
294 .bg(surface)
295 .text_size(px(font))
296 .child(body);
297
298 let mut chrome = Field::new().child(if self.disabled {
299 field.opacity(0.6)
300 } else {
301 field
302 });
303 if let Some(label) = self.label.clone() {
304 chrome = chrome.label(label);
305 }
306 if let Some(error) = self.error.clone() {
307 chrome = chrome.error(error);
308 } else if let Some(description) = self.description.clone() {
309 chrome = chrome.description(description);
310 }
311 chrome
312 }
313}