use std::cell::RefCell; use std::ops::Range; use std::rc::Rc; use std::time::{Duration, Instant}; use unicode_segmentation::UnicodeSegmentation; use gpui::prelude::FluentBuilder as _; use gpui::{ App, AppContext as _, Bounds, ClipboardItem, Context, DispatchPhase, ElementId, Entity, EntityInputHandler, EventEmitter, Font, InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, TextAlign, TextRun, UTF16Selection, Window, actions, canvas, div, fill, point, px, }; use gpui_native_theme::{ActiveTheme as _, ColorTokens, Size}; use crate::common::{Sizable, h_flex}; /// The key context input bindings match against. pub const INPUT_CONTEXT: &str = "Input"; /// Consecutive edits within this interval merge into one undo entry. const DEFAULT_GROUP_INTERVAL: Duration = Duration::from_millis(300); /// Maximum undo entries kept; the oldest drops off the front. const MAX_HISTORY: usize = 100; /// Significant changes in an [`InputState`], observable via `cx.subscribe`. #[derive(Clone, Debug, PartialEq, Eq)] pub enum InputStateEvent { /// The text content changed: typing, paste, IME commit, or `set_value`. TextChanged, /// The field gained focus. Focused, /// The field lost focus. Blurred, /// Enter was pressed Submitted, } actions!( input, [ Escape, Submit, Backspace, Delete, DeleteWordLeft, DeleteWordRight, DeleteToBeginningOfLine, Left, Right, SelectLeft, SelectRight, WordLeft, WordRight, SelectWordLeft, SelectWordRight, Home, End, SelectHome, SelectEnd, SelectAll, Copy, Cut, Paste, Undo, Redo, ] ); /// Keybindings for input actions. #[derive(Clone, Debug)] pub struct InputBindings { pub escape: Option, pub submit: Option, pub backspace: Option, pub delete: Option, pub delete_word_left: Option, pub delete_word_right: Option, pub delete_to_beginning_of_line: Option, pub left: Option, pub right: Option, pub select_left: Option, pub select_right: Option, pub word_left: Option, pub word_right: Option, pub select_word_left: Option, pub select_word_right: Option, pub home: Option, pub end: Option, pub select_home: Option, pub select_end: Option, pub select_all: Option, pub copy: Option, pub cut: Option, pub paste: Option, pub undo: Option, pub redo: Option, } impl Default for InputBindings { /// The platform-specific defaults: cmd on macOS, ctrl elsewhere. fn default() -> Self { let context = Some(INPUT_CONTEXT); #[cfg(target_os = "macos")] let bindings = Self { escape: Some(KeyBinding::new("escape", Escape, context)), submit: Some(KeyBinding::new("enter", Submit, context)), backspace: Some(KeyBinding::new("backspace", Backspace, context)), delete: Some(KeyBinding::new("delete", Delete, context)), delete_word_left: Some(KeyBinding::new("alt-backspace", DeleteWordLeft, context)), delete_word_right: Some(KeyBinding::new("alt-delete", DeleteWordRight, context)), delete_to_beginning_of_line: Some(KeyBinding::new( "cmd-backspace", DeleteToBeginningOfLine, context, )), left: Some(KeyBinding::new("left", Left, context)), right: Some(KeyBinding::new("right", Right, context)), select_left: Some(KeyBinding::new("shift-left", SelectLeft, context)), select_right: Some(KeyBinding::new("shift-right", SelectRight, context)), word_left: Some(KeyBinding::new("alt-left", WordLeft, context)), word_right: Some(KeyBinding::new("alt-right", WordRight, context)), select_word_left: Some(KeyBinding::new("alt-shift-left", SelectWordLeft, context)), select_word_right: Some(KeyBinding::new("alt-shift-right", SelectWordRight, context)), home: Some(KeyBinding::new("home", Home, context)), end: Some(KeyBinding::new("end", End, context)), select_home: Some(KeyBinding::new("shift-home", SelectHome, context)), select_end: Some(KeyBinding::new("shift-end", SelectEnd, context)), select_all: Some(KeyBinding::new("cmd-a", SelectAll, context)), copy: Some(KeyBinding::new("cmd-c", Copy, context)), cut: Some(KeyBinding::new("cmd-x", Cut, context)), paste: Some(KeyBinding::new("cmd-v", Paste, context)), undo: Some(KeyBinding::new("cmd-z", Undo, context)), redo: Some(KeyBinding::new("cmd-shift-z", Redo, context)), }; #[cfg(not(target_os = "macos"))] let bindings = Self { escape: Some(KeyBinding::new("escape", Escape, context)), submit: Some(KeyBinding::new("enter", Submit, context)), backspace: Some(KeyBinding::new("backspace", Backspace, context)), delete: Some(KeyBinding::new("delete", Delete, context)), delete_word_left: Some(KeyBinding::new("ctrl-backspace", DeleteWordLeft, context)), delete_word_right: Some(KeyBinding::new("ctrl-delete", DeleteWordRight, context)), delete_to_beginning_of_line: Some(KeyBinding::new( "ctrl-shift-backspace", DeleteToBeginningOfLine, context, )), left: Some(KeyBinding::new("left", Left, context)), right: Some(KeyBinding::new("right", Right, context)), select_left: Some(KeyBinding::new("shift-left", SelectLeft, context)), select_right: Some(KeyBinding::new("shift-right", SelectRight, context)), word_left: Some(KeyBinding::new("ctrl-left", WordLeft, context)), word_right: Some(KeyBinding::new("ctrl-right", WordRight, context)), select_word_left: Some(KeyBinding::new("ctrl-shift-left", SelectWordLeft, context)), select_word_right: Some(KeyBinding::new( "ctrl-shift-right", SelectWordRight, context, )), home: Some(KeyBinding::new("home", Home, context)), end: Some(KeyBinding::new("end", End, context)), select_home: Some(KeyBinding::new("shift-home", SelectHome, context)), select_end: Some(KeyBinding::new("shift-end", SelectEnd, context)), select_all: Some(KeyBinding::new("ctrl-a", SelectAll, context)), copy: Some(KeyBinding::new("ctrl-c", Copy, context)), cut: Some(KeyBinding::new("ctrl-x", Cut, context)), paste: Some(KeyBinding::new("ctrl-v", Paste, context)), undo: Some(KeyBinding::new("ctrl-z", Undo, context)), redo: Some(KeyBinding::new("ctrl-shift-z", Redo, context)), }; bindings } } impl InputBindings { /// A table with every key unbound, for building custom bindings. pub fn empty() -> Self { Self { escape: None, submit: None, backspace: None, delete: None, delete_word_left: None, delete_word_right: None, delete_to_beginning_of_line: None, left: None, right: None, select_left: None, select_right: None, word_left: None, word_right: None, select_word_left: None, select_word_right: None, home: None, end: None, select_home: None, select_end: None, select_all: None, copy: None, cut: None, paste: None, undo: None, redo: None, } } /// Collects the table into keybindings. pub fn into_bindings(self) -> Vec { let mut bindings: Vec = [ self.escape, self.submit, self.backspace, self.delete, self.delete_word_left, self.delete_word_right, self.delete_to_beginning_of_line, self.left, self.right, self.select_left, self.select_right, self.word_left, self.word_right, self.select_word_left, self.select_word_right, self.home, self.end, self.select_home, self.select_end, self.select_all, self.copy, self.cut, self.paste, self.undo, self.redo, ] .into_iter() .flatten() .collect(); #[cfg(target_os = "macos")] { let context = Some(INPUT_CONTEXT); bindings.extend([ KeyBinding::new("cmd-left", Home, context), KeyBinding::new("cmd-right", End, context), KeyBinding::new("cmd-up", Home, context), KeyBinding::new("cmd-down", End, context), KeyBinding::new("cmd-shift-left", SelectHome, context), KeyBinding::new("cmd-shift-right", SelectEnd, context), ]); } bindings } } /// Registers input keybindings on the application. /// /// Pass `None` for the platform defaults. pub fn bind_input_keys(cx: &mut App, bindings: B) where B: Into>, { cx.bind_keys(bindings.into().unwrap_or_default().into_bindings()); } /// Input layout information for hit testing and caret positioning. struct InputLayout { line: gpui::ShapedLine, bounds: Bounds, /// Where the text was painted inside `bounds` (padding minus scroll). origin: gpui::Point, } /// Handler for key down events. type KeyDownHandler = std::rc::Rc; /// Handler for mouse down events. type MouseDownHandler = std::rc::Rc; /// History entry for undo/redo operations. #[derive(Clone, Debug)] struct HistoryEntry { /// The range the replacement occupies after the edit. range: Range, /// The text the edit replaced, restored on undo. old_text: String, /// The length of the replacement. new_len: usize, /// The selection before the edit. cursor: usize, anchor: usize, /// When the edit happened, for grouping. timestamp: Instant, } impl HistoryEntry { /// Applies this entry in reverse, returning the entry that reverses it back. fn reverse(&self, text: &mut String) -> HistoryEntry { let end = (self.range.start + self.new_len).min(text.len()); let removed = text[self.range.start..end].to_string(); text.replace_range(self.range.start..end, &self.old_text); HistoryEntry { range: self.range.start..self.range.start + self.old_text.len(), old_text: removed, new_len: self.old_text.len(), cursor: self.cursor, anchor: self.anchor, timestamp: self.timestamp, } } } /// Input state for a single-line text field. pub struct InputState { text: String, /// The caret's byte offset. cursor: usize, /// The byte offset the selection extends from. anchor: usize, /// The byte range of in-progress IME composition, if any. marked_range: Option>, /// Whether the field had focus at the last paint, for focus transitions. focused: bool, focus_handle: gpui::FocusHandle, /// The key-down handler, bound to this entity with `cx.listener`. key_down_handler: Option, /// The mouse-down handler, bound to this entity with `cx.listener`. mouse_down_handler: Option, /// Whether a drag-select is in progress. dragging: bool, /// Patch history. `redo_stack` holds undone entries awaiting redo. undo_stack: Vec, redo_stack: Vec, /// Consecutive edits within this interval merge into one undo entry. group_interval: Duration, /// When set, user edits are refused, focus, movement, selection and copy stay. read_only: bool, /// The shaped line and element bounds, recorded during each paint, /// and shared with hit-testing and the IME geometry conversions. layout: Rc>>, } impl EventEmitter for InputState {} impl InputState { fn new_inner(cx: &Context) -> Self { Self { text: String::new(), cursor: 0, anchor: 0, marked_range: None, focused: false, focus_handle: cx.focus_handle(), key_down_handler: Some(Rc::new(cx.listener( |state, event: &KeyDownEvent, window, cx| { state.handle_key_down(event, window, cx); }, ))), mouse_down_handler: Some(Rc::new(cx.listener( |state, event: &MouseDownEvent, window, cx| { state.handle_mouse_down(event, window, cx); }, ))), dragging: false, undo_stack: Vec::new(), redo_stack: Vec::new(), group_interval: DEFAULT_GROUP_INTERVAL, read_only: false, layout: Rc::new(RefCell::new(None)), } } /// Creates the state entity for an application-owned input. pub fn new(cx: &mut App) -> Entity { cx.new(|cx| Self::new_inner(cx)) } /// Creates the state entity with initial text, caret at the end. /// /// Newlines become spaces. pub fn new_with_text(text: impl Into, cx: &mut App) -> Entity { let text = sanitize(text.into()); cx.new(|cx| { let mut state = Self::new_inner(cx); state.text = text; state.cursor = state.text.len(); state.anchor = state.cursor; state }) } /// The current text. pub fn value(&self) -> &str { &self.text } /// Replaces the whole value and collapses the selection to the end. /// /// Newlines become spaces. Emits [`InputStateEvent::TextChanged`] /// when the value changed, setting the current value is a no-op. pub fn set_value(&mut self, value: V, window: &mut Window, cx: &mut Context) where V: Into, { self.set_value_inner(value, true, window, cx); } /// Like [`set_value`], but does not emit `TextChanged`. pub fn set_value_silent(&mut self, value: V, window: &mut Window, cx: &mut Context) where V: Into, { self.set_value_inner(value, false, window, cx); } fn set_value_inner( &mut self, value: V, emit: bool, window: &mut Window, cx: &mut Context, ) where V: Into, { let text = sanitize(value.into()); if text == self.text { return; } self.text = text; self.cursor = self.text.len(); self.anchor = self.cursor; self.marked_range = None; self.undo_stack.clear(); self.redo_stack.clear(); if emit { cx.emit(InputStateEvent::TextChanged); } self.notify(window, cx); } /// Sets how long consecutive edits keep merging into one undo entry. pub fn set_group_interval(&mut self, interval: Duration) { self.group_interval = interval; } /// Sets whether user edits are refused. pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context) { if self.read_only != read_only { self.read_only = read_only; cx.notify(); } } /// Whether user edits are refused. pub fn is_read_only(&self) -> bool { self.read_only } /// Records `old_text` at `range` being replaced by `new_len` bytes. fn push_history(&mut self, range: Range, old_text: String, new_len: usize) { self.redo_stack.clear(); let now = Instant::now(); if let Some(last) = self.undo_stack.last_mut() && now.duration_since(last.timestamp) < self.group_interval { let last_end = last.range.start + last.new_len; // Typing that continues right after the previous edit. if range.start == last_end { last.new_len += new_len; last.timestamp = now; return; } // A deletion of text that sat right before the previous edit. if range.end == last.range.start && new_len == 0 { last.range.start = range.start; last.old_text = old_text + &last.old_text; last.timestamp = now; return; } // An edit fully inside the previous edit's replacement. if range.start >= last.range.start && range.end <= last_end { last.new_len += new_len; last.new_len -= range.len(); last.timestamp = now; return; } } self.undo_stack.push(HistoryEntry { range: range.start..range.start + new_len, old_text, new_len, cursor: self.cursor, anchor: self.anchor, timestamp: now, }); if self.undo_stack.len() > MAX_HISTORY { self.undo_stack.remove(0); } } /// The focus handle, for applications that focus the field programmatically. pub fn focus_handle(&self) -> gpui::FocusHandle { self.focus_handle.clone() } /// The selected byte range of the buffer. fn selection(&self) -> Range { self.cursor.min(self.anchor)..self.cursor.max(self.anchor) } fn notify(&mut self, window: &mut Window, cx: &mut Context) { cx.notify(); window.notify_text_input_changed(&self.focus_handle); } /// Emits `Focused`/`Blurred` when the field's focus state changed since the last paint. fn sync_focus(&mut self, focused: bool, cx: &mut Context) { if self.focused == focused { return; } self.focused = focused; cx.emit(if focused { InputStateEvent::Focused } else { InputStateEvent::Blurred }); } /// Replaces a byte range with `text` and collapses the caret to its end. fn replace_range( &mut self, range: Range, text: &str, window: &mut Window, cx: &mut Context, ) { if self.read_only { return; } if range.is_empty() && text.is_empty() { return; } let text = sanitize(text); let old_text = self.text[range.clone()].to_string(); self.push_history(range.clone(), old_text, text.len()); self.text.replace_range(range.clone(), &text); self.cursor = range.start + text.len(); self.anchor = self.cursor; self.marked_range = None; cx.emit(InputStateEvent::TextChanged); self.notify(window, cx); } /// Deletes the selection, or one character/word before the caret. fn delete_backward(&mut self, word: bool, window: &mut Window, cx: &mut Context) { if !self.selection().is_empty() { let range = self.selection(); self.replace_range(range, "", window, cx); } else if self.cursor > 0 { let start = if word { previous_word_start(&self.text, self.cursor) } else { previous_grapheme_start(&self.text, self.cursor) }; self.replace_range(start..self.cursor, "", window, cx); } } /// Deletes the selection, or one character/word after the caret. fn delete_forward(&mut self, word: bool, window: &mut Window, cx: &mut Context) { if !self.selection().is_empty() { let range = self.selection(); self.replace_range(range, "", window, cx); } else if self.cursor < self.text.len() { let end = if word { next_word_end(&self.text, self.cursor) } else { next_grapheme_start(&self.text, self.cursor) }; self.replace_range(self.cursor..end, "", window, cx); } } /// Moves the caret, extending the selection when `extend`. fn move_cursor( &mut self, to: usize, extend: bool, window: &mut Window, cx: &mut Context, ) { self.cursor = to.clamp(0, self.text.len()); if !extend { self.anchor = self.cursor; } self.notify(window, cx); } /// Selects `range`. fn select_range(&mut self, range: Range, window: &mut Window, cx: &mut Context) { self.anchor = range.start; self.cursor = range.end; self.notify(window, cx); } /// Copies the selected text to the clipboard. fn copy_selection(&mut self, _window: &mut Window, cx: &mut Context) -> bool { let range = self.selection(); if range.is_empty() { return false; } cx.write_to_clipboard(ClipboardItem::new_string( self.text[range.clone()].to_string(), )); true } /// Deletes the selected range, if any. fn delete_selection(&mut self, window: &mut Window, cx: &mut Context) { let range = self.selection(); if !range.is_empty() { self.replace_range(range, "", window, cx); } } /// Shapes the buffer with the paint-time text style. fn shape_current_text(&self, window: &Window) -> gpui::ShapedLine { let text = self.text.clone(); let text_style = window.text_style(); let font = Font { family: text_style.font_family.clone(), features: text_style.font_features.clone(), weight: text_style.font_weight, style: text_style.font_style, fallbacks: text_style.font_fallbacks.clone(), }; let font_size = text_style.font_size.to_pixels(window.rem_size()); let runs = [TextRun { len: text.len(), font, color: text_style.color, background_color: None, underline: None, strikethrough: None, }]; window .text_system() .shape_line(text.into(), font_size, &runs, None) } /// Stores the paint geometry for hit-testing and the IME conversions. fn store_layout( &mut self, line: gpui::ShapedLine, bounds: Bounds, padding: Pixels, line_height: Pixels, ) -> (gpui::Point, Pixels) { let selection = self.selection(); let caret_x = line.x_for_index(selection.end); // Horizontal scroll keeps the caret inside the field. let visible = bounds.size.width - padding * 2.0; let scroll_x = if caret_x > visible { caret_x - visible } else { Pixels::ZERO }; let origin = point( bounds.left() + padding - scroll_x, bounds.top() + (bounds.size.height - line_height) / 2.0, ); *self.layout.borrow_mut() = Some(InputLayout { line, bounds, origin, }); (origin, caret_x) } /// Paints the selection highlight behind the text. fn paint_selection( &self, origin: gpui::Point, caret_x: Pixels, bounds: Bounds, color: gpui::Hsla, window: &mut Window, ) { let selection = self.selection(); if selection.is_empty() { return; } let layout = self.layout.borrow(); let Some(layout) = layout.as_ref() else { return; }; let start_x = layout.line.x_for_index(selection.start); let rect = Bounds::from_corners( point(origin.x + start_x.min(caret_x), bounds.top() + px(1.)), point(origin.x + start_x.max(caret_x), bounds.bottom() - px(1.)), ); window.paint_quad(fill(rect, color)); } /// Paints the text itself. fn paint_text( &self, origin: gpui::Point, line_height: Pixels, window: &mut Window, cx: &mut App, ) { let layout = self.layout.borrow(); let Some(layout) = layout.as_ref() else { return; }; layout .line .paint(origin, line_height, TextAlign::Left, None, window, cx) .ok(); } /// Paints the caret at the cursor position. fn paint_caret( &self, origin: gpui::Point, caret_x: Pixels, font_size: Pixels, line_height: Pixels, color: gpui::Hsla, window: &mut Window, ) { let caret_top = origin.y + (line_height - font_size) / 2.0; let caret = Bounds::from_corners( point(origin.x + caret_x, caret_top), point(origin.x + caret_x + px(1.5), caret_top + font_size), ); window.paint_quad(fill(caret, color)); } /// Paints the field contents; returns whether a drag-select is in progress. fn paint( &mut self, line: gpui::ShapedLine, bounds: Bounds, padding: Pixels, colors: ColorTokens, window: &mut Window, cx: &mut Context, ) -> bool { let focused = self.focus_handle.is_focused(window); self.sync_focus(focused, cx); let text_style = window.text_style(); let font_size = text_style.font_size.to_pixels(window.rem_size()); let line_height = text_style .line_height .to_pixels(text_style.font_size, window.rem_size()); let (origin, caret_x) = self.store_layout(line, bounds, padding, line_height); if focused && !self.selection().is_empty() { self.paint_selection(origin, caret_x, bounds, colors.selection, window); } self.paint_text(origin, line_height, window, cx); // A caret promises that typing lands there; in read-only it does not. if focused && !self.read_only && self.selection().is_empty() { self.paint_caret( origin, caret_x, font_size, line_height, colors.foreground, window, ); } self.dragging } /// The text index at window x, using the last painted layout. fn index_at_x(&self, x: Pixels) -> usize { let layout = self.layout.borrow(); let Some(layout) = layout.as_ref() else { return 0; }; layout .line .closest_index_for_x(x - layout.origin.x) .min(self.text.len()) } /// Extends the selection to `x` while drag-selecting. fn drag_select_to(&mut self, x: Pixels, window: &mut Window, cx: &mut Context) { self.cursor = self.index_at_x(x); self.notify(window, cx); } /// Registers the window-level listeners that continue a drag-select /// while the pointer moves outside the field. fn register_drag_listeners(entity: &Entity, window: &mut Window) { window.on_mouse_event({ let state = entity.clone(); move |event: &MouseMoveEvent, phase, window, cx| { if phase != DispatchPhase::Bubble || event.pressed_button != Some(MouseButton::Left) { return; } state.update(cx, |state, cx| { state.drag_select_to(event.position.x, window, cx) }); } }); window.on_mouse_event({ let state = entity.clone(); move |event: &MouseUpEvent, phase, _window, cx| { if phase != DispatchPhase::Bubble || event.button != MouseButton::Left { return; } state.update(cx, |state, _| state.dragging = false); } }); } /// The mouse-down handler: starts a drag and places or extends the selection. fn handle_mouse_down( &mut self, event: &MouseDownEvent, window: &mut Window, cx: &mut Context, ) { self.dragging = true; let index = self.index_at_x(event.position.x); if event.click_count >= 3 { // A single-line field is one line: triple-click selects all of it. let len = self.text.len(); self.select_range(0..len, window, cx); } else if event.click_count == 2 { let start = previous_word_start(&self.text, index); let end = next_word_end(&self.text, index); self.select_range(start..end, window, cx); } else if event.modifiers.shift { self.cursor = index; self.notify(window, cx); } else { self.cursor = index; self.anchor = index; self.notify(window, cx); } } /// The key-down handler: inserts typed characters. fn handle_key_down( &mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context, ) { if !self.focus_handle.contains_focused(window, cx) { return; } let keystroke = &event.keystroke; // Chars typed with a menu modifier are shortcuts, not text, // and control characters (enter, tab) have their own semantics. if let Some(text) = &keystroke.key_char && !keystroke.modifiers.platform && !keystroke.modifiers.control && text.chars().all(|ch| !ch.is_control()) { let range = self.selection(); self.replace_range(range, text, window, cx); cx.stop_propagation(); } } fn left(&mut self, _: &Left, window: &mut Window, cx: &mut Context) { let to = previous_grapheme_start(&self.text, self.cursor); self.move_cursor(to, false, window, cx); } fn select_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context) { let to = previous_grapheme_start(&self.text, self.cursor); self.move_cursor(to, true, window, cx); } fn right(&mut self, _: &Right, window: &mut Window, cx: &mut Context) { let to = next_grapheme_start(&self.text, self.cursor); self.move_cursor(to, false, window, cx); } fn select_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context) { let to = next_grapheme_start(&self.text, self.cursor); self.move_cursor(to, true, window, cx); } fn word_left(&mut self, _: &WordLeft, window: &mut Window, cx: &mut Context) { let to = previous_word_start(&self.text, self.cursor); self.move_cursor(to, false, window, cx); } fn select_word_left( &mut self, _: &SelectWordLeft, window: &mut Window, cx: &mut Context, ) { let to = previous_word_start(&self.text, self.cursor); self.move_cursor(to, true, window, cx); } fn word_right(&mut self, _: &WordRight, window: &mut Window, cx: &mut Context) { let to = next_word_end(&self.text, self.cursor); self.move_cursor(to, false, window, cx); } fn select_word_right( &mut self, _: &SelectWordRight, window: &mut Window, cx: &mut Context, ) { let to = next_word_end(&self.text, self.cursor); self.move_cursor(to, true, window, cx); } fn home(&mut self, _: &Home, window: &mut Window, cx: &mut Context) { self.move_cursor(0, false, window, cx); } fn end(&mut self, _: &End, window: &mut Window, cx: &mut Context) { self.move_cursor(self.text.len(), false, window, cx); } fn select_home(&mut self, _: &SelectHome, window: &mut Window, cx: &mut Context) { self.move_cursor(0, true, window, cx); } fn select_end(&mut self, _: &SelectEnd, window: &mut Window, cx: &mut Context) { self.move_cursor(self.text.len(), true, window, cx); } fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context) { self.delete_backward(false, window, cx); } fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context) { self.delete_forward(false, window, cx); } fn delete_word_left( &mut self, _: &DeleteWordLeft, window: &mut Window, cx: &mut Context, ) { self.delete_backward(true, window, cx); } fn delete_word_right( &mut self, _: &DeleteWordRight, window: &mut Window, cx: &mut Context, ) { self.delete_forward(true, window, cx); } fn delete_to_beginning_of_line( &mut self, _: &DeleteToBeginningOfLine, window: &mut Window, cx: &mut Context, ) { let range = 0..self.cursor; self.replace_range(range, "", window, cx); } fn select_all(&mut self, _: &SelectAll, window: &mut Window, cx: &mut Context) { let len = self.text.len(); self.select_range(0..len, window, cx); } fn copy(&mut self, _: &Copy, window: &mut Window, cx: &mut Context) { if self.selection().is_empty() { // Let an outer handler copy its own selection instead. cx.propagate(); return; } self.copy_selection(window, cx); } fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context) { // Read-only refuses cut entirely, clipboard included. if self.read_only { return; } if self.copy_selection(window, cx) { self.delete_selection(window, cx); } } fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context) { if let Some(item) = cx.read_from_clipboard() && let Some(text) = item.text() { let range = self.selection(); self.replace_range(range, &text, window, cx); } } fn undo(&mut self, _: &Undo, window: &mut Window, cx: &mut Context) { if self.read_only { return; } let Some(entry) = self.undo_stack.pop() else { return; }; let inverse = entry.reverse(&mut self.text); self.redo_stack.push(inverse); self.cursor = entry.cursor.min(self.text.len()); self.anchor = entry.anchor.min(self.text.len()); cx.emit(InputStateEvent::TextChanged); self.notify(window, cx); } fn redo(&mut self, _: &Redo, window: &mut Window, cx: &mut Context) { if self.read_only { return; } let Some(entry) = self.redo_stack.pop() else { return; }; let inverse = entry.reverse(&mut self.text); self.undo_stack.push(inverse); // The caret lands at the end of the restored text, // not where it was before the original edit. let caret = entry.range.start + entry.old_text.len(); self.cursor = caret.min(self.text.len()); self.anchor = self.cursor; cx.emit(InputStateEvent::TextChanged); self.notify(window, cx); } fn submit(&mut self, _: &Submit, _window: &mut Window, cx: &mut Context) { cx.emit(InputStateEvent::Submitted); } fn escape(&mut self, _: &Escape, window: &mut Window, cx: &mut Context) { window.blur(cx); } } impl EntityInputHandler for InputState { fn text_for_range( &mut self, range_utf16: Range, adjusted_range: &mut Option>, _window: &mut Window, _cx: &mut Context, ) -> Option { let range = utf16_range_to_utf8(&self.text, range_utf16); adjusted_range.replace(utf8_range_to_utf16(&self.text, range.clone())); Some(self.text[range].to_string()) } fn selected_text_range( &mut self, _ignore_disabled_input: bool, _window: &mut Window, _cx: &mut Context, ) -> Option { Some(UTF16Selection { range: utf8_range_to_utf16(&self.text, self.selection()), reversed: self.cursor < self.anchor, }) } fn marked_text_range( &self, _window: &mut Window, _cx: &mut Context, ) -> Option> { self.marked_range .clone() .map(|range| utf8_range_to_utf16(&self.text, range)) } fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context) { self.marked_range = None; } fn replace_text_in_range( &mut self, range_utf16: Option>, text: &str, window: &mut Window, cx: &mut Context, ) { // Simulated typing delivers named keys' control characters here if text.chars().all(|ch| ch.is_control()) { return; } let range = range_utf16.map_or_else( || self.selection(), |range| utf16_range_to_utf8(&self.text, range), ); self.replace_range(range, text, window, cx); } fn replace_and_mark_text_in_range( &mut self, range_utf16: Option>, new_text: &str, new_selected_range_utf16: Option>, window: &mut Window, cx: &mut Context, ) { if self.read_only { return; } let range = range_utf16.map_or_else( || self.selection(), |range| utf16_range_to_utf8(&self.text, range), ); let new_text = sanitize(new_text); let old_text = self.text[range.clone()].to_string(); self.push_history(range.clone(), old_text, new_text.len()); self.text.replace_range(range.clone(), &new_text); let start = range.start; let end = start + new_text.len(); self.marked_range = Some(start..end); let selected_end = new_selected_range_utf16 .map(|selected| utf16_range_to_utf8(&self.text, selected).end) .unwrap_or(end); self.cursor = selected_end; self.anchor = selected_end; cx.emit(InputStateEvent::TextChanged); self.notify(window, cx); } fn bounds_for_range( &mut self, range_utf16: Range, _element_bounds: Bounds, _window: &mut Window, _cx: &mut Context, ) -> Option> { let layout = self.layout.borrow(); let layout = layout.as_ref()?; let start = layout .line .x_for_index(utf16_to_utf8_offset(&self.text, range_utf16.start)); let end = layout .line .x_for_index(utf16_to_utf8_offset(&self.text, range_utf16.end)); // `origin` is already an absolute window point. Some(Bounds::from_corners( point(layout.origin.x + start.min(end), layout.bounds.top()), point(layout.origin.x + start.max(end), layout.bounds.bottom()), )) } fn character_index_for_point( &mut self, window_point: gpui::Point, _window: &mut Window, _cx: &mut Context, ) -> Option { let layout = self.layout.borrow(); let layout = layout.as_ref()?; Some( layout .line .closest_index_for_x(window_point.x - layout.origin.x) .min(self.text.len()), ) } fn text_length_utf16( &mut self, _window: &mut Window, _cx: &mut Context, ) -> Option { Some(self.text.encode_utf16().count()) } } /// Newlines have no meaning in a single-line field. fn sanitize(text: impl Into) -> String { text.into().replace('\n', " ").replace('\r', "") } /// Floors `index` to the nearest grapheme-cluster boundary at or below it. fn previous_grapheme_start(text: &str, index: usize) -> usize { text.grapheme_indices(true) .rev() .find(|(offset, _)| *offset < index) .map(|(offset, _)| offset) .unwrap_or(0) } /// Advances `index` to the next grapheme-cluster boundary. fn next_grapheme_start(text: &str, index: usize) -> usize { text.grapheme_indices(true) .find(|(offset, _)| *offset > index) .map(|(offset, _)| offset) .unwrap_or(text.len()) } /// The start of the word before `from`. fn previous_word_start(text: &str, from: usize) -> usize { let mut best = 0; for (offset, word) in text.split_word_bound_indices() { if offset >= from { break; } if !word.starts_with(char::is_whitespace) { best = offset; } } best } /// The end of the word after `from`. fn next_word_end(text: &str, from: usize) -> usize { for (offset, word) in text.split_word_bound_indices() { let end = offset + word.len(); if end > from && !word.starts_with(char::is_whitespace) { return end; } } text.len() } /// Converts a UTF-16 offset into a UTF-8 byte offset. fn utf16_to_utf8_offset(text: &str, utf16_offset: usize) -> usize { let mut utf16_count = 0; for (byte_offset, char) in text.char_indices() { if utf16_count >= utf16_offset { return byte_offset; } utf16_count += char.len_utf16(); } text.len() } /// Converts a UTF-8 byte range to a UTF-16 range. fn utf8_range_to_utf16(text: &str, range: Range) -> Range { utf16_to_utf8_offset(text, range.start)..utf16_to_utf8_offset(text, range.end) } /// Converts a UTF-16 range to a UTF-8 byte range. fn utf16_range_to_utf8(text: &str, range: Range) -> Range { utf16_to_utf8_offset(text, range.start)..utf16_to_utf8_offset(text, range.end) } /// A single-line text field bound to an application-owned [`InputState`]. #[derive(IntoElement)] pub struct Input { id: ElementId, state: Entity, placeholder: SharedString, size: Size, disabled: bool, a11y_label: Option, } impl Input { /// Creates a text field bound to `state`. pub fn new(id: I, state: Entity) -> Self where I: Into, { Self { id: id.into(), state, placeholder: SharedString::default(), size: Size::default(), disabled: false, a11y_label: None, } } /// Sets the placeholder shown when the field is empty and unfocused. pub fn placeholder

