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