gpui-component/crates/ui/src/text_field/mod.rs
2024-06-26 16:16:08 +08:00

262 lines
11 KiB
Rust

mod blink_manager;
mod cursor_layout;
mod text_view;
use crate::theme::ActiveTheme;
use gpui::{
div, prelude::FluentBuilder as _, AppContext, ClipboardItem, EventEmitter, FocusHandle,
FocusableView, InteractiveElement, IntoElement, KeyDownEvent, MouseButton, ParentElement,
Render, RenderOnce, SharedString, Styled, View, ViewContext, WindowContext,
};
use std::time::Duration;
use text_view::TextView;
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
#[derive(Clone)]
pub struct TextField {
focus_handle: FocusHandle,
pub view: View<TextView>,
}
impl TextField {
pub fn new(cx: &mut WindowContext) -> Self {
let focus_handle = cx.focus_handle();
let view = TextView::init(cx, &focus_handle);
Self { focus_handle, view }
}
pub fn focus(&mut self, cx: &mut WindowContext) {
cx.focus(&self.focus_handle);
}
pub fn set_placeholder(
&mut self,
placeholder: impl Into<SharedString>,
cx: &mut WindowContext,
) {
self.view.update(cx, |text_view, cx| {
text_view.set_placeholder(placeholder, cx)
});
}
pub fn set_disabled(&mut self, disabled: bool, cx: &mut WindowContext) {
self.view
.update(cx, |text_view, cx| text_view.set_disabled(disabled, cx));
}
pub fn set_text(&mut self, text: &str, cx: &mut WindowContext) {
self.view
.update(cx, |text_view, cx| text_view.set_text(text, cx));
}
pub fn text(&self, cx: &AppContext) -> String {
self.view.read(cx).text.clone()
}
pub fn set_masked(&mut self, masked: bool, cx: &mut WindowContext) {
self.view
.update(cx, |text_view, cx| text_view.set_masked(masked, cx));
}
}
impl FocusableView for TextField {
fn focus_handle(&self, _cx: &gpui::AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for TextField {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle.clone();
let theme = cx.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(&focus_handle)
.when(!text_view.disabled, |this| {
this.on_mouse_down(
MouseButton::Left,
cx.listener(move |view, _, cx| {
cx.prevent_default();
view.focus_handle.focus(cx)
}),
)
})
.when(!disabled, |this| {
this.on_key_down(cx.listener(move |this, ev: &KeyDownEvent, cx| {
this.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 {
"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;
}
}
"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.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(),
});
}
cx.notify();
});
}))
})
.border_color(if focused { theme.ring } else { theme.input })
.border_1()
.rounded_sm()
.py_1()
.px_3()
.h_9()
.shadow_sm()
.min_w_20()
.bg(if disabled {
theme.muted
} else {
theme.background
})
.child(view)
}
}
pub enum TextEvent {
Input { text: String },
Blur,
Focus,
KeyDown(KeyDownEvent),
}
impl EventEmitter<TextEvent> for TextField {}