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:
parent
cbffb9a543
commit
340c5fef3d
5 changed files with 124 additions and 18 deletions
1
assets/icons/asterisk.svg
Normal file
1
assets/icons/asterisk.svg
Normal 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 |
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue