1use gpui::prelude::*;
20use gpui::{
21 div, px, App, Context, Entity, EventEmitter, FocusHandle, IntoElement, KeyDownEvent,
22 SharedString, Window,
23};
24
25use super::line::{self, Line, LineEditor, LineState};
26use super::{control_metrics, edit::TextEdit, Field, KeyOutcome};
27use crate::devtools::ProbedAny;
28use crate::icon::{Icon, IconName};
29use crate::reactive::Signal;
30use crate::theme::{theme, ColorName, Size};
31
32#[derive(Debug, Clone)]
34pub enum PasswordInputEvent {
35 Change(String),
37 Submit(String),
39}
40
41pub struct PasswordInput {
44 edit: TextEdit,
45 state: LineState,
46 focus: FocusHandle,
47 visible: bool,
48 placeholder: SharedString,
49 label: Option<SharedString>,
50 description: Option<SharedString>,
51 error: Option<SharedString>,
52 size: Size,
53 disabled: bool,
54 read_only: bool,
55 max_length: Option<usize>,
56}
57
58impl EventEmitter<PasswordInputEvent> for PasswordInput {}
59
60impl PasswordInput {
61 pub fn new(cx: &mut Context<Self>) -> Self {
62 PasswordInput {
63 edit: TextEdit::new(""),
64 state: LineState::new(),
65 focus: cx.focus_handle().tab_stop(true),
66 visible: false,
67 placeholder: SharedString::default(),
68 label: None,
69 description: None,
70 error: None,
71 size: Size::Sm,
72 disabled: false,
73 read_only: false,
74 max_length: None,
75 }
76 }
77
78 pub fn value(mut self, value: &str) -> Self {
79 self.edit = TextEdit::new(value);
80 self
81 }
82
83 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
84 self.placeholder = placeholder.into();
85 self
86 }
87
88 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
89 self.label = Some(label.into());
90 self
91 }
92
93 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
94 self.description = Some(description.into());
95 self
96 }
97
98 pub fn error(mut self, error: impl Into<SharedString>) -> Self {
99 self.error = Some(error.into());
100 self
101 }
102
103 pub fn size(mut self, size: Size) -> Self {
104 self.size = size;
105 self
106 }
107
108 pub fn disabled(mut self, disabled: bool) -> Self {
109 self.disabled = disabled;
110 self
111 }
112
113 pub fn visible(mut self, visible: bool) -> Self {
115 self.visible = visible;
116 self
117 }
118
119 pub fn read_only(mut self, read_only: bool) -> Self {
121 self.read_only = read_only;
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 bind(entity: &Entity<PasswordInput>, signal: &Signal<String>, cx: &mut App) {
147 let initial = signal.get(cx);
148 entity.update(cx, |this, cx| {
149 if this.text() != initial {
150 this.set_text(&initial, cx);
151 }
152 });
153 let sink = signal.clone();
154 cx.subscribe(entity, move |_input, event: &PasswordInputEvent, cx| {
155 if let PasswordInputEvent::Change(text) = event {
156 sink.set_if_changed(cx, text.clone());
157 }
158 })
159 .detach();
160 let input = entity.downgrade();
161 cx.observe(signal.entity(), move |observed, cx| {
162 let value = observed.read(cx).clone();
163 input
164 .update(cx, |this, cx| {
165 if this.text() != value {
166 this.set_text(&value, cx);
167 }
168 })
169 .ok();
170 })
171 .detach();
172 }
173
174 fn on_key(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
175 if self.disabled {
176 return;
177 }
178 match line::keys(self, event, window, cx) {
179 KeyOutcome::Submit => {
180 cx.emit(PasswordInputEvent::Submit(self.edit.text()));
181 cx.notify();
182 cx.stop_propagation();
183 }
184 KeyOutcome::Edited => {
185 self.line_changed(cx);
186 cx.stop_propagation();
187 }
188 KeyOutcome::Cancel | KeyOutcome::Pass => {}
190 }
191 }
192}
193
194impl LineEditor for PasswordInput {
195 fn edit(&self) -> &TextEdit {
196 &self.edit
197 }
198
199 fn edit_mut(&mut self) -> &mut TextEdit {
200 &mut self.edit
201 }
202
203 fn line(&self) -> &LineState {
204 &self.state
205 }
206
207 fn line_mut(&mut self) -> &mut LineState {
208 &mut self.state
209 }
210
211 fn line_focus(&self) -> &FocusHandle {
212 &self.focus
213 }
214
215 fn line_masked(&self) -> bool {
218 !self.visible
219 }
220
221 fn line_read_only(&self) -> bool {
222 self.read_only || self.disabled
223 }
224
225 fn line_max_length(&self) -> Option<usize> {
226 self.max_length
227 }
228
229 fn line_changed(&mut self, cx: &mut Context<Self>) {
230 cx.emit(PasswordInputEvent::Change(self.edit.text()));
231 cx.notify();
232 }
233}
234
235line::line_input_handler!(PasswordInput);
236line::line_focus_builders!(PasswordInput);
237
238impl Render for PasswordInput {
239 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
240 let t = theme(cx);
241 let (height, pad_x, font) = control_metrics(self.size);
242 let radius = t.radius(t.default_radius);
243 let focused = self.focus.is_focused(window) && !self.disabled;
244
245 let border = if self.error.is_some() {
246 t.color(ColorName::Red, 6)
247 } else if focused {
248 t.primary()
249 } else {
250 t.border()
251 }
252 .hsla();
253 let text_color = t.text().hsla();
254 let dimmed = t.dimmed().hsla();
255 let surface = t.surface().hsla();
256 let interior = Line::new(cx.entity()).placeholder(self.placeholder.clone(), dimmed);
257
258 let eye_icon = if self.visible {
260 IconName::EyeOff
261 } else {
262 IconName::Eye
263 };
264 let eye = div()
265 .id("guise-password-eye")
266 .flex()
267 .items_center()
268 .justify_center()
269 .w(px(height - 16.0))
270 .h(px(height - 16.0))
271 .rounded(px(4.0))
272 .text_color(dimmed)
273 .cursor_pointer()
274 .hover(move |s| s.text_color(text_color))
275 .child(Icon::new(eye_icon).size(Size::Xs))
276 .on_click(cx.listener(|this, _ev, _window, cx| {
277 if !this.disabled {
278 this.visible = !this.visible;
279 cx.notify();
280 }
281 }));
282
283 let field = line::wire(div().id("guise-passwordinput"), &self.focus, cx)
284 .on_key_down(cx.listener(Self::on_key))
285 .flex()
286 .items_center()
287 .justify_between()
288 .gap(px(8.0))
289 .w_full()
290 .overflow_hidden()
291 .h(px(height))
292 .px(px(pad_x))
293 .rounded(px(radius))
294 .border_1()
295 .border_color(border)
296 .bg(surface)
297 .text_size(px(font))
298 .line_height(px(font * 1.3))
299 .child(div().flex_1().min_w(px(0.0)).child(interior))
300 .child(eye);
301
302 let mut chrome = Field::new().child(if self.disabled {
303 field.opacity(0.6)
304 } else {
305 field
306 });
307 if let Some(label) = self.label.clone() {
308 chrome = chrome.label(label);
309 }
310 if let Some(error) = self.error.clone() {
311 chrome = chrome.error(error);
312 } else if let Some(description) = self.description.clone() {
313 chrome = chrome.description(description);
314 }
315 chrome.probe_any("PasswordInput")
316 }
317}