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()