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, } 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, 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) -> 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::>(); 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::() + &(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(), }); } 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 for TextField {}