From 88b5e5c0235e3eb6068ad142a1e33b7e48f2149e Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 31 Mar 2025 22:57:07 +0800 Subject: [PATCH] input: Add `mask_toggle` button to toggle masked state to Input. (#758) --- crates/story/src/input_story.rs | 14 ++++---- crates/ui/src/input/clear_button.rs | 1 + crates/ui/src/input/input.rs | 51 +++++++++++++++++++++++++++-- 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/crates/story/src/input_story.rs b/crates/story/src/input_story.rs index dea7c767..49072fd9 100644 --- a/crates/story/src/input_story.rs +++ b/crates/story/src/input_story.rs @@ -33,7 +33,7 @@ pub struct InputStory { number_input1: Entity, number_input2: Entity, number_input2_value: u64, - mash_input: Entity, + mask_input: Entity, disabled_input: Entity, prefix_input1: Entity, suffix_input1: Entity, @@ -138,8 +138,10 @@ impl InputStory { .detach(); let mask_input = cx.new(|cx| { - let mut input = TextInput::new(window, cx).cleanable(); - input.set_masked(true, window, cx); + let mut input = TextInput::new(window, cx) + .masked(true) + .mask_toggle() + .cleanable(); input.set_text("this-is-password", window, cx); input }); @@ -183,7 +185,7 @@ impl InputStory { number_input1_value, number_input2, number_input2_value: 0, - mash_input: mask_input, + mask_input, disabled_input: cx.new(|cx| { let mut input = TextInput::new(window, cx); input.set_text("This is disabled input", window, cx); @@ -375,7 +377,7 @@ impl FocusableCycle for InputStory { self.input1.focus_handle(cx), self.input2.focus_handle(cx), self.disabled_input.focus_handle(cx), - self.mash_input.focus_handle(cx), + self.mask_input.focus_handle(cx), self.prefix_input1.focus_handle(cx), self.both_input1.focus_handle(cx), self.suffix_input1.focus_handle(cx), @@ -450,7 +452,7 @@ impl Render for InputStory { .child( section("Input State", cx) .child(self.disabled_input.clone()) - .child(self.mash_input.clone()), + .child(self.mask_input.clone()), ), ) .child( diff --git a/crates/ui/src/input/clear_button.rs b/crates/ui/src/input/clear_button.rs index e772bdac..baec031c 100644 --- a/crates/ui/src/input/clear_button.rs +++ b/crates/ui/src/input/clear_button.rs @@ -5,6 +5,7 @@ use crate::{ ActiveTheme as _, Icon, IconName, Sizable as _, }; +#[inline] pub(crate) fn clear_button(cx: &App) -> Button { Button::new("clean") .icon(Icon::new(IconName::CircleX)) diff --git a/crates/ui/src/input/input.rs b/crates/ui/src/input/input.rs index c47db38a..c57e344d 100644 --- a/crates/ui/src/input/input.rs +++ b/crates/ui/src/input/input.rs @@ -27,13 +27,14 @@ use super::change::Change; use super::element::TextElement; use super::number_input; +use crate::button::{Button, ButtonVariants as _}; use crate::history::History; use crate::indicator::Indicator; use crate::input::clear_button; use crate::scroll::{Scrollbar, ScrollbarAxis, ScrollbarState}; -use crate::Size; use crate::StyledExt; use crate::{ActiveTheme, Root}; +use crate::{IconName, Size}; use crate::{Sizable, StyleSized}; actions!( @@ -220,6 +221,7 @@ pub struct TextInput { pub(super) selecting: bool, pub(super) disabled: bool, pub(super) masked: bool, + pub(super) mask_toggle: bool, pub(super) appearance: bool, pub(super) cleanable: bool, pub(super) size: Size, @@ -279,6 +281,7 @@ impl TextInput { selecting: false, disabled: false, masked: false, + mask_toggle: false, appearance: true, cleanable: false, loading: false, @@ -520,12 +523,24 @@ impl TextInput { cx.notify(); } + /// Set with masked state. + pub fn masked(mut self, masked: bool) -> Self { + self.masked = masked; + self + } + /// Set the masked state of the input field. pub fn set_masked(&mut self, masked: bool, _: &mut Window, cx: &mut Context) { self.masked = masked; cx.notify(); } + /// Set to enable toggle button for mask state. + pub fn mask_toggle(mut self) -> Self { + self.mask_toggle = true; + self + } + /// Set the prefix element of the input field. pub fn set_prefix(&mut self, builder: F, _: &mut Window, cx: &mut Context) where @@ -1447,6 +1462,35 @@ impl TextInput { .map(|p| p.is_match(new_text)) .unwrap_or(true) } + + fn render_toggle_mask_button( + &self, + _: &mut Window, + cx: &mut Context, + ) -> Option { + if !self.mask_toggle { + return None; + } + + Some( + Button::new("toggle-mask") + .icon(IconName::Eye) + .xsmall() + .ghost() + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, _, window, cx| { + this.set_masked(false, window, cx); + }), + ) + .on_mouse_up( + MouseButton::Left, + cx.listener(|this, _, window, cx| { + this.set_masked(true, window, cx); + }), + ), + ) + } } impl Sizable for TextInput { @@ -1642,8 +1686,8 @@ impl Render for TextInput { let focused = self.focus_handle.is_focused(window); let mut gap_x = match self.size { Size::Small => px(4.), - Size::Large => px(12.), - _ => px(8.), + Size::Large => px(8.), + _ => px(4.), }; if self.no_gap { gap_x = px(0.); @@ -1738,6 +1782,7 @@ impl Render for TextInput { .when(self.loading, |this| { this.child(Indicator::new().color(cx.theme().muted_foreground)) }) + .children(self.render_toggle_mask_button(window, cx)) .when( self.cleanable && !self.loading && !self.text.is_empty() && self.is_single_line(), |this| this.child(clear_button(cx).on_click(cx.listener(Self::clean))),