1use std::time::Duration;
32
33use gpui::prelude::*;
34use gpui::*;
35
36#[cfg(feature = "secure-password")]
37use secrecy::SecretString;
38
39use crate::theme::{get_theme_or, Theme};
40use super::cursor_blink::CursorBlink;
41use super::editing_core::EditingCore;
42use super::focus_navigation::{FocusNext, FocusPrev, handle_tab_navigation, EnabledCursorExt};
43use super::text_input::{
44 MoveLeft, MoveRight, MoveWordLeft, MoveWordRight, MoveToStart, MoveToEnd,
45 SelectLeft, SelectRight, SelectWordLeft, SelectWordRight, SelectToStart, SelectToEnd, SelectAll,
46 DeleteBackward, DeleteForward, DeleteWordBackward, DeleteWordForward,
47 Cut, Copy, Paste, Enter, Escape,
48};
49
50#[cfg(feature = "secure-password")]
51use super::sensitive_string::SensitiveString;
52
53#[derive(Debug, Clone)]
55pub enum PasswordInputEvent {
56 #[cfg(feature = "secure-password")]
58 Change(SecretString),
59 #[cfg(not(feature = "secure-password"))]
61 Change(String),
62 Enter,
64 Blur,
66}
67
68const MASK_CHAR: &str = "\u{25CF}"; pub struct PasswordInput {
82 #[cfg(feature = "secure-password")]
84 core: EditingCore<SensitiveString>,
85 #[cfg(not(feature = "secure-password"))]
86 core: EditingCore<String>,
87 input_focus_handle: FocusHandle,
89 toggle_focus_handle: FocusHandle,
91 show_password: bool,
93 placeholder: Option<SharedString>,
95 custom_theme: Option<Theme>,
97 scroll_offset: f32,
99 visible_width: f32,
101 content_origin_x: f32,
103 was_focused: bool,
105 focus_out_subscribed: bool,
107 cursor_blink: CursorBlink,
109 blink_timer_active: bool,
111 is_dragging: bool,
113 auto_scroll_active: bool,
115 auto_scroll_speed: f32,
117 pending_placeholder: Option<SharedString>,
119 pending_value: Option<String>,
121 enabled: bool,
123}
124
125impl EventEmitter<PasswordInputEvent> for PasswordInput {}
126
127impl Focusable for PasswordInput {
128 fn focus_handle(&self, _cx: &App) -> FocusHandle {
129 self.input_focus_handle.clone()
130 }
131}
132
133impl PasswordInput {
134 pub fn new(cx: &mut Context<Self>) -> Self {
136 #[cfg(feature = "secure-password")]
137 let core = EditingCore::<SensitiveString>::new().with_masked(true);
138 #[cfg(not(feature = "secure-password"))]
139 let core = EditingCore::<String>::new().with_masked(true);
140
141 Self {
142 core,
143 input_focus_handle: cx.focus_handle().tab_stop(true),
144 toggle_focus_handle: cx.focus_handle().tab_stop(true),
145 show_password: false,
146 placeholder: None,
147 custom_theme: None,
148 scroll_offset: 0.0,
149 visible_width: 200.0,
150 content_origin_x: 0.0,
151 was_focused: false,
152 focus_out_subscribed: false,
153 cursor_blink: CursorBlink::new(),
154 blink_timer_active: false,
155 is_dragging: false,
156 auto_scroll_active: false,
157 auto_scroll_speed: 0.0,
158 pending_placeholder: None,
159 pending_value: None,
160 enabled: true,
161 }
162 }
163
164 #[must_use]
166 pub fn placeholder(mut self, text: impl Into<SharedString>) -> Self {
167 self.pending_placeholder = Some(text.into());
168 self
169 }
170
171 #[must_use]
173 pub fn with_value(mut self, value: impl Into<String>) -> Self {
174 self.pending_value = Some(value.into());
175 self
176 }
177
178 #[must_use]
180 pub fn theme(mut self, theme: Theme) -> Self {
181 self.custom_theme = Some(theme);
182 self
183 }
184
185 #[must_use]
187 pub fn with_enabled(mut self, enabled: bool) -> Self {
188 self.enabled = enabled;
189 self
190 }
191
192 fn apply_pending(&mut self) {
194 if let Some(placeholder) = self.pending_placeholder.take() {
195 self.placeholder = Some(placeholder);
196 }
197 if let Some(value) = self.pending_value.take() {
198 self.core.set_content(&value);
199 }
200 }
201
202 #[cfg(feature = "secure-password")]
204 pub fn value(&self, _cx: &App) -> SecretString {
205 self.create_secret_from_content()
209 }
210
211 #[cfg(not(feature = "secure-password"))]
213 pub fn value<'a>(&'a self, _cx: &'a App) -> &'a str {
214 self.core.content()
215 }
216
217 #[cfg(feature = "secure-password")]
219 fn create_secret_from_content(&self) -> SecretString {
220 SecretString::from(self.core.content().to_string())
221 }
222
223 pub fn set_value(&mut self, value: &str, cx: &mut Context<Self>) {
225 self.core.set_content(value);
226 self.scroll_offset = 0.0;
227 self.emit_change(cx);
228 cx.notify();
229 }
230
231 #[cfg(feature = "secure-password")]
233 pub fn set_value_secret(&mut self, secret: &SecretString, cx: &mut Context<Self>) {
234 use secrecy::ExposeSecret;
235 self.core.set_content(secret.expose_secret());
236 self.scroll_offset = 0.0;
237 self.emit_change(cx);
238 cx.notify();
239 }
240
241 pub fn focus_handle(&self, _cx: &App) -> FocusHandle {
243 self.input_focus_handle.clone()
244 }
245
246 pub fn is_password_visible(&self) -> bool {
248 self.show_password
249 }
250
251 pub fn is_enabled(&self) -> bool {
253 self.enabled
254 }
255
256 pub fn set_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
258 if self.enabled != enabled {
259 self.enabled = enabled;
260 cx.notify();
261 }
262 }
263
264 fn emit_change(&self, cx: &mut Context<Self>) {
265 #[cfg(feature = "secure-password")]
266 cx.emit(PasswordInputEvent::Change(self.create_secret_from_content()));
267 #[cfg(not(feature = "secure-password"))]
268 cx.emit(PasswordInputEvent::Change(self.core.content().to_string()));
269 }
270
271 fn toggle_visibility(&mut self, cx: &mut Context<Self>) {
272 self.show_password = !self.show_password;
273 self.core.set_masked(!self.show_password);
274 cx.notify();
275 }
276
277 fn display_content(&self) -> String {
279 if self.core.is_masked() {
280 MASK_CHAR.repeat(self.core.content().chars().count())
281 } else {
282 self.core.content().to_string()
283 }
284 }
285
286 fn content_byte_to_display_byte(&self, content_pos: usize) -> usize {
288 if !self.core.is_masked() || self.core.content().is_empty() {
289 return content_pos;
290 }
291 let char_count = self.core.content()[..content_pos].chars().count();
292 char_count * MASK_CHAR.len()
293 }
294
295 fn display_byte_to_content_byte(&self, display_pos: usize) -> usize {
297 if !self.core.is_masked() || self.core.content().is_empty() {
298 return display_pos;
299 }
300 let mask_char_len = MASK_CHAR.len();
301 let char_index = display_pos / mask_char_len;
302 self.core.content()
303 .char_indices()
304 .nth(char_index)
305 .map(|(i, _)| i)
306 .unwrap_or(self.core.content().len())
307 }
308
309 fn reset_cursor_blink(&mut self) {
310 self.cursor_blink.reset();
311 }
312
313 fn shape_line(&self, window: &Window) -> Option<ShapedLine> {
314 let display = self.display_content();
315 if display.is_empty() {
316 return None;
317 }
318
319 let style = window.text_style();
320 let font_size = window.rem_size() * 0.875;
321
322 let run = TextRun {
323 len: display.len(),
324 font: style.font(),
325 color: style.color,
326 background_color: None,
327 underline: None,
328 strikethrough: None,
329 };
330
331 Some(window.text_system().shape_line(
332 SharedString::from(display),
333 font_size,
334 &[run],
335 None,
336 ))
337 }
338
339 fn cursor_at_x(&self, x: f32, window: &Window) -> usize {
340 let adjusted_x = x + self.scroll_offset;
341
342 if adjusted_x <= 0.0 || self.core.content().is_empty() {
343 return 0;
344 }
345
346 if let Some(line) = self.shape_line(window) {
347 let display_pos = line.closest_index_for_x(px(adjusted_x));
348 self.display_byte_to_content_byte(display_pos)
349 } else {
350 0
351 }
352 }
353
354 fn x_for_cursor(&self, cursor: usize, window: &Window) -> f32 {
355 if self.core.content().is_empty() || cursor == 0 {
356 return 0.0;
357 }
358
359 if let Some(line) = self.shape_line(window) {
360 let display_cursor = self.content_byte_to_display_byte(cursor);
361 let pixels = line.x_for_index(display_cursor);
362 pixels.into()
363 } else {
364 0.0
365 }
366 }
367
368 fn ensure_cursor_visible(&mut self, window: &Window) {
369 let cursor_x = self.x_for_cursor(self.core.cursor(), window);
370 let content_width = self.x_for_cursor(self.core.content().len(), window);
371 let margin = 2.0;
372 let cursor_width = 1.0;
373 let padding = 2.0;
374
375 let actual_visible = self.visible_width - padding;
376
377 if content_width <= actual_visible {
378 self.scroll_offset = 0.0;
379 return;
380 }
381
382 let visual_cursor_x = cursor_x - self.scroll_offset;
383
384 if visual_cursor_x + cursor_width > actual_visible - margin {
385 self.scroll_offset = cursor_x + cursor_width - actual_visible + margin;
386 }
387
388 if visual_cursor_x < margin {
389 self.scroll_offset = (cursor_x - margin).max(0.0);
390 }
391
392 let max_scroll = (content_width + cursor_width - actual_visible + margin).max(0.0);
393 self.scroll_offset = self.scroll_offset.clamp(0.0, max_scroll);
394 }
395
396 fn handle_drag_move(&mut self, mouse_x: f32, window: &Window) -> f32 {
397 if !self.is_dragging {
398 return 0.0;
399 }
400
401 let relative_x = mouse_x - self.content_origin_x;
402 let padding = 2.0;
403 let actual_visible = self.visible_width - padding;
404
405 let scroll_speed = if relative_x < 0.0 {
406 -self.calculate_scroll_speed(-relative_x)
407 } else if relative_x > actual_visible {
408 self.calculate_scroll_speed(relative_x - actual_visible)
409 } else {
410 0.0
411 };
412
413 let clamped_x = relative_x.clamp(0.0, actual_visible);
414 let new_cursor = self.cursor_at_x(clamped_x, window);
415 self.core.extend_selection_to(new_cursor);
416 self.reset_cursor_blink();
417
418 scroll_speed
419 }
420
421 fn calculate_scroll_speed(&self, distance: f32) -> f32 {
422 let base_speed = 0.5;
423 let max_speed = 20.0;
424 let max_distance = 100.0;
425
426 let normalized = (distance / max_distance).min(1.0);
427 let eased = 1.0 - (1.0 - normalized).powi(2);
428 base_speed + eased * (max_speed - base_speed)
429 }
430
431 fn apply_auto_scroll(&mut self, window: &Window) {
432 if self.auto_scroll_speed == 0.0 {
433 return;
434 }
435
436 let content_width = self.x_for_cursor(self.core.content().len(), window);
437 let padding = 2.0;
438 let actual_visible = self.visible_width - padding;
439 let max_scroll = (content_width - actual_visible).max(0.0);
440
441 self.scroll_offset = (self.scroll_offset + self.auto_scroll_speed).clamp(0.0, max_scroll);
442
443 if self.auto_scroll_speed < 0.0 {
444 let new_cursor = self.cursor_at_x(0.0, window);
445 self.core.extend_selection_to(new_cursor);
446 } else {
447 let new_cursor = self.cursor_at_x(actual_visible, window);
448 self.core.extend_selection_to(new_cursor);
449 }
450 }
451
452 fn stop_drag(&mut self) {
453 self.is_dragging = false;
454 self.auto_scroll_active = false;
455 self.auto_scroll_speed = 0.0;
456 }
457
458 fn spawn_auto_scroll_timer_if_needed(&mut self, scroll_speed: f32, window: &mut Window, cx: &mut Context<Self>) {
459 self.auto_scroll_speed = scroll_speed;
460 if scroll_speed != 0.0 && !self.auto_scroll_active {
461 self.auto_scroll_active = true;
462 let entity = cx.entity();
463 window.spawn(cx, async move |async_cx| {
464 loop {
465 smol::Timer::after(Duration::from_millis(32)).await;
466 let should_continue = async_cx
467 .update_entity(&entity, |this, cx| {
468 if !this.auto_scroll_active || !this.is_dragging {
469 this.auto_scroll_active = false;
470 return false;
471 }
472 cx.notify();
473 true
474 })
475 .unwrap_or(false);
476 if !should_continue {
477 break;
478 }
479 }
480 }).detach();
481 }
482 }
483
484 fn on_focus(&mut self, cx: &mut Context<Self>) {
485 self.reset_cursor_blink();
486 cx.notify();
487 }
488
489 fn on_blur(&mut self, cx: &mut Context<Self>) {
490 self.stop_drag();
491 cx.emit(PasswordInputEvent::Blur);
492 cx.notify();
493 }
494
495 fn cut(&mut self, cx: &mut Context<Self>) {
497 if self.core.delete_selection() {
499 self.reset_cursor_blink();
500 self.emit_change(cx);
501 cx.notify();
502 }
503 }
504
505 fn paste(&mut self, cx: &mut Context<Self>) {
506 if let Some(clipboard) = cx.read_from_clipboard() {
507 if let Some(text) = clipboard.text() {
508 let clean_text = text.replace(['\n', '\r'], "");
509 self.core.insert_text(&clean_text);
510 self.reset_cursor_blink();
511 self.emit_change(cx);
512 cx.notify();
513 }
514 }
515 }
516
517 fn handle_move_left(&mut self, cx: &mut Context<Self>) {
519 self.core.move_left();
520 self.reset_cursor_blink();
521 cx.notify();
522 }
523
524 fn handle_move_right(&mut self, cx: &mut Context<Self>) {
525 self.core.move_right();
526 self.reset_cursor_blink();
527 cx.notify();
528 }
529
530 fn handle_move_word_left(&mut self, cx: &mut Context<Self>) {
531 self.core.move_word_left();
532 self.reset_cursor_blink();
533 cx.notify();
534 }
535
536 fn handle_move_word_right(&mut self, cx: &mut Context<Self>) {
537 self.core.move_word_right();
538 self.reset_cursor_blink();
539 cx.notify();
540 }
541
542 fn handle_move_to_start(&mut self, cx: &mut Context<Self>) {
543 self.core.move_to_start();
544 self.reset_cursor_blink();
545 cx.notify();
546 }
547
548 fn handle_move_to_end(&mut self, cx: &mut Context<Self>) {
549 self.core.move_to_end();
550 self.reset_cursor_blink();
551 cx.notify();
552 }
553
554 fn handle_select_left(&mut self, cx: &mut Context<Self>) {
555 self.core.select_left();
556 self.reset_cursor_blink();
557 cx.notify();
558 }
559
560 fn handle_select_right(&mut self, cx: &mut Context<Self>) {
561 self.core.select_right();
562 self.reset_cursor_blink();
563 cx.notify();
564 }
565
566 fn handle_select_word_left(&mut self, cx: &mut Context<Self>) {
567 self.core.select_word_left();
568 self.reset_cursor_blink();
569 cx.notify();
570 }
571
572 fn handle_select_word_right(&mut self, cx: &mut Context<Self>) {
573 self.core.select_word_right();
574 self.reset_cursor_blink();
575 cx.notify();
576 }
577
578 fn handle_select_to_start(&mut self, cx: &mut Context<Self>) {
579 self.core.select_to_start();
580 self.reset_cursor_blink();
581 cx.notify();
582 }
583
584 fn handle_select_to_end(&mut self, cx: &mut Context<Self>) {
585 self.core.select_to_end();
586 self.reset_cursor_blink();
587 cx.notify();
588 }
589
590 fn handle_select_all(&mut self, cx: &mut Context<Self>) {
591 self.core.select_all();
592 self.reset_cursor_blink();
593 cx.notify();
594 }
595
596 fn handle_delete_backward(&mut self, cx: &mut Context<Self>) {
597 if self.core.delete_backward() {
598 self.reset_cursor_blink();
599 self.emit_change(cx);
600 }
601 cx.notify();
602 }
603
604 fn handle_delete_forward(&mut self, cx: &mut Context<Self>) {
605 if self.core.delete_forward() {
606 self.reset_cursor_blink();
607 self.emit_change(cx);
608 }
609 cx.notify();
610 }
611
612 fn handle_delete_word_backward(&mut self, cx: &mut Context<Self>) {
613 if self.core.delete_word_backward() {
614 self.reset_cursor_blink();
615 self.emit_change(cx);
616 }
617 cx.notify();
618 }
619
620 fn handle_delete_word_forward(&mut self, cx: &mut Context<Self>) {
621 if self.core.delete_word_forward() {
622 self.reset_cursor_blink();
623 self.emit_change(cx);
624 }
625 cx.notify();
626 }
627
628 fn handle_insert_text(&mut self, text: &str, cx: &mut Context<Self>) {
629 self.core.insert_text(text);
630 self.reset_cursor_blink();
631 self.emit_change(cx);
632 cx.notify();
633 }
634}
635
636impl Render for PasswordInput {
637 fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
638 self.apply_pending();
640
641 let theme = get_theme_or(cx, self.custom_theme.as_ref());
642 let enabled = self.enabled;
643 let input_focus_handle = self.input_focus_handle.clone().tab_stop(enabled);
644 let toggle_focus_handle = self.toggle_focus_handle.clone().tab_stop(enabled);
645 let input_is_focused = self.input_focus_handle.is_focused(window);
646 let toggle_is_focused = self.toggle_focus_handle.is_focused(window);
647 let either_focused = input_is_focused || toggle_is_focused;
648
649 if !self.focus_out_subscribed {
651 self.focus_out_subscribed = true;
652 let input_fh = self.input_focus_handle.clone();
653 cx.on_focus_out(&input_fh, window, |this: &mut Self, _event, window, cx| {
654 if !this.input_focus_handle.is_focused(window) && !this.toggle_focus_handle.is_focused(window) {
656 this.on_blur(cx);
657 }
658 }).detach();
659 }
660
661 if input_is_focused && !self.was_focused {
663 self.on_focus(cx);
664 }
665 self.was_focused = input_is_focused;
666
667 let render_scroll_offset = if input_is_focused { self.scroll_offset } else { 0.0 };
668
669 if input_is_focused && !self.blink_timer_active {
671 self.blink_timer_active = true;
672 let entity = cx.entity();
673 let blink_period = CursorBlink::blink_period();
674 window.spawn(cx, async move |async_cx| {
675 loop {
676 smol::Timer::after(blink_period).await;
677 let should_continue = async_cx
678 .update_entity(&entity, |this, cx| {
679 if !this.blink_timer_active {
680 return false;
681 }
682 cx.notify();
683 true
684 })
685 .unwrap_or(false);
686 if !should_continue {
687 break;
688 }
689 }
690 }).detach();
691 }
692
693 if !input_is_focused {
694 self.blink_timer_active = false;
695 }
696
697 if self.auto_scroll_active && self.is_dragging {
699 self.apply_auto_scroll(window);
700 }
701
702 if input_is_focused {
703 self.ensure_cursor_visible(window);
704 }
705
706 let display_content = self.display_content();
707 let has_content = !self.core.content().is_empty();
708 let placeholder = self.placeholder.clone();
709
710 let cursor = self.core.cursor();
711 let selection = self.core.selection();
712
713 let cursor_x = self.x_for_cursor(cursor, window) - render_scroll_offset;
714 let cursor_visible = input_is_focused && self.cursor_blink.is_visible();
715
716 let selection_bounds: Option<(f32, f32)> = if input_is_focused {
717 selection.and_then(|(start, end)| {
718 if start != end {
719 let start_x = self.x_for_cursor(start, window) - render_scroll_offset;
720 let end_x = self.x_for_cursor(end, window) - render_scroll_offset;
721 Some((start_x, end_x - start_x))
722 } else {
723 None
724 }
725 })
726 } else {
727 None
728 };
729
730 let scroll_offset = render_scroll_offset;
731
732 let bg_color = if enabled { theme.bg_input } else { theme.disabled_bg };
734 let border_color = if either_focused && enabled {
735 theme.border_focus
736 } else {
737 theme.border_input
738 };
739 let separator_color = theme.text_muted;
740 let button_text_color = if enabled { theme.text_muted } else { theme.disabled_text };
741 let selection_color = theme.selection;
742 let text_color = if enabled { theme.text_primary } else { theme.disabled_text };
743 let text_placeholder = theme.text_placeholder;
744
745 let eye_icon = if self.show_password { "\u{2296}" } else { "\u{25CE}" }; let separator = div()
750 .w(px(1.0))
751 .h_full()
752 .bg(rgb(separator_color));
753
754 div()
756 .id("ccf_password_input")
757 .flex()
758 .flex_row()
759 .items_center()
760 .h(px(28.0))
761 .bg(rgb(bg_color))
762 .border_1()
763 .border_color(rgb(border_color))
764 .rounded_md()
765 .overflow_hidden()
766 .child(
768 div()
769 .id("ccf_password_input_field")
770 .key_context("CcfTextInput")
771 .track_focus(&input_focus_handle)
772 .flex_1()
773 .h_full()
774 .px_2()
775 .when(enabled, |d| d.cursor_text())
776 .when(!enabled, |d| d.cursor_default())
777 .relative()
778 .overflow_hidden()
779 .on_action(cx.listener(|this, _: &MoveLeft, _window, cx| {
781 if !this.enabled { return; }
782 this.handle_move_left(cx);
783 }))
784 .on_action(cx.listener(|this, _: &MoveRight, _window, cx| {
785 if !this.enabled { return; }
786 this.handle_move_right(cx);
787 }))
788 .on_action(cx.listener(|this, _: &MoveWordLeft, _window, cx| {
789 if !this.enabled { return; }
790 this.handle_move_word_left(cx);
791 }))
792 .on_action(cx.listener(|this, _: &MoveWordRight, _window, cx| {
793 if !this.enabled { return; }
794 this.handle_move_word_right(cx);
795 }))
796 .on_action(cx.listener(|this, _: &MoveToStart, _window, cx| {
797 if !this.enabled { return; }
798 this.handle_move_to_start(cx);
799 }))
800 .on_action(cx.listener(|this, _: &MoveToEnd, _window, cx| {
801 if !this.enabled { return; }
802 this.handle_move_to_end(cx);
803 }))
804 .on_action(cx.listener(|this, _: &SelectLeft, _window, cx| {
806 if !this.enabled { return; }
807 this.handle_select_left(cx);
808 }))
809 .on_action(cx.listener(|this, _: &SelectRight, _window, cx| {
810 if !this.enabled { return; }
811 this.handle_select_right(cx);
812 }))
813 .on_action(cx.listener(|this, _: &SelectWordLeft, _window, cx| {
814 if !this.enabled { return; }
815 this.handle_select_word_left(cx);
816 }))
817 .on_action(cx.listener(|this, _: &SelectWordRight, _window, cx| {
818 if !this.enabled { return; }
819 this.handle_select_word_right(cx);
820 }))
821 .on_action(cx.listener(|this, _: &SelectToStart, _window, cx| {
822 if !this.enabled { return; }
823 this.handle_select_to_start(cx);
824 }))
825 .on_action(cx.listener(|this, _: &SelectToEnd, _window, cx| {
826 if !this.enabled { return; }
827 this.handle_select_to_end(cx);
828 }))
829 .on_action(cx.listener(|this, _: &SelectAll, _window, cx| {
830 if !this.enabled { return; }
831 this.handle_select_all(cx);
832 }))
833 .on_action(cx.listener(|this, _: &DeleteBackward, _window, cx| {
835 if !this.enabled { return; }
836 this.handle_delete_backward(cx);
837 }))
838 .on_action(cx.listener(|this, _: &DeleteForward, _window, cx| {
839 if !this.enabled { return; }
840 this.handle_delete_forward(cx);
841 }))
842 .on_action(cx.listener(|this, _: &DeleteWordBackward, _window, cx| {
843 if !this.enabled { return; }
844 this.handle_delete_word_backward(cx);
845 }))
846 .on_action(cx.listener(|this, _: &DeleteWordForward, _window, cx| {
847 if !this.enabled { return; }
848 this.handle_delete_word_forward(cx);
849 }))
850 .on_action(cx.listener(|this, _: &Cut, _window, cx| {
852 if !this.enabled { return; }
853 this.cut(cx);
854 }))
855 .on_action(cx.listener(|_this, _: &Copy, _window, _cx| {
856 }))
858 .on_action(cx.listener(|this, _: &Paste, _window, cx| {
859 if !this.enabled { return; }
860 this.paste(cx);
861 }))
862 .on_action(cx.listener(|this, _: &Enter, _window, cx| {
864 if !this.enabled { return; }
865 cx.emit(PasswordInputEvent::Enter);
866 }))
867 .on_action(cx.listener(|this, _: &Escape, _window, cx| {
868 if !this.enabled { return; }
869 this.on_blur(cx);
870 }))
871 .on_action(cx.listener(|_this, _: &FocusNext, window, _cx| {
873 window.focus_next();
874 }))
875 .on_action(cx.listener(|_this, _: &FocusPrev, window, _cx| {
876 window.focus_prev();
877 }))
878 .on_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| {
880 if handle_tab_navigation(event, window) {
881 return;
882 }
883 if !this.enabled { return; }
884 if !event.keystroke.modifiers.alt
885 && !event.keystroke.modifiers.control
886 && !event.keystroke.modifiers.platform
887 {
888 if let Some(ref ch) = event.keystroke.key_char {
889 this.handle_insert_text(ch, cx);
890 }
891 }
892 }))
893 .when(enabled, |d| d.on_mouse_down(MouseButton::Left, cx.listener(|this, event: &MouseDownEvent, window, cx| {
895 let was_focused = this.input_focus_handle.is_focused(window);
896 this.input_focus_handle.focus(window);
897
898 if !was_focused && this.core.selection().is_some() {
899 this.reset_cursor_blink();
900 cx.notify();
901 return;
902 }
903
904 let click_x: f32 = event.position.x.into();
905 let relative_x = (click_x - this.content_origin_x).max(0.0);
906 let new_cursor = this.cursor_at_x(relative_x, window);
907
908 if event.modifiers.shift {
909 this.core.start_selection_from_cursor();
910 this.core.extend_selection_to(new_cursor);
911 } else {
912 this.core.clear_selection();
913 this.core.set_cursor(new_cursor);
914 this.core.start_selection_from_cursor();
915 }
916
917 this.is_dragging = true;
918 this.reset_cursor_blink();
919 cx.notify();
920 })))
921 .when(enabled, |d| d.on_mouse_move(cx.listener(|this, event: &MouseMoveEvent, window, cx| {
923 if !this.is_dragging {
924 return;
925 }
926 let mouse_x: f32 = event.position.x.into();
927 let scroll_speed = this.handle_drag_move(mouse_x, window);
928 this.spawn_auto_scroll_timer_if_needed(scroll_speed, window, cx);
929 cx.notify();
930 })))
931 .when(enabled, |d| d.on_mouse_up(MouseButton::Left, cx.listener(|this, _event: &MouseUpEvent, _window, cx| {
933 this.stop_drag();
934 cx.notify();
935 })))
936 .child({
938 let entity = cx.entity();
939 let entity_paint = entity.clone();
940 let is_dragging = self.is_dragging;
941
942 div()
943 .size_full()
944 .flex()
945 .items_center()
946 .relative()
947 .child(
949 canvas(
950 move |bounds, _window, cx| {
951 let width: f32 = bounds.size.width.into();
952 let origin_x: f32 = bounds.origin.x.into();
953 entity.update(cx, |this: &mut PasswordInput, _cx| {
954 this.visible_width = width;
955 this.content_origin_x = origin_x;
956 });
957 },
958 {
959 let entity = entity_paint;
960 move |_bounds, _, window, _cx| {
961 if is_dragging {
962 let entity_move = entity.clone();
963 window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| {
964 if phase != DispatchPhase::Capture {
965 return;
966 }
967 let mouse_x: f32 = event.position.x.into();
968 entity_move.update(cx, |this: &mut PasswordInput, cx| {
969 let scroll_speed = this.handle_drag_move(mouse_x, window);
970 this.spawn_auto_scroll_timer_if_needed(scroll_speed, window, cx);
971 cx.notify();
972 });
973 });
974
975 let entity_up = entity.clone();
976 window.on_mouse_event(move |_event: &MouseUpEvent, phase, _window, cx| {
977 if phase != DispatchPhase::Capture {
978 return;
979 }
980 entity_up.update(cx, |this: &mut PasswordInput, cx| {
981 this.stop_drag();
982 cx.notify();
983 });
984 });
985 }
986 }
987 },
988 )
989 .size_full()
990 .absolute()
991 )
992 .child(
994 div()
995 .relative()
996 .h_full()
997 .flex()
998 .items_center()
999 .min_w_0()
1000 .when_some(selection_bounds, |d, (start_x, width)| {
1002 d.child(
1003 div()
1004 .absolute()
1005 .top_0()
1006 .bottom_0()
1007 .left(px(start_x))
1008 .w(px(width))
1009 .bg(rgb(selection_color))
1010 )
1011 })
1012 .child(
1014 div()
1015 .absolute()
1016 .left(px(-scroll_offset))
1017 .text_sm()
1018 .text_color(rgb(text_color))
1019 .whitespace_nowrap()
1020 .child(display_content.clone())
1021 )
1022 .when(cursor_visible, |d| {
1024 d.child(
1025 div()
1026 .absolute()
1027 .top(px(4.))
1028 .bottom(px(4.))
1029 .left(px(cursor_x))
1030 .w(px(1.))
1031 .bg(rgb(text_color))
1032 )
1033 })
1034 )
1035 .when(!has_content, |d| {
1037 if let Some(ph) = placeholder {
1038 d.child(
1039 div()
1040 .absolute()
1041 .left_0()
1042 .text_sm()
1043 .text_color(rgb(text_placeholder))
1044 .child(ph)
1045 )
1046 } else {
1047 d
1048 }
1049 })
1050 })
1051 )
1052 .child(separator)
1053 .child(
1055 div()
1056 .id("password_toggle_button")
1057 .flex()
1058 .items_center()
1059 .justify_center()
1060 .w(px(28.0))
1061 .h_full()
1062 .cursor_for_enabled(enabled)
1063 .text_color(rgb(button_text_color))
1064 .when(toggle_is_focused && enabled, |d| d.bg(rgb(theme.bg_hover)))
1065 .when(enabled, |d| d.hover(|d| d.bg(rgb(theme.bg_hover))))
1066 .track_focus(&toggle_focus_handle)
1067 .on_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| {
1068 if handle_tab_navigation(event, window) {
1069 return;
1070 }
1071 if matches!(event.keystroke.key.as_str(), "enter" | "space") {
1072 if !this.enabled { return; }
1073 this.toggle_visibility(cx);
1074 }
1075 }))
1076 .when(enabled, |d| d.on_click(cx.listener(|this, _event, _window, cx| {
1077 this.toggle_visibility(cx);
1078 })))
1079 .child(
1080 div()
1081 .text_sm()
1082 .child(eye_icon)
1083 )
1084 )
1085 }
1086}