From 340c5fef3db53dcd4f267f0b570c33b53bf5d30f Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Wed, 7 Aug 2024 11:39:02 +0800 Subject: [PATCH] Improve OtpInput to add more size, use Icon to render mask. (#116) - Add `default_value`, `set_value`, `masked` method. - Add `set_value`, `value` method to set/get from OptInput. image --- assets/icons/asterisk.svg | 1 + crates/story/src/input_story.rs | 64 +++++++++++++++++++++++++--- crates/story/src/lib.rs | 4 +- crates/ui/src/icon.rs | 2 + crates/ui/src/input/otp_input.rs | 71 +++++++++++++++++++++++++++----- 5 files changed, 124 insertions(+), 18 deletions(-) create mode 100644 assets/icons/asterisk.svg diff --git a/assets/icons/asterisk.svg b/assets/icons/asterisk.svg new file mode 100644 index 00000000..b2d266f7 --- /dev/null +++ b/assets/icons/asterisk.svg @@ -0,0 +1 @@ + diff --git a/crates/story/src/input_story.rs b/crates/story/src/input_story.rs index a3e391a9..c9418e1b 100644 --- a/crates/story/src/input_story.rs +++ b/crates/story/src/input_story.rs @@ -1,15 +1,16 @@ use gpui::{ - actions, div, AppContext, FocusHandle, InteractiveElement, IntoElement, KeyBinding, + actions, div, px, AppContext, FocusHandle, InteractiveElement, IntoElement, KeyBinding, ParentElement as _, Render, SharedString, Styled, View, ViewContext, VisualContext, WindowContext, }; use ui::{ button::Button, + checkbox::Checkbox, h_flex, input::{InputEvent, OtpInput, TextInput}, prelude::FluentBuilder as _, - v_flex, Clickable, FocusableCycle, IconName, Sizable, + v_flex, Clickable, FocusableCycle, IconName, Selection, Sizable, }; use crate::section; @@ -33,9 +34,12 @@ pub struct InputStory { both_input1: View, large_input: View, small_input: View, + otp_masked: bool, otp_input: View, otp_value: Option, - opt_input2: View, + otp_input_small: View, + otp_input_large: View, + opt_input_sized: View, } impl InputStory { @@ -117,9 +121,30 @@ impl InputStory { prefix_input1, suffix_input1, both_input1, + otp_masked: true, otp_input, otp_value: None, - opt_input2: cx.new_view(|cx| OtpInput::new(6, cx).groups(3)), + otp_input_small: cx.new_view(|cx| { + OtpInput::new(6, cx) + .default_value("123456") + .masked(true) + .small() + .groups(1) + }), + otp_input_large: cx.new_view(|cx| { + OtpInput::new(6, cx) + .groups(3) + .large() + .default_value("012345") + .masked(true) + }), + opt_input_sized: cx.new_view(|cx| { + OtpInput::new(4, cx) + .groups(1) + .masked(true) + .default_value("654321") + .with_size(px(55.)) + }), } } @@ -144,6 +169,18 @@ impl InputStory { InputEvent::Blur => println!("Blur"), }; } + + fn toggle_opt_masked(&mut self, _: &Selection, cx: &mut ViewContext) { + self.otp_masked = !self.otp_masked; + self.otp_input + .update(cx, |input, cx| input.set_masked(self.otp_masked, cx)); + self.otp_input_small + .update(cx, |input, cx| input.set_masked(self.otp_masked, cx)); + self.otp_input_large + .update(cx, |input, cx| input.set_masked(self.otp_masked, cx)); + self.opt_input_sized + .update(cx, |input, cx| input.set_masked(self.otp_masked, cx)); + } } impl FocusableCycle for InputStory { @@ -206,14 +243,29 @@ impl Render for InputStory { ), ) .child( - section("OTP Input", cx).child( + section( + h_flex() + .items_center() + .justify_between() + .child("OTP Input") + .child( + Checkbox::new("otp-mask") + .label("Masked") + .checked(self.otp_masked) + .on_click(cx.listener(Self::toggle_opt_masked)), + ), + cx, + ) + .child( v_flex() .gap_3() + .child(self.otp_input_small.clone()) .child(self.otp_input.clone()) .when_some(self.otp_value.clone(), |this, otp| { this.child(format!("Your OTP: {}", otp)) }) - .child(self.opt_input2.clone()), + .child(self.otp_input_large.clone()) + .child(self.opt_input_sized.clone()), ), ) .child( diff --git a/crates/story/src/lib.rs b/crates/story/src/lib.rs index eeff914a..3bf3de88 100644 --- a/crates/story/src/lib.rs +++ b/crates/story/src/lib.rs @@ -50,7 +50,7 @@ pub fn init(cx: &mut AppContext) { input_story::init(cx); } -pub fn section(title: impl Into, cx: &WindowContext) -> Div { +pub fn section(title: impl IntoElement, cx: &WindowContext) -> Div { use ui::theme::ActiveTheme; let theme = cx.theme(); @@ -64,7 +64,7 @@ pub fn section(title: impl Into, cx: &WindowContext) -> Div { .border_color(theme.border) .flex_wrap() .justify_around() - .child(div().flex_none().w_full().child(title.into())) + .child(div().flex_none().w_full().child(title)) } pub struct StoryContainer { diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index fc64d0f7..637afe56 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -10,6 +10,7 @@ pub enum IconName { ArrowLeft, ArrowRight, ArrowUp, + Asterisk, Check, ChevronDown, ChevronLeft, @@ -55,6 +56,7 @@ impl IconName { IconName::ArrowLeft => "icons/arrow-left.svg", IconName::ArrowRight => "icons/arrow-right.svg", IconName::ArrowUp => "icons/arrow-up.svg", + IconName::Asterisk => "icons/asterisk.svg", IconName::Check => "icons/check.svg", IconName::ChevronDown => "icons/chevron-down.svg", IconName::ChevronLeft => "icons/chevron-left.svg", diff --git a/crates/ui/src/input/otp_input.rs b/crates/ui/src/input/otp_input.rs index 996c3af0..4222cddd 100644 --- a/crates/ui/src/input/otp_input.rs +++ b/crates/ui/src/input/otp_input.rs @@ -1,10 +1,10 @@ use gpui::{ - div, prelude::FluentBuilder, AnyElement, Context, EventEmitter, FocusHandle, FocusableView, + div, prelude::FluentBuilder, px, AnyElement, Context, EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, KeyDownEvent, Model, MouseButton, MouseDownEvent, ParentElement as _, Render, SharedString, Styled as _, ViewContext, }; -use crate::{h_flex, theme::ActiveTheme, v_flex}; +use crate::{h_flex, theme::ActiveTheme, v_flex, Icon, IconName, Sizable, Size}; use super::{blink_cursor::BlinkCursor, InputEvent}; @@ -20,6 +20,7 @@ pub struct OtpInput { masked: bool, value: SharedString, blink_cursor: Model, + size: Size, } impl OtpInput { @@ -33,6 +34,7 @@ impl OtpInput { value: SharedString::default(), masked: false, blink_cursor: blink_cursor.clone(), + size: Size::Medium, }; // Observe the blink cursor to repaint the view when it changes. @@ -62,12 +64,35 @@ impl OtpInput { self } + /// Set default value of the OTP Input. + pub fn default_value(mut self, value: impl Into) -> Self { + self.value = value.into(); + self + } + + /// Set value of the OTP Input. + pub fn set_value(&mut self, value: impl Into, cx: &mut ViewContext) { + self.value = value.into(); + cx.notify(); + } + + /// Return the value of the OTP Input. + pub fn value(&self) -> SharedString { + self.value.clone() + } + /// Set masked to true use masked input. pub fn masked(mut self, masked: bool) -> Self { self.masked = masked; self } + /// Set masked to true use masked input. + pub fn set_masked(&mut self, masked: bool, cx: &mut ViewContext) { + self.masked = masked; + cx.notify(); + } + pub fn focus(&self, cx: &mut ViewContext) { self.focus_handle.focus(cx); } @@ -132,6 +157,13 @@ impl OtpInput { } } +impl Sizable for OtpInput { + fn with_size(mut self, size: impl Into) -> Self { + self.size = size.into(); + self + } +} + impl FocusableView for OtpInput { fn focus_handle(&self, _: &gpui::AppContext) -> FocusHandle { self.focus_handle.clone() @@ -144,6 +176,14 @@ impl Render for OtpInput { let blink_show = self.blink_cursor.read(cx).visible(); let is_focused = self.focus_handle.is_focused(cx); + let text_size = match self.size { + Size::XSmall => px(14.), + Size::Small => px(14.), + Size::Medium => px(16.), + Size::Large => px(18.), + Size::Size(v) => v * 0.5, + }; + let mut groups: Vec> = Vec::with_capacity(self.number_of_groups); let mut group_ix = 0; let group_items_count = self.length / self.number_of_groups; @@ -170,16 +210,27 @@ impl Render for OtpInput { .items_center() .justify_center() .rounded_md() - .w_8() - .h_8() - .text_lg() + .text_size(text_size) + .map(|this| match self.size { + Size::XSmall => this.w_6().h_6(), + Size::Small => this.w_6().h_6(), + Size::Medium => this.w_8().h_8(), + Size::Large => this.w_11().h_11(), + Size::Size(px) => this.w(px).h(px), + }) .on_mouse_down(MouseButton::Left, cx.listener(Self::on_input_mouse_down)) .map(|this| match c { - Some(c) => this.child(if self.masked { - SharedString::from("•") - } else { - SharedString::from(c.to_string()) - }), + Some(c) => { + if self.masked { + this.child( + Icon::new(IconName::Asterisk) + .text_color(cx.theme().secondary_foreground) + .with_size(text_size), + ) + } else { + this.child(c.to_string()) + } + } None => this.when(is_input_focused && blink_show, |this| { this.child( div()