use std::cell::RefCell; use std::ops::Range; use std::rc::Rc; use unicode_segmentation::UnicodeSegmentation; use gpui::prelude::FluentBuilder as _; use gpui::{ App, AppContext as _, Bounds, ClipboardItem, Context, DispatchPhase, ElementId, Entity, EntityInputHandler, 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 _; use crate::common::h_flex; /// 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, } /// 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>, focus_handle: gpui::FocusHandle, layout: Rc>>, } impl InputState { fn new_inner(cx: &mut App) -> Self { Self { text: String::new(), cursor: 0, anchor: 0, marked_range: None, focus_handle: cx.focus_handle(), 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. pub fn new_with_text(text: impl Into, cx: &mut App) -> Entity { let text = 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. pub fn set_value(&mut self, value: V, window: &mut Window, cx: &mut Context) where V: Into, { self.text = value.into(); self.cursor = self.text.len(); self.anchor = self.cursor; self.marked_range = None; 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); } /// 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, ) { self.text.replace_range(range.clone(), text); self.cursor = range.start + text.len(); self.anchor = self.cursor; self.marked_range = None; 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 or cuts the selection to the clipboard. fn copy_or_cut(&mut self, cut: bool, window: &mut Window, cx: &mut Context) { let range = self.selection(); if range.is_empty() { return; } cx.write_to_clipboard(ClipboardItem::new_string( self.text[range.clone()].to_string(), )); if cut { self.replace_range(range, "", window, 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, ) { 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, ) { let range = range_utf16.map_or_else( || self.selection(), |range| utf16_range_to_utf8(&self.text, range), ); 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; 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()) } } /// 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, 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(), 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 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 focus_handle = self.state.read(cx).focus_handle.clone(); let layout_cell = self.state.read(cx).layout.clone(); // Drag selection crosses frames, so it lives in keyed element state. let dragging = window.use_keyed_state("input-dragging", cx, |_, _| false); let down_dragging = dragging.clone(); let paint_dragging = dragging.clone(); let focused = focus_handle.is_focused(window); let is_empty = self.state.read(cx).text.is_empty(); let key_state = self.state.clone(); let key_focus = focus_handle.clone(); let down_state = self.state.clone(); let down_layout = layout_cell.clone(); 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()) .relative() .overflow_hidden() .w_full() .h_7() .px_2() .rounded(radius.control) .border_1() .border_color(colors.border) .bg(colors.input) .text_sm() .text_color(colors.foreground) .role(Role::TextInput) .aria_label(fallback_label) .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| { let text = state.read(cx).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) } }, { let state = self.state.clone(); let layout_cell = layout_cell.clone(); let focus = focus_handle.clone(); let dragging = paint_dragging; let disabled = self.disabled; // `line` comes from the prepaint closure above. move |bounds, line, window, cx| { let (font_size, line_height) = { let text_style = window.text_style(); ( text_style.font_size.to_pixels(window.rem_size()), text_style .line_height .to_pixels(text_style.font_size, window.rem_size()), ) }; let padding = px(8.); let selection = state.read(cx).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, ); *layout_cell.borrow_mut() = Some(InputLayout { line, bounds, origin, }); let layout = layout_cell.borrow(); let layout = layout.as_ref().expect("layout just stored"); let line = &layout.line; let origin = layout.origin; if focused && !selection.is_empty() { let start_x = 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, colors.selection)); } let _ = line.paint(origin, line_height, TextAlign::Left, None, window, cx); if focused && selection.is_empty() { 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, colors.foreground)); } if disabled { return; } window.handle_input( &focus, gpui::ElementInputHandler::new(bounds, state.clone()), cx, ); if *dragging.read(cx) { window.on_mouse_event({ let state = state.clone(); let layout_cell = layout_cell.clone(); move |event: &MouseMoveEvent, phase, window, cx| { if phase != DispatchPhase::Bubble || event.pressed_button != Some(MouseButton::Left) { return; } state.update(cx, |state, cx| { let index = index_at_x( &layout_cell, event.position.x, state.text.len(), ); state.cursor = index; state.notify(window, cx); }); } }); } window.on_mouse_event({ let dragging = dragging.clone(); move |event: &MouseUpEvent, phase, _window, cx| { if phase != DispatchPhase::Bubble || event.button != MouseButton::Left { return; } dragging.update(cx, |dragging, _| *dragging = false); } }); } }, ) .absolute() .inset_0(), ) .when(!self.disabled, |input| { input .on_mouse_down(MouseButton::Left, { let state = down_state; let layout_cell = down_layout; let dragging = down_dragging; move |event: &MouseDownEvent, window, cx| { dragging.update(cx, |dragging, _| *dragging = true); state.update(cx, |state, cx| { let index = index_at_x(&layout_cell, event.position.x, state.text.len()); if event.click_count == 2 { let start = previous_word_start(&state.text, index); let end = next_word_end(&state.text, index); state.select_range(start..end, window, cx); } else if event.modifiers.shift { state.cursor = index; state.notify(window, cx); } else { state.cursor = index; state.anchor = index; state.notify(window, cx); } }); } }) .on_key_down({ move |event: &KeyDownEvent, window, cx| { if !key_focus.contains_focused(window, cx) { return; } let keystroke = &event.keystroke; let command = keystroke.modifiers.platform || keystroke.modifiers.control; key_state.update(cx, |state, cx| match keystroke.key.as_str() { "left" => { let to = if command || keystroke.modifiers.alt { previous_word_start(&state.text, state.cursor) } else { previous_grapheme_start(&state.text, state.cursor) }; state.move_cursor(to, keystroke.modifiers.shift, window, cx); cx.stop_propagation(); } "right" => { let to = if command || keystroke.modifiers.alt { next_word_end(&state.text, state.cursor) } else { next_grapheme_start(&state.text, state.cursor) }; state.move_cursor(to, keystroke.modifiers.shift, window, cx); cx.stop_propagation(); } "home" => { state.move_cursor(0, keystroke.modifiers.shift, window, cx); cx.stop_propagation(); } "end" => { state.move_cursor( state.text.len(), keystroke.modifiers.shift, window, cx, ); cx.stop_propagation(); } "backspace" => { state.delete_backward( command || keystroke.modifiers.alt, window, cx, ); cx.stop_propagation(); } "delete" => { state.delete_forward( command || keystroke.modifiers.alt, window, cx, ); cx.stop_propagation(); } "a" if command => { let len = state.text.len(); state.select_range(0..len, window, cx); cx.stop_propagation(); } "c" if command => { state.copy_or_cut(false, window, cx); cx.stop_propagation(); } "x" if command => { state.copy_or_cut(true, window, cx); cx.stop_propagation(); } "v" if command => { if let Some(item) = cx.read_from_clipboard() && let Some(text) = item.text() { let range = state.selection(); state.replace_range(range, &text, window, cx); } cx.stop_propagation(); } "enter" | "tab" => {} _ => { if let Some(text) = &keystroke.key_char && !command { let range = state.selection(); state.replace_range(range, text, window, cx); cx.stop_propagation(); } } }); } }) }) } } /// The text index at window x, using the last painted layout. fn index_at_x(layout_cell: &RefCell>, x: Pixels, text_len: usize) -> usize { let layout = layout_cell.borrow(); let Some(layout) = layout.as_ref() else { return 0; }; layout .line .closest_index_for_x(x - layout.origin.x) .min(text_len) }