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::{ AnyElement, App, AppContext as _, Bounds, ClipboardItem, ContentMask, Context, CursorStyle, DefiniteLength, DispatchPhase, ElementId, Entity, EntityInputHandler, EventEmitter, Font, HitboxBehavior, InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, RenderOnce, Role, ScrollDelta, ScrollEvent, SharedString, StatefulInteractiveElement as _, Styled as _, Subscription, Task, TextAlign, TextRun, UTF16Selection, Window, WrappedLine, actions, canvas, div, fill, point, px, }; use gpui_native_icons::IconRole; use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, ColorTokens, Size}; use gpui_native_utils::focus_ring; use crate::button::{Button, ButtonVariant}; 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); /// How often the focused caret blinks. const CARET_BLINK_INTERVAL: Duration = Duration::from_millis(500); /// Pixel height of one scroll "line" for wheel scrolling. const SCROLL_LINES_HEIGHT: f32 = 20.0; /// The row count a multiline field shows until the app sets one. const DEFAULT_ROWS: usize = 4; /// Maximum undo entries kept; the oldest drops off the front. const MAX_HISTORY: usize = 100; /// The character a password field paints instead of each grapheme. const MASK: &str = "\u{2022}"; /// The byte length of [`MASK`]; the masked display is `graphemes × MASK_LEN`. const MASK_LEN: usize = 3; /// Handler for key down events. type KeyDownHandler = std::rc::Rc; /// Handler for mouse down events. type MouseDownHandler = std::rc::Rc; /// 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, } /// Which keystroke submits a multiline field. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum SubmitOn { /// Enter inserts a newline; modifier + Enter submits. #[default] ModifiedEnter, /// Enter submits; Shift + Enter inserts a newline. Enter, } actions!( input, [ Escape, Submit, SecondarySubmit, InsertNewline, Up, Down, SelectUp, SelectDown, 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 secondary_submit: Option, pub insert_newline: 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 up: Option, pub down: Option, pub select_left: Option, pub select_right: Option, pub select_up: Option, pub select_down: 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)), secondary_submit: Some(KeyBinding::new("cmd-enter", SecondarySubmit, context)), insert_newline: Some(KeyBinding::new("shift-enter", InsertNewline, 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)), up: Some(KeyBinding::new("up", Up, context)), down: Some(KeyBinding::new("down", Down, context)), select_left: Some(KeyBinding::new("shift-left", SelectLeft, context)), select_right: Some(KeyBinding::new("shift-right", SelectRight, context)), select_up: Some(KeyBinding::new("shift-up", SelectUp, context)), select_down: Some(KeyBinding::new("shift-down", SelectDown, 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)), secondary_submit: Some(KeyBinding::new("ctrl-enter", SecondarySubmit, context)), insert_newline: Some(KeyBinding::new("shift-enter", InsertNewline, 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)), up: Some(KeyBinding::new("up", Up, context)), down: Some(KeyBinding::new("down", Down, context)), select_left: Some(KeyBinding::new("shift-left", SelectLeft, context)), select_right: Some(KeyBinding::new("shift-right", SelectRight, context)), select_up: Some(KeyBinding::new("shift-up", SelectUp, context)), select_down: Some(KeyBinding::new("shift-down", SelectDown, 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, secondary_submit: None, insert_newline: None, backspace: None, delete: None, delete_word_left: None, delete_word_right: None, delete_to_beginning_of_line: None, left: None, right: None, up: None, down: None, select_left: None, select_right: None, select_up: None, select_down: 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.secondary_submit, self.insert_newline, self.backspace, self.delete, self.delete_word_left, self.delete_word_right, self.delete_to_beginning_of_line, self.left, self.right, self.up, self.down, self.select_left, self.select_right, self.select_up, self.select_down, 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 } } /// The shaped text: one wrapped line per buffer line, plus the mask map when masking. struct ShapedText { lines: Vec, mask_map: Option>, } /// One shaped source line and its first byte's offset in the display text. struct ShapedLineEntry { start: usize, line: WrappedLine, } /// The field's inner padding. #[derive(Clone, Copy)] struct FieldPadding { left: Pixels, right: Pixels, /// Top inset; multiline fields balance it below the last row. top: Pixels, } /// One visual (wrapped) row: a display-byte range and its y offset from the text origin. struct VisualRow { range: Range, y: Pixels, /// Index of the source line this row belongs to. line_ix: usize, } /// Input layout information for hit testing and caret positioning. struct InputLayout { lines: Vec, /// The visual rows in order, top to bottom. rows: Vec, bounds: Bounds, /// Where the text origin (top-left of row 0) sits inside `bounds`. origin: gpui::Point, /// Maps display offsets to real buffer offsets for masked input. mask_map: Option>, /// The height of one visual row. line_height: Pixels, /// Inner width available to the text. text_width: Pixels, /// Height of all rows stacked. content_height: Pixels, /// Height of the region the rows scroll within. visible_height: Pixels, } impl InputLayout { /// The x of a display index relative to its row's start. fn row_x(&self, row_ix: usize, display: usize) -> Pixels { let row = &self.rows[row_ix]; let entry = &self.lines[row.line_ix]; let layout = &entry.line.unwrapped_layout; let row_start = row.range.start - entry.start; layout.x_for_index(display - entry.start) - layout.x_for_index(row_start) } /// The row containing `display` and the caret's x relative to the row start. fn row_for_index(&self, display: usize) -> Option<(usize, Pixels)> { if self.rows.is_empty() { return None; } let row_ix = row_ix_for_display(&self.rows, display); let display = display.min(self.rows[row_ix].range.end); Some((row_ix, self.row_x(row_ix, display))) } /// The display index within `row_ix` closest to `x`, which is relative to the row start. fn index_at_row_x(&self, row_ix: usize, x: Pixels) -> usize { let row = &self.rows[row_ix]; let entry = &self.lines[row.line_ix]; let layout = &entry.line.unwrapped_layout; let row_start = row.range.start - entry.start; layout .closest_index_for_x(layout.x_for_index(row_start) + x) .clamp(row_start, row.range.end - entry.start) + entry.start } /// The display index at a window point, using the last painted layout. fn index_at(&self, position: gpui::Point) -> usize { if self.rows.is_empty() { return 0; } let rel_y = (position.y - self.origin.y) / self.line_height; let row_ix = (rel_y.floor().max(0.0) as usize).min(self.rows.len() - 1); self.index_at_row_x(row_ix, position.x - self.origin.x) } /// The display range of the row containing `display`. fn row_range_at(&self, display: usize) -> Option> { self.rows .iter() .find(|row| display <= row.range.end) .map(|row| row.range.clone()) } /// The overlaps of a display range with each visual row: /// `(row index, clipped range, the range continues past the row)`. /// /// A range that strictly passes an empty row paints that row full width. fn row_overlaps(&self, range: Range) -> Vec<(usize, Range, bool)> { let mut overlaps = Vec::new(); for (row_ix, row) in self.rows.iter().enumerate() { let touches = range.start < row.range.end || (row.range.is_empty() && range.start <= row.range.start); let beyond = touches && range.end > row.range.end; let start = range.start.clamp(row.range.start, row.range.end); let end = range.end.clamp(row.range.start, row.range.end); if start < end || beyond { overlaps.push((row_ix, start..end, beyond)); } } overlaps } } /// 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, } } } /// Caret blink controller. struct CaretBlink { interval: Duration, generation: usize, visible: bool, active: bool, paused: bool, /// Pending blink timers; held so they die with the controller. tasks: Vec>, } impl CaretBlink { fn new(_cx: &mut Context) -> Self { Self { interval: CARET_BLINK_INTERVAL, generation: 0, visible: true, active: false, paused: false, tasks: Vec::new(), } } fn visible(&self) -> bool { self.visible } /// Starts blinking; the caret shows first and hides after one interval. fn enable(&mut self, cx: &mut Context) { if self.active { return; } self.active = true; self.visible = true; self.paused = false; self.schedule(cx); } /// Stops blinking. The caret stays hidden; paint gates on focus anyway. fn disable(&mut self, cx: &mut Context) { self.active = false; self.visible = false; self.paused = false; cx.notify(); } /// Holds the caret visible and restarts the hide timer. fn pause(&mut self, cx: &mut Context) { if !self.active { return; } if !self.visible { self.visible = true; cx.notify(); } self.paused = true; self.generation += 1; self.schedule_pause(cx); } fn tick(&mut self, cx: &mut Context) { if !self.active || self.paused { return; } self.visible = !self.visible; cx.notify(); self.schedule(cx); } fn schedule(&mut self, cx: &mut Context) { self.generation += 1; self.schedule_pause(cx); } fn schedule_pause(&mut self, cx: &mut Context) { let generation = self.generation; let interval = self.interval; let task = cx.spawn(async move |this, cx| { cx.background_executor().timer(interval).await; this.update(cx, |this, cx| { if this.generation == generation { this.paused = false; this.tick(cx); } }) .ok(); }); self.tasks.push(task); } } /// The application-owned state of a 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, /// When set, the field is a password field. password: bool, /// Whether a password field currently shows its content. revealed: bool, /// Whether the field holds multiple lines of text. multiline: bool, /// Which keystroke submits a multiline field. submit_on: SubmitOn, /// The x the caret keeps across vertical movement, reset on any other move. goal_x: Option, /// Content scroll inside the field: x for single-line, y for multiline, clamped at paint. scroll_offset: gpui::Point, /// Set by every caret-affecting change. scroll_to_caret_pending: bool, /// Drives the focused caret's blink. caret_blink: Entity, /// The shaped line and element bounds, recorded during each paint, /// and shared with hit-testing and the IME geometry conversions. layout: Rc>>, /// Keeps the field repainting on blink ticks. Dropping it unsubscribes. _caret_blink_subscription: Subscription, } impl EventEmitter for InputState {} impl InputState { fn new_inner(cx: &mut Context) -> Self { let caret_blink = cx.new(CaretBlink::new); 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, password: false, revealed: false, multiline: false, submit_on: SubmitOn::default(), goal_x: None, scroll_offset: point(Pixels::ZERO, Pixels::ZERO), scroll_to_caret_pending: true, caret_blink: caret_blink.clone(), layout: Rc::new(RefCell::new(None)), _caret_blink_subscription: cx.observe(&caret_blink, |_, _, cx| cx.notify()), } } /// Creates the state entity for an application-owned input. pub fn new(cx: &mut App) -> Entity { cx.new(Self::new_inner) } /// Creates the state entity with initial text, caret at the end. /// /// Newlines become spaces; flip the field to multiline with [`set_multiline`] /// before `set_value` to seed lines. pub fn new_with_text(text: impl Into, cx: &mut App) -> Entity { let text = sanitize(text.into(), false); 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 unless the field is multiline. 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(), self.multiline); 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 } /// Sets whether the field is a password field. pub fn set_password(&mut self, password: bool, cx: &mut Context) { if self.password != password { self.password = password; if !password { self.revealed = false; } cx.notify(); } } /// Whether the field is a password field. pub fn is_password(&self) -> bool { self.password } /// Sets whether a password field currently shows its content. pub fn set_revealed(&mut self, revealed: bool, cx: &mut Context) { if self.revealed != revealed { self.revealed = revealed; cx.notify(); } } /// Whether a password field currently shows its content. pub fn is_revealed(&self) -> bool { self.revealed } /// Sets whether the field holds multiple lines. pub fn set_multiline(&mut self, multiline: bool, cx: &mut Context) { if self.multiline != multiline { self.multiline = multiline; self.scroll_offset = point(Pixels::ZERO, Pixels::ZERO); cx.notify(); } } /// Whether the field holds multiple lines. pub fn is_multiline(&self) -> bool { self.multiline } /// Sets which keystroke submits a multiline field. pub fn set_submit_on(&mut self, submit_on: SubmitOn, cx: &mut Context) { if self.submit_on != submit_on { self.submit_on = submit_on; cx.notify(); } } /// Which keystroke submits a multiline field. pub fn submit_on(&self) -> SubmitOn { self.submit_on } /// Whether the painted line masks the buffer. fn masked(&self) -> bool { self.password && !self.revealed } /// 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(); self.scroll_to_caret_pending = true; self.goal_x = None; self.caret_blink.update(cx, |blink, cx| blink.pause(cx)); window.notify_text_input_changed(&self.focus_handle); } /// Scrolls the field by a wheel delta, clamped at paint. fn scroll_by( &mut self, delta: gpui::Point, window: &mut Window, cx: &mut Context, ) -> bool { if self.multiline { self.scroll_offset.y = (self.scroll_offset.y + delta.y).max(Pixels::ZERO); } else { self.scroll_offset.x = (self.scroll_offset.x - delta.x).max(Pixels::ZERO); } let scrollable = self .layout .borrow() .as_ref() .is_some_and(|layout| layout.content_height > layout.visible_height); cx.notify(); window.notify_text_input_changed(&self.focus_handle); self.multiline && scrollable } /// 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; if focused { self.caret_blink.update(cx, |blink, cx| blink.enable(cx)); } else { self.caret_blink.update(cx, |blink, cx| blink.disable(cx)); } cx.emit(if focused { InputStateEvent::Focused } else { InputStateEvent::Blurred }); } /// Whether the caret should paint. pub fn caret_visible(&mut self, focused: bool, cx: &mut Context) -> bool { focused && !self.read_only && self.caret_blink.read(cx).visible() } /// 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, self.multiline); 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 { if self.password { return false; } 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 masked or plain buffer, wrapping to `wrap_width` when set, /// with the paint-time text style. fn shape_current_text(&self, wrap_width: Option, window: &Window) -> ShapedText { let (text, mask_map) = if self.masked() { masked_display(&self.text) } else { (self.text.clone(), None) }; 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, }]; let shaped = window .text_system() .shape_text(text.into(), font_size, &runs, wrap_width, None); let mut lines = Vec::new(); match shaped { Ok(shaped) => { let mut start = 0; for line in shaped { let end = start + line.len(); lines.push(ShapedLineEntry { start, line }); start = end + '\n'.len_utf8(); } } Err(err) => eprintln!("shaping input text failed: {err}"), } ShapedText { lines, mask_map } } /// Stores the paint geometry for hit-testing and the IME conversions. fn store_layout( &mut self, text: ShapedText, bounds: Bounds, padding: FieldPadding, line_height: Pixels, ) -> gpui::Point { // The visual row table: one entry per wrapped row, top to bottom. let mut rows = Vec::new(); let mut y = Pixels::ZERO; for (line_ix, entry) in text.lines.iter().enumerate() { let mut row_start = 0; let boundaries = entry.line.wrap_boundaries().iter().map(|boundary| { entry.line.runs()[boundary.run_ix].glyphs[boundary.glyph_ix].index }); for end in boundaries.chain([entry.line.len()]) { rows.push(VisualRow { range: entry.start + row_start..entry.start + end, y, line_ix, }); y += line_height; row_start = end; } } let content_height = y; let visible_height = if self.multiline { (bounds.size.height - padding.top * 2.0).max(Pixels::ZERO) } else { bounds.size.height }; let caret = if rows.is_empty() { (Pixels::ZERO, Pixels::ZERO) } else { let display = display_index(&text.mask_map, self.selection().end); let row_ix = row_ix_for_display(&rows, display); let row = &rows[row_ix]; let entry = &text.lines[row.line_ix]; let shaped = &entry.line.unwrapped_layout; let row_start = row.range.start - entry.start; let x = shaped.x_for_index(display.min(row.range.end) - entry.start) - shaped.x_for_index(row_start); (row.y, x) }; if self.multiline { let max_scroll = (content_height - visible_height).max(Pixels::ZERO); if self.scroll_to_caret_pending { self.scroll_to_caret_pending = false; let (caret_y, _) = caret; if caret_y + line_height > self.scroll_offset.y + visible_height { self.scroll_offset.y = caret_y + line_height - visible_height; } if caret_y < self.scroll_offset.y { self.scroll_offset.y = caret_y; } } self.scroll_offset.y = self.scroll_offset.y.clamp(Pixels::ZERO, max_scroll); self.scroll_offset.x = Pixels::ZERO; } else { let visible = (bounds.size.width - padding.left - padding.right).max(Pixels::ZERO); let line_width = text .lines .first() .map(|entry| entry.line.width()) .unwrap_or(Pixels::ZERO); let max_scroll = (line_width - visible).max(Pixels::ZERO); if self.scroll_to_caret_pending { self.scroll_to_caret_pending = false; // Pull the scroll just far enough to reveal the caret on whichever edge it left. let (_, caret_x) = caret; if caret_x - self.scroll_offset.x > visible { self.scroll_offset.x = caret_x - visible; } if caret_x < self.scroll_offset.x { self.scroll_offset.x = caret_x; } } self.scroll_offset.x = self.scroll_offset.x.clamp(Pixels::ZERO, max_scroll); self.scroll_offset.y = Pixels::ZERO; } let origin = point( bounds.left() + padding.left - self.scroll_offset.x, if self.multiline { bounds.top() + padding.top - self.scroll_offset.y } else { bounds.top() + (bounds.size.height - line_height) / 2.0 }, ); let text_width = (bounds.size.width - padding.left - padding.right).max(Pixels::ZERO); *self.layout.borrow_mut() = Some(InputLayout { lines: text.lines, rows, bounds, origin, mask_map: text.mask_map, line_height, text_width, content_height, visible_height, }); origin } /// Paints the selection highlight behind the text. fn paint_selection(&self, origin: gpui::Point, 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 = display_index(&layout.mask_map, selection.start); let end = display_index(&layout.mask_map, selection.end); for (row_ix, clipped, beyond) in layout.row_overlaps(start..end) { let top = origin.y + layout.rows[row_ix].y; let left = origin.x + layout.row_x(row_ix, clipped.start); let right = origin.x + if beyond { layout.text_width } else { layout.row_x(row_ix, clipped.end) }; let rect = Bounds::from_corners(point(left, top), point(right, top + layout.line_height)); window.paint_quad(fill(rect, color)); } } /// Paints the text itself. fn paint_text( &self, origin: gpui::Point, right_padding: Pixels, window: &mut Window, cx: &mut App, ) { let layout = self.layout.borrow(); let Some(layout) = layout.as_ref() else { return; }; let mask = Bounds::from_corners( point(layout.bounds.left(), layout.bounds.top()), point( layout.bounds.right() - right_padding, layout.bounds.bottom(), ), ); window.with_content_mask(Some(ContentMask { bounds: mask }), |window| { let mut line_top = Pixels::ZERO; for entry in &layout.lines { let line_origin = point(origin.x, origin.y + line_top); entry .line .paint( line_origin, layout.line_height, TextAlign::Left, None, window, cx, ) .ok(); line_top += layout.line_height * (entry.line.wrap_boundaries().len() + 1) as f32; } }); } /// Paints the caret at the cursor position. fn paint_caret( &self, origin: gpui::Point, font_size: Pixels, color: gpui::Hsla, window: &mut Window, ) { let layout = self.layout.borrow(); let Some(layout) = layout.as_ref() else { return; }; let display = display_index(&layout.mask_map, self.cursor); let Some((row_ix, caret_x)) = layout.row_for_index(display) else { return; }; let caret_top = origin.y + layout.rows[row_ix].y + (layout.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, text: ShapedText, bounds: Bounds, padding: FieldPadding, 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 = self.store_layout(text, bounds, padding, line_height); if focused && !self.selection().is_empty() { self.paint_selection(origin, colors.selection, window); } self.paint_text(origin, padding.right, window, cx); // The composition underline shows the in-progress IME text. if let Some(range) = self.marked_range.clone() && !range.is_empty() { self.paint_marked_underline(origin, range, colors.foreground, window); } // A caret promises that typing lands there; in read-only it does not. if focused && self.caret_visible(focused, cx) && self.selection().is_empty() { self.paint_caret(origin, font_size, colors.foreground, window); } self.dragging } /// The text index at a window point, using the last painted layout. fn index_at(&self, position: gpui::Point) -> usize { let layout = self.layout.borrow(); let Some(layout) = layout.as_ref() else { return 0; }; real_index(&layout.mask_map, layout.index_at(position)) } /// Extends the selection to `position` while drag-selecting. fn drag_select_to( &mut self, position: gpui::Point, window: &mut Window, cx: &mut Context, ) { self.cursor = self.index_at(position); self.notify(window, cx); } /// Paints the composition underline under the marked range. fn paint_marked_underline( &self, origin: gpui::Point, range: Range, color: gpui::Hsla, window: &mut Window, ) { let layout = self.layout.borrow(); let Some(layout) = layout.as_ref() else { return; }; let start = display_index(&layout.mask_map, range.start); let end = display_index(&layout.mask_map, range.end); let thickness = px(1.0); for (row_ix, clipped, beyond) in layout.row_overlaps(start..end) { let top = origin.y + layout.rows[row_ix].y + layout.line_height - thickness; let left = origin.x + layout.row_x(row_ix, clipped.start); let right = origin.x + if beyond { layout.text_width } else { layout.row_x(row_ix, clipped.end) }; let rect = Bounds::from_corners(point(left, top), point(right, top + thickness)); window.paint_quad(fill(rect, color)); } } /// Register drag listeners. 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, 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(event.position); if event.click_count >= 3 { // Triple-click selects the clicked row, which is all of it when single-line. let row_range = self.layout.borrow().as_ref().and_then(|layout| { let display = display_index(&layout.mask_map, index); layout.row_range_at(display).map(|range| { real_index(&layout.mask_map, range.start) ..real_index(&layout.mask_map, range.end) }) }); match row_range { Some(range) => self.select_range(range, window, cx), None => { 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 up(&mut self, _: &Up, window: &mut Window, cx: &mut Context) { self.move_vertical(-1, false, window, cx); } fn down(&mut self, _: &Down, window: &mut Window, cx: &mut Context) { self.move_vertical(1, false, window, cx); } fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context) { self.move_vertical(-1, true, window, cx); } fn select_down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Context) { self.move_vertical(1, true, window, cx); } /// Moves the caret one visual row up or down, keeping the goal column. fn move_vertical( &mut self, delta: isize, extend: bool, window: &mut Window, cx: &mut Context, ) { if !self.multiline { return; } let target = { let layout = self.layout.borrow(); let Some(layout) = layout.as_ref() else { return; }; let display = display_index(&layout.mask_map, self.cursor); let Some((row_ix, caret_x)) = layout.row_for_index(display) else { return; }; let target = row_ix as isize + delta; let (display, goal_x) = if target < 0 { (0, None) } else if target as usize >= layout.rows.len() { (display_index(&layout.mask_map, self.text.len()), None) } else { let goal_x = self.goal_x.unwrap_or(caret_x); (layout.index_at_row_x(target as usize, goal_x), Some(goal_x)) }; Some((real_index(&layout.mask_map, display), goal_x)) }; let Some((to, goal_x)) = target else { return; }; self.move_cursor(to, extend, window, cx); // Set after `move_cursor`, which resets the goal on every move. self.goal_x = goal_x; } 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) { if self.read_only || self.password { 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) { // A multiline field in the default mode turns Enter into a newline if self.multiline && self.submit_on == SubmitOn::ModifiedEnter { let range = self.selection(); self.replace_range(range, "\n", _window, cx); } else { cx.emit(InputStateEvent::Submitted); } } fn secondary_submit( &mut self, _: &SecondarySubmit, _window: &mut Window, cx: &mut Context, ) { cx.emit(InputStateEvent::Submitted); } fn insert_newline(&mut self, _: &InsertNewline, window: &mut Window, cx: &mut Context) { if self.multiline { let range = self.selection(); self.replace_range(range, "\n", window, cx); } } 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, self.multiline); 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 = display_index( &layout.mask_map, utf16_to_utf8_offset(&self.text, range_utf16.start), ); let end = display_index( &layout.mask_map, utf16_to_utf8_offset(&self.text, range_utf16.end), ); // `origin` is already an absolute window point. if start == end { let (row_ix, x) = layout.row_for_index(start)?; let top = layout.origin.y + layout.rows[row_ix].y; return Some(Bounds::from_corners( point(layout.origin.x + x, top), point(layout.origin.x + x, top + layout.line_height), )); } let overlaps = layout.row_overlaps(start..end); let (first_ix, first_range, _) = overlaps.first().cloned()?; let (last_ix, last_range, last_beyond) = overlaps.last().cloned()?; let left = layout.origin.x + layout.row_x(first_ix, first_range.start); let right = layout.origin.x + if last_beyond { layout.text_width } else { layout.row_x(last_ix, last_range.end) }; Some(Bounds::from_corners( point(left, layout.origin.y + layout.rows[first_ix].y), point( right, layout.origin.y + layout.rows[last_ix].y + layout.line_height, ), )) } 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(real_index(&layout.mask_map, layout.index_at(window_point)).min(self.text.len())) } fn text_length_utf16( &mut self, _window: &mut Window, _cx: &mut Context, ) -> Option { Some(self.text.encode_utf16().count()) } } /// A 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, /// Multiline field height in rows; ignored when the state is single-line. rows: usize, prefix: Option, suffix: 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, rows: DEFAULT_ROWS, prefix: None, suffix: 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 } /// Sets the height of a multiline field, in rows. pub fn rows(mut self, rows: usize) -> Self { self.rows = rows.max(1); 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 } /// Docks an element inside the field's leading edge. pub fn prefix(mut self, prefix: E) -> Self where E: IntoElement, { self.prefix = Some(prefix.into_any_element()); self } /// Docks an element inside the field's trailing edge. pub fn suffix(mut self, suffix: E) -> Self where E: IntoElement, { self.suffix = Some(suffix.into_any_element()); 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 is_password = self.state.read(cx).is_password(); let revealed = self.state.read(cx).is_revealed(); let multiline = self.state.read(cx).is_multiline(); let small = theme.sizes.resolve(Size::Small); let docked_height = small.control_height + BORDER_WIDTH * 2.0; let edge_inset = ((metrics.input_height - docked_height) / 2.0).max(Pixels::ZERO); let vertical_inset = metrics.dense_padding_x * 0.5; let reveal_width = small.dense_padding_x * 2.0 + small.icon_size; let field_padding = FieldPadding { left: Pixels::ZERO, right: if is_password { (edge_inset + reveal_width - metrics.dense_padding_x).max(Pixels::ZERO) } else { Pixels::ZERO }, top: vertical_inset, }; let line_height = multiline.then(|| match window.text_style().line_height { DefiniteLength::Fraction(ratio) => metrics.text_size * ratio, DefiniteLength::Absolute(length) => length.to_pixels(window.rem_size()), }); let field_height = line_height.map_or(metrics.input_height, |line_height| { line_height * self.rows as f32 + vertical_inset * 2.0 }) + BORDER_WIDTH * 2.0; 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())); let edge_zone = |name: &'static str, element: AnyElement| { div() .debug_selector(|| name.to_string()) .flex_none() .h_full() .flex() .items_center() .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) .child(element) }; h_flex() .id(self.id.clone()) .debug_selector(|| "input-field".to_string()) .key_context(INPUT_CONTEXT) .role(Role::TextInput) .aria_label(fallback_label) .relative() .overflow_hidden() .w_full() .h(field_height) .pl(if self.prefix.is_some() { edge_inset } else { metrics.dense_padding_x }) .pr(if self.suffix.is_some() { edge_inset } else { metrics.dense_padding_x }) .gap_1p5() .rounded(radius.control) .bg(colors.input) .text_size(metrics.text_size) .text_color(colors.foreground) .border_1() .border_color(if theme.borders.fields { colors.border } else { colors.input }) .when(self.disabled, |input| input.opacity(0.5)) .when(!self.disabled, |input| { input.track_focus(&focus_handle).tab_stop(true) }) .when(focused && !theme.borders.focus_ring, |input| { input.border_color(colors.ring) }) .when(focused && theme.borders.focus_ring, |input| { input.shadow(focus_ring(colors.ring)) }) .when_some(self.prefix, |input, prefix| { input.child(edge_zone("input-prefix", prefix)) }) .child( div() .relative() .flex_1() .min_w_0() .h_full() .overflow_hidden() .flex() .flex_row() .items_center() .when_some(line_height, |region, line_height| { region.items_start().line_height(line_height) }) .when(is_empty && !focused, |region| { region.child( div() .text_color(colors.muted_foreground) .when(multiline, |placeholder| placeholder.pt(vertical_inset)) .child(self.placeholder.clone()), ) }) .child( canvas( { let state = self.state.clone(); move |bounds, window, cx| { // The hitbox backs the I-beam hover cursor. let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); let wrap_width = multiline.then(|| { (bounds.size.width - field_padding.left - field_padding.right) .max(Pixels::ZERO) }); let text = state.read(cx).shape_current_text(wrap_width, window); (text, hitbox) } }, { let state = self.state.clone(); let focus = focus_handle.clone(); let disabled = self.disabled; move |bounds, (text, hitbox), window, cx| { if !disabled { window.set_cursor_style(CursorStyle::IBeam, &hitbox); } let dragging = state.update(cx, |state, cx| { state.paint(text, bounds, field_padding, colors, window, cx) }); if disabled { return; } window.handle_input( &focus, gpui::ElementInputHandler::new(bounds, state.clone()), cx, ); window.on_mouse_event({ let state = state.clone(); move |event: &ScrollEvent, phase, window, cx| { if phase != DispatchPhase::Bubble || !bounds.contains(&event.position) { return; } let delta = match event.delta { ScrollDelta::Pixels(delta) => delta, ScrollDelta::Lines(lines) => point( px(lines.x * SCROLL_LINES_HEIGHT), px(lines.y * SCROLL_LINES_HEIGHT), ), }; let consumed = state.update(cx, |state, cx| { state.scroll_by(delta, window, cx) }); if consumed { cx.stop_propagation(); } } }); if dragging { InputState::register_drag_listeners(&state, window); } } }, ) .absolute() .inset_0(), ), ) .when_some(self.suffix, |input, suffix| { input.child(edge_zone("input-suffix", suffix)) }) .when(is_password && !self.disabled, |input| { input.child( div() .id((self.id.clone(), "input-reveal")) .debug_selector(|| "input-reveal".to_string()) .absolute() .right(edge_inset) .top_0() .bottom_0() .flex() .items_center() .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) .child( Button::new((self.id.clone(), "input-reveal-button")) .debug_selector(|| "input-reveal-button".to_string()) .icon(if revealed { IconRole::EyeOff } else { IconRole::Eye }) .variant(ButtonVariant::Ghost) .small() .accessibility_label(if revealed { "Hide password" } else { "Show password" }) .on_click({ let state = self.state.clone(); move |_, _, cx| { state.update(cx, |state, cx| { state.set_revealed(!state.is_revealed(), cx) }); } }), ), ) }) .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: &SecondarySubmit, window, cx| { state.update(cx, |state, cx| state.secondary_submit(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &InsertNewline, window, cx| { state.update(cx, |state, cx| state.insert_newline(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: &Up, window, cx| { state.update(cx, |state, cx| state.up(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &Down, window, cx| { state.update(cx, |state, cx| state.down(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &SelectUp, window, cx| { state.update(cx, |state, cx| state.select_up(action, window, cx)) } }) .on_action({ let state = self.state.clone(); move |action: &SelectDown, window, cx| { state.update(cx, |state, cx| state.select_down(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)) } }) } } /// 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()); } /// The row holding `display`. fn row_ix_for_display(rows: &[VisualRow], display: usize) -> usize { rows.iter() .position(|row| display <= row.range.end) .unwrap_or(rows.len() - 1) } /// Builds the masked display: one bullet per grapheme, newlines kept as line breaks. fn masked_display(text: &str) -> (String, Option>) { let mut display = String::new(); let mut map = Vec::with_capacity(text.len() + 1); for (offset, grapheme) in text.grapheme_indices(true) { if grapheme == "\n" { display.push('\n'); map.push((offset, 1)); } else { display.push_str(MASK); map.push((offset, MASK_LEN)); } } // The end sentinel maps a click past the last glyph to the buffer end. map.push((text.len(), 0)); (display, Some(map)) } /// Maps a real buffer offset onto the masked display; identity when unmasked. fn display_index(mask_map: &Option>, real: usize) -> usize { match mask_map { None => real, Some(map) => map .iter() .take_while(|&&(offset, _)| offset < real) .map(|&(_, len)| len) .sum(), } } /// Maps a display offset from a click back onto the real buffer. fn real_index(mask_map: &Option>, display: usize) -> usize { match mask_map { None => display, Some(map) => { let mut consumed = 0; for &(offset, len) in map { if consumed >= display { return offset; } consumed += len; } map.last().map(|&(offset, _)| offset).unwrap_or(0) } } } /// Newlines have no meaning in a single-line field; multiline fields keep /// them and drop carriage returns. fn sanitize(text: impl Into, multiline: bool) -> String { let text = text.into(); if multiline { text.replace('\r', "") } else { text.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) }