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.
 
<img width="390" alt="image"
src="https://github.com/user-attachments/assets/ef5368e9-132d-4de6-8506-aeecc47a9189">
This commit is contained in:
Jason Lee 2024-08-07 11:39:02 +08:00 committed by GitHub
parent cbffb9a543
commit 340c5fef3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 124 additions and 18 deletions

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-asterisk"><path d="M12 6v12"/><path d="M17.196 9 6.804 15"/><path d="m6.804 9 10.392 6"/></svg>

After

Width:  |  Height:  |  Size: 298 B

View file

@ -1,15 +1,16 @@
use gpui::{ 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, ParentElement as _, Render, SharedString, Styled, View, ViewContext, VisualContext,
WindowContext, WindowContext,
}; };
use ui::{ use ui::{
button::Button, button::Button,
checkbox::Checkbox,
h_flex, h_flex,
input::{InputEvent, OtpInput, TextInput}, input::{InputEvent, OtpInput, TextInput},
prelude::FluentBuilder as _, prelude::FluentBuilder as _,
v_flex, Clickable, FocusableCycle, IconName, Sizable, v_flex, Clickable, FocusableCycle, IconName, Selection, Sizable,
}; };
use crate::section; use crate::section;
@ -33,9 +34,12 @@ pub struct InputStory {
both_input1: View<TextInput>, both_input1: View<TextInput>,
large_input: View<TextInput>, large_input: View<TextInput>,
small_input: View<TextInput>, small_input: View<TextInput>,
otp_masked: bool,
otp_input: View<OtpInput>, otp_input: View<OtpInput>,
otp_value: Option<SharedString>, otp_value: Option<SharedString>,
opt_input2: View<OtpInput>, otp_input_small: View<OtpInput>,
otp_input_large: View<OtpInput>,
opt_input_sized: View<OtpInput>,
} }
impl InputStory { impl InputStory {
@ -117,9 +121,30 @@ impl InputStory {
prefix_input1, prefix_input1,
suffix_input1, suffix_input1,
both_input1, both_input1,
otp_masked: true,
otp_input, otp_input,
otp_value: None, 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"), InputEvent::Blur => println!("Blur"),
}; };
} }
fn toggle_opt_masked(&mut self, _: &Selection, cx: &mut ViewContext<Self>) {
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 { impl FocusableCycle for InputStory {
@ -206,14 +243,29 @@ impl Render for InputStory {
), ),
) )
.child( .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() v_flex()
.gap_3() .gap_3()
.child(self.otp_input_small.clone())
.child(self.otp_input.clone()) .child(self.otp_input.clone())
.when_some(self.otp_value.clone(), |this, otp| { .when_some(self.otp_value.clone(), |this, otp| {
this.child(format!("Your OTP: {}", 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( .child(

View file

@ -50,7 +50,7 @@ pub fn init(cx: &mut AppContext) {
input_story::init(cx); input_story::init(cx);
} }
pub fn section(title: impl Into<SharedString>, cx: &WindowContext) -> Div { pub fn section(title: impl IntoElement, cx: &WindowContext) -> Div {
use ui::theme::ActiveTheme; use ui::theme::ActiveTheme;
let theme = cx.theme(); let theme = cx.theme();
@ -64,7 +64,7 @@ pub fn section(title: impl Into<SharedString>, cx: &WindowContext) -> Div {
.border_color(theme.border) .border_color(theme.border)
.flex_wrap() .flex_wrap()
.justify_around() .justify_around()
.child(div().flex_none().w_full().child(title.into())) .child(div().flex_none().w_full().child(title))
} }
pub struct StoryContainer { pub struct StoryContainer {

View file

@ -10,6 +10,7 @@ pub enum IconName {
ArrowLeft, ArrowLeft,
ArrowRight, ArrowRight,
ArrowUp, ArrowUp,
Asterisk,
Check, Check,
ChevronDown, ChevronDown,
ChevronLeft, ChevronLeft,
@ -55,6 +56,7 @@ impl IconName {
IconName::ArrowLeft => "icons/arrow-left.svg", IconName::ArrowLeft => "icons/arrow-left.svg",
IconName::ArrowRight => "icons/arrow-right.svg", IconName::ArrowRight => "icons/arrow-right.svg",
IconName::ArrowUp => "icons/arrow-up.svg", IconName::ArrowUp => "icons/arrow-up.svg",
IconName::Asterisk => "icons/asterisk.svg",
IconName::Check => "icons/check.svg", IconName::Check => "icons/check.svg",
IconName::ChevronDown => "icons/chevron-down.svg", IconName::ChevronDown => "icons/chevron-down.svg",
IconName::ChevronLeft => "icons/chevron-left.svg", IconName::ChevronLeft => "icons/chevron-left.svg",

View file

@ -1,10 +1,10 @@
use gpui::{ 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, InteractiveElement, IntoElement, KeyDownEvent, Model, MouseButton, MouseDownEvent,
ParentElement as _, Render, SharedString, Styled as _, ViewContext, 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}; use super::{blink_cursor::BlinkCursor, InputEvent};
@ -20,6 +20,7 @@ pub struct OtpInput {
masked: bool, masked: bool,
value: SharedString, value: SharedString,
blink_cursor: Model<BlinkCursor>, blink_cursor: Model<BlinkCursor>,
size: Size,
} }
impl OtpInput { impl OtpInput {
@ -33,6 +34,7 @@ impl OtpInput {
value: SharedString::default(), value: SharedString::default(),
masked: false, masked: false,
blink_cursor: blink_cursor.clone(), blink_cursor: blink_cursor.clone(),
size: Size::Medium,
}; };
// Observe the blink cursor to repaint the view when it changes. // Observe the blink cursor to repaint the view when it changes.
@ -62,12 +64,35 @@ impl OtpInput {
self self
} }
/// Set default value of the OTP Input.
pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
self.value = value.into();
self
}
/// Set value of the OTP Input.
pub fn set_value(&mut self, value: impl Into<SharedString>, cx: &mut ViewContext<Self>) {
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. /// Set masked to true use masked input.
pub fn masked(mut self, masked: bool) -> Self { pub fn masked(mut self, masked: bool) -> Self {
self.masked = masked; self.masked = masked;
self self
} }
/// Set masked to true use masked input.
pub fn set_masked(&mut self, masked: bool, cx: &mut ViewContext<Self>) {
self.masked = masked;
cx.notify();
}
pub fn focus(&self, cx: &mut ViewContext<Self>) { pub fn focus(&self, cx: &mut ViewContext<Self>) {
self.focus_handle.focus(cx); self.focus_handle.focus(cx);
} }
@ -132,6 +157,13 @@ impl OtpInput {
} }
} }
impl Sizable for OtpInput {
fn with_size(mut self, size: impl Into<crate::Size>) -> Self {
self.size = size.into();
self
}
}
impl FocusableView for OtpInput { impl FocusableView for OtpInput {
fn focus_handle(&self, _: &gpui::AppContext) -> FocusHandle { fn focus_handle(&self, _: &gpui::AppContext) -> FocusHandle {
self.focus_handle.clone() self.focus_handle.clone()
@ -144,6 +176,14 @@ impl Render for OtpInput {
let blink_show = self.blink_cursor.read(cx).visible(); let blink_show = self.blink_cursor.read(cx).visible();
let is_focused = self.focus_handle.is_focused(cx); 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<AnyElement>> = Vec::with_capacity(self.number_of_groups); let mut groups: Vec<Vec<AnyElement>> = Vec::with_capacity(self.number_of_groups);
let mut group_ix = 0; let mut group_ix = 0;
let group_items_count = self.length / self.number_of_groups; let group_items_count = self.length / self.number_of_groups;
@ -170,16 +210,27 @@ impl Render for OtpInput {
.items_center() .items_center()
.justify_center() .justify_center()
.rounded_md() .rounded_md()
.w_8() .text_size(text_size)
.h_8() .map(|this| match self.size {
.text_lg() 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)) .on_mouse_down(MouseButton::Left, cx.listener(Self::on_input_mouse_down))
.map(|this| match c { .map(|this| match c {
Some(c) => this.child(if self.masked { Some(c) => {
SharedString::from("") if self.masked {
} else { this.child(
SharedString::from(c.to_string()) 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| { None => this.when(is_input_focused && blink_show, |this| {
this.child( this.child(
div() div()