diff --git a/Cargo.lock b/Cargo.lock index 8c8d89b9..57950374 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5119,6 +5119,7 @@ dependencies = [ "fake", "gpui", "image", + "regex", "serde", "ui", "workspace", @@ -5696,6 +5697,7 @@ dependencies = [ "once_cell", "paste", "raw-window-handle 0.6.2", + "regex", "resvg", "serde", "serde_json", diff --git a/crates/story/Cargo.toml b/crates/story/Cargo.toml index f464312e..096d049d 100644 --- a/crates/story/Cargo.toml +++ b/crates/story/Cargo.toml @@ -13,6 +13,7 @@ workspace.workspace = true charts-rs = "0.3" image = "0.25.1" wry = "0" +regex = "1" [lints] workspace = true diff --git a/crates/story/src/input_story.rs b/crates/story/src/input_story.rs index ff8c2209..12cb6786 100644 --- a/crates/story/src/input_story.rs +++ b/crates/story/src/input_story.rs @@ -75,7 +75,8 @@ impl InputStory { let suffix_input1 = cx.new_view(|cx| { TextInput::new(cx) .suffix(|_| IconName::Info) - .placeholder("Info here...") + .placeholder("This input only support [a-zA-Z0-9] characters.") + .pattern(regex::Regex::new(r"^[a-zA-Z0-9]*$").unwrap()) .cleanable(true) }); let both_input1 = cx.new_view(|cx| { @@ -114,7 +115,8 @@ impl InputStory { small_input: cx.new_view(|cx| { TextInput::new(cx) .size(Size::Small) - .placeholder("Small input") + .validate(|s| s.parse::().is_ok()) + .placeholder("validate to limit float number.") }), prefix_input1, suffix_input1, diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 60c9af0d..24de6f82 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -28,6 +28,7 @@ raw-window-handle = "0.6.2" winit = "0.30.3" wry = "0" smol = "1" +regex = "1" [lints] workspace = true diff --git a/crates/ui/src/input/input.rs b/crates/ui/src/input/input.rs index 3ee7553e..9aabe07b 100644 --- a/crates/ui/src/input/input.rs +++ b/crates/ui/src/input/input.rs @@ -127,6 +127,8 @@ pub struct TextInput { appearance: bool, cleanable: bool, size: Size, + pattern: Option, + validate: Option bool + 'static>>, } impl EventEmitter for TextInput {} @@ -157,6 +159,8 @@ impl TextInput { prefix: None, suffix: None, size: Size::Medium, + pattern: None, + validate: None, }; // Observe the blink cursor to repaint the view when it changes. @@ -268,6 +272,18 @@ impl TextInput { self } + /// Set the regular expression pattern of the input field. + pub fn pattern(mut self, pattern: regex::Regex) -> Self { + self.pattern = Some(pattern); + self + } + + /// Set the validation function of the input field. + pub fn validate(mut self, f: impl Fn(&str) -> bool + 'static) -> Self { + self.validate = Some(Box::new(f)); + self + } + /// Set true to show indicator at the input right. pub fn set_loading(&mut self, loading: bool, cx: &mut ViewContext) { self.loading = loading; @@ -619,6 +635,23 @@ impl TextInput { fn on_key_down_for_blink_cursor(&mut self, _: &KeyDownEvent, cx: &mut ViewContext) { self.pause_blink_cursor(cx) } + + fn is_valid_input(&self, new_text: &str) -> bool { + if new_text.is_empty() { + return true; + } + + if let Some(validate) = &self.validate { + if !validate(new_text) { + return false; + } + } + + self.pattern + .as_ref() + .map(|p| p.is_match(new_text)) + .unwrap_or(true) + } } impl ViewInputHandler for TextInput { @@ -661,9 +694,14 @@ impl ViewInputHandler for TextInput { .or(self.marked_range.clone()) .unwrap_or(self.selected_range.clone()); - self.push_history(&range, new_text, cx); - self.text = + let pending_text: SharedString = (self.text[0..range.start].to_owned() + new_text + &self.text[range.end..]).into(); + if !self.is_valid_input(&pending_text) { + return; + } + + self.push_history(&range, new_text, cx); + self.text = pending_text; self.selected_range = range.start + new_text.len()..range.start + new_text.len(); self.marked_range.take(); cx.emit(InputEvent::Change(self.text.clone())); @@ -686,10 +724,14 @@ impl ViewInputHandler for TextInput { .map(|range_utf16| self.range_from_utf16(range_utf16)) .or(self.marked_range.clone()) .unwrap_or(self.selected_range.clone()); + let pending_text: SharedString = + (self.text[0..range.start].to_owned() + new_text + &self.text[range.end..]).into(); + if !self.is_valid_input(&pending_text) { + return; + } self.push_history(&range, new_text, cx); - self.text = - (self.text[0..range.start].to_owned() + new_text + &self.text[range.end..]).into(); + self.text = pending_text; self.marked_range = Some(range.start..range.start + new_text.len()); self.selected_range = new_selected_range_utf16 .as_ref()