1use gpui::prelude::*;
8use gpui::{
9 div, px, App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, IntoElement,
10 KeyDownEvent, MouseButton, SharedString, Window,
11};
12
13use super::{control_metrics, Field, TextEdit};
14use crate::devtools::ProbedAny;
15use crate::reactive::Signal;
16use crate::theme::{theme, ColorName, Size};
17
18#[derive(Debug, Clone)]
20pub struct TextAreaEvent(pub String);
21
22#[derive(Debug, Clone)]
27pub struct TextAreaSubmit(pub String);
28
29pub struct TextArea {
31 edit: TextEdit,
32 focus: FocusHandle,
33 placeholder: SharedString,
34 label: Option<SharedString>,
35 description: Option<SharedString>,
36 error: Option<SharedString>,
37 rows: usize,
38 max_rows: Option<usize>,
39 submit_on_enter: bool,
40 size: Size,
41 disabled: bool,
42}
43
44impl EventEmitter<TextAreaEvent> for TextArea {}
45impl EventEmitter<TextAreaSubmit> for TextArea {}
46
47fn line(text: &str) -> SharedString {
49 if text.is_empty() {
50 SharedString::new_static(" ")
51 } else {
52 SharedString::from(text.to_string())
53 }
54}
55
56impl TextArea {
57 pub fn new(cx: &mut Context<Self>) -> Self {
58 TextArea {
59 edit: TextEdit::new(""),
60 focus: cx.focus_handle().tab_stop(true),
61 placeholder: SharedString::default(),
62 label: None,
63 description: None,
64 error: None,
65 rows: 3,
66 max_rows: None,
67 submit_on_enter: false,
68 size: Size::Sm,
69 disabled: false,
70 }
71 }
72
73 pub fn value(mut self, value: &str) -> Self {
74 self.edit = TextEdit::new(value);
75 self
76 }
77
78 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
79 self.placeholder = placeholder.into();
80 self
81 }
82
83 pub fn set_placeholder(
86 &mut self,
87 placeholder: impl Into<SharedString>,
88 cx: &mut Context<Self>,
89 ) {
90 self.placeholder = placeholder.into();
91 cx.notify();
92 }
93
94 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
95 self.label = Some(label.into());
96 self
97 }
98
99 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
100 self.description = Some(description.into());
101 self
102 }
103
104 pub fn error(mut self, error: impl Into<SharedString>) -> Self {
105 self.error = Some(error.into());
106 self
107 }
108
109 pub fn max_rows(mut self, rows: usize) -> Self {
113 self.max_rows = Some(rows.max(1));
114 self
115 }
116
117 pub fn submit_on_enter(mut self, submit: bool) -> Self {
120 self.submit_on_enter = submit;
121 self
122 }
123
124 pub fn rows(mut self, rows: usize) -> Self {
126 self.rows = rows.max(1);
127 self
128 }
129
130 pub fn size(mut self, size: Size) -> Self {
131 self.size = size;
132 self
133 }
134
135 pub fn disabled(mut self, disabled: bool) -> Self {
136 self.disabled = disabled;
137 self
138 }
139
140 pub fn focus_handle(&self) -> FocusHandle {
142 self.focus.clone()
143 }
144
145 pub fn text(&self) -> String {
146 self.edit.text()
147 }
148
149 pub fn is_blank(&self) -> bool {
153 self.edit.chars().iter().all(|c| c.is_whitespace())
154 }
155
156 pub fn set_text(&mut self, value: &str, cx: &mut Context<Self>) {
157 self.edit = TextEdit::new(value);
158 cx.notify();
159 }
160
161 pub fn bind(entity: &Entity<TextArea>, signal: &Signal<String>, cx: &mut App) {
166 let initial = signal.get(cx);
167 entity.update(cx, |this, cx| {
168 if this.text() != initial {
169 this.set_text(&initial, cx);
170 }
171 });
172 let sink = signal.clone();
173 cx.subscribe(entity, move |_area, event: &TextAreaEvent, cx| {
174 sink.set_if_changed(cx, event.0.clone());
175 })
176 .detach();
177 let area = entity.downgrade();
178 cx.observe(signal.entity(), move |observed, cx| {
179 let value = observed.read(cx).clone();
180 area.update(cx, |this, cx| {
181 if this.text() != value {
182 this.set_text(&value, cx);
183 }
184 })
185 .ok();
186 })
187 .detach();
188 }
189
190 fn copy(&self, cx: &mut Context<Self>) {
191 if let Some(text) = self.edit.selected_text() {
192 cx.write_to_clipboard(ClipboardItem::new_string(text));
193 }
194 cx.stop_propagation();
195 }
196
197 fn cut(&mut self, cx: &mut Context<Self>) {
198 if let Some(text) = self.edit.selected_text() {
199 cx.write_to_clipboard(ClipboardItem::new_string(text));
200 self.edit.delete_selection();
201 cx.emit(TextAreaEvent(self.edit.text()));
202 cx.notify();
203 }
204 cx.stop_propagation();
205 }
206
207 fn paste(&mut self, cx: &mut Context<Self>) {
208 if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
209 self.edit
210 .insert(&text.replace("\r\n", "\n").replace('\r', "\n"));
211 cx.emit(TextAreaEvent(self.edit.text()));
212 cx.notify();
213 }
214 cx.stop_propagation();
215 }
216
217 fn on_key(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
218 if self.disabled {
219 return;
220 }
221 let ks = &event.keystroke;
222 let m = &ks.modifiers;
223 if m.platform && !m.alt && !m.control {
224 match ks.key.as_str() {
225 "a" => {
226 self.edit.select_all();
227 cx.notify();
228 cx.stop_propagation();
229 return;
230 }
231 "c" => return self.copy(cx),
232 "x" => return self.cut(cx),
233 "v" => return self.paste(cx),
234 "z" | "y" => {
235 let undo = ks.key == "z" && !m.shift;
236 let changed = if undo {
237 self.edit.undo()
238 } else {
239 self.edit.redo()
240 };
241 if changed {
242 cx.emit(TextAreaEvent(self.edit.text()));
243 }
244 cx.notify();
245 cx.stop_propagation();
246 return;
247 }
248 _ => {}
249 }
250 }
251 if ks.key == "tab" && !m.platform && !m.control {
255 if m.shift {
256 window.focus_prev();
257 } else {
258 window.focus_next();
259 }
260 cx.notify();
261 cx.stop_propagation();
262 return;
263 }
264 if ks.key == "escape" {
265 return;
266 }
267 let edited = match ks.key.as_str() {
268 "enter" if self.submit_on_enter && !m.shift => {
269 cx.emit(TextAreaSubmit(self.edit.text()));
270 cx.notify();
271 cx.stop_propagation();
272 return;
273 }
274 "enter" => {
275 self.edit.insert("\n");
276 true
277 }
278 "left" => {
279 if !m.shift && !m.platform && !m.alt && self.edit.collapse_selection_start() {
280 true
281 } else {
282 self.edit.pre_move(m.shift);
283 if m.platform {
284 self.edit.line_home();
285 } else if m.alt {
286 self.edit.word_left();
287 } else {
288 self.edit.left();
289 }
290 true
291 }
292 }
293 "right" => {
294 if !m.shift && !m.platform && !m.alt && self.edit.collapse_selection_end() {
295 true
296 } else {
297 self.edit.pre_move(m.shift);
298 if m.platform {
299 self.edit.line_end();
300 } else if m.alt {
301 self.edit.word_right();
302 } else {
303 self.edit.right();
304 }
305 true
306 }
307 }
308 "up" => {
309 self.edit.pre_move(m.shift);
310 if m.platform {
311 self.edit.home();
312 } else {
313 self.edit.up();
314 }
315 true
316 }
317 "down" => {
318 self.edit.pre_move(m.shift);
319 if m.platform {
320 self.edit.end();
321 } else {
322 self.edit.down();
323 }
324 true
325 }
326 "home" => {
327 self.edit.pre_move(m.shift);
328 self.edit.line_home();
329 true
330 }
331 "end" => {
332 self.edit.pre_move(m.shift);
333 self.edit.line_end();
334 true
335 }
336 "backspace" => {
337 if m.platform {
338 self.edit.delete_to_start();
339 } else if m.alt {
340 self.edit.delete_word_back();
341 } else {
342 self.edit.backspace();
343 }
344 true
345 }
346 "delete" => {
347 if m.platform {
348 self.edit.delete_to_end();
349 } else if m.alt {
350 self.edit.delete_word_forward();
351 } else {
352 self.edit.delete();
353 }
354 true
355 }
356 "k" if m.control => {
357 self.edit.delete_to_end();
358 true
359 }
360 "a" if m.control => {
361 self.edit.home();
362 true
363 }
364 "e" if m.control => {
365 self.edit.end();
366 true
367 }
368 _ => {
369 if !m.platform && !m.control {
370 if let Some(text) = ks.key_char.as_deref().filter(|t| !t.is_empty()) {
371 self.edit.insert(text);
372 true
373 } else {
374 false
375 }
376 } else {
377 false
378 }
379 }
380 };
381 if edited {
382 cx.emit(TextAreaEvent(self.edit.text()));
383 cx.notify();
384 cx.stop_propagation();
385 }
386 }
387}
388
389impl Render for TextArea {
390 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
391 let t = theme(cx);
392 let (_, pad_x, font) = control_metrics(self.size);
393 let radius = t.radius(t.default_radius);
394 let focused = self.focus.is_focused(window) && !self.disabled;
395 let line_h = font * 1.5;
396 let pad_y = 8.0;
397 let min_h = self.rows as f32 * line_h + pad_y * 2.0;
398 let max_h = self
399 .max_rows
400 .map(|rows| rows as f32 * line_h + pad_y * 2.0)
401 .filter(|max| *max >= min_h);
402
403 let border = if self.error.is_some() {
404 t.color(ColorName::Red, 6)
405 } else if focused {
406 t.primary()
407 } else {
408 t.border()
409 }
410 .hsla();
411 let text_color = t.text().hsla();
412 let dimmed = t.dimmed().hsla();
413 let surface = t.surface().hsla();
414 let caret = t.primary().hsla();
415 let selection_bg = t.selection();
416
417 let mut body = div().flex().flex_col().text_color(text_color);
418 if focused && self.edit.is_empty() && !self.placeholder.is_empty() {
423 body = body.child(
424 div()
425 .flex()
426 .items_center()
427 .h(px(line_h))
428 .child(div().w(px(1.0)).h(px(font * 1.15)).bg(caret))
429 .child(div().text_color(dimmed).child(self.placeholder.clone())),
430 );
431 } else if focused {
432 if let Some((before, selected, after)) = self.edit.split_selection() {
433 let before_lines: Vec<&str> = before.split('\n').collect();
434 let selected_lines: Vec<&str> = selected.split('\n').collect();
435 let after_lines: Vec<&str> = after.split('\n').collect();
436 let before_last = before_lines.len() - 1;
437 for text in &before_lines[..before_last] {
438 body = body.child(div().h(px(line_h)).child(line(text)));
439 }
440 let selected_part =
441 |text: &str| div().bg(selection_bg).rounded(px(2.0)).child(line(text));
442 body = body.child(
443 div()
444 .flex()
445 .items_center()
446 .h(px(line_h))
447 .child(SharedString::from(before_lines[before_last].to_string()))
448 .child(selected_part(selected_lines[0]))
449 .when(selected_lines.len() == 1, |row| {
450 row.child(SharedString::from(after_lines[0].to_string()))
451 }),
452 );
453 if selected_lines.len() > 1 {
454 for text in &selected_lines[1..selected_lines.len() - 1] {
455 body = body.child(div().flex().h(px(line_h)).child(selected_part(text)));
456 }
457 body = body.child(
458 div()
459 .flex()
460 .items_center()
461 .h(px(line_h))
462 .child(selected_part(selected_lines[selected_lines.len() - 1]))
463 .child(SharedString::from(after_lines[0].to_string())),
464 );
465 }
466 for text in &after_lines[1..] {
467 body = body.child(div().h(px(line_h)).child(line(text)));
468 }
469 } else {
470 let (before, after) = self.edit.split();
471 let before_lines: Vec<&str> = before.split('\n').collect();
472 let after_lines: Vec<&str> = after.split('\n').collect();
473 let last = before_lines.len() - 1;
474 for text in &before_lines[..last] {
475 body = body.child(div().h(px(line_h)).child(line(text)));
476 }
477 body = body.child(
478 div()
479 .flex()
480 .items_center()
481 .h(px(line_h))
482 .child(SharedString::from(before_lines[last].to_string()))
483 .child(div().w(px(1.0)).h(px(font * 1.15)).bg(caret))
484 .child(SharedString::from(after_lines[0].to_string())),
485 );
486 for text in &after_lines[1..] {
487 body = body.child(div().h(px(line_h)).child(line(text)));
488 }
489 }
490 } else if self.edit.is_empty() {
491 body = body
492 .text_color(dimmed)
493 .child(div().h(px(line_h)).child(self.placeholder.clone()));
494 } else {
495 for l in self.edit.text().split('\n') {
496 body = body.child(div().h(px(line_h)).child(line(l)));
497 }
498 }
499
500 let field = div()
501 .id("guise-textarea")
502 .track_focus(&self.focus)
503 .on_key_down(cx.listener(Self::on_key))
504 .on_mouse_down(
505 MouseButton::Left,
506 cx.listener(|this, _ev, window, cx| {
507 window.focus(&this.focus);
508 cx.notify();
509 }),
510 )
511 .flex()
512 .items_start()
513 .overflow_hidden()
514 .min_h(px(min_h))
515 .when_some(max_h, |field, max| field.max_h(px(max)))
516 .w_full()
517 .px(px(pad_x))
518 .py(px(pad_y))
519 .rounded(px(radius))
520 .border_1()
521 .border_color(border)
522 .bg(surface)
523 .text_size(px(font))
524 .child(div().w_full().min_w(px(0.0)).overflow_hidden().child(body));
525
526 let mut chrome = Field::new().child(if self.disabled {
527 field.opacity(0.6)
528 } else {
529 field
530 });
531 if let Some(label) = self.label.clone() {
532 chrome = chrome.label(label);
533 }
534 if let Some(error) = self.error.clone() {
535 chrome = chrome.error(error);
536 } else if let Some(description) = self.description.clone() {
537 chrome = chrome.description(description);
538 }
539 chrome.probe_any("TextArea")
540 }
541}