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::{
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<TextInput>,
large_input: View<TextInput>,
small_input: View<TextInput>,
otp_masked: bool,
otp_input: View<OtpInput>,
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 {
@ -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>) {
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(

View file

@ -50,7 +50,7 @@ pub fn init(cx: &mut AppContext) {
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;
let theme = cx.theme();
@ -64,7 +64,7 @@ pub fn section(title: impl Into<SharedString>, 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 {

View file

@ -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",

View file

@ -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<BlinkCursor>,
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<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.
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>) {
self.masked = masked;
cx.notify();
}
pub fn focus(&self, cx: &mut ViewContext<Self>) {
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 {
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<AnyElement>> = 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()