Update input new method

This commit is contained in:
Jason Lee 2024-06-24 17:39:56 +08:00
parent 94cb714ebf
commit dad6f4c431
5 changed files with 256 additions and 168 deletions

View file

@ -1,6 +1,6 @@
use gpui::{ use gpui::{
div, ClickEvent, IntoElement, ParentElement as _, Render, Styled as _, ViewContext, div, ClickEvent, IntoElement, ParentElement as _, Render, Styled as _, View, ViewContext,
WindowContext, VisualContext, WindowContext,
}; };
use crate::text_field::TextField; use crate::text_field::TextField;
@ -10,18 +10,25 @@ use super::story_case;
pub struct InputStory { pub struct InputStory {
input1: TextField, input1: TextField,
input2: TextField, input2: TextField,
mash_input: TextField,
disabled_input: TextField,
} }
impl InputStory { impl InputStory {
pub(crate) fn new(cx: &mut ViewContext<Self>) -> Self { pub(crate) fn new(cx: &mut WindowContext) -> Self {
let input1 = TextField::new("Enter text here...", false, cx); let input1 = TextField::new(cx).set_text("Hello 世界", cx);
input1
.view let mask_input = TextField::new(cx)
.update(cx, |text_view, cx| text_view.set_text("Hello 世界", cx)); .set_masked(true, cx)
.set_text("this-is-password", cx);
Self { Self {
input1, 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 { impl Render for InputStory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let input1 = self.input1.clone();
let input2 = self.input2.clone();
story_case("Input", "A text input field.").child( story_case("Input", "A text input field.").child(
div() div()
.flex() .flex()
.flex_col() .flex_col()
.justify_start() .justify_start()
.gap_3() .gap_3()
.child(input1) .child(self.input1.clone())
.child(input2), .child(self.input2.clone())
.child(self.disabled_input.clone())
.child(self.mash_input.clone()),
) )
} }
} }

View file

@ -86,7 +86,7 @@ impl Stories {
Self { Self {
active: StoryType::Button, active: StoryType::Button,
button_story: cx.new_view(|cx| ButtonStory {}), button_story: cx.new_view(|cx| ButtonStory {}),
input_story: cx.new_view(InputStory::new), input_story: cx.new_view(|cx| InputStory::new(cx)),
} }
} }

View file

@ -8,9 +8,9 @@ use crate::{
}; };
use blink_manager::BlinkManager; use blink_manager::BlinkManager;
use gpui::{ use gpui::{
div, ClipboardItem, Context, EventEmitter, FocusHandle, InteractiveElement, IntoElement, div, prelude::FluentBuilder as _, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
KeyDownEvent, Model, ParentElement, Render, RenderOnce, Styled, View, ViewContext, InteractiveElement, IntoElement, KeyDownEvent, Model, MouseButton, ParentElement, RenderOnce,
WindowContext, Styled, View, ViewContext, WindowContext,
}; };
use std::time::Duration; use std::time::Duration;
use text_view::TextView; use text_view::TextView;
@ -20,33 +20,53 @@ const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
#[derive(Clone, IntoElement)] #[derive(Clone, IntoElement)]
pub struct TextField { pub struct TextField {
focus_handle: FocusHandle, focus_handle: FocusHandle,
disable: bool,
blink_manager: Model<BlinkManager>, blink_manager: Model<BlinkManager>,
pub view: View<TextView>, pub view: View<TextView>,
} }
impl TextField { 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 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)); 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 { Self {
focus_handle, focus_handle,
view, view,
blink_manager, blink_manager,
disable,
} }
} }
pub fn focus(&self, cx: &mut WindowContext) { pub fn focus(&mut self, cx: &mut WindowContext) {
cx.focus(&self.focus_handle); 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<Self>) { fn handle_focus(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(TextEvent::Focus); cx.emit(TextEvent::Focus);
self.blink_manager.update(cx, BlinkManager::enable); 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 { impl RenderOnce for TextField {
fn render(self, cx: &mut WindowContext) -> impl IntoElement { fn render(self, cx: &mut WindowContext) -> impl IntoElement {
cx.focus(&self.focus_handle); let focus_handle = self.focus_handle.clone();
let theme = cx.global::<Theme>(); let theme = cx.global::<Theme>();
let view = self.view.clone(); 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() div()
.track_focus(&self.focus_handle) .track_focus(&focus_handle)
.on_key_down(move |ev, cx| { .when(!text_view.disabled, |this| {
self.view.update(cx, |text_view, cx| { this.on_mouse_down(MouseButton::Left, move |_, cx| {
let prev = text_view.text.clone(); cx.prevent_default();
cx.emit(TextEvent::KeyDown(ev.clone())); self.focus_handle.focus(cx)
let keystroke = &ev.keystroke.key; })
let chars = text_view.text.chars().collect::<Vec<char>>(); })
let m = ev.keystroke.modifiers.secondary(); .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::<Vec<char>>();
let m = ev.keystroke.modifiers.secondary();
if m { if m {
match keystroke.as_str() { match keystroke {
"a" => { "a" => {
text_view.selection = 0..chars.len(); 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;
} }
} "c" => {
"x" => { // if !text_view.masked {
let selected_text = let selected_text =
chars[text_view.selection.clone()].iter().collect(); chars[text_view.selection.clone()].iter().collect();
cx.write_to_clipboard(ClipboardItem::new(selected_text)); 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;
} }
} "v" => {
"right" => { let clipboard = cx.read_from_clipboard();
if text_view.selection.end < text_view.text.len() { if let Some(clipboard) = clipboard {
let i = if text_view.selection.start == text_view.selection.end let text = clipboard.text();
{ text_view.text.replace_range(
text_view.selection.end + 1 text_view.char_range_to_text_range(&text_view.text),
} else { text,
text_view.selection.end );
}; let i = text_view.selection.start + text.chars().count();
text_view.selection = i..i; text_view.selection = i..i;
}
} }
} "x" => {
"backspace" => { let selected_text =
if text_view.text.is_empty() && !ev.is_held { chars[text_view.selection.clone()].iter().collect();
// cx.emit(TextEvent::Back); cx.write_to_clipboard(ClipboardItem::new(selected_text));
} 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::<String>()
+ &(chars[text_view.selection.end.min(chars.len())..]
.iter()
.collect::<String>());
text_view.selection = i..i;
} else {
text_view.text.replace_range( text_view.text.replace_range(
text_view.char_range_to_text_range(&text_view.text), text_view.char_range_to_text_range(&text_view.text),
"", "",
); );
text_view.selection.end = text_view.selection.start; text_view.selection.end = text_view.selection.start;
} }
_ => {}
} }
"enter" => { } else if ev.keystroke.modifiers.control {
if ev.keystroke.modifiers.shift { // On macOS, ctrl+a, ctrl+e are used for moving cursor to start/end of line
text_view.text.insert( match keystroke {
text_view.char_range_to_text_range(&text_view.text).start, "a" => {
'\n', // Move cursor to first of line
); text_view.selection = 0..0;
let i = text_view.selection.start + 1;
text_view.selection = i..i;
} }
"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::<String>()
+ &(chars[text_view.selection.end.min(chars.len())..]
.iter()
.collect::<String>());
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 { if prev != text_view.text {
cx.emit(TextEvent::Input { cx.emit(TextEvent::Input {
text: text_view.text.clone(), text: text_view.text.clone(),
}); });
dbg!(&text_view.text, &text_view.selection); }
} cx.notify();
cx.notify(); });
}); })
}) })
.border_color(if self.focus_handle.is_focused(cx) { .when(!disabled, |this| {
theme.ring this.when(focused, |this| this.border_color(theme.ring))
} else {
theme.input
}) })
.border_color(theme.input)
.border_1() .border_1()
.rounded_sm() .rounded_sm()
.py_1p5() .py_1()
.px_3() .px_3()
.h_9()
.shadow_sm()
.min_w_20() .min_w_20()
.bg(if self.disable { .bg(if disabled {
theme.background.opacity(0.5) theme.muted
} else { } else {
theme.background theme.transparent
}) })
.child(view) .child(view)
} }

View file

@ -15,21 +15,17 @@ pub struct TextView {
pub placeholder: String, pub placeholder: String,
pub word_click: (usize, u16), pub word_click: (usize, u16),
pub selection: Range<usize>, pub selection: Range<usize>,
pub disable: bool, pub disabled: bool,
pub blink_manager: Model<BlinkManager>, pub blink_manager: Model<BlinkManager>,
pub cursor: CursorLayout, pub cursor: CursorLayout,
pub masked: bool, pub masked: bool,
pub focused: bool,
} }
impl EventEmitter<TextEvent> for TextView {} impl EventEmitter<TextEvent> for TextView {}
impl TextView { impl TextView {
pub fn init( pub fn init(cx: &mut WindowContext, focus_handle: &FocusHandle) -> View<Self> {
cx: &mut WindowContext,
focus_handle: &FocusHandle,
placeholder: &str,
disable: bool,
) -> View<Self> {
let blink_manager = cx.new_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); let blink_manager = cx.new_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
let theme = cx.global::<Theme>(); let theme = cx.global::<Theme>();
@ -44,29 +40,24 @@ impl TextView {
let m = Self { let m = Self {
text: String::new(), text: String::new(),
placeholder: placeholder.to_string(), placeholder: "".to_string(),
word_click: (0, 0), word_click: (0, 0),
selection: 0..0, selection: 0..0,
blink_manager, blink_manager,
cursor, cursor,
disable, disabled: false,
masked: false, masked: false,
focused: false,
}; };
let view = cx.new_view(|cx| { let view = cx.new_view(|cx| {
cx.on_blur( cx.on_blur(focus_handle, |view: &mut TextView, cx| {
focus_handle, view.blur(cx);
|view: &mut TextView, cx: &mut ViewContext<'_, TextView>| { })
view.blink_manager.update(cx, BlinkManager::disable);
cx.emit(TextEvent::Blur);
},
)
.detach(); .detach();
cx.on_focus(focus_handle, |view, cx| { cx.on_focus(focus_handle, |view, cx| {
view.blink_manager.update(cx, |bm, cx| { view.focus(cx);
bm.blink_cursor(0, cx);
});
}) })
.detach(); .detach();
m m
@ -99,6 +90,22 @@ impl TextView {
cx.notify(); cx.notify();
} }
pub fn blur(&mut self, cx: &mut ViewContext<Self>) {
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>) {
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<Range<usize>> { pub fn word_ranges(&self) -> Vec<Range<usize>> {
let mut words = Vec::new(); let mut words = Vec::new();
let mut last_was_boundary = true; let mut last_was_boundary = true;
@ -150,6 +157,21 @@ impl TextView {
}); });
} }
pub fn set_masked(&mut self, masked: bool, cx: &mut ViewContext<Self>) {
self.masked = masked;
cx.notify();
}
pub fn set_placeholder(&mut self, placeholder: impl ToString, cx: &mut ViewContext<Self>) {
self.placeholder = placeholder.to_string();
cx.notify();
}
pub fn set_disabled(&mut self, disabled: bool, cx: &mut ViewContext<Self>) {
self.disabled = disabled;
cx.notify();
}
fn paint_cursors(&self, layout: &TextLayout, cx: &mut WindowContext) { fn paint_cursors(&self, layout: &TextLayout, cx: &mut WindowContext) {
let mut cursor = self.cursor.clone(); let mut cursor = self.cursor.clone();
dbg!("--------- paint_cursors", &cursor); dbg!("--------- paint_cursors", &cursor);
@ -217,8 +239,9 @@ impl Element for TextView {
impl Render for TextView { impl Render for TextView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let theme = cx.global::<Theme>(); let theme = cx.global::<Theme>();
let view = cx.view().clone();
let mut text = self.text.clone(); let mut text = self.text.clone();
dbg!("textView render", &text);
let mut style = TextStyle { let mut style = TextStyle {
color: theme.foreground, color: theme.foreground,
@ -241,8 +264,11 @@ impl Render for TextView {
highlights = vec![]; highlights = vec![];
} }
if !self.focused {
highlights = vec![];
}
let styled_text = StyledText::new(text + " ").with_highlights(&style, highlights); 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| { InteractiveText::new("text", styled_text).on_click(self.word_ranges(), move |ev, cx| {
view.update(cx, |text_view, cx| { view.update(cx, |text_view, cx| {

View file

@ -162,6 +162,7 @@ impl Colors {
#[derive(Debug)] #[derive(Debug)]
pub struct Theme { pub struct Theme {
pub transparent: Hsla,
pub background: Hsla, pub background: Hsla,
pub foreground: Hsla, pub foreground: Hsla,
pub card: Hsla, pub card: Hsla,
@ -189,6 +190,7 @@ impl Global for Theme {}
impl From<Colors> for Theme { impl From<Colors> for Theme {
fn from(colors: Colors) -> Self { fn from(colors: Colors) -> Self {
Theme { Theme {
transparent: Hsla::transparent_black(),
background: colors.background, background: colors.background,
foreground: colors.foreground, foreground: colors.foreground,
card: colors.card, card: colors.card,
@ -220,7 +222,7 @@ pub enum ThemeMode {
impl Theme { impl Theme {
fn new() -> Self { fn new() -> Self {
Self::from(Colors::light()) Self::from(Colors::dark())
} }
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {