1use std::ops::Range;
2use std::rc::Rc;
3
4use gpui::{
5 AnyElement, Bounds, Context, ElementInputHandler, EntityInputHandler, FocusHandle, Focusable,
6 KeyDownEvent, MouseButton, Pixels, Render, UTF16Selection, canvas,
7};
8
9use crate::prelude::*;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub enum InputValidationState {
15 #[default]
16 Neutral,
17 Error,
18 Success,
19 Warning,
20}
21
22pub struct TextInput {
31 content: String,
32 placeholder: SharedString,
33 focus_handle: FocusHandle,
34 multiline: bool,
35 submit_on_enter: bool,
36 validation: InputValidationState,
37 read_only: bool,
38 marked_range: Option<Range<usize>>,
41 on_submit: Option<Rc<dyn Fn(&mut Window, &mut Context<Self>) + 'static>>,
42}
43
44impl TextInput {
45 pub fn new(cx: &mut App) -> Self {
46 Self {
47 content: String::new(),
48 placeholder: SharedString::default(),
49 focus_handle: cx.focus_handle(),
50 multiline: false,
51 submit_on_enter: false,
52 validation: InputValidationState::Neutral,
53 read_only: false,
54 marked_range: None,
55 on_submit: None,
56 }
57 }
58
59 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
60 self.placeholder = placeholder.into();
61 self
62 }
63
64 pub fn multiline(mut self, multiline: bool) -> Self {
65 self.multiline = multiline;
66 self
67 }
68
69 pub fn submit_on_enter(mut self, submit_on_enter: bool) -> Self {
75 self.submit_on_enter = submit_on_enter;
76 self
77 }
78
79 pub fn on_submit(
82 mut self,
83 handler: impl Fn(&mut Window, &mut Context<Self>) + 'static,
84 ) -> Self {
85 self.on_submit = Some(Rc::new(handler));
86 self
87 }
88
89 pub fn read_only(mut self, read_only: bool) -> Self {
92 self.read_only = read_only;
93 self
94 }
95
96 pub fn invalid(mut self, invalid: bool) -> Self {
99 self.validation = if invalid {
100 InputValidationState::Error
101 } else {
102 InputValidationState::Neutral
103 };
104 self
105 }
106
107 pub fn success(mut self, success: bool) -> Self {
110 self.validation = if success {
111 InputValidationState::Success
112 } else {
113 InputValidationState::Neutral
114 };
115 self
116 }
117
118 pub fn warning(mut self, warning: bool) -> Self {
121 self.validation = if warning {
122 InputValidationState::Warning
123 } else {
124 InputValidationState::Neutral
125 };
126 self
127 }
128
129 pub fn text(&self) -> &str {
131 &self.content
132 }
133
134 pub fn set_text(&mut self, text: impl Into<String>, cx: &mut Context<Self>) {
137 self.content = text.into();
138 cx.notify();
139 }
140
141 pub fn clear(&mut self, cx: &mut Context<Self>) {
144 self.content.clear();
145 cx.notify();
146 }
147
148 pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
152 self.read_only = read_only;
153 cx.notify();
154 }
155
156 fn on_key_down(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
157 if self.read_only {
158 return;
159 }
160 let keystroke = &event.keystroke;
161
162 if self.submit_on_enter && keystroke.key == "enter" {
163 let wants_newline = self.multiline
164 && (keystroke.modifiers.shift
165 || keystroke.modifiers.control
166 || keystroke.modifiers.platform);
167 if wants_newline {
168 self.content.push('\n');
169 } else if let Some(on_submit) = self.on_submit.clone() {
170 on_submit(window, cx);
171 }
172 cx.notify();
173 return;
174 }
175
176 if keystroke.modifiers.control || keystroke.modifiers.platform {
178 return;
179 }
180 let ime_in_progress = keystroke.is_ime_in_progress();
186 match keystroke.key.as_str() {
187 "backspace" => {
188 if let Some(range) = self.marked_range.take() {
189 self.content.replace_range(range, "");
190 } else {
191 self.content.pop();
192 }
193 }
194 "enter" if self.multiline => self.content.push('\n'),
195 "space" if !ime_in_progress => self.content.push(' '),
196 _ => {
197 if !ime_in_progress
198 && let Some(text) = &keystroke.key_char
199 {
200 self.content.push_str(text);
201 }
202 }
203 }
204 cx.notify();
205 }
206}
207
208impl Render for TextInput {
209 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
210 let focused = self.focus_handle.is_focused(window);
211 let is_empty = self.content.is_empty();
212 let text_color = if is_empty {
213 semantic::text_placeholder(cx)
214 } else {
215 semantic::text(cx)
216 };
217 let border_color = match self.validation {
218 InputValidationState::Error => palette::danger(500),
219 InputValidationState::Success => palette::success(500),
220 InputValidationState::Warning => palette::warning(500),
221 InputValidationState::Neutral => semantic::border(cx),
222 };
223 let ring_color = match self.validation {
224 InputValidationState::Error => palette::danger(500),
225 InputValidationState::Success => palette::success(500),
226 InputValidationState::Warning => palette::warning(500),
227 InputValidationState::Neutral => palette::primary(500),
228 };
229 let show_cursor = focused && !is_empty && !self.read_only;
230 let cursor = || div().w(px(1.)).h(px(16.)).bg(palette::primary(500));
231
232 let content: AnyElement = if self.multiline {
237 let text: SharedString = if is_empty {
238 self.placeholder.clone()
239 } else {
240 self.content.clone().into()
241 };
242 let lines: Vec<String> = text.split('\n').map(str::to_string).collect();
243 let last_ix = lines.len().saturating_sub(1);
244 v_flex()
245 .w_full()
246 .children(lines.into_iter().enumerate().map(|(ix, line)| {
247 h_flex()
248 .min_h(px(20.))
249 .items_center()
250 .gap(DynamicSpacing::Base02.rems(cx))
251 .child(SharedString::from(line))
252 .when(ix == last_ix && show_cursor, |this| this.child(cursor()))
253 }))
254 .into_any_element()
255 } else {
256 let display: SharedString = if is_empty {
257 self.placeholder.clone()
258 } else {
259 self.content.clone().into()
260 };
261 h_flex()
262 .flex_wrap()
263 .items_center()
264 .gap(DynamicSpacing::Base02.rems(cx))
265 .child(display)
266 .when(show_cursor, |this| this.child(cursor()))
267 .into_any_element()
268 };
269
270 let field = div()
271 .track_focus(&self.focus_handle)
272 .on_key_down(cx.listener(Self::on_key_down))
273 .on_mouse_down(
274 MouseButton::Left,
275 cx.listener(|this, _event, window, cx| {
276 window.focus(&this.focus_handle, cx);
277 cx.notify();
278 }),
279 )
280 .w_full()
281 .when(self.multiline, |this| this.min_h(px(96.)))
282 .px(DynamicSpacing::Base12.px(cx))
283 .py(DynamicSpacing::Base08.px(cx))
284 .rounded_md()
285 .bg(semantic::surface(cx))
286 .border_1()
287 .border_color(border_color)
288 .text_color(text_color)
289 .child(content)
290 .child({
291 let focus_handle = self.focus_handle.clone();
296 let entity = cx.entity();
297 canvas(
298 move |_bounds, _window, _cx| {},
299 move |bounds, _state, window, cx| {
300 window.handle_input(
301 &focus_handle,
302 ElementInputHandler::new(bounds, entity.clone()),
303 cx,
304 );
305 },
306 )
307 .absolute()
308 .size_full()
309 });
310
311 focus_ring(field, focused, ring_color)
312 }
313}
314
315impl Focusable for TextInput {
316 fn focus_handle(&self, _cx: &App) -> FocusHandle {
317 self.focus_handle.clone()
318 }
319}
320
321impl EntityInputHandler for TextInput {
322 fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context<Self>) -> bool {
323 !self.read_only
324 }
325
326 fn selected_text_range(
330 &mut self,
331 _ignore_disabled_input: bool,
332 _window: &mut Window,
333 _cx: &mut Context<Self>,
334 ) -> Option<UTF16Selection> {
335 None
336 }
337
338 fn marked_text_range(
340 &self,
341 _window: &mut Window,
342 _cx: &mut Context<Self>,
343 ) -> Option<Range<usize>> {
344 self.marked_range.clone()
345 }
346
347 fn text_for_range(
350 &mut self,
351 range_utf16: Range<usize>,
352 _adjusted_range: &mut Option<Range<usize>>,
353 _window: &mut Window,
354 _cx: &mut Context<Self>,
355 ) -> Option<String> {
356 let bytes = utf16_range_to_byte_range(&self.content, range_utf16)?;
357 Some(self.content[bytes].to_string())
358 }
359
360 fn replace_text_in_range(
363 &mut self,
364 range: Option<Range<usize>>,
365 text: &str,
366 _window: &mut Window,
367 cx: &mut Context<Self>,
368 ) {
369 if self.read_only {
370 return;
371 }
372 if let Some(marked) = self.marked_range.take() {
373 self.content.replace_range(marked, text);
374 } else if let Some(range) = range {
375 let bytes = utf16_range_to_byte_range(&self.content, range);
376 if let Some(bytes) = bytes {
377 self.content.replace_range(bytes, text);
378 } else {
379 self.content.push_str(text);
380 }
381 } else {
382 self.content.push_str(text);
383 }
384 cx.notify();
385 }
386
387 fn replace_and_mark_text_in_range(
390 &mut self,
391 range: Option<Range<usize>>,
392 new_text: &str,
393 _new_selected_range: Option<Range<usize>>,
394 _window: &mut Window,
395 cx: &mut Context<Self>,
396 ) {
397 if self.read_only {
398 return;
399 }
400 let start_byte = if let Some(marked) = self.marked_range.clone() {
401 self.content.replace_range(marked.clone(), new_text);
402 marked.start
403 } else if let Some(range) = range {
404 let bytes = utf16_range_to_byte_range(&self.content, range);
405 if let Some(bytes) = bytes {
406 self.content.replace_range(bytes.clone(), new_text);
407 bytes.start
408 } else {
409 self.content.push_str(new_text);
410 self.content.len().saturating_sub(new_text.len())
411 }
412 } else {
413 let start = self.content.len();
414 self.content.push_str(new_text);
415 start
416 };
417 let end_byte = start_byte + new_text.len();
418 self.marked_range = Some(start_byte..end_byte);
419 cx.notify();
420 }
421
422 fn unmark_text(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
424 if self.marked_range.take().is_some() {
425 cx.notify();
426 }
427 }
428
429 fn bounds_for_range(
433 &mut self,
434 _range_utf16: Range<usize>,
435 element_bounds: Bounds<Pixels>,
436 _window: &mut Window,
437 _cx: &mut Context<Self>,
438 ) -> Option<Bounds<Pixels>> {
439 Some(element_bounds)
440 }
441
442 fn character_index_for_point(
443 &mut self,
444 _point: gpui::Point<Pixels>,
445 _window: &mut Window,
446 _cx: &mut Context<Self>,
447 ) -> Option<usize> {
448 None
449 }
450}
451
452fn utf16_range_to_byte_range(text: &str, range_utf16: Range<usize>) -> Option<Range<usize>> {
455 let mut start_byte = None;
456 let mut end_byte = None;
457 let mut utf16_index = 0usize;
458 for (byte_idx, ch) in text.char_indices() {
459 if start_byte.is_none() && utf16_index >= range_utf16.start {
460 start_byte = Some(byte_idx);
461 }
462 utf16_index += ch.len_utf16();
463 if end_byte.is_none() && utf16_index >= range_utf16.end {
464 end_byte = Some(byte_idx + ch.len_utf8());
465 break;
466 }
467 }
468 let start = start_byte.unwrap_or(text.len());
469 let end = end_byte.unwrap_or(text.len());
470 if start <= end && start <= text.len() && end <= text.len() {
471 Some(start..end)
472 } else {
473 None
474 }
475}
476
477pub type Textarea = TextInput;