1use gpui::prelude::*;
8use gpui::{
9 div, px, App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, IntoElement,
10 KeyDownEvent, MouseButton, ScrollHandle, 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 scroll: ScrollHandle,
34 placeholder: SharedString,
35 label: Option<SharedString>,
36 description: Option<SharedString>,
37 error: Option<SharedString>,
38 rows: usize,
39 max_rows: Option<usize>,
40 submit_on_enter: bool,
41 size: Size,
42 disabled: bool,
43}
44
45impl EventEmitter<TextAreaEvent> for TextArea {}
46impl EventEmitter<TextAreaSubmit> for TextArea {}
47
48fn line(text: &str) -> SharedString {
50 if text.is_empty() {
51 SharedString::new_static(" ")
52 } else {
53 SharedString::from(text.to_string())
54 }
55}
56
57impl TextArea {
58 pub fn new(cx: &mut Context<Self>) -> Self {
59 TextArea {
60 edit: TextEdit::new(""),
61 focus: cx.focus_handle().tab_stop(true),
62 scroll: ScrollHandle::new(),
63 placeholder: SharedString::default(),
64 label: None,
65 description: None,
66 error: None,
67 rows: 3,
68 max_rows: None,
69 submit_on_enter: false,
70 size: Size::Sm,
71 disabled: false,
72 }
73 }
74
75 pub fn value(mut self, value: &str) -> Self {
76 self.edit = TextEdit::new(value);
77 self
78 }
79
80 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
81 self.placeholder = placeholder.into();
82 self
83 }
84
85 pub fn set_placeholder(&mut self, placeholder: impl Into<SharedString>, cx: &mut Context<Self>) {
88 self.placeholder = placeholder.into();
89 cx.notify();
90 }
91
92 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
93 self.label = Some(label.into());
94 self
95 }
96
97 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
98 self.description = Some(description.into());
99 self
100 }
101
102 pub fn error(mut self, error: impl Into<SharedString>) -> Self {
103 self.error = Some(error.into());
104 self
105 }
106
107 pub fn max_rows(mut self, rows: usize) -> Self {
111 self.max_rows = Some(rows.max(1));
112 self
113 }
114
115 pub fn submit_on_enter(mut self, submit: bool) -> Self {
118 self.submit_on_enter = submit;
119 self
120 }
121
122 pub fn rows(mut self, rows: usize) -> Self {
124 self.rows = rows.max(1);
125 self
126 }
127
128 pub fn size(mut self, size: Size) -> Self {
129 self.size = size;
130 self
131 }
132
133 pub fn disabled(mut self, disabled: bool) -> Self {
134 self.disabled = disabled;
135 self
136 }
137
138 pub fn focus_handle(&self) -> FocusHandle {
140 self.focus.clone()
141 }
142
143 pub fn text(&self) -> String {
144 self.edit.text()
145 }
146
147 pub fn is_blank(&self) -> bool {
151 self.edit.chars().iter().all(|c| c.is_whitespace())
152 }
153
154 pub fn set_text(&mut self, value: &str, cx: &mut Context<Self>) {
155 self.edit = TextEdit::new(value);
156 cx.notify();
157 }
158
159 pub fn bind(entity: &Entity<TextArea>, signal: &Signal<String>, cx: &mut App) {
164 let initial = signal.get(cx);
165 entity.update(cx, |this, cx| {
166 if this.text() != initial {
167 this.set_text(&initial, cx);
168 }
169 });
170 let sink = signal.clone();
171 cx.subscribe(entity, move |_area, event: &TextAreaEvent, cx| {
172 sink.set_if_changed(cx, event.0.clone());
173 })
174 .detach();
175 let area = entity.downgrade();
176 cx.observe(signal.entity(), move |observed, cx| {
177 let value = observed.read(cx).clone();
178 area
179 .update(cx, |this, cx| {
180 if this.text() != value {
181 this.set_text(&value, cx);
182 }
183 })
184 .ok();
185 })
186 .detach();
187 }
188
189 fn copy(&self, cx: &mut Context<Self>) {
190 if let Some(text) = self.edit.selected_text() {
191 cx.write_to_clipboard(ClipboardItem::new_string(text));
192 }
193 cx.stop_propagation();
194 }
195
196 fn cut(&mut self, cx: &mut Context<Self>) {
197 if let Some(text) = self.edit.selected_text() {
198 cx.write_to_clipboard(ClipboardItem::new_string(text));
199 self.edit.delete_selection();
200 cx.emit(TextAreaEvent(self.edit.text()));
201 cx.notify();
202 }
203 cx.stop_propagation();
204 }
205
206 fn paste(&mut self, cx: &mut Context<Self>) {
207 if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) {
208 self
209 .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 .flex_wrap()
427 .items_center()
428 .w_full()
429 .min_h(px(line_h))
430 .child(div().w(px(1.0)).h(px(font * 1.15)).bg(caret))
431 .child(
432 div()
433 .flex_1()
434 .min_w(px(0.0))
435 .text_color(dimmed)
436 .child(self.placeholder.clone()),
437 ),
438 );
439 } else if focused {
440 if let Some((before, selected, after)) = self.edit.split_selection() {
441 let before_lines: Vec<&str> = before.split('\n').collect();
442 let selected_lines: Vec<&str> = selected.split('\n').collect();
443 let after_lines: Vec<&str> = after.split('\n').collect();
444 let before_last = before_lines.len() - 1;
445 for text in &before_lines[..before_last] {
446 body = body.child(div().w_full().min_h(px(line_h)).child(line(text)));
447 }
448 let selected_part = |text: &str| div().bg(selection_bg).rounded(px(2.0)).child(line(text));
449 body = body.child(
450 div()
451 .flex()
452 .flex_wrap()
453 .items_center()
454 .w_full()
455 .min_h(px(line_h))
456 .child(SharedString::from(before_lines[before_last].to_string()))
457 .child(selected_part(selected_lines[0]))
458 .when(selected_lines.len() == 1, |row| {
459 row.child(SharedString::from(after_lines[0].to_string()))
460 }),
461 );
462 if selected_lines.len() > 1 {
463 for text in &selected_lines[1..selected_lines.len() - 1] {
464 body = body.child(
465 div()
466 .flex()
467 .flex_wrap()
468 .w_full()
469 .min_h(px(line_h))
470 .child(selected_part(text)),
471 );
472 }
473 body = body.child(
474 div()
475 .flex()
476 .flex_wrap()
477 .items_center()
478 .w_full()
479 .min_h(px(line_h))
480 .child(selected_part(selected_lines[selected_lines.len() - 1]))
481 .child(SharedString::from(after_lines[0].to_string())),
482 );
483 }
484 for text in &after_lines[1..] {
485 body = body.child(div().w_full().min_h(px(line_h)).child(line(text)));
486 }
487 } else {
488 let (before, after) = self.edit.split();
489 let before_lines: Vec<&str> = before.split('\n').collect();
490 let after_lines: Vec<&str> = after.split('\n').collect();
491 let last = before_lines.len() - 1;
492 for text in &before_lines[..last] {
493 body = body.child(div().w_full().min_h(px(line_h)).child(line(text)));
494 }
495 body = body.child(
496 div()
497 .flex()
498 .flex_wrap()
499 .items_center()
500 .w_full()
501 .min_h(px(line_h))
502 .child(SharedString::from(before_lines[last].to_string()))
503 .child(div().w(px(1.0)).h(px(font * 1.15)).bg(caret))
504 .child(SharedString::from(after_lines[0].to_string())),
505 );
506 for text in &after_lines[1..] {
507 body = body.child(div().w_full().min_h(px(line_h)).child(line(text)));
508 }
509 }
510 } else if self.edit.is_empty() {
511 body = body.text_color(dimmed).child(
512 div()
513 .w_full()
514 .min_h(px(line_h))
515 .child(self.placeholder.clone()),
516 );
517 } else {
518 for l in self.edit.text().split('\n') {
519 body = body.child(div().w_full().min_h(px(line_h)).child(line(l)));
520 }
521 }
522
523 let mut field = div()
524 .id("guise-textarea")
525 .track_focus(&self.focus)
526 .on_key_down(cx.listener(Self::on_key))
527 .on_mouse_down(
528 MouseButton::Left,
529 cx.listener(|this, _ev, window, cx| {
530 window.focus(&this.focus);
531 cx.notify();
532 }),
533 )
534 .flex()
535 .items_start()
536 .overflow_x_hidden()
537 .min_h(px(min_h))
538 .w_full()
539 .px(px(pad_x))
540 .py(px(pad_y))
541 .rounded(px(radius))
542 .border_1()
543 .border_color(border)
544 .bg(surface)
545 .text_size(px(font))
546 .line_height(px(line_h))
547 .child(div().w_full().min_w(px(0.0)).child(body));
548
549 if let Some(max) = max_h {
550 field = field
551 .max_h(px(max))
552 .overflow_y_scroll()
553 .track_scroll(&self.scroll);
554 }
555
556 let mut chrome = Field::new().child(if self.disabled {
557 field.opacity(0.6)
558 } else {
559 field
560 });
561 if let Some(label) = self.label.clone() {
562 chrome = chrome.label(label);
563 }
564 if let Some(error) = self.error.clone() {
565 chrome = chrome.error(error);
566 } else if let Some(description) = self.description.clone() {
567 chrome = chrome.description(description);
568 }
569 chrome.probe_any("TextArea")
570 }
571}