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::{
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>) -> 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<Self>) -> 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()),
)
}
}

View file

@ -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)),
}
}

View file

@ -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<BlinkManager>,
pub view: View<TextView>,
}
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<Self>) {
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::<Theme>();
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::<Vec<char>>();
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::<Vec<char>>();
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::<String>()
+ &(chars[text_view.selection.end.min(chars.len())..]
.iter()
.collect::<String>());
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::<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 {
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)
}

View file

@ -15,21 +15,17 @@ pub struct TextView {
pub placeholder: String,
pub word_click: (usize, u16),
pub selection: Range<usize>,
pub disable: bool,
pub disabled: bool,
pub blink_manager: Model<BlinkManager>,
pub cursor: CursorLayout,
pub masked: bool,
pub focused: bool,
}
impl EventEmitter<TextEvent> for TextView {}
impl TextView {
pub fn init(
cx: &mut WindowContext,
focus_handle: &FocusHandle,
placeholder: &str,
disable: bool,
) -> View<Self> {
pub fn init(cx: &mut WindowContext, focus_handle: &FocusHandle) -> View<Self> {
let blink_manager = cx.new_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
let theme = cx.global::<Theme>();
@ -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>) {
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>> {
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>) {
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) {
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<Self>) -> impl IntoElement {
let theme = cx.global::<Theme>();
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| {

View file

@ -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<Colors> 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) {