ui/components/
text_input.rs1use gpui::{Context, FocusHandle, Focusable, KeyDownEvent, MouseButton, Render};
2
3use crate::prelude::*;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub enum InputValidationState {
9 #[default]
10 Neutral,
11 Error,
12 Success,
13 Warning,
14}
15
16pub struct TextInput {
23 content: String,
24 placeholder: SharedString,
25 focus_handle: FocusHandle,
26 multiline: bool,
27 validation: InputValidationState,
28}
29
30impl TextInput {
31 pub fn new(cx: &mut App) -> Self {
32 Self {
33 content: String::new(),
34 placeholder: SharedString::default(),
35 focus_handle: cx.focus_handle(),
36 multiline: false,
37 validation: InputValidationState::Neutral,
38 }
39 }
40
41 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
42 self.placeholder = placeholder.into();
43 self
44 }
45
46 pub fn multiline(mut self, multiline: bool) -> Self {
47 self.multiline = multiline;
48 self
49 }
50
51 pub fn invalid(mut self, invalid: bool) -> Self {
54 self.validation = if invalid {
55 InputValidationState::Error
56 } else {
57 InputValidationState::Neutral
58 };
59 self
60 }
61
62 pub fn success(mut self, success: bool) -> Self {
65 self.validation = if success {
66 InputValidationState::Success
67 } else {
68 InputValidationState::Neutral
69 };
70 self
71 }
72
73 pub fn warning(mut self, warning: bool) -> Self {
76 self.validation = if warning {
77 InputValidationState::Warning
78 } else {
79 InputValidationState::Neutral
80 };
81 self
82 }
83
84 pub fn text(&self) -> &str {
86 &self.content
87 }
88
89 pub fn set_text(&mut self, text: impl Into<String>, cx: &mut Context<Self>) {
92 self.content = text.into();
93 cx.notify();
94 }
95
96 pub fn clear(&mut self, cx: &mut Context<Self>) {
99 self.content.clear();
100 cx.notify();
101 }
102
103 fn on_key_down(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
104 let keystroke = &event.keystroke;
105 if keystroke.modifiers.control || keystroke.modifiers.platform {
107 return;
108 }
109 match keystroke.key.as_str() {
110 "backspace" => {
111 self.content.pop();
112 }
113 "enter" if self.multiline => self.content.push('\n'),
114 "space" => self.content.push(' '),
115 _ => {
116 if let Some(text) = &keystroke.key_char {
117 self.content.push_str(text);
118 }
119 }
120 }
121 cx.notify();
122 }
123}
124
125impl Render for TextInput {
126 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
127 let focused = self.focus_handle.is_focused(window);
128 let is_empty = self.content.is_empty();
129 let display: SharedString = if is_empty {
130 self.placeholder.clone()
131 } else {
132 self.content.clone().into()
133 };
134 let text_color = if is_empty {
135 semantic::text_placeholder(cx)
136 } else {
137 semantic::text(cx)
138 };
139 let border_color = match self.validation {
140 InputValidationState::Error => palette::danger(500),
141 InputValidationState::Success => palette::success(500),
142 InputValidationState::Warning => palette::warning(500),
143 InputValidationState::Neutral => semantic::border(cx),
144 };
145 let ring_color = match self.validation {
146 InputValidationState::Error => palette::danger(500),
147 InputValidationState::Success => palette::success(500),
148 InputValidationState::Warning => palette::warning(500),
149 InputValidationState::Neutral => palette::primary(500),
150 };
151
152 let field = div()
153 .track_focus(&self.focus_handle)
154 .on_key_down(cx.listener(Self::on_key_down))
155 .on_mouse_down(
156 MouseButton::Left,
157 cx.listener(|this, _event, window, cx| {
158 window.focus(&this.focus_handle, cx);
159 cx.notify();
160 }),
161 )
162 .w_full()
163 .when(self.multiline, |this| this.min_h(px(96.)))
164 .flex()
165 .flex_wrap()
166 .items_center()
167 .gap_0p5()
168 .px_3()
169 .py_2()
170 .rounded_md()
171 .bg(semantic::surface(cx))
172 .border_1()
173 .border_color(border_color)
174 .text_color(text_color)
175 .child(display)
176 .when(focused && !is_empty, |this| {
177 this.child(div().w(px(1.)).h(px(16.)).bg(palette::primary(500)))
178 });
179
180 focus_ring(field, focused, ring_color)
181 }
182}
183
184impl Focusable for TextInput {
185 fn focus_handle(&self, _cx: &App) -> FocusHandle {
186 self.focus_handle.clone()
187 }
188}
189
190pub type Textarea = TextInput;