From dad6f4c431932c743a2effbeace5071e0134c5c4 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 24 Jun 2024 17:39:56 +0800 Subject: [PATCH] Update input new method --- crates/ui/src/story/input_story.rs | 32 +-- crates/ui/src/story/mod.rs | 2 +- crates/ui/src/text_field/mod.rs | 318 +++++++++++++++----------- crates/ui/src/text_field/text_view.rs | 68 ++++-- crates/ui/src/theme.rs | 4 +- 5 files changed, 256 insertions(+), 168 deletions(-) diff --git a/crates/ui/src/story/input_story.rs b/crates/ui/src/story/input_story.rs index cac1a6fd..0dce6afa 100644 --- a/crates/ui/src/story/input_story.rs +++ b/crates/ui/src/story/input_story.rs @@ -1,6 +1,6 @@ use gpui::{ - div, ClickEvent, IntoElement, ParentElement as _, Render, Styled as _, ViewContext, - WindowContext, + div, ClickEvent, IntoElement, ParentElement as _, Render, Styled as _, View, ViewContext, + VisualContext, WindowContext, }; use crate::text_field::TextField; @@ -10,18 +10,25 @@ use super::story_case; pub struct InputStory { input1: TextField, input2: TextField, + mash_input: TextField, + disabled_input: TextField, } impl InputStory { - pub(crate) fn new(cx: &mut ViewContext) -> Self { - let input1 = TextField::new("Enter text here...", false, cx); - input1 - .view - .update(cx, |text_view, cx| text_view.set_text("Hello 世界", cx)); + pub(crate) fn new(cx: &mut WindowContext) -> Self { + let input1 = TextField::new(cx).set_text("Hello 世界", cx); + + let mask_input = TextField::new(cx) + .set_masked(true, cx) + .set_text("this-is-password", cx); Self { input1, - input2: TextField::new("Enter text here...", true, cx), + input2: TextField::new(cx).set_placeholder("Enter text here...", cx), + mash_input: mask_input, + disabled_input: TextField::new(cx) + .set_text("This is disabled input", cx) + .set_disabled(true, cx), } } @@ -33,17 +40,16 @@ impl InputStory { impl Render for InputStory { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let input1 = self.input1.clone(); - let input2 = self.input2.clone(); - story_case("Input", "A text input field.").child( div() .flex() .flex_col() .justify_start() .gap_3() - .child(input1) - .child(input2), + .child(self.input1.clone()) + .child(self.input2.clone()) + .child(self.disabled_input.clone()) + .child(self.mash_input.clone()), ) } } diff --git a/crates/ui/src/story/mod.rs b/crates/ui/src/story/mod.rs index 9573e5f1..ac255640 100644 --- a/crates/ui/src/story/mod.rs +++ b/crates/ui/src/story/mod.rs @@ -86,7 +86,7 @@ impl Stories { Self { active: StoryType::Button, button_story: cx.new_view(|cx| ButtonStory {}), - input_story: cx.new_view(InputStory::new), + input_story: cx.new_view(|cx| InputStory::new(cx)), } } diff --git a/crates/ui/src/text_field/mod.rs b/crates/ui/src/text_field/mod.rs index a638fe6c..5337a649 100644 --- a/crates/ui/src/text_field/mod.rs +++ b/crates/ui/src/text_field/mod.rs @@ -8,9 +8,9 @@ use crate::{ }; use blink_manager::BlinkManager; use gpui::{ - div, ClipboardItem, Context, EventEmitter, FocusHandle, InteractiveElement, IntoElement, - KeyDownEvent, Model, ParentElement, Render, RenderOnce, Styled, View, ViewContext, - WindowContext, + div, prelude::FluentBuilder as _, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, + InteractiveElement, IntoElement, KeyDownEvent, Model, MouseButton, ParentElement, RenderOnce, + Styled, View, ViewContext, WindowContext, }; use std::time::Duration; use text_view::TextView; @@ -20,33 +20,53 @@ const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); #[derive(Clone, IntoElement)] pub struct TextField { focus_handle: FocusHandle, - disable: bool, blink_manager: Model, pub view: View, } impl TextField { - pub fn new(placeholder: &str, disable: bool, cx: &mut WindowContext) -> Self { + pub fn new(cx: &mut WindowContext) -> Self { let focus_handle = cx.focus_handle(); - let view = TextView::init(cx, &focus_handle, placeholder, disable); + let view = TextView::init(cx, &focus_handle); let blink_manager = cx.new_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); - // cx.on_focus(&focus_handle, Self::handle_focus).detach(); - // cx.on_blur(&focus_handle, Self::handle_blur).detach(); - Self { focus_handle, view, blink_manager, - disable, } } - pub fn focus(&self, cx: &mut WindowContext) { + pub fn focus(&mut self, cx: &mut WindowContext) { cx.focus(&self.focus_handle); } + pub fn set_placeholder(self, placeholder: &str, cx: &mut WindowContext) -> Self { + self.view.update(cx, |text_view, cx| { + text_view.set_placeholder(placeholder, cx) + }); + self + } + + pub fn set_disabled(self, disabled: bool, cx: &mut WindowContext) -> Self { + self.view + .update(cx, |text_view, cx| text_view.set_disabled(disabled, cx)); + self + } + + pub fn set_text(self, text: &str, cx: &mut WindowContext) -> Self { + self.view + .update(cx, |text_view, cx| text_view.set_text(text, cx)); + self + } + + pub fn set_masked(self, masked: bool, cx: &mut WindowContext) -> Self { + self.view + .update(cx, |text_view, cx| text_view.set_masked(masked, cx)); + self + } + fn handle_focus(&mut self, cx: &mut ViewContext) { cx.emit(TextEvent::Focus); self.blink_manager.update(cx, BlinkManager::enable); @@ -64,156 +84,190 @@ impl TextField { } } -impl Disableable for TextField { - fn disabled(mut self, disabled: bool) -> Self { - self.disable = disabled; - self - } -} impl RenderOnce for TextField { fn render(self, cx: &mut WindowContext) -> impl IntoElement { - cx.focus(&self.focus_handle); + let focus_handle = self.focus_handle.clone(); let theme = cx.global::(); let view = self.view.clone(); + let text_view = view.read(cx); + + let focused = self.focus_handle.is_focused(cx); + let disabled = text_view.disabled; div() - .track_focus(&self.focus_handle) - .on_key_down(move |ev, cx| { - self.view.update(cx, |text_view, cx| { - let prev = text_view.text.clone(); - cx.emit(TextEvent::KeyDown(ev.clone())); - let keystroke = &ev.keystroke.key; - let chars = text_view.text.chars().collect::>(); - let m = ev.keystroke.modifiers.secondary(); + .track_focus(&focus_handle) + .when(!text_view.disabled, |this| { + this.on_mouse_down(MouseButton::Left, move |_, cx| { + cx.prevent_default(); + self.focus_handle.focus(cx) + }) + }) + .when(!disabled, |this| { + this.on_key_down(move |ev, cx| { + if !focused { + return; + } - dbg!(&text_view.text, &text_view.selection); + self.view.update(cx, |text_view, cx| { + let prev = text_view.text.clone(); + cx.emit(TextEvent::KeyDown(ev.clone())); + let keystroke = ev.keystroke.key.as_str(); + let chars = text_view.text.chars().collect::>(); + let m = ev.keystroke.modifiers.secondary(); - if m { - match keystroke.as_str() { - "a" => { - text_view.selection = 0..chars.len(); - } - "c" => { - // if !text_view.masked { - let selected_text = - chars[text_view.selection.clone()].iter().collect(); - cx.write_to_clipboard(ClipboardItem::new(selected_text)); - // } - } - "v" => { - let clipboard = cx.read_from_clipboard(); - if let Some(clipboard) = clipboard { - let text = clipboard.text(); - text_view.text.replace_range( - text_view.char_range_to_text_range(&text_view.text), - text, - ); - let i = text_view.selection.start + text.chars().count(); - text_view.selection = i..i; + if m { + match keystroke { + "a" => { + text_view.selection = 0..chars.len(); } - } - "x" => { - let selected_text = - chars[text_view.selection.clone()].iter().collect(); - cx.write_to_clipboard(ClipboardItem::new(selected_text)); - text_view.text.replace_range( - text_view.char_range_to_text_range(&text_view.text), - "", - ); - text_view.selection.end = text_view.selection.start; - } - _ => {} - } - } else if !ev.keystroke.ime_key.clone().unwrap_or_default().is_empty() { - let ime_key = &ev.keystroke.ime_key.clone().unwrap_or_default(); - text_view.text.replace_range( - text_view.char_range_to_text_range(&text_view.text), - ime_key, - ); - let i = text_view.selection.start + ime_key.chars().count(); - text_view.selection = i..i; - } else { - match keystroke.as_str() { - "left" => { - if text_view.selection.start > 0 { - let i = if text_view.selection.start == text_view.selection.end - { - text_view.selection.start - 1 - } else { - text_view.selection.start - }; - text_view.selection = i..i; + "c" => { + // if !text_view.masked { + let selected_text = + chars[text_view.selection.clone()].iter().collect(); + cx.write_to_clipboard(ClipboardItem::new(selected_text)); + // } } - } - "right" => { - if text_view.selection.end < text_view.text.len() { - let i = if text_view.selection.start == text_view.selection.end - { - text_view.selection.end + 1 - } else { - text_view.selection.end - }; - text_view.selection = i..i; + "v" => { + let clipboard = cx.read_from_clipboard(); + if let Some(clipboard) = clipboard { + let text = clipboard.text(); + text_view.text.replace_range( + text_view.char_range_to_text_range(&text_view.text), + text, + ); + let i = text_view.selection.start + text.chars().count(); + text_view.selection = i..i; + } } - } - "backspace" => { - if text_view.text.is_empty() && !ev.is_held { - // cx.emit(TextEvent::Back); - } else if text_view.selection.start == text_view.selection.end - && text_view.selection.start > 0 - { - let i = (text_view.selection.start - 1).min(chars.len()); - text_view.text = chars[0..i].iter().collect::() - + &(chars[text_view.selection.end.min(chars.len())..] - .iter() - .collect::()); - text_view.selection = i..i; - } else { + "x" => { + let selected_text = + chars[text_view.selection.clone()].iter().collect(); + cx.write_to_clipboard(ClipboardItem::new(selected_text)); text_view.text.replace_range( text_view.char_range_to_text_range(&text_view.text), "", ); text_view.selection.end = text_view.selection.start; } + _ => {} } - "enter" => { - if ev.keystroke.modifiers.shift { - text_view.text.insert( - text_view.char_range_to_text_range(&text_view.text).start, - '\n', - ); - let i = text_view.selection.start + 1; - text_view.selection = i..i; + } else if ev.keystroke.modifiers.control { + // On macOS, ctrl+a, ctrl+e are used for moving cursor to start/end of line + match keystroke { + "a" => { + // Move cursor to first of line + text_view.selection = 0..0; } + "e" => { + // Move cursor to end of line + text_view.selection = chars.len()..chars.len(); + } + _ => {} } - _ => {} - }; - } + } else if !ev.keystroke.ime_key.clone().unwrap_or_default().is_empty() { + let ime_key = &ev.keystroke.ime_key.clone().unwrap_or_default(); + text_view.text.replace_range( + text_view.char_range_to_text_range(&text_view.text), + ime_key, + ); + let i = text_view.selection.start + ime_key.chars().count(); + text_view.selection = i..i; + } else { + match keystroke { + "left" => { + if text_view.selection.start > 0 { + let i = if text_view.selection.start + == text_view.selection.end + { + text_view.selection.start - 1 + } else { + text_view.selection.start + }; + text_view.selection = i..i; + } + } + "right" => { + if text_view.selection.end < text_view.text.len() { + let i = if text_view.selection.start + == text_view.selection.end + { + text_view.selection.end + 1 + } else { + text_view.selection.end + }; + text_view.selection = i..i; + } + } + "backspace" => { + if text_view.text.is_empty() && !ev.is_held { + // cx.emit(TextEvent::Back); + } else if text_view.selection.start == text_view.selection.end + && text_view.selection.start > 0 + { + let i = (text_view.selection.start - 1).min(chars.len()); + text_view.text = chars[0..i].iter().collect::() + + &(chars[text_view.selection.end.min(chars.len())..] + .iter() + .collect::()); + text_view.selection = i..i; + } else { + text_view.text.replace_range( + text_view.char_range_to_text_range(&text_view.text), + "", + ); + text_view.selection.end = text_view.selection.start; + } + } + "enter" => { + if ev.keystroke.modifiers.shift { + text_view.text.insert( + text_view + .char_range_to_text_range(&text_view.text) + .start, + '\n', + ); + let i = text_view.selection.start + 1; + text_view.selection = i..i; + } + } + _ => { + if let Some(c) = keystroke.chars().next() { + text_view.text.replace_range( + text_view.char_range_to_text_range(&text_view.text), + c.to_string().as_str(), + ); + let i = text_view.selection.start + 1; + text_view.selection = i..i; + } + } + }; + } - if prev != text_view.text { - cx.emit(TextEvent::Input { - text: text_view.text.clone(), - }); - dbg!(&text_view.text, &text_view.selection); - } - cx.notify(); - }); + if prev != text_view.text { + cx.emit(TextEvent::Input { + text: text_view.text.clone(), + }); + } + cx.notify(); + }); + }) }) - .border_color(if self.focus_handle.is_focused(cx) { - theme.ring - } else { - theme.input + .when(!disabled, |this| { + this.when(focused, |this| this.border_color(theme.ring)) }) + .border_color(theme.input) .border_1() .rounded_sm() - .py_1p5() + .py_1() .px_3() + .h_9() + .shadow_sm() .min_w_20() - .bg(if self.disable { - theme.background.opacity(0.5) + .bg(if disabled { + theme.muted } else { - theme.background + theme.transparent }) .child(view) } diff --git a/crates/ui/src/text_field/text_view.rs b/crates/ui/src/text_field/text_view.rs index 8a8ed855..5c5787ea 100644 --- a/crates/ui/src/text_field/text_view.rs +++ b/crates/ui/src/text_field/text_view.rs @@ -15,21 +15,17 @@ pub struct TextView { pub placeholder: String, pub word_click: (usize, u16), pub selection: Range, - pub disable: bool, + pub disabled: bool, pub blink_manager: Model, pub cursor: CursorLayout, pub masked: bool, + pub focused: bool, } impl EventEmitter for TextView {} impl TextView { - pub fn init( - cx: &mut WindowContext, - focus_handle: &FocusHandle, - placeholder: &str, - disable: bool, - ) -> View { + pub fn init(cx: &mut WindowContext, focus_handle: &FocusHandle) -> View { let blink_manager = cx.new_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); let theme = cx.global::(); @@ -44,29 +40,24 @@ impl TextView { let m = Self { text: String::new(), - placeholder: placeholder.to_string(), + placeholder: "".to_string(), word_click: (0, 0), selection: 0..0, blink_manager, cursor, - disable, + disabled: false, masked: false, + focused: false, }; let view = cx.new_view(|cx| { - cx.on_blur( - focus_handle, - |view: &mut TextView, cx: &mut ViewContext<'_, TextView>| { - view.blink_manager.update(cx, BlinkManager::disable); - cx.emit(TextEvent::Blur); - }, - ) + cx.on_blur(focus_handle, |view: &mut TextView, cx| { + view.blur(cx); + }) .detach(); cx.on_focus(focus_handle, |view, cx| { - view.blink_manager.update(cx, |bm, cx| { - bm.blink_cursor(0, cx); - }); + view.focus(cx); }) .detach(); m @@ -99,6 +90,22 @@ impl TextView { cx.notify(); } + pub fn blur(&mut self, cx: &mut ViewContext) { + self.focused = false; + self.blink_manager.update(cx, BlinkManager::disable); + cx.notify(); + cx.emit(TextEvent::Blur); + } + + pub fn focus(&mut self, cx: &mut ViewContext) { + self.focused = true; + self.blink_manager.update(cx, |bm, cx| { + bm.blink_cursor(0, cx); + }); + cx.notify(); + cx.emit(TextEvent::Focus); + } + pub fn word_ranges(&self) -> Vec> { let mut words = Vec::new(); let mut last_was_boundary = true; @@ -150,6 +157,21 @@ impl TextView { }); } + pub fn set_masked(&mut self, masked: bool, cx: &mut ViewContext) { + self.masked = masked; + cx.notify(); + } + + pub fn set_placeholder(&mut self, placeholder: impl ToString, cx: &mut ViewContext) { + self.placeholder = placeholder.to_string(); + cx.notify(); + } + + pub fn set_disabled(&mut self, disabled: bool, cx: &mut ViewContext) { + self.disabled = disabled; + cx.notify(); + } + fn paint_cursors(&self, layout: &TextLayout, cx: &mut WindowContext) { let mut cursor = self.cursor.clone(); dbg!("--------- paint_cursors", &cursor); @@ -217,8 +239,9 @@ impl Element for TextView { impl Render for TextView { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let theme = cx.global::(); + + let view = cx.view().clone(); let mut text = self.text.clone(); - dbg!("textView render", &text); let mut style = TextStyle { color: theme.foreground, @@ -241,8 +264,11 @@ impl Render for TextView { highlights = vec![]; } + if !self.focused { + highlights = vec![]; + } + let styled_text = StyledText::new(text + " ").with_highlights(&style, highlights); - let view = cx.view().clone(); InteractiveText::new("text", styled_text).on_click(self.word_ranges(), move |ev, cx| { view.update(cx, |text_view, cx| { diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index 1bb78710..5577d74d 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -162,6 +162,7 @@ impl Colors { #[derive(Debug)] pub struct Theme { + pub transparent: Hsla, pub background: Hsla, pub foreground: Hsla, pub card: Hsla, @@ -189,6 +190,7 @@ impl Global for Theme {} impl From for Theme { fn from(colors: Colors) -> Self { Theme { + transparent: Hsla::transparent_black(), background: colors.background, foreground: colors.foreground, card: colors.card, @@ -220,7 +222,7 @@ pub enum ThemeMode { impl Theme { fn new() -> Self { - Self::from(Colors::light()) + Self::from(Colors::dark()) } pub fn init(cx: &mut AppContext) {