input: Add mask pattern to format display. (#854)
- Add `mask_pattern` to Input, NumberInput. - Fix Input `set_text` to move cursor to end. https://github.com/user-attachments/assets/2d7e362a-3f80-47a4-94e3-efd729bffcac
This commit is contained in:
parent
1535df796e
commit
c0e0d82594
8 changed files with 795 additions and 77 deletions
6
Cargo.lock
generated
6
Cargo.lock
generated
|
|
@ -605,7 +605,7 @@ dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"itertools 0.11.0",
|
"itertools 0.12.1",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"lazycell",
|
"lazycell",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -628,7 +628,7 @@ dependencies = [
|
||||||
"bitflags 2.6.0",
|
"bitflags 2.6.0",
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"itertools 0.11.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
|
@ -4225,7 +4225,7 @@ version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
|
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-crate 1.3.1",
|
"proc-macro-crate 3.2.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.96",
|
"syn 2.0.96",
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use crate::section;
|
||||||
use gpui_component::{
|
use gpui_component::{
|
||||||
button::{Button, ButtonVariant, ButtonVariants as _},
|
button::{Button, ButtonVariant, ButtonVariants as _},
|
||||||
h_flex,
|
h_flex,
|
||||||
input::{InputEvent, TextInput},
|
input::{InputEvent, MaskPattern, TextInput},
|
||||||
v_flex, ContextModal, FocusableCycle, Icon, IconName, Sizable,
|
v_flex, ContextModal, FocusableCycle, Icon, IconName, Sizable,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -34,6 +34,9 @@ pub struct InputStory {
|
||||||
both_input1: Entity<TextInput>,
|
both_input1: Entity<TextInput>,
|
||||||
large_input: Entity<TextInput>,
|
large_input: Entity<TextInput>,
|
||||||
small_input: Entity<TextInput>,
|
small_input: Entity<TextInput>,
|
||||||
|
phone_input: Entity<TextInput>,
|
||||||
|
mask_input2: Entity<TextInput>,
|
||||||
|
currency_input: Entity<TextInput>,
|
||||||
|
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
@ -118,9 +121,19 @@ impl InputStory {
|
||||||
.placeholder("This input have prefix and suffix.")
|
.placeholder("This input have prefix and suffix.")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let phone_input = cx.new(|cx| TextInput::new(window, cx).mask_pattern("(999)-999-9999"));
|
||||||
|
let mask_input2 = cx.new(|cx| TextInput::new(window, cx).mask_pattern("AAA-###-AAA"));
|
||||||
|
let currency_input = cx.new(|cx| {
|
||||||
|
TextInput::new(window, cx).mask_pattern(MaskPattern::Number {
|
||||||
|
separator: Some(','),
|
||||||
|
fraction: Some(3),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
let _subscriptions = vec![
|
let _subscriptions = vec![
|
||||||
cx.subscribe_in(&input1, window, Self::on_input_event),
|
cx.subscribe_in(&input1, window, Self::on_input_event),
|
||||||
cx.subscribe_in(&input2, window, Self::on_input_event),
|
cx.subscribe_in(&input2, window, Self::on_input_event),
|
||||||
|
cx.subscribe_in(&phone_input, window, Self::on_input_event),
|
||||||
];
|
];
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -148,6 +161,9 @@ impl InputStory {
|
||||||
prefix_input1,
|
prefix_input1,
|
||||||
suffix_input1,
|
suffix_input1,
|
||||||
both_input1,
|
both_input1,
|
||||||
|
phone_input,
|
||||||
|
mask_input2,
|
||||||
|
currency_input,
|
||||||
_subscriptions,
|
_subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -228,6 +244,40 @@ impl Render for InputStory {
|
||||||
.child(self.both_input1.clone())
|
.child(self.both_input1.clone())
|
||||||
.child(self.suffix_input1.clone()),
|
.child(self.suffix_input1.clone()),
|
||||||
)
|
)
|
||||||
|
.child(
|
||||||
|
section("Currency Input with thousands separator")
|
||||||
|
.max_w_md()
|
||||||
|
.child(self.currency_input.clone())
|
||||||
|
.child(
|
||||||
|
div().child(format!("Value: {:?}", self.currency_input.read(cx).text())),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
section("Input with mask pattern: (999)-999-9999")
|
||||||
|
.max_w_md()
|
||||||
|
.child(self.phone_input.clone())
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.child(format!("Value: {:?}", self.phone_input.read(cx).text()))
|
||||||
|
.child(format!(
|
||||||
|
"Unmask Value: {:?}",
|
||||||
|
self.phone_input.read(cx).unmask_text()
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
section("Input with mask pattern: AAA-###-AAA")
|
||||||
|
.max_w_md()
|
||||||
|
.child(self.mask_input2.clone())
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.child(format!("Value: {:?}", self.mask_input2.read(cx).text()))
|
||||||
|
.child(format!(
|
||||||
|
"Unmask Value: {:?}",
|
||||||
|
self.mask_input2.read(cx).unmask_text()
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
section("Input Size")
|
section("Input Size")
|
||||||
.max_w_md()
|
.max_w_md()
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ pub struct Gallery {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Gallery {
|
impl Gallery {
|
||||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(init_story: Option<&str>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let search_input = cx.new(|cx| {
|
let search_input = cx.new(|cx| {
|
||||||
TextInput::new(window, cx)
|
TextInput::new(window, cx)
|
||||||
.appearance(false)
|
.appearance(false)
|
||||||
|
|
@ -82,7 +82,7 @@ impl Gallery {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
Self {
|
let mut this = Self {
|
||||||
search_input,
|
search_input,
|
||||||
stories,
|
stories,
|
||||||
active_group_index: Some(0),
|
active_group_index: Some(0),
|
||||||
|
|
@ -90,11 +90,31 @@ impl Gallery {
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
sidebar_state: ResizableState::new(cx),
|
sidebar_state: ResizableState::new(cx),
|
||||||
_subscriptions,
|
_subscriptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(init_story) = init_story {
|
||||||
|
this.set_active_story(init_story, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
fn set_active_story(&mut self, name: &str, cx: &mut App) {
|
||||||
cx.new(|cx| Self::new(window, cx))
|
let group_index = 1;
|
||||||
|
let Some(story_index) = self.stories.get(group_index).and_then(|(_, stories)| {
|
||||||
|
stories
|
||||||
|
.iter()
|
||||||
|
.position(|story| story.read(cx).name.to_lowercase().replace("story", "") == name)
|
||||||
|
}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.active_group_index = Some(group_index);
|
||||||
|
self.active_index = Some(story_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(init_story: Option<&str>, window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||||
|
cx.new(|cx| Self::new(init_story, window, cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,10 +291,17 @@ impl Render for Gallery {
|
||||||
fn main() {
|
fn main() {
|
||||||
let app = Application::new().with_assets(Assets);
|
let app = Application::new().with_assets(Assets);
|
||||||
|
|
||||||
|
// Parse `cargo run -- <story_name>`
|
||||||
|
let name = std::env::args().nth(1);
|
||||||
|
|
||||||
app.run(move |cx| {
|
app.run(move |cx| {
|
||||||
story::init(cx);
|
story::init(cx);
|
||||||
cx.activate(true);
|
cx.activate(true);
|
||||||
|
|
||||||
story::create_new_window("Gallery of GPUI Component", Gallery::view, cx);
|
story::create_new_window(
|
||||||
|
"Gallery of GPUI Component",
|
||||||
|
move |window, cx| Gallery::view(name.as_deref(), window, cx),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use regex::Regex;
|
||||||
|
|
||||||
use crate::section;
|
use crate::section;
|
||||||
use gpui_component::{
|
use gpui_component::{
|
||||||
input::{InputEvent, NumberInput, NumberInputEvent, StepAction},
|
input::{InputEvent, MaskPattern, NumberInput, NumberInputEvent, StepAction},
|
||||||
v_flex, FocusableCycle, Sizable,
|
v_flex, FocusableCycle, Sizable,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -26,6 +26,8 @@ pub struct NumberInputStory {
|
||||||
number_input1: Entity<NumberInput>,
|
number_input1: Entity<NumberInput>,
|
||||||
number_input2: Entity<NumberInput>,
|
number_input2: Entity<NumberInput>,
|
||||||
number_input2_value: u64,
|
number_input2_value: u64,
|
||||||
|
number_input3: Entity<NumberInput>,
|
||||||
|
number_input3_value: f64,
|
||||||
|
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
@ -68,9 +70,23 @@ impl NumberInputStory {
|
||||||
.small()
|
.small()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let number_input3 = cx.new(|cx| {
|
||||||
|
NumberInput::new(window, cx)
|
||||||
|
.placeholder("Number Input with mask pattern", window, cx)
|
||||||
|
.mask_pattern(
|
||||||
|
MaskPattern::Number {
|
||||||
|
separator: Some(','),
|
||||||
|
fraction: Some(2),
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
let _subscriptions = vec![
|
let _subscriptions = vec![
|
||||||
cx.subscribe_in(&number_input1, window, Self::on_number_input1_event),
|
cx.subscribe_in(&number_input1, window, Self::on_number_input_event),
|
||||||
cx.subscribe_in(&number_input2, window, Self::on_number_input2_event),
|
cx.subscribe_in(&number_input2, window, Self::on_number_input_event),
|
||||||
|
cx.subscribe_in(&number_input3, window, Self::on_number_input_event),
|
||||||
];
|
];
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -78,6 +94,8 @@ impl NumberInputStory {
|
||||||
number_input1_value,
|
number_input1_value,
|
||||||
number_input2,
|
number_input2,
|
||||||
number_input2_value: 0,
|
number_input2_value: 0,
|
||||||
|
number_input3,
|
||||||
|
number_input3_value: 0.0,
|
||||||
_subscriptions,
|
_subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -90,9 +108,9 @@ impl NumberInputStory {
|
||||||
self.cycle_focus(false, window, cx);
|
self.cycle_focus(false, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_number_input1_event(
|
fn on_number_input_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &Entity<NumberInput>,
|
this: &Entity<NumberInput>,
|
||||||
event: &NumberInputEvent,
|
event: &NumberInputEvent,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
|
|
@ -100,8 +118,18 @@ impl NumberInputStory {
|
||||||
match event {
|
match event {
|
||||||
NumberInputEvent::Input(input_event) => match input_event {
|
NumberInputEvent::Input(input_event) => match input_event {
|
||||||
InputEvent::Change(text) => {
|
InputEvent::Change(text) => {
|
||||||
if let Ok(value) = text.parse::<i64>() {
|
if this == &self.number_input1 {
|
||||||
self.number_input1_value = value;
|
if let Ok(value) = text.parse::<i64>() {
|
||||||
|
self.number_input1_value = value;
|
||||||
|
}
|
||||||
|
} else if this == &self.number_input2 {
|
||||||
|
if let Ok(value) = text.parse::<u64>() {
|
||||||
|
self.number_input2_value = value;
|
||||||
|
}
|
||||||
|
} else if this == &self.number_input3 {
|
||||||
|
if let Ok(value) = text.parse::<f64>() {
|
||||||
|
self.number_input3_value = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
println!("Change: {}", text);
|
println!("Change: {}", text);
|
||||||
}
|
}
|
||||||
|
|
@ -113,58 +141,40 @@ impl NumberInputStory {
|
||||||
},
|
},
|
||||||
NumberInputEvent::Step(step_action) => match step_action {
|
NumberInputEvent::Step(step_action) => match step_action {
|
||||||
StepAction::Decrement => {
|
StepAction::Decrement => {
|
||||||
self.number_input1_value = self.number_input1_value - 1;
|
if this == &self.number_input1 {
|
||||||
self.number_input1.update(cx, |input, cx| {
|
self.number_input1_value = self.number_input1_value - 1;
|
||||||
input.set_value(self.number_input1_value.to_string(), window, cx);
|
this.update(cx, |input, cx| {
|
||||||
});
|
input.set_value(self.number_input1_value.to_string(), window, cx);
|
||||||
|
});
|
||||||
|
} else if this == &self.number_input2 {
|
||||||
|
self.number_input2_value = self.number_input2_value - 1;
|
||||||
|
this.update(cx, |input, cx| {
|
||||||
|
input.set_value(self.number_input2_value.to_string(), window, cx);
|
||||||
|
});
|
||||||
|
} else if this == &self.number_input3 {
|
||||||
|
self.number_input3_value = self.number_input3_value - 1.0;
|
||||||
|
this.update(cx, |input, cx| {
|
||||||
|
input.set_value(self.number_input3_value.to_string(), window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
StepAction::Increment => {
|
StepAction::Increment => {
|
||||||
self.number_input1_value = self.number_input1_value + 1;
|
if this == &self.number_input1 {
|
||||||
self.number_input1.update(cx, |input, cx| {
|
self.number_input1_value = self.number_input1_value + 1;
|
||||||
input.set_value(self.number_input1_value.to_string(), window, cx);
|
this.update(cx, |input, cx| {
|
||||||
});
|
input.set_value(self.number_input1_value.to_string(), window, cx);
|
||||||
}
|
});
|
||||||
},
|
} else if this == &self.number_input2 {
|
||||||
}
|
self.number_input2_value = self.number_input2_value + 1;
|
||||||
}
|
this.update(cx, |input, cx| {
|
||||||
|
input.set_value(self.number_input2_value.to_string(), window, cx);
|
||||||
fn on_number_input2_event(
|
});
|
||||||
&mut self,
|
} else if this == &self.number_input3 {
|
||||||
_: &Entity<NumberInput>,
|
self.number_input3_value = self.number_input3_value + 1.0;
|
||||||
event: &NumberInputEvent,
|
this.update(cx, |input, cx| {
|
||||||
window: &mut Window,
|
input.set_value(self.number_input3_value.to_string(), window, cx);
|
||||||
cx: &mut Context<Self>,
|
});
|
||||||
) {
|
|
||||||
match event {
|
|
||||||
NumberInputEvent::Input(input_event) => match input_event {
|
|
||||||
InputEvent::Change(text) => {
|
|
||||||
if let Ok(value) = text.parse::<u64>() {
|
|
||||||
self.number_input2_value = value;
|
|
||||||
}
|
}
|
||||||
println!("Change: {}", text);
|
|
||||||
}
|
|
||||||
InputEvent::PressEnter { secondary } => {
|
|
||||||
println!("PressEnter secondary: {}", secondary);
|
|
||||||
}
|
|
||||||
InputEvent::Focus => println!("Focus"),
|
|
||||||
InputEvent::Blur => println!("Blur"),
|
|
||||||
},
|
|
||||||
NumberInputEvent::Step(step_action) => match step_action {
|
|
||||||
StepAction::Decrement => {
|
|
||||||
if self.number_input2_value.le(&0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.number_input2_value = self.number_input2_value - 1;
|
|
||||||
self.number_input2.update(cx, |input, cx| {
|
|
||||||
input.set_value(self.number_input2_value.to_string(), window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
StepAction::Increment => {
|
|
||||||
self.number_input2_value = self.number_input2_value + 1;
|
|
||||||
self.number_input2.update(cx, |input, cx| {
|
|
||||||
input.set_value(self.number_input2_value.to_string(), window, cx);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -202,5 +212,10 @@ impl Render for NumberInputStory {
|
||||||
.max_w_md()
|
.max_w_md()
|
||||||
.child(self.number_input2.clone()),
|
.child(self.number_input2.clone()),
|
||||||
)
|
)
|
||||||
|
.child(
|
||||||
|
section("With mask pattern")
|
||||||
|
.max_w_md()
|
||||||
|
.child(self.number_input3.clone()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ use gpui::{
|
||||||
use super::blink_cursor::BlinkCursor;
|
use super::blink_cursor::BlinkCursor;
|
||||||
use super::change::Change;
|
use super::change::Change;
|
||||||
use super::element::TextElement;
|
use super::element::TextElement;
|
||||||
|
use super::mask_pattern::MaskPattern;
|
||||||
use super::number_input;
|
use super::number_input;
|
||||||
|
|
||||||
use crate::button::{Button, ButtonVariants as _};
|
use crate::button::{Button, ButtonVariants as _};
|
||||||
|
|
@ -243,7 +244,7 @@ pub struct TextInput {
|
||||||
/// For special case, e.g.: NumberInput + - button
|
/// For special case, e.g.: NumberInput + - button
|
||||||
pub(super) no_gap: bool,
|
pub(super) no_gap: bool,
|
||||||
pub(super) height: Option<gpui::DefiniteLength>,
|
pub(super) height: Option<gpui::DefiniteLength>,
|
||||||
pattern: Option<regex::Regex>,
|
pub(super) pattern: Option<regex::Regex>,
|
||||||
validate: Option<Box<dyn Fn(&str) -> bool + 'static>>,
|
validate: Option<Box<dyn Fn(&str) -> bool + 'static>>,
|
||||||
pub(crate) scroll_handle: ScrollHandle,
|
pub(crate) scroll_handle: ScrollHandle,
|
||||||
scrollbar_state: Rc<Cell<ScrollbarState>>,
|
scrollbar_state: Rc<Cell<ScrollbarState>>,
|
||||||
|
|
@ -252,6 +253,8 @@ pub struct TextInput {
|
||||||
/// To remember the horizontal column (x-coordinate) of the cursor position.
|
/// To remember the horizontal column (x-coordinate) of the cursor position.
|
||||||
preferred_x_offset: Option<Pixels>,
|
preferred_x_offset: Option<Pixels>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
|
/// The mask pattern for formatting the input text
|
||||||
|
pub(crate) mask_pattern: MaskPattern,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<InputEvent> for TextInput {}
|
impl EventEmitter<InputEvent> for TextInput {}
|
||||||
|
|
@ -320,6 +323,7 @@ impl TextInput {
|
||||||
scroll_size: gpui::size(px(0.), px(0.)),
|
scroll_size: gpui::size(px(0.), px(0.)),
|
||||||
preferred_x_offset: None,
|
preferred_x_offset: None,
|
||||||
_subscriptions,
|
_subscriptions,
|
||||||
|
mask_pattern: MaskPattern::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -502,7 +506,7 @@ impl TextInput {
|
||||||
self.replace_text(text, window, cx);
|
self.replace_text(text, window, cx);
|
||||||
self.history.ignore = false;
|
self.history.ignore = false;
|
||||||
// Ensure cursor to start when set text
|
// Ensure cursor to start when set text
|
||||||
self.selected_range = 0..0;
|
self.selected_range = self.text.len()..self.text.len();
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
@ -553,19 +557,19 @@ impl TextInput {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set with masked state.
|
/// Set with password masked state.
|
||||||
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 the masked state of the input field.
|
/// Set the password masked state of the input field.
|
||||||
pub fn set_masked(&mut self, masked: bool, _: &mut Window, cx: &mut Context<Self>) {
|
pub fn set_masked(&mut self, masked: bool, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.masked = masked;
|
self.masked = masked;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set to enable toggle button for mask state.
|
/// Set to enable toggle button for password mask state.
|
||||||
pub fn mask_toggle(mut self) -> Self {
|
pub fn mask_toggle(mut self) -> Self {
|
||||||
self.mask_toggle = true;
|
self.mask_toggle = true;
|
||||||
self
|
self
|
||||||
|
|
@ -713,6 +717,11 @@ impl TextInput {
|
||||||
&self.text
|
&self.text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the text without mask.
|
||||||
|
pub fn unmask_text(&self) -> SharedString {
|
||||||
|
self.mask_pattern.unmask(&self.text).into()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn disabled(&self) -> bool {
|
pub fn disabled(&self) -> bool {
|
||||||
self.disabled
|
self.disabled
|
||||||
}
|
}
|
||||||
|
|
@ -1543,6 +1552,10 @@ impl TextInput {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !self.mask_pattern.is_valid(new_text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
self.pattern
|
self.pattern
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|p| p.is_match(new_text))
|
.map(|p| p.is_match(new_text))
|
||||||
|
|
@ -1577,6 +1590,36 @@ impl TextInput {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the mask pattern for formatting the input text.
|
||||||
|
///
|
||||||
|
/// The pattern can contain:
|
||||||
|
/// - 9: Any digit or dot
|
||||||
|
/// - A: Any letter
|
||||||
|
/// - *: Any character
|
||||||
|
/// - Other characters will be treated as literal mask characters
|
||||||
|
///
|
||||||
|
/// Example: "(999)999-999" for phone numbers
|
||||||
|
pub fn mask_pattern(mut self, pattern: impl Into<MaskPattern>) -> Self {
|
||||||
|
self.mask_pattern = pattern.into();
|
||||||
|
if let Some(placeholder) = self.mask_pattern.placeholder() {
|
||||||
|
self.placeholder = placeholder.into();
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_mask_pattern(
|
||||||
|
&mut self,
|
||||||
|
pattern: impl Into<MaskPattern>,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.mask_pattern = pattern.into();
|
||||||
|
if let Some(placeholder) = self.mask_pattern.placeholder() {
|
||||||
|
self.placeholder = placeholder.into();
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sizable for TextInput {
|
impl Sizable for TextInput {
|
||||||
|
|
@ -1644,18 +1687,23 @@ impl EntityInputHandler for TextInput {
|
||||||
|
|
||||||
let pending_text: SharedString =
|
let pending_text: SharedString =
|
||||||
(self.text[0..range.start].to_owned() + new_text + &self.text[range.end..]).into();
|
(self.text[0..range.start].to_owned() + new_text + &self.text[range.end..]).into();
|
||||||
|
// Check if the new text is valid
|
||||||
if !self.is_valid_input(&pending_text) {
|
if !self.is_valid_input(&pending_text) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.push_history(&range, new_text, window, cx);
|
let mask_text = self.mask_pattern.mask(&pending_text);
|
||||||
self.text = pending_text;
|
let new_text_len = (new_text.len() + mask_text.len()).saturating_sub(pending_text.len());
|
||||||
self.selected_range = range.start + new_text.len()..range.start + new_text.len();
|
let new_pos = (range.start + new_text_len).min(mask_text.len());
|
||||||
|
|
||||||
|
self.push_history(&range, &new_text, window, cx);
|
||||||
|
self.text = mask_text;
|
||||||
|
self.selected_range = new_pos..new_pos;
|
||||||
self.marked_range.take();
|
self.marked_range.take();
|
||||||
self.update_preferred_x_offset(cx);
|
self.update_preferred_x_offset(cx);
|
||||||
self.update_scroll_offset(None, cx);
|
self.update_scroll_offset(None, cx);
|
||||||
self.check_to_auto_grow(window, cx);
|
self.check_to_auto_grow(window, cx);
|
||||||
cx.emit(InputEvent::Change(self.text.clone()));
|
cx.emit(InputEvent::Change(self.unmask_text()));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1690,7 +1738,7 @@ impl EntityInputHandler for TextInput {
|
||||||
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
||||||
.map(|new_range| new_range.start + range.start..new_range.end + range.end)
|
.map(|new_range| new_range.start + range.start..new_range.end + range.end)
|
||||||
.unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
|
.unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
|
||||||
cx.emit(InputEvent::Change(self.text.clone()));
|
cx.emit(InputEvent::Change(self.unmask_text()));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1835,7 +1883,6 @@ impl Render for TextInput {
|
||||||
.on_action(cx.listener(Self::cut))
|
.on_action(cx.listener(Self::cut))
|
||||||
.on_action(cx.listener(Self::undo))
|
.on_action(cx.listener(Self::undo))
|
||||||
.on_action(cx.listener(Self::redo))
|
.on_action(cx.listener(Self::redo))
|
||||||
.on_action(cx.listener(Self::redo))
|
|
||||||
.on_key_down(cx.listener(Self::on_key_down))
|
.on_key_down(cx.listener(Self::on_key_down))
|
||||||
.on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
|
.on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
|
||||||
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
|
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
|
||||||
|
|
|
||||||
562
crates/ui/src/input/mask_pattern.rs
Normal file
562
crates/ui/src/input/mask_pattern.rs
Normal file
|
|
@ -0,0 +1,562 @@
|
||||||
|
use gpui::SharedString;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
|
pub enum MaskToken {
|
||||||
|
/// 0 Digit, equivalent to `[0]`
|
||||||
|
// Digit0,
|
||||||
|
/// Digit, equivalent to `[0-9]`
|
||||||
|
Digit,
|
||||||
|
/// Letter, equivalent to `[a-zA-Z]`
|
||||||
|
Letter,
|
||||||
|
/// Letter or digit, equivalent to `[a-zA-Z0-9]`
|
||||||
|
LetterOrDigit,
|
||||||
|
/// Separator
|
||||||
|
Sep(char),
|
||||||
|
/// Any character
|
||||||
|
Any,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
impl MaskToken {
|
||||||
|
/// Check if the token is any character.
|
||||||
|
pub fn is_any(&self) -> bool {
|
||||||
|
matches!(self, MaskToken::Any)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the token is a match for the given character.
|
||||||
|
///
|
||||||
|
/// The separator is always a match any input character.
|
||||||
|
fn is_match(&self, ch: char) -> bool {
|
||||||
|
match self {
|
||||||
|
MaskToken::Digit => ch.is_ascii_digit(),
|
||||||
|
MaskToken::Letter => ch.is_ascii_alphabetic(),
|
||||||
|
MaskToken::LetterOrDigit => ch.is_ascii_alphanumeric(),
|
||||||
|
MaskToken::Any => true,
|
||||||
|
MaskToken::Sep(c) => *c == ch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is the token a separator (Can be ignored)
|
||||||
|
fn is_sep(&self) -> bool {
|
||||||
|
matches!(self, MaskToken::Sep(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the token is a number.
|
||||||
|
pub fn is_number(&self) -> bool {
|
||||||
|
matches!(self, MaskToken::Digit)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn placeholder(&self) -> char {
|
||||||
|
match self {
|
||||||
|
MaskToken::Sep(c) => *c,
|
||||||
|
_ => '_',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mask_char(&self, ch: char) -> char {
|
||||||
|
match self {
|
||||||
|
MaskToken::Digit | MaskToken::LetterOrDigit | MaskToken::Letter => ch,
|
||||||
|
MaskToken::Sep(c) => *c,
|
||||||
|
MaskToken::Any => ch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unmask_char(&self, ch: char) -> Option<char> {
|
||||||
|
match self {
|
||||||
|
MaskToken::Digit => Some(ch),
|
||||||
|
MaskToken::Letter => Some(ch),
|
||||||
|
MaskToken::LetterOrDigit => Some(ch),
|
||||||
|
MaskToken::Any => Some(ch),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub enum MaskPattern {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
Pattern {
|
||||||
|
pattern: SharedString,
|
||||||
|
tokens: Vec<MaskToken>,
|
||||||
|
},
|
||||||
|
Number {
|
||||||
|
/// Group separator, e.g. "," or " "
|
||||||
|
separator: Option<char>,
|
||||||
|
/// Number of fraction digits, e.g. 2 for 123.45
|
||||||
|
fraction: Option<usize>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for MaskPattern {
|
||||||
|
fn from(pattern: &str) -> Self {
|
||||||
|
Self::new(pattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MaskPattern {
|
||||||
|
/// Create a new mask pattern
|
||||||
|
///
|
||||||
|
/// - `9` - Digit
|
||||||
|
/// - `A` - Letter
|
||||||
|
/// - `#` - Letter or Digit
|
||||||
|
/// - `*` - Any character
|
||||||
|
/// - other characters - Separator
|
||||||
|
///
|
||||||
|
/// For example:
|
||||||
|
///
|
||||||
|
/// - `(999)999-9999` - US phone number: (123)456-7890
|
||||||
|
/// - `99999-9999` - ZIP code: 12345-6789
|
||||||
|
/// - `AAAA-99-####` - Custom pattern: ABCD-12-3AB4
|
||||||
|
/// - `*999*` - Custom pattern: (123) or [123]
|
||||||
|
pub fn new(pattern: &str) -> Self {
|
||||||
|
let tokens = pattern
|
||||||
|
.chars()
|
||||||
|
.map(|ch| match ch {
|
||||||
|
// '0' => MaskToken::Digit0,
|
||||||
|
'9' => MaskToken::Digit,
|
||||||
|
'A' => MaskToken::Letter,
|
||||||
|
'#' => MaskToken::LetterOrDigit,
|
||||||
|
'*' => MaskToken::Any,
|
||||||
|
_ => MaskToken::Sep(ch),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self::Pattern {
|
||||||
|
pattern: pattern.to_owned().into(),
|
||||||
|
tokens,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
fn tokens(&self) -> Option<&Vec<MaskToken>> {
|
||||||
|
match self {
|
||||||
|
Self::Pattern { tokens, .. } => Some(tokens),
|
||||||
|
Self::Number { .. } => None,
|
||||||
|
Self::None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new mask pattern with group separator, e.g. "," or " "
|
||||||
|
pub fn number(sep: Option<char>) -> Self {
|
||||||
|
Self::Number {
|
||||||
|
separator: sep,
|
||||||
|
fraction: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn placeholder(&self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
Self::Pattern { tokens, .. } => {
|
||||||
|
Some(tokens.iter().map(|token| token.placeholder()).collect())
|
||||||
|
}
|
||||||
|
Self::Number { .. } => None,
|
||||||
|
Self::None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return true if the mask pattern is None or no any pattern.
|
||||||
|
pub fn is_none(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Pattern { tokens, .. } => tokens.is_empty(),
|
||||||
|
Self::Number { .. } => false,
|
||||||
|
Self::None => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check is the mask text is valid.
|
||||||
|
///
|
||||||
|
/// If the mask pattern is None, always return true.
|
||||||
|
pub fn is_valid(&self, mask_text: &str) -> bool {
|
||||||
|
if self.is_none() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut text_index = 0;
|
||||||
|
let mask_text_chars: Vec<char> = mask_text.chars().collect();
|
||||||
|
match self {
|
||||||
|
Self::Pattern { tokens, .. } => {
|
||||||
|
for token in tokens {
|
||||||
|
if text_index >= mask_text_chars.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ch = mask_text_chars[text_index];
|
||||||
|
if token.is_match(ch) {
|
||||||
|
text_index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text_index == mask_text.len()
|
||||||
|
}
|
||||||
|
Self::Number { separator, .. } => {
|
||||||
|
if mask_text.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the text is valid number
|
||||||
|
let mut parts = mask_text.split('.');
|
||||||
|
let int_part = parts.next().unwrap_or("");
|
||||||
|
let frac_part = parts.next();
|
||||||
|
|
||||||
|
if int_part.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the integer part is valid
|
||||||
|
if !int_part
|
||||||
|
.chars()
|
||||||
|
.all(|ch| ch.is_ascii_digit() || Some(ch) == *separator)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the fraction part is valid
|
||||||
|
if let Some(frac) = frac_part {
|
||||||
|
if !frac
|
||||||
|
.chars()
|
||||||
|
.all(|ch| ch.is_ascii_digit() || Some(ch) == *separator)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Self::None => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if valid input char at the given position.
|
||||||
|
pub fn is_valid_at(&self, ch: char, pos: usize) -> bool {
|
||||||
|
if self.is_none() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Self::Pattern { tokens, .. } => {
|
||||||
|
if let Some(token) = tokens.get(pos) {
|
||||||
|
if token.is_match(ch) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if token.is_sep() {
|
||||||
|
// If next token is match, it's valid
|
||||||
|
if let Some(next_token) = tokens.get(pos + 1) {
|
||||||
|
if next_token.is_match(ch) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
Self::Number { .. } => true,
|
||||||
|
Self::None => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format the text according to the mask pattern
|
||||||
|
///
|
||||||
|
/// For example:
|
||||||
|
///
|
||||||
|
/// - pattern: (999)999-999
|
||||||
|
/// - text: 123456789
|
||||||
|
/// - mask_text: (123)456-789
|
||||||
|
pub fn mask(&self, text: &str) -> SharedString {
|
||||||
|
if self.is_none() {
|
||||||
|
return text.to_owned().into();
|
||||||
|
}
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Self::Number {
|
||||||
|
separator,
|
||||||
|
fraction,
|
||||||
|
} => {
|
||||||
|
if let Some(sep) = *separator {
|
||||||
|
// Remove the existing group separator
|
||||||
|
let text = text.replace(sep, "");
|
||||||
|
|
||||||
|
let mut parts = text.split('.');
|
||||||
|
let int_part = parts.next().unwrap_or("");
|
||||||
|
|
||||||
|
// Limit the fraction part to the given range, if not enough, pad with 0
|
||||||
|
let frac_part = parts.next().map(|part| {
|
||||||
|
part.chars()
|
||||||
|
.take(fraction.unwrap_or(usize::MAX))
|
||||||
|
.collect::<String>()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reverse the integer part for easier grouping
|
||||||
|
let chars: Vec<char> = int_part.chars().rev().collect();
|
||||||
|
let mut result = String::new();
|
||||||
|
for (i, ch) in chars.iter().enumerate() {
|
||||||
|
if i > 0 && i % 3 == 0 {
|
||||||
|
result.push(sep);
|
||||||
|
}
|
||||||
|
result.push(*ch);
|
||||||
|
}
|
||||||
|
let int_with_sep: String = result.chars().rev().collect();
|
||||||
|
|
||||||
|
let final_str = if let Some(frac) = frac_part {
|
||||||
|
if fraction == &Some(0) {
|
||||||
|
int_with_sep
|
||||||
|
} else {
|
||||||
|
format!("{}.{}", int_with_sep, frac)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int_with_sep
|
||||||
|
};
|
||||||
|
return final_str.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
text.to_owned().into()
|
||||||
|
}
|
||||||
|
Self::Pattern { tokens, .. } => {
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut text_index = 0;
|
||||||
|
let text_chars: Vec<char> = text.chars().collect();
|
||||||
|
for (pos, token) in tokens.iter().enumerate() {
|
||||||
|
if text_index >= text_chars.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let ch = text_chars[text_index];
|
||||||
|
// Break if expected char is not match
|
||||||
|
if !token.is_sep() && !self.is_valid_at(ch, pos) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let mask_ch = token.mask_char(ch);
|
||||||
|
result.push(mask_ch);
|
||||||
|
if ch == mask_ch {
|
||||||
|
text_index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.into()
|
||||||
|
}
|
||||||
|
Self::None => text.to_owned().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract original text from masked text
|
||||||
|
pub fn unmask(&self, mask_text: &str) -> String {
|
||||||
|
match self {
|
||||||
|
Self::Number { separator, .. } => {
|
||||||
|
if let Some(sep) = *separator {
|
||||||
|
let mut result = String::new();
|
||||||
|
for ch in mask_text.chars() {
|
||||||
|
if ch == sep {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.push(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.contains('.') {
|
||||||
|
result = result.trim_end_matches('0').to_string();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mask_text.to_owned();
|
||||||
|
}
|
||||||
|
Self::Pattern { tokens, .. } => {
|
||||||
|
let mut result = String::new();
|
||||||
|
let mask_text_chars: Vec<char> = mask_text.chars().collect();
|
||||||
|
for (text_index, token) in tokens.iter().enumerate() {
|
||||||
|
if text_index >= mask_text_chars.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let ch = mask_text_chars[text_index];
|
||||||
|
let unmask_ch = token.unmask_char(ch);
|
||||||
|
if let Some(ch) = unmask_ch {
|
||||||
|
result.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
Self::None => mask_text.to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::input::mask_pattern::{MaskPattern, MaskToken};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_match() {
|
||||||
|
assert_eq!(MaskToken::Sep('(').is_match('('), true);
|
||||||
|
assert_eq!(MaskToken::Sep('-').is_match('('), false);
|
||||||
|
assert_eq!(MaskToken::Sep('-').is_match('3'), false);
|
||||||
|
|
||||||
|
assert_eq!(MaskToken::Digit.is_match('0'), true);
|
||||||
|
assert_eq!(MaskToken::Digit.is_match('9'), true);
|
||||||
|
assert_eq!(MaskToken::Digit.is_match('a'), false);
|
||||||
|
assert_eq!(MaskToken::Digit.is_match('C'), false);
|
||||||
|
|
||||||
|
assert_eq!(MaskToken::Letter.is_match('a'), true);
|
||||||
|
assert_eq!(MaskToken::Letter.is_match('Z'), true);
|
||||||
|
assert_eq!(MaskToken::Letter.is_match('3'), false);
|
||||||
|
assert_eq!(MaskToken::Letter.is_match('-'), false);
|
||||||
|
|
||||||
|
assert_eq!(MaskToken::LetterOrDigit.is_match('0'), true);
|
||||||
|
assert_eq!(MaskToken::LetterOrDigit.is_match('9'), true);
|
||||||
|
assert_eq!(MaskToken::LetterOrDigit.is_match('a'), true);
|
||||||
|
assert_eq!(MaskToken::LetterOrDigit.is_match('Z'), true);
|
||||||
|
assert_eq!(MaskToken::LetterOrDigit.is_match('3'), true);
|
||||||
|
|
||||||
|
assert_eq!(MaskToken::Any.is_match('a'), true);
|
||||||
|
assert_eq!(MaskToken::Any.is_match('3'), true);
|
||||||
|
assert_eq!(MaskToken::Any.is_match('-'), true);
|
||||||
|
assert_eq!(MaskToken::Any.is_match(' '), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mask_none() {
|
||||||
|
let mask = MaskPattern::None;
|
||||||
|
assert_eq!(mask.is_none(), true);
|
||||||
|
assert_eq!(mask.is_valid("1124124ASLDJKljk"), true);
|
||||||
|
assert_eq!(mask.mask("hello-world"), "hello-world");
|
||||||
|
assert_eq!(mask.unmask("hello-world"), "hello-world");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mask_pattern1() {
|
||||||
|
let mask = MaskPattern::new("(AA)999-999");
|
||||||
|
assert_eq!(
|
||||||
|
mask.tokens(),
|
||||||
|
Some(&vec![
|
||||||
|
MaskToken::Sep('('),
|
||||||
|
MaskToken::Letter,
|
||||||
|
MaskToken::Letter,
|
||||||
|
MaskToken::Sep(')'),
|
||||||
|
MaskToken::Digit,
|
||||||
|
MaskToken::Digit,
|
||||||
|
MaskToken::Digit,
|
||||||
|
MaskToken::Sep('-'),
|
||||||
|
MaskToken::Digit,
|
||||||
|
MaskToken::Digit,
|
||||||
|
MaskToken::Digit,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(mask.is_valid_at('(', 0), true);
|
||||||
|
assert_eq!(mask.is_valid_at('H', 0), true);
|
||||||
|
assert_eq!(mask.is_valid_at('3', 0), false);
|
||||||
|
assert_eq!(mask.is_valid_at('-', 0), false);
|
||||||
|
assert_eq!(mask.is_valid_at(')', 1), false);
|
||||||
|
assert_eq!(mask.is_valid_at('H', 1), true);
|
||||||
|
assert_eq!(mask.is_valid_at('1', 1), false);
|
||||||
|
assert_eq!(mask.is_valid_at('e', 2), true);
|
||||||
|
assert_eq!(mask.is_valid_at(')', 3), true);
|
||||||
|
assert_eq!(mask.is_valid_at('1', 3), true);
|
||||||
|
assert_eq!(mask.is_valid_at('2', 4), true);
|
||||||
|
|
||||||
|
assert_eq!(mask.is_valid("(AB)123-456"), true);
|
||||||
|
|
||||||
|
assert_eq!(mask.mask("AB123456"), "(AB)123-456");
|
||||||
|
assert_eq!(mask.mask("(AB)123-456"), "(AB)123-456");
|
||||||
|
assert_eq!(mask.mask("(AB123456"), "(AB)123-456");
|
||||||
|
assert_eq!(mask.mask("AB123-456"), "(AB)123-456");
|
||||||
|
assert_eq!(mask.mask("AB123-"), "(AB)123-");
|
||||||
|
assert_eq!(mask.mask("AB123--"), "(AB)123-");
|
||||||
|
assert_eq!(mask.mask("AB123-4"), "(AB)123-4");
|
||||||
|
|
||||||
|
let unmasked_text = mask.unmask("(AB)123-456");
|
||||||
|
assert_eq!(unmasked_text, "AB123456");
|
||||||
|
|
||||||
|
assert_eq!(mask.is_valid("12AB345"), false);
|
||||||
|
assert_eq!(mask.is_valid("(11)123-456"), false);
|
||||||
|
assert_eq!(mask.is_valid("##"), false);
|
||||||
|
assert_eq!(mask.is_valid("(AB)123456"), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mask_pattern2() {
|
||||||
|
let mask = MaskPattern::new("999-999-******");
|
||||||
|
assert_eq!(
|
||||||
|
mask.tokens(),
|
||||||
|
Some(&vec![
|
||||||
|
MaskToken::Digit,
|
||||||
|
MaskToken::Digit,
|
||||||
|
MaskToken::Digit,
|
||||||
|
MaskToken::Sep('-'),
|
||||||
|
MaskToken::Digit,
|
||||||
|
MaskToken::Digit,
|
||||||
|
MaskToken::Digit,
|
||||||
|
MaskToken::Sep('-'),
|
||||||
|
MaskToken::Any,
|
||||||
|
MaskToken::Any,
|
||||||
|
MaskToken::Any,
|
||||||
|
MaskToken::Any,
|
||||||
|
MaskToken::Any,
|
||||||
|
MaskToken::Any,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
let text = "123456A(111)";
|
||||||
|
let masked_text = mask.mask(text);
|
||||||
|
assert_eq!(masked_text, "123-456-A(111)");
|
||||||
|
let unmasked_text = mask.unmask(&masked_text);
|
||||||
|
assert_eq!(unmasked_text, "123456A(111)");
|
||||||
|
assert_eq!(mask.is_valid(&masked_text), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_number_with_group_separator() {
|
||||||
|
// Use comma as group separator
|
||||||
|
let mask = MaskPattern::number(Some(','));
|
||||||
|
assert_eq!(mask.mask("1234567"), "1,234,567");
|
||||||
|
assert_eq!(mask.mask("1,234,567"), "1,234,567");
|
||||||
|
assert_eq!(mask.unmask("1,234,567"), "1234567");
|
||||||
|
let mask = MaskPattern::number(Some(','));
|
||||||
|
assert_eq!(mask.mask("1234567.89"), "1,234,567.89");
|
||||||
|
assert_eq!(mask.unmask("1,234,567.89"), "1234567.89");
|
||||||
|
|
||||||
|
// Use space as group separator
|
||||||
|
let mask = MaskPattern::number(Some(' '));
|
||||||
|
assert_eq!(mask.mask("1234567"), "1 234 567");
|
||||||
|
assert_eq!(mask.unmask("1 234 567"), "1234567");
|
||||||
|
let mask = MaskPattern::number(Some(' '));
|
||||||
|
assert_eq!(mask.mask("1234567.89"), "1 234 567.89");
|
||||||
|
assert_eq!(mask.unmask("1 234 567.89"), "1234567.89");
|
||||||
|
|
||||||
|
// No group separator
|
||||||
|
let mask = MaskPattern::number(None);
|
||||||
|
assert_eq!(mask.mask("1234567"), "1234567");
|
||||||
|
assert_eq!(mask.unmask("1234567"), "1234567");
|
||||||
|
let mask = MaskPattern::number(None);
|
||||||
|
assert_eq!(mask.mask("1234567.89"), "1234567.89");
|
||||||
|
assert_eq!(mask.unmask("1234567.89"), "1234567.89");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_number_with_fraction_digits() {
|
||||||
|
let mask = MaskPattern::Number {
|
||||||
|
separator: Some(','),
|
||||||
|
fraction: Some(4),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(mask.mask("1234567"), "1,234,567");
|
||||||
|
assert_eq!(mask.unmask("1,234,567"), "1234567");
|
||||||
|
assert_eq!(mask.mask("1234567."), "1,234,567.");
|
||||||
|
assert_eq!(mask.mask("1234567.89"), "1,234,567.89");
|
||||||
|
assert_eq!(mask.unmask("1,234,567.890"), "1234567.89");
|
||||||
|
assert_eq!(mask.mask("1234567.891"), "1,234,567.891");
|
||||||
|
assert_eq!(mask.mask("1234567.891234"), "1,234,567.8912");
|
||||||
|
|
||||||
|
let mask = MaskPattern::Number {
|
||||||
|
separator: Some(','),
|
||||||
|
fraction: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(mask.mask("1234567.1234567"), "1,234,567.1234567");
|
||||||
|
|
||||||
|
let mask = MaskPattern::Number {
|
||||||
|
separator: Some(','),
|
||||||
|
fraction: Some(0),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(mask.mask("1234567.1234567"), "1,234,567");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,10 +3,12 @@ mod change;
|
||||||
mod clear_button;
|
mod clear_button;
|
||||||
mod element;
|
mod element;
|
||||||
mod input;
|
mod input;
|
||||||
|
mod mask_pattern;
|
||||||
mod number_input;
|
mod number_input;
|
||||||
mod otp_input;
|
mod otp_input;
|
||||||
|
|
||||||
pub(crate) use clear_button::*;
|
pub(crate) use clear_button::*;
|
||||||
pub use input::*;
|
pub use input::*;
|
||||||
|
pub use mask_pattern::MaskPattern;
|
||||||
pub use number_input::{NumberInput, NumberInputEvent, StepAction};
|
pub use number_input::{NumberInput, NumberInputEvent, StepAction};
|
||||||
pub use otp_input::*;
|
pub use otp_input::*;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ use crate::{
|
||||||
ActiveTheme, IconName, Sizable, Size, StyleSized, StyledExt as _,
|
ActiveTheme, IconName, Sizable, Size, StyleSized, StyledExt as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::MaskPattern;
|
||||||
|
|
||||||
actions!(number_input, [Increment, Decrement]);
|
actions!(number_input, [Increment, Decrement]);
|
||||||
|
|
||||||
const KEY_CONTENT: &str = "NumberInput";
|
const KEY_CONTENT: &str = "NumberInput";
|
||||||
|
|
@ -93,6 +95,19 @@ impl NumberInput {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mask_pattern(
|
||||||
|
self,
|
||||||
|
mask_pattern: MaskPattern,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
self.input.update(cx, |input, cx| {
|
||||||
|
input.pattern = None;
|
||||||
|
input.set_mask_pattern(mask_pattern, window, cx)
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_value(
|
pub fn set_value(
|
||||||
&self,
|
&self,
|
||||||
text: impl Into<SharedString>,
|
text: impl Into<SharedString>,
|
||||||
|
|
@ -100,7 +115,7 @@ impl NumberInput {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
self.input
|
self.input
|
||||||
.update(cx, |input, cx| input.set_text(text, window, cx))
|
.update(cx, |input, cx| input.set_text(text, window, cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_disabled(&self, disabled: bool, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn set_disabled(&self, disabled: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue