//! A text input field that allows the user to enter text. //! //! Based on the `Input` example from the `gpui` crate. //! https://github.com/zed-industries/zed/blob/main/crates/gpui/examples/input.rs use std::cell::Cell; use std::ops::Range; use std::rc::Rc; use gpui::prelude::FluentBuilder as _; use gpui::{ Action, App, AppContext, Bounds, ClipboardItem, Context, Edges, Entity, EntityInputHandler, EventEmitter, FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Styled as _, Subscription, TextAlign, UTF16Selection, Window, actions, div, point, px, }; use ropey::{Rope, RopeSlice}; use serde::Deserialize; use sum_tree::Bias; use unicode_segmentation::*; use super::blink_cursor::BlinkCursor; use super::change::Change; use super::element::{EditorScrollbarSnapshot, TextElement}; use super::mask_pattern::MaskPattern; use super::mode::InputMode; use super::{DisplayMap, MASK_CHAR}; use crate::actions::{SelectDown, SelectLeft, SelectRight, SelectUp}; use crate::history::History; use crate::input::blink_cursor::CURSOR_WIDTH; use crate::input::display_map::LineLayout; use crate::input::element::RIGHT_MARGIN; use crate::input::movement::MoveDirection; use crate::input::rope_ext::Position; use crate::input::{RopeExt as _, Selection}; use crate::{Root, Size}; #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = input, no_json)] pub struct Enter { /// Is confirm with secondary. pub secondary: bool, /// Whether the Shift modifier was held when Enter was pressed. pub shift: bool, } impl Enter { /// Returns true if `action` is a primary `Enter` action (`secondary: false`), /// regardless of whether Shift was held. pub fn is_primary(action: &dyn Action) -> bool { action.partial_eq(&Enter { secondary: false, shift: false, }) || action.partial_eq(&Enter { secondary: false, shift: true, }) } } actions!( input, [ Backspace, Delete, DeleteToBeginningOfLine, DeleteToEndOfLine, DeleteToPreviousWordStart, DeleteToNextWordEnd, Indent, Outdent, IndentInline, OutdentInline, MoveUp, MoveDown, MoveLeft, MoveRight, MoveHome, MoveEnd, MovePageUp, MovePageDown, SelectAll, SelectToStartOfLine, SelectToEndOfLine, SelectToStart, SelectToEnd, SelectToPreviousWordStart, SelectToNextWordEnd, ShowCharacterPalette, Copy, Cut, Paste, Undo, Redo, MoveToStartOfLine, MoveToEndOfLine, MoveToStart, MoveToEnd, MoveToPreviousWord, MoveToNextWord, Escape, ToggleCodeActions, Search, GoToDefinition, ] ); #[derive(Clone)] pub enum InputEvent { Change, PressEnter { secondary: bool, shift: bool }, Focus, Blur, } pub(super) const CONTEXT: &str = "Input"; pub(crate) fn init(cx: &mut App) { cx.bind_keys([ KeyBinding::new("backspace", Backspace, Some(CONTEXT)), KeyBinding::new("shift-backspace", Backspace, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("ctrl-backspace", Backspace, Some(CONTEXT)), KeyBinding::new("delete", Delete, Some(CONTEXT)), KeyBinding::new("shift-delete", Delete, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-backspace", DeleteToBeginningOfLine, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-delete", DeleteToEndOfLine, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("alt-backspace", DeleteToPreviousWordStart, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-backspace", DeleteToPreviousWordStart, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("alt-delete", DeleteToNextWordEnd, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-delete", DeleteToNextWordEnd, Some(CONTEXT)), KeyBinding::new( "enter", Enter { secondary: false, shift: false, }, Some(CONTEXT), ), KeyBinding::new( "shift-enter", Enter { secondary: false, shift: true, }, Some(CONTEXT), ), KeyBinding::new( "secondary-enter", Enter { secondary: true, shift: false, }, Some(CONTEXT), ), KeyBinding::new("escape", Escape, Some(CONTEXT)), KeyBinding::new("up", MoveUp, Some(CONTEXT)), KeyBinding::new("down", MoveDown, Some(CONTEXT)), KeyBinding::new("left", MoveLeft, Some(CONTEXT)), KeyBinding::new("right", MoveRight, Some(CONTEXT)), KeyBinding::new("pageup", MovePageUp, Some(CONTEXT)), KeyBinding::new("pagedown", MovePageDown, Some(CONTEXT)), KeyBinding::new("tab", IndentInline, Some(CONTEXT)), KeyBinding::new("shift-tab", OutdentInline, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-]", Indent, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-]", Indent, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-[", Outdent, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-[", Outdent, Some(CONTEXT)), KeyBinding::new("shift-left", SelectLeft, Some(CONTEXT)), KeyBinding::new("shift-right", SelectRight, Some(CONTEXT)), KeyBinding::new("shift-up", SelectUp, Some(CONTEXT)), KeyBinding::new("shift-down", SelectDown, Some(CONTEXT)), KeyBinding::new("home", MoveHome, Some(CONTEXT)), KeyBinding::new("end", MoveEnd, Some(CONTEXT)), KeyBinding::new("shift-home", SelectToStartOfLine, Some(CONTEXT)), KeyBinding::new("shift-end", SelectToEndOfLine, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("ctrl-shift-a", SelectToStartOfLine, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("ctrl-shift-e", SelectToEndOfLine, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("shift-cmd-left", SelectToStartOfLine, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("shift-cmd-right", SelectToEndOfLine, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("alt-shift-left", SelectToPreviousWordStart, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-shift-left", SelectToPreviousWordStart, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("alt-shift-right", SelectToNextWordEnd, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-shift-right", SelectToNextWordEnd, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-a", SelectAll, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-a", SelectAll, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-c", Copy, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-c", Copy, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-x", Cut, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-x", Cut, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-v", Paste, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-v", Paste, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("ctrl-a", MoveHome, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-left", MoveHome, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("ctrl-e", MoveEnd, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-right", MoveEnd, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-z", Undo, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-shift-z", Redo, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-up", MoveToStart, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-down", MoveToEnd, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("alt-left", MoveToPreviousWord, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("alt-right", MoveToNextWord, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-left", MoveToPreviousWord, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-right", MoveToNextWord, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-shift-up", SelectToStart, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-shift-down", SelectToEnd, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-z", Undo, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-y", Redo, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-.", ToggleCodeActions, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-.", ToggleCodeActions, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-f", Search, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] KeyBinding::new("ctrl-f", Search, Some(CONTEXT)), ]); } /// Whitespace indicators for rendering spaces and tabs. #[derive(Clone, Default)] pub(crate) struct WhitespaceIndicators { /// Shaped line for space character indicator (β€’) pub(crate) space: ShapedLine, /// Shaped line for tab character indicator (β†’) pub(crate) tab: ShapedLine, } #[derive(Clone)] pub(super) struct LastLayout { /// The visible range (no wrap) of lines in the viewport, the value is row (0-based) index. /// This is the buffer line range that encompasses all visible lines. pub(super) visible_range: Range, /// The list of visible buffer line indices (excludes hidden/folded lines). /// Parallel to `lines`: `visible_buffer_lines[i]` is the buffer line index of `lines[i]`. pub(super) visible_buffer_lines: Vec, /// Byte offset of each visible buffer line in the Rope (parallel to visible_buffer_lines/lines). pub(super) visible_line_byte_offsets: Vec, /// The first visible line top position in scroll viewport. pub(super) visible_top: Pixels, /// The range of byte offset of the visible lines. pub(super) visible_range_offset: Range, /// The last layout lines (Only have visible lines, no empty entries for hidden lines). pub(super) lines: Rc>, /// The line_height of text layout, this will change will InputElement painted. pub(super) line_height: Pixels, /// The wrap width of text layout, this will change will InputElement painted. pub(super) wrap_width: Option, /// The line number area width of text layout, if not line number, this will be 0px. pub(super) line_number_width: Pixels, /// The cursor position (top, left) in pixels. pub(super) cursor_bounds: Option>, /// The text align of the text layout. pub(super) text_align: TextAlign, /// The content width of the text layout. pub(super) content_width: Pixels, } impl LastLayout { /// Get the line layout for the given buffer row (0-based). /// /// Uses binary search on `visible_buffer_lines` to find the line. /// Returns None if the row is not visible (out of range or folded). pub(crate) fn line(&self, row: usize) -> Option<&LineLayout> { let pos = self.visible_buffer_lines.binary_search(&row).ok()?; self.lines.get(pos) } /// Get the alignment offset for the given line width. pub(super) fn alignment_offset(&self, line_width: Pixels) -> Pixels { match self.text_align { TextAlign::Left => px(0.), TextAlign::Center => (self.content_width - line_width).half().max(px(0.)), TextAlign::Right => (self.content_width - line_width).max(px(0.)), } } } /// InputState to keep editing state of the [`super::Input`]. #[allow(clippy::type_complexity)] pub struct InputState { pub(super) focus_handle: FocusHandle, pub(super) mode: InputMode, pub(super) text: Rope, pub(super) display_map: DisplayMap, pub(super) history: History, pub(super) blink_cursor: Entity, pub loading: bool, /// Range in UTF-8 length for the selected text. /// /// - "Hello δΈ–η•ŒπŸ’" = 16 /// - "πŸ’" = 4 pub(super) selected_range: Selection, pub(super) replaceable: bool, /// Range for save the selected word, use to keep word range when drag move. pub(super) selected_word_range: Option, pub(super) selection_reversed: bool, /// The marked range is the temporary insert text on IME typing. pub(super) ime_marked_range: Option, pub(super) last_layout: Option, pub(super) last_cursor: Option, /// The input container bounds pub(super) input_bounds: Bounds, /// The text bounds pub(super) last_bounds: Option>, pub(super) last_selected_range: Option, pub(super) selecting: bool, pub(super) size: Size, pub(super) disabled: bool, pub(super) masked: bool, pub(super) clean_on_escape: bool, pub(super) submit_on_enter: bool, pub(super) soft_wrap: bool, pub(super) show_whitespaces: bool, /// This flag tells the renderer to prefer the end of the current visual line. pub(crate) cursor_line_end_affinity: bool, pub(super) pattern: Option, pub(super) validate: Option) -> bool + 'static>>, pub(crate) scroll_handle: ScrollHandle, /// The deferred scroll offset to apply on next layout. pub(crate) deferred_scroll_offset: Option>, /// The size of the scrollable content. pub(crate) scroll_size: gpui::Size, pub(super) editor_scrollbar_paddings: Cell>, pub(super) editor_scrollbar_snapshot: Cell>, pub(super) text_align: TextAlign, /// The mask pattern for formatting the input text pub(crate) mask_pattern: MaskPattern, pub(super) placeholder: SharedString, /// A flag to indicate if we should ignore the next completion event. pub(super) silent_replace_text: bool, /// A flag to indicate if we should emit InputEvents. pub(super) emit_events: bool, /// To remember the horizontal column (x-coordinate) of the cursor position for keep column for move up/down. /// /// The first element is the x-coordinate (Pixels), preferred to use this. /// The second element is the column (usize), fallback to use this. pub(super) preferred_column: Option<(Pixels, usize)>, _subscriptions: Vec, } impl EventEmitter for InputState {} impl InputState { /// Create a Input state with default [`InputMode::SingleLine`] mode. /// /// See also: [`Self::multi_line`], [`Self::auto_grow`] to set other mode. pub fn new(window: &mut Window, cx: &mut Context) -> Self { let focus_handle = cx.focus_handle().tab_stop(true); let blink_cursor = cx.new(|_| BlinkCursor::new()); let history = History::new().group_interval(std::time::Duration::from_secs(1)); let _subscriptions = vec![ // Observe the blink cursor to repaint the view when it changes. cx.observe(&blink_cursor, |_, _, cx| cx.notify()), // Blink the cursor when the window is active, pause when it's not. cx.observe_window_activation(window, |input, window, cx| { if window.is_window_active() { let focus_handle = input.focus_handle.clone(); if focus_handle.is_focused(window) { input.blink_cursor.update(cx, |blink_cursor, cx| { blink_cursor.start(cx); }); } } }), cx.on_focus(&focus_handle, window, Self::on_focus), cx.on_blur(&focus_handle, window, Self::on_blur), ]; let text_style = window.text_style(); Self { focus_handle: focus_handle.clone(), text: "".into(), display_map: DisplayMap::new(text_style.font(), window.rem_size(), None), blink_cursor, history, selected_range: Selection::default(), replaceable: true, selected_word_range: None, selection_reversed: false, ime_marked_range: None, input_bounds: Bounds::default(), selecting: false, disabled: false, masked: false, clean_on_escape: false, submit_on_enter: false, soft_wrap: true, show_whitespaces: false, loading: false, pattern: None, validate: None, mode: InputMode::default(), last_layout: None, last_bounds: None, last_selected_range: None, last_cursor: None, scroll_handle: ScrollHandle::new(), scroll_size: gpui::size(px(0.), px(0.)), editor_scrollbar_paddings: Cell::new(Edges { top: px(0.), right: px(0.), bottom: px(0.), left: px(0.), }), editor_scrollbar_snapshot: Cell::new(None), deferred_scroll_offset: None, preferred_column: None, placeholder: SharedString::default(), mask_pattern: MaskPattern::default(), text_align: TextAlign::Left, silent_replace_text: false, emit_events: true, size: Size::default(), _subscriptions, cursor_line_end_affinity: false, } } /// Set Input to use multi line mode. /// /// Default rows is 2. pub fn multi_line(mut self, multi_line: bool) -> Self { self.mode = self.mode.multi_line(multi_line); self } /// Set Input to use [`InputMode::AutoGrow`] mode with min, max rows limit. pub fn auto_grow(mut self, min_rows: usize, max_rows: usize) -> Self { self.mode = InputMode::auto_grow(min_rows, max_rows); self } /// Set Input to use [`InputMode::CodeEditor`] mode. /// /// Default options: /// /// - line_number: true /// - tab_size: 2 /// - hard_tabs: false /// - height: 100% /// - multi_line: true /// - indent_guides: true /// /// If `highlighter` is None, will use the default highlighter. /// /// Code Editor aim for help used to simple code editing or display, not a full-featured code editor. /// /// ## Features /// /// - Syntax Highlighting /// - Auto Indent /// - Line Number /// - Large Text support, up to 50K lines. pub fn code_editor(mut self, language: impl Into) -> Self { let language: SharedString = language.into(); self.mode = InputMode::code_editor(language); self } /// Set whether search UI allows replacement, default is true. pub fn replaceable(mut self, allow: bool) -> Self { self.replaceable = allow; self } /// Set placeholder pub fn placeholder(mut self, placeholder: impl Into) -> Self { self.placeholder = placeholder.into(); self } /// Set enable/disable code folding, only for [`InputMode::CodeEditor`] mode. /// /// Default: true pub fn folding(mut self, folding: bool) -> Self { debug_assert!(self.mode.is_code_editor()); if let InputMode::CodeEditor { folding: f, .. } = &mut self.mode { *f = folding; } self } /// Set code folding at runtime, only for [`InputMode::CodeEditor`] mode. /// /// When disabling, all existing folds are cleared. pub fn set_folding(&mut self, folding: bool, _: &mut Window, cx: &mut Context) { debug_assert!(self.mode.is_code_editor()); if let InputMode::CodeEditor { folding: f, .. } = &mut self.mode { *f = folding; } if !folding { self.display_map.clear_folds(); } cx.notify(); } /// Set enable/disable line number, only for [`InputMode::CodeEditor`] mode. pub fn line_number(mut self, line_number: bool) -> Self { debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line()); if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode { *l = line_number; } self } /// Set line number, only for [`InputMode::CodeEditor`] mode. pub fn set_line_number(&mut self, line_number: bool, _: &mut Window, cx: &mut Context) { debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line()); if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode { *l = line_number; } cx.notify(); } /// Set the number of rows for the multi-line Textarea. /// /// This is only used when `multi_line` is set to true. /// /// default: 2 pub fn rows(mut self, rows: usize) -> Self { match &mut self.mode { InputMode::PlainText { rows: r, .. } | InputMode::CodeEditor { rows: r, .. } => { *r = rows } InputMode::AutoGrow { max_rows: max_r, rows: r, .. } => { *r = rows; *max_r = rows; } } self } /// Set highlighter language for for [`InputMode::CodeEditor`] mode. pub fn set_highlighter( &mut self, new_language: impl Into, cx: &mut Context, ) { if let InputMode::CodeEditor { language, parse_task, .. } = &mut self.mode { *language = new_language.into(); parse_task.borrow_mut().take(); } cx.notify(); } fn reset_highlighter(&mut self, cx: &mut Context) { if let InputMode::CodeEditor { parse_task, .. } = &mut self.mode { parse_task.borrow_mut().take(); } cx.notify(); } /// Set placeholder pub fn set_placeholder( &mut self, placeholder: impl Into, _: &mut Window, cx: &mut Context, ) { self.placeholder = placeholder.into(); cx.notify(); } /// Find which line and sub-line the given offset belongs to, along with the position within that sub-line. /// /// Returns: /// /// - The index of the line (zero-based) containing the offset. /// - The index of the sub-line (zero-based) within the line containing the offset. /// - The position of the offset. pub(super) fn line_and_position_for_offset( &self, offset: usize, ) -> (usize, usize, Option>) { let Some(last_layout) = &self.last_layout else { return (0, 0, None); }; let line_height = last_layout.line_height; let mut y_offset = last_layout.visible_top; for (vi, line) in last_layout.lines.iter().enumerate() { let prev_lines_offset = last_layout.visible_line_byte_offsets[vi]; let local_offset = offset.saturating_sub(prev_lines_offset); if let Some(pos) = line.position_for_index(local_offset, last_layout, false) { let sub_line_index = (pos.y / line_height) as usize; let adjusted_pos = point(pos.x + last_layout.line_number_width, pos.y + y_offset); return (vi, sub_line_index, Some(adjusted_pos)); } y_offset += line.size(line_height).height; } (0, 0, None) } /// Set the text of the input field. /// /// And the selection_range will be reset to 0..0. pub fn set_value( &mut self, value: impl Into, window: &mut Window, cx: &mut Context, ) { self.history.ignore = true; self.emit_events = false; self.replace_text(value, window, cx); self.history.ignore = false; self.emit_events = true; // Ensure cursor to start when set text if self.mode.is_single_line() { self.selected_range = (self.text.len()..self.text.len()).into(); } else { self.selected_range.clear(); } // Move scroll to top self.scroll_handle.set_offset(point(px(0.), px(0.))); self.history.clear(); cx.notify(); } /// Insert text at the current cursor position. /// /// And the cursor will be moved to the end of inserted text. pub fn insert( &mut self, text: impl Into, window: &mut Window, cx: &mut Context, ) { let was_disabled = self.disabled; self.disabled = false; let text: SharedString = text.into(); let range_utf16 = self.range_to_utf16(&(self.cursor()..self.cursor())); self.replace_text_in_range_silent(Some(range_utf16), &text, window, cx); self.selected_range = (self.selected_range.end..self.selected_range.end).into(); self.disabled = was_disabled; } /// Replace text at the current cursor position. /// /// And the cursor will be moved to the end of replaced text. pub fn replace( &mut self, text: impl Into, window: &mut Window, cx: &mut Context, ) { let was_disabled = self.disabled; self.disabled = false; let text: SharedString = text.into(); self.replace_text_in_range_silent(None, &text, window, cx); self.selected_range = (self.selected_range.end..self.selected_range.end).into(); self.disabled = was_disabled; } fn replace_text( &mut self, text: impl Into, window: &mut Window, cx: &mut Context, ) { let was_disabled = self.disabled; self.disabled = false; let text: SharedString = text.into(); let range = 0..self.text.chars().map(|c| c.len_utf16()).sum(); self.replace_text_in_range_silent(Some(range), &text, window, cx); self.reset_highlighter(cx); self.disabled = was_disabled; } /// Set with disabled mode. /// /// See also: [`Self::set_disabled`], [`Self::is_disabled`]. #[allow(unused)] pub(crate) fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } /// Set with password masked state. /// /// Only for [`InputMode::SingleLine`] mode. pub fn masked(mut self, masked: bool) -> Self { debug_assert!(self.mode.is_single_line()); self.masked = masked; self } /// Set the password masked state of the input field. /// /// Only for [`InputMode::SingleLine`] mode. pub fn set_masked(&mut self, masked: bool, _: &mut Window, cx: &mut Context) { debug_assert!(self.mode.is_single_line()); self.masked = masked; cx.notify(); } /// Set true to clear the input by pressing Escape key. pub fn clean_on_escape(mut self) -> Self { self.clean_on_escape = true; self } /// Set true to treat `Enter` as a submit action in multi-line mode, /// while `Shift+Enter` inserts a newline. /// /// Default is `false` (both `Enter` and `Shift+Enter` insert a newline). pub fn submit_on_enter(mut self, submit: bool) -> Self { self.submit_on_enter = submit; self } /// Set the soft wrap mode for multi-line input, default is true. pub fn soft_wrap(mut self, wrap: bool) -> Self { debug_assert!(self.mode.is_multi_line()); self.soft_wrap = wrap; self } /// Set whether to show whitespace characters. pub fn show_whitespaces(mut self, show: bool) -> Self { self.show_whitespaces = show; self } /// Update the soft wrap mode for multi-line input, default is true. pub fn set_soft_wrap(&mut self, wrap: bool, _: &mut Window, cx: &mut Context) { debug_assert!(self.mode.is_multi_line()); self.soft_wrap = wrap; if wrap { let wrap_width = self .last_layout .as_ref() .and_then(|b| b.wrap_width) .unwrap_or(self.input_bounds.size.width); self.display_map.on_layout_changed(Some(wrap_width), cx); // Reset scroll to left 0 let mut offset = self.scroll_handle.offset(); offset.x = px(0.); self.scroll_handle.set_offset(offset); } else { self.display_map.on_layout_changed(None, cx); } cx.notify(); } /// Update whether to show whitespace characters. pub fn set_show_whitespaces(&mut self, show: bool, _: &mut Window, cx: &mut Context) { self.show_whitespaces = show; cx.notify(); } /// Set the regular expression pattern of the input field. /// /// Only for [`InputMode::SingleLine`] mode. pub fn pattern(mut self, pattern: regex::Regex) -> Self { debug_assert!(self.mode.is_single_line()); self.pattern = Some(pattern); self } /// Set the regular expression pattern of the input field with reference. /// /// Only for [`InputMode::SingleLine`] mode. pub fn set_pattern( &mut self, pattern: regex::Regex, _window: &mut Window, _cx: &mut Context, ) { debug_assert!(self.mode.is_single_line()); self.pattern = Some(pattern); } /// Set the validation function of the input field. /// /// Only for [`InputMode::SingleLine`] mode. pub fn validate(mut self, f: impl Fn(&str, &mut Context) -> bool + 'static) -> Self { debug_assert!(self.mode.is_single_line()); self.validate = Some(Box::new(f)); self } /// Set true to show spinner at the input right. /// /// Only for [`InputMode::SingleLine`] mode. pub fn set_loading(&mut self, loading: bool, cx: &mut Context) { debug_assert!(self.mode.is_single_line()); self.loading = loading; cx.notify(); } /// Set the default value of the input field. pub fn default_value(mut self, value: impl Into) -> Self { let text: SharedString = value.into(); self.text = Rope::from(text.as_str()); self } /// Return the value of the input field. pub fn value(&self) -> SharedString { SharedString::new(self.text.to_string()) } /// Return the portion of the value within the input field that /// is selected by the user pub fn selected_value(&self) -> SharedString { SharedString::new(self.selected_text().to_string()) } /// Return the value without mask. pub fn unmask_value(&self) -> SharedString { self.mask_pattern.unmask(&self.text.to_string()).into() } /// Return the text [`Rope`] of the input field. pub fn text(&self) -> &Rope { &self.text } /// Return the (0-based) [`Position`] of the cursor. pub fn cursor_position(&self) -> Position { let offset = self.cursor(); self.text.offset_to_position(offset) } /// Set (0-based) [`Position`] of the cursor. /// /// This will move the cursor to the specified line and column, and update the selection range. pub fn set_cursor_position( &mut self, position: impl Into, window: &mut Window, cx: &mut Context, ) { let position: Position = position.into(); let offset = self.text.position_to_offset(&position); self.move_to(offset, None, cx); self.update_preferred_column(); self.focus(window, cx); } /// Focus the input field. pub fn focus(&self, window: &mut Window, cx: &mut Context) { self.focus_handle.focus(window, cx); self.blink_cursor.update(cx, |cursor, cx| { cursor.start(cx); }); } pub(super) fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context) { self.select_to(self.previous_boundary(self.cursor()), cx); } pub(super) fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context) { self.select_to(self.next_boundary(self.cursor()), cx); } pub(super) fn select_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context) { if self.mode.is_single_line() { return; } let offset = self.start_of_line().saturating_sub(1); self.select_to(self.previous_boundary(offset), cx); } pub(super) fn select_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context) { if self.mode.is_single_line() { return; } let offset = (self.end_of_line() + 1).min(self.text.len()); self.select_to(self.next_boundary(offset), cx); } pub(super) fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context) { self.selected_range = (0..self.text.len()).into(); cx.notify(); } pub(super) fn select_to_start( &mut self, _: &SelectToStart, _: &mut Window, cx: &mut Context, ) { self.select_to(0, cx); } pub(super) fn select_to_end( &mut self, _: &SelectToEnd, _: &mut Window, cx: &mut Context, ) { let end = self.text.len(); self.select_to(end, cx); } pub(super) fn select_to_start_of_line( &mut self, _: &SelectToStartOfLine, _: &mut Window, cx: &mut Context, ) { let offset = self.start_of_line(); self.select_to(offset, cx); } pub(super) fn select_to_end_of_line( &mut self, _: &SelectToEndOfLine, _: &mut Window, cx: &mut Context, ) { let offset = self.end_of_line(); self.select_to(offset, cx); } pub(super) fn select_to_previous_word( &mut self, _: &SelectToPreviousWordStart, _: &mut Window, cx: &mut Context, ) { let offset = self.previous_start_of_word(); self.select_to(offset, cx); } pub(super) fn select_to_next_word( &mut self, _: &SelectToNextWordEnd, _: &mut Window, cx: &mut Context, ) { let offset = self.next_end_of_word(); self.select_to(offset, cx); } /// Return the start offset of the previous word. pub(super) fn previous_start_of_word(&mut self) -> usize { let offset = self.selected_range.start; let offset = self.offset_from_utf16(self.offset_to_utf16(offset)); // FIXME: Avoid to_string let left_part = self.text.slice(0..offset).to_string(); UnicodeSegmentation::split_word_bound_indices(left_part.as_str()) .rfind(|(_, s)| !s.trim_start().is_empty()) .map(|(i, _)| i) .unwrap_or(0) } /// Return the next end offset of the next word. pub(super) fn next_end_of_word(&mut self) -> usize { let offset = self.cursor(); let offset = self.offset_from_utf16(self.offset_to_utf16(offset)); let right_part = self.text.slice(offset..self.text.len()).to_string(); UnicodeSegmentation::split_word_bound_indices(right_part.as_str()) .find(|(_, s)| !s.trim_start().is_empty()) .map(|(i, s)| offset + i + s.len()) .unwrap_or(self.text.len()) } /// Get start of line byte offset of cursor. /// /// When soft wrap is active, first press goes to visual line start, /// second press (already at visual start) goes to logical line start. pub(super) fn start_of_line(&self) -> usize { if self.mode.is_single_line() { return 0; } let row = self.text.offset_to_point(self.cursor()).row; let logical_start = self.text.line_start_offset(row); if self.soft_wrap && self.mode.is_code_editor() { let wrap_point = self.display_map.offset_to_wrap_display_point(self.cursor()); if let Some(line) = self.display_map.lines().get(row) && let Some(range) = line.wrapped_lines.get(wrap_point.local_row) { let visual_start = logical_start + range.start; if self.cursor() != visual_start { return visual_start; } } } logical_start } /// Get end of line byte offset of cursor. /// /// When soft wrap is active, first press goes to visual line end, /// second press (already at visual end) goes to logical line end. pub(super) fn end_of_line(&self) -> usize { if self.mode.is_single_line() { return self.text.len(); } let row = self.text.offset_to_point(self.cursor()).row; let logical_start = self.text.line_start_offset(row); let logical_end = self.text.line_end_offset(row); if self.soft_wrap && self.mode.is_code_editor() { let wrap_point = self.display_map.offset_to_wrap_display_point(self.cursor()); if let Some(line) = self.display_map.lines().get(row) && let Some(range) = line.wrapped_lines.get(wrap_point.local_row) { let visual_end = logical_start + range.end; if self.cursor() != visual_end { return visual_end; } } } logical_end } /// Get start line of selection start or end (The min value). /// /// This is means is always get the first line of selection. pub(super) fn start_of_line_of_selection( &mut self, window: &mut Window, cx: &mut Context, ) -> usize { if self.mode.is_single_line() { return 0; } let mut offset = self.previous_boundary(self.selected_range.start.min(self.selected_range.end)); if self.text.char_at(offset) == Some('\r') { offset += 1; } self.text_for_range(self.range_to_utf16(&(0..offset + 1)), &mut None, window, cx) .unwrap_or_default() .rfind('\n') .map(|i| i + 1) .unwrap_or(0) } /// Get indent string of next line. /// /// To get current and next line indent, to return more depth one. pub(super) fn indent_of_next_line(&mut self) -> String { if self.mode.is_single_line() { return "".into(); } let mut current_indent = String::new(); let mut next_indent = String::new(); let current_line_start_pos = self.start_of_line(); let next_line_start_pos = self.end_of_line(); for c in self.text.slice(current_line_start_pos..).chars() { if !c.is_whitespace() { break; } if c == '\n' || c == '\r' { break; } current_indent.push(c); } for c in self.text.slice(next_line_start_pos..).chars() { if !c.is_whitespace() { break; } if c == '\n' || c == '\r' { break; } next_indent.push(c); } if next_indent.len() > current_indent.len() { next_indent } else { current_indent } } pub(super) fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context) { if self.selected_range.is_empty() { self.select_to(self.previous_boundary(self.cursor()), cx) } self.replace_text_in_range(None, "", window, cx); self.pause_blink_cursor(cx); } pub(super) fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context) { if self.selected_range.is_empty() { self.select_to(self.next_boundary(self.cursor()), cx) } self.replace_text_in_range(None, "", window, cx); self.pause_blink_cursor(cx); } pub(super) fn delete_to_beginning_of_line( &mut self, _: &DeleteToBeginningOfLine, window: &mut Window, cx: &mut Context, ) { if !self.selected_range.is_empty() { self.replace_text_in_range(None, "", window, cx); self.pause_blink_cursor(cx); return; } let mut offset = self.start_of_line(); if offset == self.cursor() { offset = offset.saturating_sub(1); } self.replace_text_in_range_silent( Some(self.range_to_utf16(&(offset..self.cursor()))), "", window, cx, ); self.pause_blink_cursor(cx); } pub(super) fn delete_to_end_of_line( &mut self, _: &DeleteToEndOfLine, window: &mut Window, cx: &mut Context, ) { if !self.selected_range.is_empty() { self.replace_text_in_range(None, "", window, cx); self.pause_blink_cursor(cx); return; } let mut offset = self.end_of_line(); if offset == self.cursor() { offset = (offset + 1).clamp(0, self.text.len()); } self.replace_text_in_range_silent( Some(self.range_to_utf16(&(self.cursor()..offset))), "", window, cx, ); self.pause_blink_cursor(cx); } pub(super) fn delete_previous_word( &mut self, _: &DeleteToPreviousWordStart, window: &mut Window, cx: &mut Context, ) { if !self.selected_range.is_empty() { self.replace_text_in_range(None, "", window, cx); self.pause_blink_cursor(cx); return; } let offset = self.previous_start_of_word(); self.replace_text_in_range_silent( Some(self.range_to_utf16(&(offset..self.cursor()))), "", window, cx, ); self.pause_blink_cursor(cx); } pub(super) fn delete_next_word( &mut self, _: &DeleteToNextWordEnd, window: &mut Window, cx: &mut Context, ) { if !self.selected_range.is_empty() { self.replace_text_in_range(None, "", window, cx); self.pause_blink_cursor(cx); return; } let offset = self.next_end_of_word(); self.replace_text_in_range_silent( Some(self.range_to_utf16(&(self.cursor()..offset))), "", window, cx, ); self.pause_blink_cursor(cx); } pub(super) fn enter(&mut self, action: &Enter, window: &mut Window, cx: &mut Context) { // In multi-line mode with `submit_on_enter` enabled, a plain `Enter` // (without Shift) is treated as submit: propagate the action and emit // PressEnter without inserting a newline. `Shift+Enter` still inserts // a newline. let insert_newline = self.mode.is_multi_line() && (!self.submit_on_enter || action.shift); if insert_newline { // Get current line indent let indent = if self.mode.is_code_editor() { self.indent_of_next_line() } else { "".to_string() }; // Add newline and indent let new_line_text = format!("\n{}", indent); self.replace_text_in_range_silent(None, &new_line_text, window, cx); self.pause_blink_cursor(cx); } else { // Single line input or submit-on-enter: just emit the event // (e.g.: in a dialog to confirm, or a chat textarea to send). cx.propagate(); } cx.emit(InputEvent::PressEnter { secondary: action.secondary, shift: action.shift, }); } pub(super) fn clean(&mut self, window: &mut Window, cx: &mut Context) { self.replace_text("", window, cx); self.selected_range = (0..0).into(); self.scroll_to(0, None, cx); } pub(super) fn escape(&mut self, _action: &Escape, window: &mut Window, cx: &mut Context) { if self.ime_marked_range.is_some() { self.unmark_text(window, cx); } if self.clean_on_escape { return self.clean(window, cx); } cx.propagate(); } pub(super) fn on_mouse_down( &mut self, event: &MouseDownEvent, window: &mut Window, cx: &mut Context, ) { // If there have IME marked range and is empty (Means pressed Esc to abort IME typing) // Clear the marked range. if let Some(ime_marked_range) = &self.ime_marked_range && ime_marked_range.is_empty() { self.ime_marked_range = None; } self.selecting = true; let offset = self.index_for_mouse_position(event.position); // Triple click to select line if event.button == MouseButton::Left && event.click_count >= 3 { self.select_line(offset, window, cx); return; } // Double click to select word if event.button == MouseButton::Left && event.click_count == 2 { self.select_word(offset, window, cx); return; } if event.modifiers.shift { self.select_to(offset, cx); } else { self.move_to(offset, None, cx) } } pub(super) fn on_mouse_up( &mut self, _: &MouseUpEvent, _window: &mut Window, _cx: &mut Context, ) { if self.selected_range.is_empty() { self.selection_reversed = false; } self.selecting = false; self.selected_word_range = None; } pub(super) fn on_scroll_wheel( &mut self, event: &ScrollWheelEvent, window: &mut Window, cx: &mut Context, ) { let line_height = self .last_layout .as_ref() .map(|layout| layout.line_height) .unwrap_or(window.line_height()); let delta = event.delta.pixel_delta(line_height); let old_offset = self.scroll_handle.offset(); self.update_scroll_offset(Some(old_offset + delta), cx); // Only stop propagation if the offset actually changed if self.scroll_handle.offset() != old_offset { cx.stop_propagation(); } } pub(super) fn update_scroll_offset( &mut self, offset: Option>, cx: &mut Context, ) { let mut offset = offset.unwrap_or(self.scroll_handle.offset()); // In addition to left alignment, a cursor position will be reserved on the right side let safe_x_offset = if self.text_align == TextAlign::Left { px(0.) } else { -CURSOR_WIDTH }; let safe_y_range = (-self.scroll_size.height + self.input_bounds.size.height).min(px(0.0))..px(0.); let safe_x_range = (-self.scroll_size.width + self.input_bounds.size.width + safe_x_offset) .min(safe_x_offset)..px(0.); offset.y = if self.mode.is_single_line() { px(0.) } else { offset.y.clamp(safe_y_range.start, safe_y_range.end) }; offset.x = offset.x.clamp(safe_x_range.start, safe_x_range.end); self.scroll_handle.set_offset(offset); cx.notify(); } /// Scroll to make the given offset visible. /// /// If `direction` is Some, will keep edges at the same side. pub(crate) fn scroll_to( &mut self, offset: usize, direction: Option, cx: &mut Context, ) { let Some(last_layout) = self.last_layout.as_ref() else { return; }; let Some(bounds) = self.last_bounds.as_ref() else { return; }; let mut scroll_offset = self.scroll_handle.offset(); let was_offset = scroll_offset; let line_height = last_layout.line_height; let point = self.text.offset_to_point(offset); let row = point.row; let mut row_offset_y = px(0.); for (ix, _wrap_line) in self.display_map.lines().iter().enumerate() { if ix == row { break; } // Only accumulate height for visible (non-folded) wrap rows let visible_wrap_rows = self.display_map.visible_wrap_row_count_for_buffer_line(ix); row_offset_y += line_height * visible_wrap_rows; } // For Right alignment use 0 margin: the cursor indicator is clamped inside bounds // in layout_cursor, so shifting the text here would cause a first-click visual jump. let safety_margin = match last_layout.text_align { TextAlign::Left => RIGHT_MARGIN, TextAlign::Right => px(0.), TextAlign::Center => CURSOR_WIDTH, }; if let Some(line) = last_layout .lines .get(row.saturating_sub(last_layout.visible_range.start)) { // Check to scroll horizontally and soft wrap lines if let Some(pos) = line.position_for_index(point.column, last_layout, false) { let bounds_width = bounds.size.width - last_layout.line_number_width; let col_offset_x = pos.x; row_offset_y += pos.y; if col_offset_x - safety_margin < -scroll_offset.x { // If the position is out of the visible area, scroll to make it visible scroll_offset.x = -col_offset_x + safety_margin; } else if col_offset_x + safety_margin > -scroll_offset.x + bounds_width { scroll_offset.x = -(col_offset_x - bounds_width + safety_margin); } } } // Check if row_offset_y is out of the viewport // If row offset is not in the viewport, scroll to make it visible let edge_height = if direction.is_some() && self.mode.is_code_editor() { 3 * line_height } else { line_height }; if row_offset_y - edge_height + line_height < -scroll_offset.y { // Scroll up scroll_offset.y = -row_offset_y + edge_height - line_height; } else if row_offset_y + edge_height > -scroll_offset.y + bounds.size.height { // Scroll down scroll_offset.y = -(row_offset_y - bounds.size.height + edge_height); } // Avoid necessary scroll, when it was already in the correct position. if direction == Some(MoveDirection::Up) { scroll_offset.y = scroll_offset.y.max(was_offset.y); } else if direction == Some(MoveDirection::Down) { scroll_offset.y = scroll_offset.y.min(was_offset.y); } scroll_offset.x = scroll_offset.x.min(px(0.)); scroll_offset.y = scroll_offset.y.min(px(0.)); self.deferred_scroll_offset = Some(scroll_offset); cx.notify(); } pub(super) fn show_character_palette( &mut self, _: &ShowCharacterPalette, window: &mut Window, _: &mut Context, ) { window.show_character_palette(); } pub(super) fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context) { if self.selected_range.is_empty() { return; } let selected_text = self.text.slice(self.selected_range).to_string(); cx.write_to_clipboard(ClipboardItem::new_string(selected_text)); } pub(super) fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context) { if self.selected_range.is_empty() { return; } let selected_text = self.text.slice(self.selected_range).to_string(); cx.write_to_clipboard(ClipboardItem::new_string(selected_text)); self.replace_text_in_range_silent(None, "", window, cx); } pub(super) fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context) { if let Some(clipboard) = cx.read_from_clipboard() { let mut new_text = clipboard.text().unwrap_or_default(); if !self.mode.is_multi_line() { new_text = new_text.replace('\n', ""); } self.replace_text_in_range_silent(None, &new_text, window, cx); self.scroll_to(self.cursor(), None, cx); } } fn push_history(&mut self, text: &Rope, range: &Range, new_text: &str) { if self.history.ignore { return; } let range = text.clip_offset(range.start, Bias::Left)..text.clip_offset(range.end, Bias::Right); let old_text = text.slice(range.clone()).to_string(); let new_range = range.start..range.start + new_text.len(); self.history .push(Change::new(range, &old_text, new_range, new_text)); } pub(super) fn undo(&mut self, _: &Undo, window: &mut Window, cx: &mut Context) { self.history.ignore = true; if let Some(changes) = self.history.undo() { for change in changes { let range_utf16 = self.range_to_utf16(&change.new_range.into()); self.replace_text_in_range_silent(Some(range_utf16), &change.old_text, window, cx); } } self.history.ignore = false; } pub(super) fn redo(&mut self, _: &Redo, window: &mut Window, cx: &mut Context) { self.history.ignore = true; if let Some(changes) = self.history.redo() { for change in changes { let range_utf16 = self.range_to_utf16(&change.old_range.into()); self.replace_text_in_range_silent(Some(range_utf16), &change.new_text, window, cx); } } self.history.ignore = false; } /// Get byte offset of the cursor. /// /// The offset is the UTF-8 offset. pub fn cursor(&self) -> usize { if let Some(ime_marked_range) = &self.ime_marked_range { return ime_marked_range.end; } if self.selection_reversed { self.selected_range.start } else { self.selected_range.end } } /// Visible row range in the last laid-out viewport, `None` before first layout. pub fn visible_row_range(&self) -> Option> { self.last_layout.as_ref().map(|l| l.visible_range.clone()) } /// Current scroll offset of the editor viewport. pub fn scroll_offset(&self) -> gpui::Point { self.scroll_handle.offset() } /// Laid-out line height; `None` before first layout. pub fn line_height(&self) -> Option { self.last_layout.as_ref().map(|l| l.line_height) } /// Returns the current selection as a byte range into the text. /// /// The range is empty (`start == end`) when no text is selected; in /// that case the offset equals `cursor()`. Byte offsets are measured /// in the underlying rope's byte units. pub fn selected_range(&self) -> std::ops::Range { self.selected_range.into() } pub(crate) fn index_for_mouse_position(&self, position: Point) -> usize { // If the text is empty, always return 0 if self.text.len() == 0 { return 0; } let (Some(bounds), Some(last_layout)) = (self.last_bounds.as_ref(), self.last_layout.as_ref()) else { return 0; }; let line_height = last_layout.line_height; let line_number_width = last_layout.line_number_width; // TIP: About the IBeam cursor // // If cursor style is IBeam, the mouse mouse position is in the middle of the cursor (This is special in OS) // The position is relative to the bounds of the text input // // bounds.origin: // // - included the input padding. // - included the scroll offset. let inner_position = position - bounds.origin - point(line_number_width, px(0.)); let mut y_offset = last_layout.visible_top; // Traverse visible buffer lines (compact, no hidden entries) for (vi, (line_layout, _buffer_line)) in last_layout .lines .iter() .zip(last_layout.visible_buffer_lines.iter()) .enumerate() { let line_start_offset = last_layout.visible_line_byte_offsets[vi]; // Calculate line origin for this display row let line_origin = point(px(0.), y_offset); let pos = inner_position - line_origin; // Return offset by use closest_index_for_x if is single line mode. if self.mode.is_single_line() { let local_index = line_layout.closest_index_for_x(pos.x, last_layout); let index = line_start_offset + local_index; return if self.masked { self.text.char_index_to_offset(index / MASK_CHAR.len_utf8()) } else { index.min(self.text.len()) }; } // Check if mouse is in this line's bounds if let Some(local_index) = line_layout.closest_index_for_position(pos, last_layout) { let index = line_start_offset + local_index; return if self.masked { self.text.char_index_to_offset(index / MASK_CHAR.len_utf8()) } else { index.min(self.text.len()) }; } else if pos.y < px(0.) { // Mouse is above this line, return start of this line return if self.masked { self.text .char_index_to_offset(line_start_offset / MASK_CHAR.len_utf8()) } else { line_start_offset }; } y_offset += line_layout.size(line_height).height; } // Mouse is below all visible lines, return end of text self.text.len() } /// Returns a y offsetted point for the line origin. /// Select the text from the current cursor position to the given offset. /// /// The offset is the UTF-8 offset. /// /// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset. pub(crate) fn select_to(&mut self, offset: usize, cx: &mut Context) { let offset = offset.clamp(0, self.text.len()); if self.selection_reversed { self.selected_range.start = offset } else { self.selected_range.end = offset }; if self.selected_range.end < self.selected_range.start { self.selection_reversed = !self.selection_reversed; self.selected_range = (self.selected_range.end..self.selected_range.start).into(); } // Ensure keep word selected range if let Some(word_range) = self.selected_word_range.as_ref() { if self.selected_range.start > word_range.start { self.selected_range.start = word_range.start; } if self.selected_range.end < word_range.end { self.selected_range.end = word_range.end; } } if self.selected_range.is_empty() { self.update_preferred_column(); } cx.notify() } /// Unselects the currently selected text. pub fn unselect(&mut self, _: &mut Window, cx: &mut Context) { let offset = self.cursor(); self.selected_range = (offset..offset).into(); cx.notify() } #[inline] pub(super) fn offset_from_utf16(&self, offset: usize) -> usize { self.text.offset_utf16_to_offset(offset) } #[inline] pub(super) fn offset_to_utf16(&self, offset: usize) -> usize { self.text.offset_to_offset_utf16(offset) } #[inline] pub(super) fn range_to_utf16(&self, range: &Range) -> Range { self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end) } #[inline] pub(super) fn range_from_utf16(&self, range_utf16: &Range) -> Range { self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end) } /// If offset falls on a hidden (folded) line, clamp backward to the end of /// the fold header line (last visible position before the fold). fn clamp_offset_to_visible_backward(&self, offset: usize) -> usize { let line = self.text.offset_to_point(offset).row; if self.display_map.is_buffer_line_hidden(line) { for fold in self.display_map.folded_ranges() { if line > fold.start_line && line <= fold.end_line { return self.text.line_end_offset(fold.start_line); } } } offset } /// If offset falls on a hidden (folded) line, clamp forward to the start of /// the fold end line (first visible position after the fold). fn clamp_offset_to_visible_forward(&self, offset: usize) -> usize { let line = self.text.offset_to_point(offset).row; if self.display_map.is_buffer_line_hidden(line) { for fold in self.display_map.folded_ranges() { if line > fold.start_line && line <= fold.end_line { return self.text.line_start_offset(fold.end_line); } } } offset } pub(super) fn previous_boundary(&self, offset: usize) -> usize { let mut offset = self.text.clip_offset(offset.saturating_sub(1), Bias::Left); if let Some(ch) = self.text.char_at(offset) && ch == '\r' { offset -= 1; } self.clamp_offset_to_visible_backward(offset) } pub(super) fn next_boundary(&self, offset: usize) -> usize { let mut offset = self.text.clip_offset(offset + 1, Bias::Right); if let Some(ch) = self.text.char_at(offset) && ch == '\r' { offset += 1; } self.clamp_offset_to_visible_forward(offset) } /// Returns the true to let InputElement to render cursor, when Input is focused and current BlinkCursor is visible. pub(crate) fn show_cursor(&self, window: &Window, cx: &App) -> bool { self.focus_handle.is_focused(window) && !self.disabled && self.blink_cursor.read(cx).visible() && window.is_window_active() } fn on_focus(&mut self, _: &mut Window, cx: &mut Context) { self.blink_cursor.update(cx, |cursor, cx| { cursor.start(cx); }); cx.emit(InputEvent::Focus); } fn on_blur(&mut self, window: &mut Window, cx: &mut Context) { self.blink_cursor.update(cx, |cursor, cx| { cursor.stop(cx); }); Root::update(window, cx, |root, _, _| { root.focused_input = None; }); cx.emit(InputEvent::Blur); cx.notify(); } pub(super) fn pause_blink_cursor(&mut self, cx: &mut Context) { self.blink_cursor.update(cx, |cursor, cx| { cursor.pause(cx); }); } pub(super) fn on_key_down(&mut self, _: &KeyDownEvent, _: &mut Window, cx: &mut Context) { self.pause_blink_cursor(cx); } pub(super) fn on_drag_move( &mut self, event: &MouseMoveEvent, window: &mut Window, cx: &mut Context, ) { if self.text.len() == 0 { return; } if self.last_layout.is_none() { return; } if !self.focus_handle.is_focused(window) { return; } if !self.selecting { return; } let offset = self.index_for_mouse_position(event.position); self.select_to(offset, cx); } fn is_valid_input(&self, new_text: &str, cx: &mut Context) -> bool { if new_text.is_empty() { return true; } if let Some(validate) = &self.validate && !validate(new_text, cx) { return false; } if !self.mask_pattern.is_valid(new_text) { return false; } let Some(pattern) = &self.pattern else { return true; }; pattern.is_match(new_text) } /// Set the mask pattern for formatting the input text. /// /// The pattern can contain: /// - 9: Any digit or dot /// - A: Any letter /// - *: Any character /// - Other characters will be treated as literal mask characters /// /// Example: "(999)999-999" for phone numbers pub fn mask_pattern(mut self, pattern: impl Into) -> Self { self.mask_pattern = pattern.into(); if let Some(placeholder) = self.mask_pattern.placeholder() { self.placeholder = placeholder.into(); } self } pub fn set_mask_pattern( &mut self, pattern: impl Into, _: &mut Window, cx: &mut Context, ) { self.mask_pattern = pattern.into(); if let Some(placeholder) = self.mask_pattern.placeholder() { self.placeholder = placeholder.into(); } cx.notify(); } pub(super) fn set_input_bounds(&mut self, new_bounds: Bounds, cx: &mut Context) { let wrap_width_changed = self.input_bounds.size.width != new_bounds.size.width; self.input_bounds = new_bounds; // Update display_map wrap_width if changed. if let Some(last_layout) = self.last_layout.as_ref() && wrap_width_changed { let wrap_width = if !self.soft_wrap { // None to disable wrapping (will use Pixels::MAX) None } else { last_layout.wrap_width }; self.display_map.on_layout_changed(wrap_width, cx); self.mode.update_auto_grow(&self.display_map); cx.notify(); } } pub(super) fn selected_text(&self) -> RopeSlice<'_> { let range_utf16 = self.range_to_utf16(&self.selected_range.into()); let range = self.range_from_utf16(&range_utf16); self.text.slice(range) } /// Return the rendered bounds for a UTF-8 byte range in the current input contents. /// /// Returns `None` when the requested range is not currently laid out or visible. pub fn range_to_bounds(&self, range: &Range) -> Option> { let last_layout = self.last_layout.as_ref()?; let last_bounds = self.last_bounds?; let (_, _, start_pos) = self.line_and_position_for_offset(range.start); let (_, _, end_pos) = self.line_and_position_for_offset(range.end); let start_pos = start_pos?; let end_pos = end_pos?; Some(Bounds::from_corners( last_bounds.origin + start_pos, last_bounds.origin + end_pos + point(px(0.), last_layout.line_height), )) } /// Replace text in range in silent. /// /// This will not trigger any UI interaction, such as auto-completion. pub(crate) fn replace_text_in_range_silent( &mut self, range_utf16: Option>, new_text: &str, window: &mut Window, cx: &mut Context, ) { self.silent_replace_text = true; self.replace_text_in_range(range_utf16, new_text, window, cx); self.silent_replace_text = false; } } 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 = self.range_from_utf16(&range_utf16); adjusted_range.replace(self.range_to_utf16(&range)); Some(self.text.slice(range).to_string()) } fn selected_text_range( &mut self, _ignore_disabled_input: bool, _window: &mut Window, _cx: &mut Context, ) -> Option { Some(UTF16Selection { range: self.range_to_utf16(&self.selected_range.into()), reversed: false, }) } fn marked_text_range( &self, _window: &mut Window, _cx: &mut Context, ) -> Option> { self.ime_marked_range .map(|range| self.range_to_utf16(&range.into())) } fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context) { self.ime_marked_range = None; } /// Replace text in range. /// /// - If the new text is invalid, it will not be replaced. /// - If `range_utf16` is not provided, the current selected range will be used. fn replace_text_in_range( &mut self, range_utf16: Option>, new_text: &str, _window: &mut Window, cx: &mut Context, ) { if self.disabled { return; } if self.blink_cursor.read(cx).visible() { self.pause_blink_cursor(cx); } let range = range_utf16 .as_ref() .map(|range_utf16| self.range_from_utf16(range_utf16)) .or(self.ime_marked_range.map(|range| { let range = self.range_to_utf16(&(range.start..range.end)); self.range_from_utf16(&range) })) .unwrap_or(self.selected_range.into()); let old_text = self.text.clone(); self.text.replace(range.clone(), new_text); let mut new_offset = (range.start + new_text.len()).min(self.text.len()); if self.mode.is_single_line() { let pending_text = self.text.to_string(); // Check if the new text is valid if !self.is_valid_input(&pending_text, cx) { self.text = old_text; return; } if !self.mask_pattern.is_none() { let mask_text = self.mask_pattern.mask(&pending_text); self.text = Rope::from(mask_text.as_str()); let new_text_len = (new_text.len() + mask_text.len()).saturating_sub(pending_text.len()); new_offset = (range.start + new_text_len).min(mask_text.len()); } } self.push_history(&old_text, &range, new_text); self.history.end_grouping(); // Adjust folds before updating wrap map: remove overlapping folds and shift others self.display_map .adjust_folds_for_edit(&old_text, &range, new_text); self.display_map .on_text_changed(&self.text, &range, &Rope::from(new_text), cx); self.selected_range = (new_offset..new_offset).into(); self.ime_marked_range.take(); self.update_preferred_column(); self.mode.update_auto_grow(&self.display_map); if self.emit_events { cx.emit(InputEvent::Change); } cx.notify(); } /// Mark text is the IME temporary insert on typing. 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.disabled { return; } let range = range_utf16 .as_ref() .map(|range_utf16| self.range_from_utf16(range_utf16)) .or(self.ime_marked_range.map(|range| { let range = self.range_to_utf16(&(range.start..range.end)); self.range_from_utf16(&range) })) .unwrap_or(self.selected_range.into()); let old_text = self.text.clone(); self.text.replace(range.clone(), new_text); if self.mode.is_single_line() { let pending_text = self.text.to_string(); if !self.is_valid_input(&pending_text, cx) { self.text = old_text; return; } } // Adjust folds before updating wrap map: remove overlapping folds and shift others self.display_map .adjust_folds_for_edit(&old_text, &range, new_text); self.display_map .on_text_changed(&self.text, &range, &Rope::from(new_text), cx); if new_text.is_empty() { // Cancel selection, when cancel IME input. self.selected_range = (range.start..range.start).into(); self.ime_marked_range = None; } else { self.ime_marked_range = Some((range.start..range.start + new_text.len()).into()); self.selected_range = new_selected_range_utf16 .as_ref() .map(|range_utf16| self.range_from_utf16(range_utf16)) .map(|new_range| new_range.start + range.start..new_range.end + range.end) .unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len()) .into(); } self.mode.update_auto_grow(&self.display_map); self.history.start_grouping(); self.push_history(&old_text, &range, new_text); cx.notify(); } /// Used to position IME candidates. fn bounds_for_range( &mut self, range_utf16: Range, bounds: Bounds, _window: &mut Window, _cx: &mut Context, ) -> Option> { let last_layout = self.last_layout.as_ref()?; let line_height = last_layout.line_height; let line_number_width = last_layout.line_number_width; let range = self.range_from_utf16(&range_utf16); let mut start_origin = None; let mut end_origin = None; let line_number_origin = point(line_number_width, px(0.)); let mut y_offset = last_layout.visible_top; for (vi, line) in last_layout.lines.iter().enumerate() { if start_origin.is_some() && end_origin.is_some() { break; } let index_offset = last_layout.visible_line_byte_offsets[vi]; if start_origin.is_none() && let Some(p) = line.position_for_index( range.start.saturating_sub(index_offset), last_layout, false, ) { start_origin = Some(p + point(px(0.), y_offset)); } if end_origin.is_none() && let Some(p) = line.position_for_index( range.end.saturating_sub(index_offset), last_layout, false, ) { end_origin = Some(p + point(px(0.), y_offset)); } y_offset += line.size(line_height).height; } let start_origin = start_origin.unwrap_or_default(); let mut end_origin = end_origin.unwrap_or_default(); // Ensure at same line. end_origin.y = start_origin.y; Some(Bounds::from_corners( bounds.origin + line_number_origin + start_origin, // + line_height for show IME panel under the cursor line. bounds.origin + line_number_origin + point(end_origin.x, end_origin.y + line_height), )) } fn character_index_for_point( &mut self, point: gpui::Point, _window: &mut Window, _cx: &mut Context, ) -> Option { let last_layout = self.last_layout.as_ref()?; let line_point = self.last_bounds?.localize(&point)?; for (vi, line) in last_layout.lines.iter().enumerate() { let offset = last_layout.visible_line_byte_offsets[vi]; if let Some(utf8_index) = line.index_for_position(line_point, last_layout) { return Some(self.offset_to_utf16(offset + utf8_index)); } } None } } impl Focusable for InputState { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() } } impl Render for InputState { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { div() .id("input-state") .flex_1() .when(self.mode.is_multi_line(), |this| this.h_full()) .flex_grow_1() .overflow_x_hidden() .child(TextElement::new(cx.entity().clone()).placeholder(self.placeholder.clone())) } }