(mut self, placeholder: P) -> Self where P: Into, { self.placeholder = placeholder.into(); self } /// Renders the field inert, unfocusable, and dimmed; disabled always wins. pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } /// Overrides the accessibility label. pub fn accessibility_label(mut self, label: L) -> Self where L: Into, { self.a11y_label = Some(label.into()); self } } impl Sizable for Input { fn apply_size(&mut self, size: Size) { self.size = size; } } impl RenderOnce for Input { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); let colors = theme.colors; let radius = theme.radius; let metrics = theme.sizes.resolve(self.size); let focus_handle = self.state.read(cx).focus_handle.clone(); let focused = focus_handle.is_focused(window); let is_empty = self.state.read(cx).text.is_empty(); let fallback_label = self .a11y_label .clone() .or_else(|| (!self.placeholder.is_empty()).then_some(self.placeholder.clone())) .unwrap_or_else(|| SharedString::from(self.id.to_string())); h_flex() .id(self.id.clone()) .key_context(INPUT_CONTEXT) .role(Role::TextInput) .aria_label(fallback_label) .relative() .overflow_hidden() .w_full() .h(metrics.input_height) .px(metrics.padding_x / 2.0) .rounded(radius.control) .border_1() .border_color(colors.border) .bg(colors.input) .text_size(metrics.text_size) .text_color(colors.foreground) .when(self.disabled, |input| input.opacity(0.5)) .when(!self.disabled, |input| { input.track_focus(&focus_handle).tab_stop(true) }) .when(focused, |input| input.border_color(colors.ring)) .when(is_empty && !focused, |input| { input.child( div() .text_color(colors.muted_foreground) .child(self.placeholder.clone()), ) }) .child( canvas( { let state = self.state.clone(); move |_bounds, window, cx| state.read(cx).shape_current_text(window) }, { let state = self.state.clone(); let focus = focus_handle.clone(); let padding = metrics.padding_x / 2.0; let disabled = self.disabled; // `line` comes from the prepaint closure above. move |bounds, line, window, cx| { let dragging = state.update(cx, |state, cx| { state.paint(line, bounds, padding, colors, window, cx) }); if disabled { return; } window.handle_input( &focus, gpui::ElementInputHandler::new(bounds, state.clone()), cx, ); if dragging { InputState::register_drag_listeners(&state, window); } } }, ) .absolute() .inset_0(), ) .when(!self.disabled, |input| { input .on_mouse_down(MouseButton::Left, { let mouse_down_handler = self .state .read(cx) .mouse_down_handler .clone() .expect("input state always carries its mouse-down handler"); move |event: &MouseDownEvent, window, cx| { mouse_down_handler(event, window, cx); } }) .on_key_down({ let key_down_handler = self .state .read(cx) .key_down_handler .clone() .expect("input state always carries its key handler"); move |event: &KeyDownEvent, window, cx| { key_down_handler(event, window, cx); } }) }) .on_action({ let state = self.state.clone(); move |action: &Escape, window, cx| { state.update(cx, |state, cx| state.escape(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &Submit, window, cx| { state.update(cx, |state, cx| state.submit(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &Backspace, window, cx| { state.update(cx, |state, cx| state.backspace(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &Delete, window, cx| { state.update(cx, |state, cx| state.delete(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &DeleteWordLeft, window, cx| { state.update(cx, |state, cx| state.delete_word_left(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &DeleteWordRight, window, cx| { state.update(cx, |state, cx| state.delete_word_right(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &DeleteToBeginningOfLine, window, cx| { state.update(cx, |state, cx| { state.delete_to_beginning_of_line(action, window, cx) }) } }) .on_action({ let state = self.state.clone(); move |action: &Left, window, cx| { state.update(cx, |state, cx| state.left(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &Right, window, cx| { state.update(cx, |state, cx| state.right(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &SelectLeft, window, cx| { state.update(cx, |state, cx| state.select_left(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &SelectRight, window, cx| { state.update(cx, |state, cx| state.select_right(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &WordLeft, window, cx| { state.update(cx, |state, cx| state.word_left(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &WordRight, window, cx| { state.update(cx, |state, cx| state.word_right(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &SelectWordLeft, window, cx| { state.update(cx, |state, cx| state.select_word_left(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &SelectWordRight, window, cx| { state.update(cx, |state, cx| state.select_word_right(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &Home, window, cx| { state.update(cx, |state, cx| state.home(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &End, window, cx| { state.update(cx, |state, cx| state.end(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &SelectHome, window, cx| { state.update(cx, |state, cx| state.select_home(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &SelectEnd, window, cx| { state.update(cx, |state, cx| state.select_end(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &SelectAll, window, cx| { state.update(cx, |state, cx| state.select_all(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &Copy, window, cx| { state.update(cx, |state, cx| state.copy(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &Cut, window, cx| { state.update(cx, |state, cx| state.cut(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &Paste, window, cx| { state.update(cx, |state, cx| state.paste(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &Undo, window, cx| { state.update(cx, |state, cx| state.undo(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &Redo, window, cx| { state.update(cx, |state, cx| state.redo(action, window, cx)) } }) } }