remote_connection: Add visibility toggle to password prompt (#52297)

Xiaobo Liu and Danilo Leal created

This adds an eye icon button to the right of the password input field
when connecting to a remote device, allowing users to toggle the
visibility of the typed password. The toggle defaults to masked and only
appears for actual password prompts, avoiding yes/no confirmations.

After modification, an eye button has been added:


https://github.com/user-attachments/assets/76fb5a89-0aa3-4017-b050-d6bb9c2ad4a8

--- 

Release Notes:

- Added a toggle to control password visibility when connecting to a
remote project.

---------

Signed-off-by: Xiaobo Liu <cppcoffee@gmail.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>

Change summary

crates/remote_connection/src/remote_connection.rs | 101 ++++++++++++----
1 file changed, 77 insertions(+), 24 deletions(-)

Detailed changes

crates/remote_connection/src/remote_connection.rs 🔗

@@ -15,8 +15,8 @@ use semver::Version;
 use settings::Settings;
 use theme::ThemeSettings;
 use ui::{
-    ActiveTheme, Color, CommonAnimationExt, Context, InteractiveElement, IntoElement, KeyBinding,
-    LabelCommon, ListItem, Styled, Window, prelude::*,
+    ActiveTheme, CommonAnimationExt, Context, InteractiveElement, KeyBinding, ListItem, Tooltip,
+    prelude::*,
 };
 use ui_input::{ERASED_EDITOR_FACTORY, ErasedEditor};
 use workspace::{DismissDecision, ModalView};
@@ -30,6 +30,8 @@ pub struct RemoteConnectionPrompt {
     prompt: Option<(Entity<Markdown>, oneshot::Sender<EncryptedPassword>)>,
     cancellation: Option<oneshot::Sender<()>>,
     editor: Arc<dyn ErasedEditor>,
+    is_password_prompt: bool,
+    is_masked: bool,
 }
 
 impl Drop for RemoteConnectionPrompt {
@@ -70,6 +72,8 @@ impl RemoteConnectionPrompt {
             status_message: None,
             cancellation: None,
             prompt: None,
+            is_password_prompt: false,
+            is_masked: true,
         }
     }
 
@@ -85,7 +89,9 @@ impl RemoteConnectionPrompt {
         cx: &mut Context<Self>,
     ) {
         let is_yes_no = prompt.contains("yes/no");
-        self.editor.set_masked(!is_yes_no, window, cx);
+        self.is_password_prompt = !is_yes_no;
+        self.is_masked = !is_yes_no;
+        self.editor.set_masked(self.is_masked, window, cx);
 
         let markdown = cx.new(|cx| Markdown::new_text(prompt.into(), cx));
         self.prompt = Some((markdown, tx));
@@ -133,40 +139,87 @@ impl Render for RemoteConnectionPrompt {
             ..Default::default()
         };
 
+        let is_password_prompt = self.is_password_prompt;
+        let is_masked = self.is_masked;
+        let (masked_password_icon, masked_password_tooltip) = if is_masked {
+            (IconName::Eye, "Toggle to Unmask Password")
+        } else {
+            (IconName::EyeOff, "Toggle to Mask Password")
+        };
+
         v_flex()
             .key_context("PasswordPrompt")
             .p_2()
             .size_full()
-            .text_buffer(cx)
-            .when_some(self.status_message.clone(), |el, status_message| {
-                el.child(
+            .when_some(self.prompt.as_ref(), |this, prompt| {
+                this.child(
+                    v_flex()
+                        .text_sm()
+                        .size_full()
+                        .overflow_hidden()
+                        .child(
+                            h_flex()
+                                .w_full()
+                                .justify_between()
+                                .child(MarkdownElement::new(prompt.0.clone(), markdown_style))
+                                .when(is_password_prompt, |this| {
+                                    this.child(
+                                        IconButton::new("toggle_mask", masked_password_icon)
+                                            .icon_size(IconSize::Small)
+                                            .tooltip(Tooltip::text(masked_password_tooltip))
+                                            .on_click(cx.listener(|this, _, window, cx| {
+                                                this.is_masked = !this.is_masked;
+                                                this.editor.set_masked(this.is_masked, window, cx);
+                                                window.focus(&this.editor.focus_handle(cx), cx);
+                                                cx.notify();
+                                            })),
+                                    )
+                                }),
+                        )
+                        .child(div().flex_1().child(self.editor.render(window, cx))),
+                )
+                .when(window.capslock().on, |this| {
+                    this.child(
+                        h_flex()
+                            .py_0p5()
+                            .min_w_0()
+                            .w_full()
+                            .gap_1()
+                            .child(
+                                Icon::new(IconName::Warning)
+                                    .size(IconSize::Small)
+                                    .color(Color::Muted),
+                            )
+                            .child(
+                                Label::new("Caps lock is on.")
+                                    .size(LabelSize::Small)
+                                    .color(Color::Muted),
+                            ),
+                    )
+                })
+            })
+            .when_some(self.status_message.clone(), |this, status_message| {
+                this.child(
                     h_flex()
-                        .gap_2()
+                        .min_w_0()
+                        .w_full()
+                        .mt_1()
+                        .gap_1()
                         .child(
-                            Icon::new(IconName::ArrowCircle)
+                            Icon::new(IconName::LoadCircle)
+                                .size(IconSize::Small)
                                 .color(Color::Muted)
                                 .with_rotate_animation(2),
                         )
                         .child(
-                            div()
-                                .text_ellipsis()
-                                .overflow_x_hidden()
-                                .child(format!("{}…", status_message)),
+                            Label::new(format!("{}…", status_message))
+                                .size(LabelSize::Small)
+                                .color(Color::Muted)
+                                .truncate()
+                                .flex_1(),
                         ),
                 )
             })
-            .when_some(self.prompt.as_ref(), |el, prompt| {
-                el.child(
-                    div()
-                        .size_full()
-                        .overflow_hidden()
-                        .child(MarkdownElement::new(prompt.0.clone(), markdown_style))
-                        .child(self.editor.render(window, cx)),
-                )
-                .when(window.capslock().on, |el| {
-                    el.child(Label::new("⚠️ ⇪ is on"))
-                })
-            })
     }
 }