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

(mut self, placeholder: P) -> Self where P: Into, { self.placeholder = placeholder.into(); self } /// Renders the field inert, unfocusable, and dimmed; disabled always wins. pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } /// Overrides the accessibility label. pub fn accessibility_label(mut self, label: L) -> Self where L: Into, { self.a11y_label = Some(label.into()); self } } impl Sizable for Input { fn apply_size(&mut self, size: Size) { self.size = size; } } impl RenderOnce for Input { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); let colors = theme.colors; let radius = theme.radius; let metrics = theme.sizes.resolve(self.size); let focus_handle = self.state.read(cx).focus_handle.clone(); let focused = focus_handle.is_focused(window); let is_empty = self.state.read(cx).text.is_empty(); let fallback_label = self .a11y_label .clone() .or_else(|| (!self.placeholder.is_empty()).then_some(self.placeholder.clone())) .unwrap_or_else(|| SharedString::from(self.id.to_string())); h_flex() .id(self.id.clone()) .role(Role::TextInput) .aria_label(fallback_label) .relative() .overflow_hidden() .w_full() .h(metrics.input_height) .px(metrics.padding_x / 2.0) .rounded(radius.control) .border_1() .border_color(colors.border) .bg(colors.input) .text_size(metrics.text_size) .text_color(colors.foreground) .when(self.disabled, |input| input.opacity(0.5)) .when(!self.disabled, |input| { input.track_focus(&focus_handle).tab_stop(true) }) .when(focused, |input| input.border_color(colors.ring)) .when(is_empty && !focused, |input| { input.child( div() .text_color(colors.muted_foreground) .child(self.placeholder.clone()), ) }) .child( canvas( { let state = self.state.clone(); move |_bounds, window, cx| state.read(cx).shape_current_text(window) }, { let state = self.state.clone(); let focus = focus_handle.clone(); let padding = metrics.padding_x / 2.0; let disabled = self.disabled; // `line` comes from the prepaint closure above. move |bounds, line, window, cx| { let dragging = state.update(cx, |state, cx| { state.paint(line, bounds, padding, colors, window, cx) }); if disabled { return; } window.handle_input( &focus, gpui::ElementInputHandler::new(bounds, state.clone()), cx, ); if dragging { InputState::register_drag_listeners(&state, window); } } }, ) .absolute() .inset_0(), ) .when(!self.disabled, |input| { input .on_mouse_down(MouseButton::Left, { let mouse_down_handler = self .state .read(cx) .mouse_down_handler .clone() .expect("input state always carries its mouse-down handler"); move |event: &MouseDownEvent, window, cx| { mouse_down_handler(event, window, cx); } }) .on_key_down({ let key_down_handler = self .state .read(cx) .key_down_handler .clone() .expect("input state always carries its key handler"); move |event: &KeyDownEvent, window, cx| { key_down_handler(event, window, cx); } }) }) } }