1use gpui::prelude::*;
20use gpui::{
21 div, px, App, Context, Entity, EventEmitter, FocusHandle, IntoElement, KeyDownEvent,
22 MouseButton, SharedString, Window,
23};
24
25use super::{apply_key, control_metrics, edit::TextEdit, Field, KeyOutcome};
26use crate::icon::{Icon, IconName};
27use crate::reactive::Signal;
28use crate::theme::{theme, ColorName, Size};
29
30#[derive(Debug, Clone)]
32pub enum PasswordInputEvent {
33 Change(String),
35 Submit(String),
37}
38
39pub struct PasswordInput {
42 edit: TextEdit,
43 focus: FocusHandle,
44 visible: bool,
45 placeholder: SharedString,
46 label: Option<SharedString>,
47 description: Option<SharedString>,
48 error: Option<SharedString>,
49 size: Size,
50 disabled: bool,
51}
52
53impl EventEmitter<PasswordInputEvent> for PasswordInput {}
54
55impl PasswordInput {
56 pub fn new(cx: &mut Context<Self>) -> Self {
57 PasswordInput {
58 edit: TextEdit::new(""),
59 focus: cx.focus_handle(),
60 visible: false,
61 placeholder: SharedString::default(),
62 label: None,
63 description: None,
64 error: None,
65 size: Size::Sm,
66 disabled: false,
67 }
68 }
69
70 pub fn value(mut self, value: &str) -> Self {
71 self.edit = TextEdit::new(value);
72 self
73 }
74
75 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
76 self.placeholder = placeholder.into();
77 self
78 }
79
80 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
81 self.label = Some(label.into());
82 self
83 }
84
85 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
86 self.description = Some(description.into());
87 self
88 }
89
90 pub fn error(mut self, error: impl Into<SharedString>) -> Self {
91 self.error = Some(error.into());
92 self
93 }
94
95 pub fn size(mut self, size: Size) -> Self {
96 self.size = size;
97 self
98 }
99
100 pub fn disabled(mut self, disabled: bool) -> Self {
101 self.disabled = disabled;
102 self
103 }
104
105 pub fn visible(mut self, visible: bool) -> Self {
107 self.visible = visible;
108 self
109 }
110
111 pub fn focus_handle(&self) -> FocusHandle {
113 self.focus.clone()
114 }
115
116 pub fn text(&self) -> String {
118 self.edit.text()
119 }
120
121 pub fn set_text(&mut self, value: &str, cx: &mut Context<Self>) {
123 self.edit = TextEdit::new(value);
124 cx.notify();
125 }
126
127 pub fn bind(entity: &Entity<PasswordInput>, signal: &Signal<String>, cx: &mut App) {
132 let initial = signal.get(cx);
133 entity.update(cx, |this, cx| {
134 if this.text() != initial {
135 this.set_text(&initial, cx);
136 }
137 });
138 let sink = signal.clone();
139 cx.subscribe(entity, move |_input, event: &PasswordInputEvent, cx| {
140 if let PasswordInputEvent::Change(text) = event {
141 sink.set_if_changed(cx, text.clone());
142 }
143 })
144 .detach();
145 let input = entity.downgrade();
146 cx.observe(signal.entity(), move |observed, cx| {
147 let value = observed.read(cx).clone();
148 input
149 .update(cx, |this, cx| {
150 if this.text() != value {
151 this.set_text(&value, cx);
152 }
153 })
154 .ok();
155 })
156 .detach();
157 }
158
159 fn on_key(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
160 if self.disabled {
161 return;
162 }
163 match apply_key(&mut self.edit, &event.keystroke) {
164 KeyOutcome::Submit => {
165 cx.emit(PasswordInputEvent::Submit(self.edit.text()));
166 cx.notify();
167 cx.stop_propagation();
168 }
169 KeyOutcome::Edited => {
170 cx.emit(PasswordInputEvent::Change(self.edit.text()));
171 cx.notify();
172 cx.stop_propagation();
173 }
174 KeyOutcome::Cancel | KeyOutcome::Pass => {}
176 }
177 }
178
179 fn mask(&self, s: String) -> String {
180 if self.visible {
181 s
182 } else {
183 "\u{2022}".repeat(s.chars().count())
184 }
185 }
186}
187
188impl Render for PasswordInput {
189 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
190 let t = theme(cx);
191 let (height, pad_x, font) = control_metrics(self.size);
192 let radius = t.radius(t.default_radius);
193 let focused = self.focus.is_focused(window) && !self.disabled;
194
195 let border = if self.error.is_some() {
196 t.color(ColorName::Red, 6)
197 } else if focused {
198 t.primary()
199 } else {
200 t.border()
201 }
202 .hsla();
203 let text_color = t.text().hsla();
204 let dimmed = t.dimmed().hsla();
205 let surface = t.surface().hsla();
206 let caret = t.primary().hsla();
207
208 let interior = if focused {
209 let (before, after) = self.edit.split();
210 div()
211 .flex()
212 .items_center()
213 .text_color(text_color)
214 .child(SharedString::from(self.mask(before)))
215 .child(div().w(px(1.0)).h(px(font * 1.15)).bg(caret))
216 .child(SharedString::from(self.mask(after)))
217 } else if self.edit.is_empty() {
218 div().text_color(dimmed).child(self.placeholder.clone())
219 } else {
220 div()
221 .text_color(text_color)
222 .child(SharedString::from(self.mask(self.edit.text())))
223 };
224
225 let eye_icon = if self.visible {
227 IconName::EyeOff
228 } else {
229 IconName::Eye
230 };
231 let eye = div()
232 .id("guise-password-eye")
233 .flex()
234 .items_center()
235 .justify_center()
236 .w(px(height - 16.0))
237 .h(px(height - 16.0))
238 .rounded(px(4.0))
239 .text_color(dimmed)
240 .cursor_pointer()
241 .hover(move |s| s.text_color(text_color))
242 .child(Icon::new(eye_icon).size(Size::Xs))
243 .on_click(cx.listener(|this, _ev, _window, cx| {
244 if !this.disabled {
245 this.visible = !this.visible;
246 cx.notify();
247 }
248 }));
249
250 let field = div()
251 .id("guise-passwordinput")
252 .track_focus(&self.focus)
253 .on_key_down(cx.listener(Self::on_key))
254 .on_mouse_down(
255 MouseButton::Left,
256 cx.listener(|this, _ev, window, cx| {
257 window.focus(&this.focus);
258 cx.notify();
259 }),
260 )
261 .flex()
262 .items_center()
263 .justify_between()
264 .gap(px(8.0))
265 .h(px(height))
266 .px(px(pad_x))
267 .rounded(px(radius))
268 .border_1()
269 .border_color(border)
270 .bg(surface)
271 .text_size(px(font))
272 .child(interior)
273 .child(eye);
274
275 let mut chrome = Field::new().child(if self.disabled {
276 field.opacity(0.6)
277 } else {
278 field
279 });
280 if let Some(label) = self.label.clone() {
281 chrome = chrome.label(label);
282 }
283 if let Some(error) = self.error.clone() {
284 chrome = chrome.error(error);
285 } else if let Some(description) = self.description.clone() {
286 chrome = chrome.description(description);
287 }
288 chrome
289 }
290}