1use std::ops::Range;
8
9use gpui::{
10 App, AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
11 ParentElement, Render, SharedString, Styled, Subscription, Window, div,
12 prelude::FluentBuilder as _, px,
13};
14use gpui_kit_assets::Icon;
15use gpui_kit_theme::{ActiveTheme, ControlSize, TypeScale};
16use unicode_segmentation::UnicodeSegmentation;
17
18use crate::controls::button::Button;
19use crate::controls::field::{FieldState, field_shell};
20use crate::controls::input::{TextInput, TextInputEvent};
21use crate::foundation::direction::{ActiveDirection, DirectionalExt};
22use crate::foundation::{Disableable, Ident, Sizable, text as foundation_text};
23use crate::strings::{ActiveStrings, StringKey};
24
25const DEFAULT_CODE_SLOTS: usize = 6;
26const MIN_CODE_SLOTS: usize = 1;
27const MAX_CODE_SLOTS: usize = 12;
28
29#[derive(Clone, PartialEq, Eq)]
31pub enum PasswordInputEvent {
32 Change(SharedString),
33 Submit,
34 Cancel,
35 BackspaceAtStart,
36 Focus,
37 Blur,
38}
39
40impl std::fmt::Debug for PasswordInputEvent {
41 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 match self {
43 Self::Change(_) => formatter
44 .debug_tuple("Change")
45 .field(&"[REDACTED]")
46 .finish(),
47 Self::Submit => formatter.write_str("Submit"),
48 Self::Cancel => formatter.write_str("Cancel"),
49 Self::BackspaceAtStart => formatter.write_str("BackspaceAtStart"),
50 Self::Focus => formatter.write_str("Focus"),
51 Self::Blur => formatter.write_str("Blur"),
52 }
53 }
54}
55
56impl EventEmitter<PasswordInputEvent> for PasswordInput {}
57
58pub struct PasswordInput {
64 ident: Ident,
65 field: Entity<TextInput>,
66 reveal_focus: FocusHandle,
67 placeholder: Option<SharedString>,
68 name: Option<SharedString>,
69 initial: Option<SharedString>,
70 size: ControlSize,
71 disabled: bool,
72 invalid: bool,
73 required: bool,
74 read_only: bool,
75 revealed: bool,
76 seeded: bool,
77 configured: bool,
78 _subscriptions: Vec<Subscription>,
79}
80
81impl std::fmt::Debug for PasswordInput {
82 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 formatter
84 .debug_struct("PasswordInput")
85 .field("ident", &self.ident)
86 .field("size", &self.size)
87 .field("disabled", &self.disabled)
88 .field("invalid", &self.invalid)
89 .field("required", &self.required)
90 .field("read_only", &self.read_only)
91 .field("revealed", &self.revealed)
92 .finish()
93 }
94}
95
96impl PasswordInput {
97 pub fn new(ident: impl Into<Ident>, window: &mut Window, cx: &mut Context<Self>) -> Self {
98 let ident = ident.into();
99 let field = cx.new(|cx| {
100 TextInput::new(ident.clone(), window, cx)
101 .secret(true)
102 .bare(true)
103 });
104 let subscription = cx.subscribe(&field, |_password, _field, event, cx| {
105 let event = match event {
106 TextInputEvent::Change(value) => PasswordInputEvent::Change(value.clone()),
107 TextInputEvent::Submit => PasswordInputEvent::Submit,
108 TextInputEvent::Cancel => PasswordInputEvent::Cancel,
109 TextInputEvent::BackspaceAtStart => PasswordInputEvent::BackspaceAtStart,
110 TextInputEvent::Focus => PasswordInputEvent::Focus,
111 TextInputEvent::Blur => PasswordInputEvent::Blur,
112 };
113 cx.emit(event);
114 });
115 Self {
116 ident,
117 field,
118 reveal_focus: cx.focus_handle(),
119 placeholder: None,
120 name: None,
121 initial: None,
122 size: ControlSize::Md,
123 disabled: false,
124 invalid: false,
125 required: false,
126 read_only: false,
127 revealed: false,
128 seeded: false,
129 configured: false,
130 _subscriptions: vec![subscription],
131 }
132 }
133
134 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
135 self.placeholder = Some(placeholder.into());
136 self
137 }
138
139 pub fn name(mut self, name: impl Into<SharedString>) -> Self {
142 self.name = Some(name.into());
143 self
144 }
145
146 pub fn text(mut self, text: impl Into<SharedString>) -> Self {
148 self.initial = Some(text.into());
149 self
150 }
151
152 pub fn invalid(mut self, invalid: bool) -> Self {
153 self.invalid = invalid;
154 self
155 }
156
157 pub fn required(mut self, required: bool) -> Self {
158 self.required = required;
159 self
160 }
161
162 pub fn read_only(mut self, read_only: bool) -> Self {
163 self.read_only = read_only;
164 self
165 }
166
167 pub fn value(&self, cx: &App) -> SharedString {
168 self.field.read(cx).value().clone()
169 }
170
171 pub fn is_revealed(&self) -> bool {
172 self.revealed
173 }
174
175 pub fn selected_range(&self, cx: &App) -> Range<usize> {
176 self.field.read(cx).selected_range()
177 }
178
179 pub fn set_value(&mut self, value: impl Into<SharedString>, cx: &mut Context<Self>) {
180 self.seeded = true;
181 self.field
182 .update(cx, |field, cx| field.set_value(value, cx));
183 }
184
185 pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
186 self.disabled = disabled;
187 self.field
188 .update(cx, |field, cx| field.set_disabled(disabled, cx));
189 cx.notify();
190 }
191
192 pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
193 self.read_only = read_only;
194 self.field
195 .update(cx, |field, cx| field.set_read_only(read_only, cx));
196 cx.notify();
197 }
198
199 pub fn set_invalid(&mut self, invalid: bool, cx: &mut Context<Self>) {
200 self.invalid = invalid;
201 self.field
202 .update(cx, |field, cx| field.set_invalid(invalid, cx));
203 cx.notify();
204 }
205
206 fn configure(&mut self, cx: &mut Context<Self>) {
207 if self.configured {
208 return;
209 }
210 self.configured = true;
211 let placeholder = self.placeholder.take();
212 let name = self.name.take();
213 let initial = self.initial.take().filter(|_| !self.seeded);
214 self.seeded = true;
215 let (disabled, invalid, required, read_only, size) = (
216 self.disabled,
217 self.invalid,
218 self.required,
219 self.read_only,
220 self.size,
221 );
222 self.field.update(cx, move |field, cx| {
223 if let Some(placeholder) = placeholder {
224 field.set_placeholder(placeholder, cx);
225 }
226 if let Some(name) = name {
227 field.set_name(name, cx);
228 }
229 if let Some(initial) = initial {
230 field.set_text_quietly(initial, cx);
231 }
232 field.set_disabled(disabled, cx);
233 field.set_invalid(invalid, cx);
234 field.set_required(required, cx);
235 field.set_read_only(read_only, cx);
236 field.set_control_size(size, cx);
237 });
238 }
239
240 fn toggle_reveal(&mut self, cx: &mut Context<Self>) {
241 if self.disabled {
242 return;
243 }
244 self.revealed = !self.revealed;
245 let masked = !self.revealed;
246 self.field
247 .update(cx, |field, cx| field.set_visually_masked(masked, cx));
248 cx.notify();
249 }
250}
251
252impl Disableable for PasswordInput {
253 fn disabled(mut self, disabled: bool) -> Self {
254 self.disabled = disabled;
255 self
256 }
257}
258
259impl Sizable for PasswordInput {
260 fn control_size(mut self, size: ControlSize) -> Self {
261 self.size = size;
262 self
263 }
264}
265
266impl Focusable for PasswordInput {
267 fn focus_handle(&self, cx: &App) -> FocusHandle {
268 self.field.read(cx).focus_handle(cx)
269 }
270}
271
272impl Render for PasswordInput {
273 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
274 self.configure(cx);
275 let theme = cx.theme().clone();
276 let focused = self.field.read(cx).focus_handle(cx).is_focused(window);
277 let name = cx.strings().text(if self.revealed {
278 StringKey::PasswordConceal
279 } else {
280 StringKey::PasswordReveal
281 });
282 let control = cx.entity().downgrade();
283 let mut reveal = Button::new(self.ident.child("reveal"))
284 .ghost()
285 .icon_only(Icon::Key, name)
286 .checked_state(self.revealed)
287 .control_size(self.size)
288 .semantic_parent(self.ident.semantic_id())
289 .disabled(self.disabled);
290 if !self.disabled {
291 reveal = reveal
292 .track_focus(&self.reveal_focus)
293 .on_click(move |_, cx| {
294 control
295 .update(cx, |password, cx| password.toggle_reveal(cx))
296 .ok();
297 });
298 }
299
300 field_shell(
301 &theme,
302 self.size,
303 FieldState::default()
304 .focused(focused)
305 .invalid(self.invalid)
306 .disabled(self.disabled),
307 )
308 .child(div().flex_1().min_w_0().child(self.field.clone()))
309 .child(reveal)
310 }
311}
312
313#[derive(Clone, PartialEq, Eq)]
315pub enum OneTimeCodeInputEvent {
316 Change(SharedString),
317 Submit,
318}
319
320impl std::fmt::Debug for OneTimeCodeInputEvent {
321 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322 match self {
323 Self::Change(_) => formatter
324 .debug_tuple("Change")
325 .field(&"[REDACTED]")
326 .finish(),
327 Self::Submit => formatter.write_str("Submit"),
328 }
329 }
330}
331
332impl EventEmitter<OneTimeCodeInputEvent> for OneTimeCodeInput {}
333
334pub struct OneTimeCodeInput {
340 ident: Ident,
341 field: Entity<TextInput>,
342 name: Option<SharedString>,
343 initial: Option<SharedString>,
344 slots: usize,
345 size: ControlSize,
346 disabled: bool,
347 invalid: bool,
348 required: bool,
349 read_only: bool,
350 seeded: bool,
351 configured: bool,
352 _subscriptions: Vec<Subscription>,
353}
354
355impl std::fmt::Debug for OneTimeCodeInput {
356 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
357 formatter
358 .debug_struct("OneTimeCodeInput")
359 .field("ident", &self.ident)
360 .field("slots", &self.slots)
361 .field("disabled", &self.disabled)
362 .field("invalid", &self.invalid)
363 .field("required", &self.required)
364 .field("read_only", &self.read_only)
365 .finish()
366 }
367}
368
369impl OneTimeCodeInput {
370 pub fn new(ident: impl Into<Ident>, window: &mut Window, cx: &mut Context<Self>) -> Self {
371 let ident = ident.into();
372 let field = cx.new(|cx| {
373 TextInput::new(ident.clone(), window, cx)
374 .secret(true)
375 .bare(true)
376 });
377 let subscription = cx.subscribe(&field, |_code, _field, event, cx| match event {
378 TextInputEvent::Change(value) => {
379 cx.emit(OneTimeCodeInputEvent::Change(value.clone()));
380 }
381 TextInputEvent::Submit => cx.emit(OneTimeCodeInputEvent::Submit),
382 _ => {}
383 });
384 Self {
385 ident,
386 field,
387 name: None,
388 initial: None,
389 slots: DEFAULT_CODE_SLOTS,
390 size: ControlSize::Md,
391 disabled: false,
392 invalid: false,
393 required: false,
394 read_only: false,
395 seeded: false,
396 configured: false,
397 _subscriptions: vec![subscription],
398 }
399 }
400
401 pub fn name(mut self, name: impl Into<SharedString>) -> Self {
404 self.name = Some(name.into());
405 self
406 }
407
408 pub fn text(mut self, text: impl Into<SharedString>) -> Self {
410 self.initial = Some(text.into());
411 self
412 }
413
414 pub fn slots(mut self, slots: usize) -> Self {
417 self.slots = slots.clamp(MIN_CODE_SLOTS, MAX_CODE_SLOTS);
418 self
419 }
420
421 pub fn invalid(mut self, invalid: bool) -> Self {
422 self.invalid = invalid;
423 self
424 }
425
426 pub fn required(mut self, required: bool) -> Self {
427 self.required = required;
428 self
429 }
430
431 pub fn read_only(mut self, read_only: bool) -> Self {
432 self.read_only = read_only;
433 self
434 }
435
436 pub fn value(&self, cx: &App) -> SharedString {
437 self.field.read(cx).value().clone()
438 }
439
440 pub fn len(&self, cx: &App) -> usize {
441 self.field.read(cx).value().graphemes(true).count()
442 }
443
444 pub fn is_empty(&self, cx: &App) -> bool {
445 self.field.read(cx).is_empty()
446 }
447
448 pub fn is_complete(&self, cx: &App) -> bool {
449 self.len(cx) == self.slots
450 }
451
452 pub fn slot_count(&self) -> usize {
453 self.slots
454 }
455
456 pub fn set_value(&mut self, value: impl Into<SharedString>, cx: &mut Context<Self>) {
457 self.seeded = true;
458 let value = value.into();
459 let value = value.graphemes(true).take(self.slots).collect::<String>();
460 self.field
461 .update(cx, |field, cx| field.set_value(value, cx));
462 }
463
464 pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
465 self.disabled = disabled;
466 self.field
467 .update(cx, |field, cx| field.set_disabled(disabled, cx));
468 cx.notify();
469 }
470
471 pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
472 self.read_only = read_only;
473 self.field
474 .update(cx, |field, cx| field.set_read_only(read_only, cx));
475 cx.notify();
476 }
477
478 pub fn set_invalid(&mut self, invalid: bool, cx: &mut Context<Self>) {
479 self.invalid = invalid;
480 self.field
481 .update(cx, |field, cx| field.set_invalid(invalid, cx));
482 cx.notify();
483 }
484
485 fn configure(&mut self, cx: &mut Context<Self>) {
486 if self.configured {
487 return;
488 }
489 self.configured = true;
490 let name = self.name.take();
491 let initial = self
492 .initial
493 .take()
494 .filter(|_| !self.seeded)
495 .map(|value| value.graphemes(true).take(self.slots).collect::<String>());
496 self.seeded = true;
497 let (slots, disabled, invalid, required, read_only, size) = (
498 self.slots,
499 self.disabled,
500 self.invalid,
501 self.required,
502 self.read_only,
503 self.size,
504 );
505 self.field.update(cx, move |field, cx| {
506 field.set_sensitive_slots(slots, cx);
507 if let Some(name) = name {
508 field.set_name(name, cx);
509 }
510 if let Some(initial) = initial {
511 field.set_text_quietly(initial, cx);
512 }
513 field.set_disabled(disabled, cx);
514 field.set_invalid(invalid, cx);
515 field.set_required(required, cx);
516 field.set_read_only(read_only, cx);
517 field.set_control_size(size, cx);
518 });
519 }
520}
521
522impl Disableable for OneTimeCodeInput {
523 fn disabled(mut self, disabled: bool) -> Self {
524 self.disabled = disabled;
525 self
526 }
527}
528
529impl Sizable for OneTimeCodeInput {
530 fn control_size(mut self, size: ControlSize) -> Self {
531 self.size = size;
532 self
533 }
534}
535
536impl Focusable for OneTimeCodeInput {
537 fn focus_handle(&self, cx: &App) -> FocusHandle {
538 self.field.read(cx).focus_handle(cx)
539 }
540}
541
542impl Render for OneTimeCodeInput {
543 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
544 self.configure(cx);
545 let theme = cx.theme().clone();
546 let metrics = theme.control.get(self.size);
547 let (focused, value, selection, cursor) = {
548 let field = self.field.read(cx);
549 (
550 field.focus_handle(cx).is_focused(window),
551 field.value().clone(),
552 field.selected_range(),
553 field.cursor_offset(),
554 )
555 };
556
557 let length = value.graphemes(true).count();
558 let selected_start = value[..selection.start].graphemes(true).count();
559 let selected_end = value[..selection.end].graphemes(true).count();
560 let cursor = value[..cursor].graphemes(true).count();
561 let direction = cx.layout_direction();
562 let slots = (0..self.slots).map(|index| {
563 let selected = selected_start <= index && index < selected_end;
564 let active = focused && selection.is_empty() && cursor == index;
565 div()
566 .flex_1()
567 .h(px(metrics.height))
568 .flex()
569 .items_center()
570 .justify_center()
571 .when(index > 0, |slot| {
572 slot.border_s(direction, px(theme.borders.hairline))
573 .border_color(theme.colors.hairline)
574 })
575 .when(selected, |slot| slot.bg(theme.colors.selected))
576 .when(active, |slot| slot.bg(theme.colors.hover))
577 .child(foundation_text(
578 &theme,
579 TypeScale::Label,
580 if index < length { "•" } else { "" },
581 ))
582 });
583
584 field_shell(
585 &theme,
586 self.size,
587 FieldState::default()
588 .focused(focused)
589 .invalid(self.invalid)
590 .disabled(self.disabled),
591 )
592 .relative()
593 .px(px(0.0))
594 .child(
595 div()
596 .row_reading(direction)
597 .w_full()
598 .overflow_hidden()
599 .children(slots),
600 )
601 .child(div().absolute().inset_0().child(self.field.clone()))
605 }
606}