1use gpui::prelude::*;
12use gpui::{
13 div, px, App, Context, Entity, EventEmitter, FocusHandle, IntoElement, KeyDownEvent,
14 SharedString, Window,
15};
16
17use super::line::{self, Line, LineEditor, LineState};
18use super::{control_metrics, edit::TextEdit, Field, KeyOutcome};
19use crate::devtools::ProbedAny;
20use crate::reactive::Signal;
21use crate::theme::{theme, ColorName, Size};
22
23#[derive(Debug, Clone)]
25pub enum TextInputEvent {
26 Change(String),
28 Submit(String),
30}
31
32pub struct TextInput {
34 edit: TextEdit,
35 state: LineState,
36 focus: FocusHandle,
37 placeholder: SharedString,
38 label: Option<SharedString>,
39 description: Option<SharedString>,
40 error: Option<SharedString>,
41 size: Size,
42 radius: Option<Size>,
43 disabled: bool,
44 read_only: bool,
45 password: bool,
46 max_length: Option<usize>,
47}
48
49impl EventEmitter<TextInputEvent> for TextInput {}
50
51impl TextInput {
52 pub fn new(cx: &mut Context<Self>) -> Self {
53 TextInput {
54 edit: TextEdit::new(""),
55 state: LineState::new(),
56 focus: cx.focus_handle().tab_stop(true),
60 placeholder: SharedString::default(),
61 label: None,
62 description: None,
63 error: None,
64 size: Size::Sm,
65 radius: None,
66 disabled: false,
67 read_only: false,
68 password: false,
69 max_length: None,
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 label(mut self, label: impl Into<SharedString>) -> Self {
84 self.label = Some(label.into());
85 self
86 }
87
88 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
89 self.description = Some(description.into());
90 self
91 }
92
93 pub fn error(mut self, error: impl Into<SharedString>) -> Self {
94 self.error = Some(error.into());
95 self
96 }
97
98 pub fn size(mut self, size: Size) -> Self {
99 self.size = size;
100 self
101 }
102
103 pub fn radius(mut self, radius: Size) -> Self {
104 self.radius = Some(radius);
105 self
106 }
107
108 pub fn disabled(mut self, disabled: bool) -> Self {
109 self.disabled = disabled;
110 self
111 }
112
113 pub fn read_only(mut self, read_only: bool) -> Self {
116 self.read_only = read_only;
117 self
118 }
119
120 pub fn password(mut self, password: bool) -> Self {
121 self.password = password;
122 self
123 }
124
125 pub fn max_length(mut self, max: usize) -> Self {
127 self.max_length = Some(max);
128 self
129 }
130
131 pub fn text(&self) -> String {
133 self.edit.text()
134 }
135
136 pub fn set_text(&mut self, value: &str, cx: &mut Context<Self>) {
138 self.edit.set_text(value);
139 cx.notify();
140 }
141
142 pub fn select_all(&mut self, cx: &mut Context<Self>) {
145 self.edit.select_all();
146 cx.notify();
147 }
148
149 pub fn bind(entity: &Entity<TextInput>, signal: &Signal<String>, cx: &mut App) {
154 let initial = signal.get(cx);
155 entity.update(cx, |this, cx| this.sync_text(initial, cx));
156 let sink = signal.clone();
157 cx.subscribe(entity, move |_input, event: &TextInputEvent, cx| {
158 if let TextInputEvent::Change(text) = event {
159 sink.set_if_changed(cx, text.clone());
160 }
161 })
162 .detach();
163 let field = entity.downgrade();
164 cx.observe(signal.entity(), move |observed, cx| {
165 let text = observed.read(cx).clone();
166 field.update(cx, |this, cx| this.sync_text(text, cx)).ok();
167 })
168 .detach();
169 }
170
171 fn sync_text(&mut self, text: String, cx: &mut Context<Self>) {
173 if self.edit.text() != text {
174 self.edit.set_text(&text);
175 cx.notify();
176 }
177 }
178
179 fn on_key(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
180 if self.disabled {
181 return;
182 }
183 match line::keys(self, event, window, cx) {
184 KeyOutcome::Submit => {
185 cx.emit(TextInputEvent::Submit(self.edit.text()));
186 cx.notify();
187 cx.stop_propagation();
188 }
189 KeyOutcome::Edited => {
190 self.line_changed(cx);
191 cx.stop_propagation();
192 }
193 KeyOutcome::Cancel | KeyOutcome::Pass => {}
197 }
198 }
199}
200
201impl LineEditor for TextInput {
202 fn edit(&self) -> &TextEdit {
203 &self.edit
204 }
205
206 fn edit_mut(&mut self) -> &mut TextEdit {
207 &mut self.edit
208 }
209
210 fn line(&self) -> &LineState {
211 &self.state
212 }
213
214 fn line_mut(&mut self) -> &mut LineState {
215 &mut self.state
216 }
217
218 fn line_focus(&self) -> &FocusHandle {
219 &self.focus
220 }
221
222 fn line_masked(&self) -> bool {
223 self.password
224 }
225
226 fn line_read_only(&self) -> bool {
227 self.read_only || self.disabled
228 }
229
230 fn line_max_length(&self) -> Option<usize> {
231 self.max_length
232 }
233
234 fn line_changed(&mut self, cx: &mut Context<Self>) {
235 cx.emit(TextInputEvent::Change(self.edit.text()));
236 cx.notify();
237 }
238}
239
240line::line_input_handler!(TextInput);
241line::line_focus_builders!(TextInput);
242
243impl Render for TextInput {
244 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
245 let t = theme(cx);
246 let (height, pad_x, font) = control_metrics(self.size);
247 let radius = t.radius(self.radius.unwrap_or(t.default_radius));
248 let focused = self.focus.is_focused(window) && !self.disabled;
249 let has_error = self.error.is_some();
250
251 let border = if has_error {
252 t.color(ColorName::Red, 6)
253 } else if focused {
254 t.primary()
255 } else {
256 t.border()
257 }
258 .hsla();
259 let dimmed = t.dimmed().hsla();
260 let surface = t.surface().hsla();
261
262 let line = Line::new(cx.entity()).placeholder(self.placeholder.clone(), dimmed);
263
264 let field = line::wire(div().id("guise-textinput"), &self.focus, cx)
265 .on_key_down(cx.listener(Self::on_key))
266 .flex()
267 .items_center()
268 .w_full()
269 .overflow_hidden()
270 .h(px(height))
271 .px(px(pad_x))
272 .rounded(px(radius))
273 .border_1()
274 .border_color(border)
275 .bg(surface)
276 .text_size(px(font))
277 .line_height(px(font * 1.3))
278 .child(div().flex_1().min_w(px(0.0)).child(line));
279
280 let mut chrome = Field::new().child(if self.disabled {
281 field.opacity(0.6)
282 } else {
283 field
284 });
285 if let Some(label) = self.label.clone() {
286 chrome = chrome.label(label);
287 }
288 if let Some(error) = self.error.clone() {
289 chrome = chrome.error(error);
290 } else if let Some(description) = self.description.clone() {
291 chrome = chrome.description(description);
292 }
293 chrome.probe_any("TextInput")
294 }
295}