Files
coop/crates/ui/src/input/state.rs
2025-05-21 18:48:00 +07:00

1856 lines
59 KiB
Rust

use serde::Deserialize;
use smallvec::SmallVec;
use std::{cell::Cell, ops::Range, rc::Rc};
use unicode_segmentation::*;
use gpui::{
actions, div, impl_internal_actions, point, prelude::FluentBuilder as _, px, App, AppContext,
Bounds, ClipboardItem, Context, DefiniteLength, Entity, EntityInputHandler, EventEmitter,
FocusHandle, Focusable, InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent,
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point,
Render, ScrollHandle, ScrollWheelEvent, SharedString, Styled as _, Subscription,
UTF16Selection, Window, WrappedLine,
};
// TODO:
// - Move cursor to skip line eof empty chars.
use super::{
blink_cursor::BlinkCursor, change::Change, element::TextElement, mask_pattern::MaskPattern,
text_wrapper::TextWrapper,
};
use crate::{history::History, scroll::ScrollbarState, Root};
#[derive(Clone, PartialEq, Eq, Deserialize)]
pub struct Enter {
/// Is confirm with secondary.
pub secondary: bool,
}
impl_internal_actions!(input, [Enter]);
actions!(
input,
[
Backspace,
Delete,
DeleteToBeginningOfLine,
DeleteToEndOfLine,
DeleteToPreviousWordStart,
DeleteToNextWordEnd,
Up,
Down,
Left,
Right,
SelectUp,
SelectDown,
SelectLeft,
SelectRight,
SelectAll,
Home,
End,
SelectToStartOfLine,
SelectToEndOfLine,
SelectToStart,
SelectToEnd,
SelectToPreviousWordStart,
SelectToNextWordEnd,
ShowCharacterPalette,
Copy,
Cut,
Paste,
Undo,
Redo,
NewLine,
MoveToStartOfLine,
MoveToEndOfLine,
MoveToStart,
MoveToEnd,
MoveToPreviousWord,
MoveToNextWord,
TextChanged,
Escape
]
);
#[derive(Clone)]
pub enum InputEvent {
Change(SharedString),
PressEnter { secondary: bool },
Focus,
Blur,
}
pub(super) const CONTEXT: &str = "Input";
pub fn init(cx: &mut App) {
cx.bind_keys([
KeyBinding::new("backspace", Backspace, Some(CONTEXT)),
KeyBinding::new("delete", Delete, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-backspace", DeleteToBeginningOfLine, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-delete", DeleteToEndOfLine, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("alt-backspace", DeleteToPreviousWordStart, Some(CONTEXT)),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-backspace", DeleteToPreviousWordStart, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("alt-delete", DeleteToNextWordEnd, Some(CONTEXT)),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-delete", DeleteToNextWordEnd, Some(CONTEXT)),
KeyBinding::new("enter", Enter { secondary: false }, Some(CONTEXT)),
KeyBinding::new("secondary-enter", Enter { secondary: true }, Some(CONTEXT)),
KeyBinding::new("escape", Escape, Some(CONTEXT)),
KeyBinding::new("up", Up, Some(CONTEXT)),
KeyBinding::new("down", Down, Some(CONTEXT)),
KeyBinding::new("left", Left, Some(CONTEXT)),
KeyBinding::new("right", Right, Some(CONTEXT)),
KeyBinding::new("shift-left", SelectLeft, Some(CONTEXT)),
KeyBinding::new("shift-right", SelectRight, Some(CONTEXT)),
KeyBinding::new("shift-up", SelectUp, Some(CONTEXT)),
KeyBinding::new("shift-down", SelectDown, Some(CONTEXT)),
KeyBinding::new("home", Home, Some(CONTEXT)),
KeyBinding::new("end", End, Some(CONTEXT)),
KeyBinding::new("shift-home", SelectToStartOfLine, Some(CONTEXT)),
KeyBinding::new("shift-end", SelectToEndOfLine, Some(CONTEXT)),
KeyBinding::new("shift-enter", NewLine, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("ctrl-shift-a", SelectToStartOfLine, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("ctrl-shift-e", SelectToEndOfLine, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("shift-cmd-left", SelectToStartOfLine, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("shift-cmd-right", SelectToEndOfLine, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("alt-shift-left", SelectToPreviousWordStart, Some(CONTEXT)),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-shift-left", SelectToPreviousWordStart, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("alt-shift-right", SelectToNextWordEnd, Some(CONTEXT)),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-shift-right", SelectToNextWordEnd, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-a", SelectAll, Some(CONTEXT)),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-a", SelectAll, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-c", Copy, Some(CONTEXT)),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-c", Copy, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-x", Cut, Some(CONTEXT)),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-x", Cut, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-v", Paste, Some(CONTEXT)),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-v", Paste, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("ctrl-a", Home, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-left", Home, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("ctrl-e", End, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-right", End, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-z", Undo, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-shift-z", Redo, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-up", MoveToStart, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-down", MoveToEnd, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("alt-left", MoveToPreviousWord, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("alt-right", MoveToNextWord, Some(CONTEXT)),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-left", MoveToPreviousWord, Some(CONTEXT)),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-right", MoveToNextWord, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-shift-up", SelectToStart, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-shift-down", SelectToEnd, Some(CONTEXT)),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-z", Undo, Some(CONTEXT)),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-y", Redo, Some(CONTEXT)),
]);
}
#[derive(Debug, Default, Clone)]
pub enum InputMode {
#[default]
SingleLine,
MultiLine {
rows: usize,
height: Option<DefiniteLength>,
},
AutoGrow {
rows: usize,
min_rows: usize,
max_rows: usize,
},
}
impl InputMode {
pub(super) fn set_rows(&mut self, new_rows: usize) {
match self {
InputMode::MultiLine { rows, .. } => {
*rows = new_rows;
}
InputMode::AutoGrow {
rows,
min_rows,
max_rows,
} => {
*rows = new_rows.clamp(*min_rows, *max_rows);
}
_ => {}
}
}
pub(super) fn update_auto_grow(&mut self, text_wrapper: &TextWrapper) {
if let Self::AutoGrow { .. } = self {
let wrapped_lines = text_wrapper.wrapped_lines.len();
self.set_rows(wrapped_lines);
}
}
/// At least 1 row be return.
pub(super) fn rows(&self) -> usize {
match self {
InputMode::MultiLine { rows, .. } => *rows,
InputMode::AutoGrow { rows, .. } => *rows,
_ => 1,
}
.max(1)
}
/// At least 1 row be return.
#[allow(unused)]
pub(super) fn min_rows(&self) -> usize {
match self {
InputMode::MultiLine { .. } => 1,
InputMode::AutoGrow { min_rows, .. } => *min_rows,
_ => 1,
}
.max(1)
}
#[allow(unused)]
pub(super) fn max_rows(&self) -> usize {
match self {
InputMode::MultiLine { .. } => usize::MAX,
InputMode::AutoGrow { max_rows, .. } => *max_rows,
_ => 1,
}
}
pub(super) fn set_height(&mut self, new_height: Option<DefiniteLength>) {
if let InputMode::MultiLine { height, .. } = self {
*height = new_height;
}
}
pub(super) fn height(&self) -> Option<DefiniteLength> {
match self {
InputMode::MultiLine { height, .. } => *height,
_ => None,
}
}
}
/// InputState to keep editing state of the [`super::TextInput`].
pub struct InputState {
pub(super) focus_handle: FocusHandle,
pub(super) mode: InputMode,
pub(super) text: SharedString,
pub(super) new_line_on_enter: bool,
pub(super) text_wrapper: TextWrapper,
pub(super) history: History<Change>,
pub(super) blink_cursor: Entity<BlinkCursor>,
pub(super) loading: bool,
pub(super) selected_range: Range<usize>,
/// Range for save the selected word, use to keep word range when drag move.
pub(super) selected_word_range: Option<Range<usize>>,
pub(super) selection_reversed: bool,
pub(super) marked_range: Option<Range<usize>>,
pub(super) last_layout: Option<SmallVec<[WrappedLine; 1]>>,
pub(super) last_cursor_offset: Option<usize>,
/// The line_height of text layout, this will change will InputElement painted.
pub(super) last_line_height: Pixels,
/// The input container bounds
pub(super) input_bounds: Bounds<Pixels>,
/// The text bounds
pub(super) last_bounds: Option<Bounds<Pixels>>,
pub(super) last_selected_range: Option<Range<usize>>,
pub(super) selecting: bool,
pub(super) disabled: bool,
pub(super) masked: bool,
pub(super) clean_on_escape: bool,
pub(super) pattern: Option<regex::Regex>,
#[allow(clippy::type_complexity)]
pub(super) validate: Option<Box<dyn Fn(&str) -> bool + 'static>>,
pub(crate) scroll_handle: ScrollHandle,
pub(super) scrollbar_state: Rc<Cell<ScrollbarState>>,
/// The size of the scrollable content.
pub(crate) scroll_size: gpui::Size<Pixels>,
/// The mask pattern for formatting the input text
pub(crate) mask_pattern: MaskPattern,
pub(super) placeholder: SharedString,
/// To remember the horizontal column (x-coordinate) of the cursor position.
preferred_x_offset: Option<Pixels>,
_subscriptions: Vec<Subscription>,
}
impl EventEmitter<InputEvent> for InputState {}
impl InputState {
/// Create a Input state with default [`InputMode::SingleLine`] mode.
///
/// See also: [`Self::multi_line`], [`Self::auto_grow`] to set other mode.
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();
let blink_cursor = cx.new(|_| BlinkCursor::new());
let history = History::new().group_interval(std::time::Duration::from_secs(1));
let _subscriptions = vec![
// Observe the blink cursor to repaint the view when it changes.
cx.observe(&blink_cursor, |_, _, cx| cx.notify()),
// Blink the cursor when the window is active, pause when it's not.
cx.observe_window_activation(window, |input, window, cx| {
if window.is_window_active() {
let focus_handle = input.focus_handle.clone();
if focus_handle.is_focused(window) {
input.blink_cursor.update(cx, |blink_cursor, cx| {
blink_cursor.start(cx);
});
}
}
}),
cx.on_focus(&focus_handle, window, Self::on_focus),
cx.on_blur(&focus_handle, window, Self::on_blur),
];
let text_style = window.text_style();
Self {
focus_handle: focus_handle.clone(),
text: "".into(),
text_wrapper: TextWrapper::new(
text_style.font(),
text_style.font_size.to_pixels(window.rem_size()),
None,
),
new_line_on_enter: true,
blink_cursor,
history,
selected_range: 0..0,
selected_word_range: None,
selection_reversed: false,
marked_range: None,
input_bounds: Bounds::default(),
selecting: false,
disabled: false,
masked: false,
clean_on_escape: false,
loading: false,
pattern: None,
validate: None,
mode: InputMode::SingleLine,
last_layout: None,
last_bounds: None,
last_selected_range: None,
last_line_height: px(19.),
last_cursor_offset: None,
scroll_handle: ScrollHandle::new(),
scrollbar_state: Rc::new(Cell::new(ScrollbarState::default())),
scroll_size: gpui::size(px(0.), px(0.)),
preferred_x_offset: None,
placeholder: SharedString::default(),
mask_pattern: MaskPattern::default(),
_subscriptions,
}
}
/// Set Input to use [`InputMode::MultiLine`] mode.
///
/// Default rows is 2.
pub fn multi_line(mut self) -> Self {
self.mode = InputMode::MultiLine {
rows: 2,
height: None,
};
self
}
/// Set Input to use [`InputMode::AutoGrow`] mode with min, max rows limit.
pub fn auto_grow(mut self, min_rows: usize, max_rows: usize) -> Self {
self.mode = InputMode::AutoGrow {
rows: min_rows,
min_rows,
max_rows,
};
self
}
/// Disables new lines on enter when multi-line is enabled.
pub fn prevent_new_line_on_enter(mut self) -> Self {
self.new_line_on_enter = false;
self
}
/// Set placeholder
pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
self.placeholder = placeholder.into();
self
}
/// Set placeholder
pub fn set_placeholder(
&mut self,
placeholder: impl Into<SharedString>,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.placeholder = placeholder.into();
cx.notify();
}
/// Called after moving the cursor. Updates preferred_x_offset if we know where the cursor now is.
fn update_preferred_x_offset(&mut self, _cx: &mut Context<Self>) {
if let (Some(lines), Some(bounds)) = (&self.last_layout, &self.last_bounds) {
let offset = self.cursor_offset();
let line_height = self.last_line_height;
// Find which line and sub-line the cursor is on and its position
let (_line_index, _sub_line_index, cursor_pos) =
self.line_and_position_for_offset(offset, lines, line_height);
if let Some(pos) = cursor_pos {
// Adjust by scroll offset
let scroll_offset = bounds.origin;
self.preferred_x_offset = Some(pos.x + scroll_offset.x);
}
}
}
/// Find which line and sub-line the given offset belongs to, along with the position within that sub-line.
fn line_and_position_for_offset(
&self,
offset: usize,
lines: &[WrappedLine],
line_height: Pixels,
) -> (usize, usize, Option<Point<Pixels>>) {
let mut prev_lines_offset = 0;
let mut y_offset = px(0.);
for (line_index, line) in lines.iter().enumerate() {
let local_offset = offset.saturating_sub(prev_lines_offset);
if let Some(pos) = line.position_for_index(local_offset, line_height) {
let sub_line_index = (pos.y.0 / line_height.0) as usize;
let adjusted_pos = point(pos.x, pos.y + y_offset);
return (line_index, sub_line_index, Some(adjusted_pos));
}
y_offset += line.size(line_height).height;
prev_lines_offset += line.len() + 1;
}
(0, 0, None)
}
/// Move the cursor vertically by one line (up or down) while preserving the column if possible.
/// direction: -1 for up, +1 for down
fn move_vertical(&mut self, direction: i32, window: &mut Window, cx: &mut Context<Self>) {
if self.is_single_line() {
return;
}
let (Some(lines), Some(bounds)) = (&self.last_layout, &self.last_bounds) else {
return;
};
let offset = self.cursor_offset();
let line_height = self.last_line_height;
let (current_line_index, current_sub_line, current_pos) =
self.line_and_position_for_offset(offset, lines, line_height);
let Some(current_pos) = current_pos else {
return;
};
let current_x = self
.preferred_x_offset
.unwrap_or_else(|| current_pos.x + bounds.origin.x);
let mut new_line_index = current_line_index;
let mut new_sub_line = current_sub_line as i32;
new_sub_line += direction;
// Handle moving above the first line
if direction == -1 && new_line_index == 0 && new_sub_line < 0 {
// Move cursor to the beginning of the text
self.move_to(0, window, cx);
return;
}
if new_sub_line < 0 {
if new_line_index > 0 {
new_line_index -= 1;
new_sub_line = lines[new_line_index].wrap_boundaries.len() as i32;
} else {
new_sub_line = 0;
}
} else {
let max_sub_line = lines[new_line_index].wrap_boundaries.len() as i32;
if new_sub_line > max_sub_line {
if new_line_index < lines.len() - 1 {
new_line_index += 1;
new_sub_line = 0;
} else {
new_sub_line = max_sub_line;
}
}
}
// If after adjustment, still at the same position, do not proceed
if new_line_index == current_line_index && new_sub_line == current_sub_line as i32 {
return;
}
let target_line = &lines[new_line_index];
let line_x = current_x - bounds.origin.x;
let target_sub_line = new_sub_line as usize;
let approx_pos = point(line_x, px(target_sub_line as f32 * line_height.0));
let index_res = target_line.index_for_position(approx_pos, line_height);
let new_local_index = match index_res {
Ok(i) => i + 1,
Err(i) => i,
};
let mut prev_lines_offset = 0;
for (i, l) in lines.iter().enumerate() {
if i == new_line_index {
break;
}
prev_lines_offset += l.len() + 1;
}
let new_offset = (prev_lines_offset + new_local_index).min(self.text.len());
self.selected_range = new_offset..new_offset;
self.pause_blink_cursor(cx);
cx.notify();
}
#[inline]
pub(super) fn is_multi_line(&self) -> bool {
matches!(
self.mode,
InputMode::MultiLine { .. } | InputMode::AutoGrow { .. }
)
}
#[inline]
pub(super) fn is_single_line(&self) -> bool {
matches!(self.mode, InputMode::SingleLine)
}
#[inline]
pub(super) fn is_auto_grow(&self) -> bool {
matches!(self.mode, InputMode::AutoGrow { .. })
}
/// Set the number of rows for the multi-line Textarea.
///
/// This is only used when `multi_line` is set to true.
///
/// default: 2
pub fn rows(mut self, rows: usize) -> Self {
match self.mode {
InputMode::MultiLine { height, .. } => {
self.mode = InputMode::MultiLine { rows, height };
}
InputMode::AutoGrow { max_rows, .. } => {
self.mode = InputMode::AutoGrow {
rows,
min_rows: rows,
max_rows,
};
}
_ => {}
}
self
}
/// Set the text of the input field.
///
/// And the selection_range will be reset to 0..0.
pub fn set_value(
&mut self,
value: impl Into<SharedString>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.history.ignore = true;
self.replace_text(value, window, cx);
self.history.ignore = false;
// Ensure cursor to start when set text
self.selected_range = self.text.len()..self.text.len();
cx.notify();
}
/// Insert text at the current cursor position.
///
/// And the cursor will be moved to the end of inserted text.
pub fn insert(
&mut self,
text: impl Into<SharedString>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let text: SharedString = text.into();
let range = self.range_to_utf16(&(self.cursor_offset()..self.cursor_offset()));
self.replace_text_in_range(Some(range), &text, window, cx);
self.selected_range = self.selected_range.end..self.selected_range.end;
}
/// Replace text at the current cursor position.
///
/// And the cursor will be moved to the end of replaced text.
pub fn replace(
&mut self,
text: impl Into<SharedString>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let text: SharedString = text.into();
self.replace_text_in_range(None, &text, window, cx);
self.selected_range = self.selected_range.end..self.selected_range.end;
}
fn replace_text(
&mut self,
text: impl Into<SharedString>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let text: SharedString = text.into();
let range = 0..self.text.chars().map(|c| c.len_utf16()).sum();
self.replace_text_in_range(Some(range), &text, window, cx);
}
/// Set the disabled state of the input field.
pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
self.disabled = disabled;
cx.notify();
}
/// Set with password masked state.
pub fn masked(mut self, masked: bool) -> Self {
self.masked = masked;
self
}
/// Set the password masked state of the input field.
pub fn set_masked(&mut self, masked: bool, _: &mut Window, cx: &mut Context<Self>) {
self.masked = masked;
cx.notify();
}
/// Set true to clear the input by pressing Escape key.
pub fn clean_on_escape(mut self) -> Self {
self.clean_on_escape = true;
self
}
/// Set the regular expression pattern of the input field.
pub fn pattern(mut self, pattern: regex::Regex) -> Self {
self.pattern = Some(pattern);
self
}
/// Set the regular expression pattern of the input field with reference.
pub fn set_pattern(
&mut self,
pattern: regex::Regex,
_window: &mut Window,
_cx: &mut Context<Self>,
) {
self.pattern = Some(pattern);
}
/// Set the validation function of the input field.
pub fn validate(mut self, f: impl Fn(&str) -> bool + 'static) -> Self {
self.validate = Some(Box::new(f));
self
}
/// Set true to show indicator at the input right.
pub fn set_loading(&mut self, loading: bool, cx: &mut Context<Self>) {
self.loading = loading;
cx.notify();
}
/// Set the default value of the input field.
pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
self.text = value.into();
self
}
/// Return the value of the input field.
pub fn value(&self) -> &SharedString {
&self.text
}
/// Return the value without mask.
pub fn unmask_value(&self) -> SharedString {
self.mask_pattern.unmask(&self.text).into()
}
pub fn disabled(&self) -> bool {
self.disabled
}
/// Focus the input field.
pub fn focus(&self, window: &mut Window, _: &mut Context<Self>) {
self.focus_handle.focus(window);
}
pub(super) fn left(&mut self, _: &Left, window: &mut Window, cx: &mut Context<Self>) {
self.pause_blink_cursor(cx);
if self.selected_range.is_empty() {
self.move_to(self.previous_boundary(self.cursor_offset()), window, cx);
} else {
self.move_to(self.selected_range.start, window, cx)
}
}
pub(super) fn right(&mut self, _: &Right, window: &mut Window, cx: &mut Context<Self>) {
self.pause_blink_cursor(cx);
if self.selected_range.is_empty() {
self.move_to(self.next_boundary(self.selected_range.end), window, cx);
} else {
self.move_to(self.selected_range.end, window, cx)
}
}
pub(super) fn up(&mut self, _: &Up, window: &mut Window, cx: &mut Context<Self>) {
if self.is_single_line() {
return;
}
self.pause_blink_cursor(cx);
self.move_vertical(-1, window, cx);
}
pub(super) fn down(&mut self, _: &Down, window: &mut Window, cx: &mut Context<Self>) {
if self.is_single_line() {
return;
}
self.pause_blink_cursor(cx);
self.move_vertical(1, window, cx);
}
pub(super) fn select_left(
&mut self,
_: &SelectLeft,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.select_to(self.previous_boundary(self.cursor_offset()), window, cx);
}
pub(super) fn select_right(
&mut self,
_: &SelectRight,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.select_to(self.next_boundary(self.cursor_offset()), window, cx);
}
pub(super) fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context<Self>) {
if self.is_single_line() {
return;
}
let offset = self.start_of_line(window, cx).saturating_sub(1);
self.select_to(offset, window, cx);
}
pub(super) fn select_down(
&mut self,
_: &SelectDown,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.is_single_line() {
return;
}
let offset = (self.end_of_line(window, cx) + 1).min(self.text.len());
self.select_to(self.next_boundary(offset), window, cx);
}
pub(super) fn select_all(
&mut self,
_: &SelectAll,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.move_to(0, window, cx);
self.select_to(self.text.len(), window, cx)
}
pub(super) fn home(&mut self, _: &Home, window: &mut Window, cx: &mut Context<Self>) {
self.pause_blink_cursor(cx);
let offset = self.start_of_line(window, cx);
self.move_to(offset, window, cx);
}
pub(super) fn end(&mut self, _: &End, window: &mut Window, cx: &mut Context<Self>) {
self.pause_blink_cursor(cx);
let offset = self.end_of_line(window, cx);
self.move_to(offset, window, cx);
}
pub(super) fn shift_to_new_line(
&mut self,
_: &NewLine,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.is_multi_line() {
let is_eof = self.selected_range.end == self.text.len();
self.replace_text_in_range(None, "\n", window, cx);
// Move cursor to the start of the next line
let mut new_offset = self.next_boundary(self.cursor_offset()) - 1;
if is_eof {
new_offset += 1;
}
self.move_to(new_offset, window, cx);
}
}
pub(super) fn move_to_start(
&mut self,
_: &MoveToStart,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.move_to(0, window, cx);
}
pub(super) fn move_to_end(
&mut self,
_: &MoveToEnd,
window: &mut Window,
cx: &mut Context<Self>,
) {
let end = self.text.len();
self.move_to(end, window, cx);
}
pub(super) fn move_to_previous_word(
&mut self,
_: &MoveToPreviousWord,
window: &mut Window,
cx: &mut Context<Self>,
) {
let offset = self.previous_start_of_word();
self.move_to(offset, window, cx);
}
pub(super) fn move_to_next_word(
&mut self,
_: &MoveToNextWord,
window: &mut Window,
cx: &mut Context<Self>,
) {
let offset = self.next_end_of_word();
self.move_to(offset, window, cx);
}
pub(super) fn select_to_start(
&mut self,
_: &SelectToStart,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.select_to(0, window, cx);
}
pub(super) fn select_to_end(
&mut self,
_: &SelectToEnd,
window: &mut Window,
cx: &mut Context<Self>,
) {
let end = self.text.len();
self.select_to(end, window, cx);
}
pub(super) fn select_to_start_of_line(
&mut self,
_: &SelectToStartOfLine,
window: &mut Window,
cx: &mut Context<Self>,
) {
let offset = self.start_of_line(window, cx);
self.select_to(self.previous_boundary(offset), window, cx);
}
pub(super) fn select_to_end_of_line(
&mut self,
_: &SelectToEndOfLine,
window: &mut Window,
cx: &mut Context<Self>,
) {
let offset = self.end_of_line(window, cx);
self.select_to(self.next_boundary(offset), window, cx);
}
pub(super) fn select_to_previous_word(
&mut self,
_: &SelectToPreviousWordStart,
window: &mut Window,
cx: &mut Context<Self>,
) {
let offset = self.previous_start_of_word();
self.select_to(offset, window, cx);
}
pub(super) fn select_to_next_word(
&mut self,
_: &SelectToNextWordEnd,
window: &mut Window,
cx: &mut Context<Self>,
) {
let offset = self.next_end_of_word();
self.select_to(offset, window, cx);
}
/// Return the start offset of the previous word.
fn previous_start_of_word(&mut self) -> usize {
let offset = self.selected_range.start;
let prev_str = &self.text[..offset].to_string();
UnicodeSegmentation::split_word_bound_indices(prev_str as &str)
.filter(|(_, s)| !s.trim_start().is_empty())
.next_back()
.map(|(i, _)| i)
.unwrap_or(0)
}
/// Return the next end offset of the next word.
fn next_end_of_word(&mut self) -> usize {
let offset = self.cursor_offset();
let next_str = &self.text[offset..].to_string();
UnicodeSegmentation::split_word_bound_indices(next_str as &str)
.find(|(_, s)| !s.trim_start().is_empty())
.map(|(i, s)| offset + i + s.len())
.unwrap_or(self.text.len())
}
/// Get start of line
fn start_of_line(&mut self, window: &mut Window, cx: &mut Context<Self>) -> usize {
if self.is_single_line() {
return 0;
}
let offset = self.previous_boundary(self.cursor_offset());
self.text_for_range(self.range_to_utf16(&(0..offset + 1)), &mut None, window, cx)
.unwrap_or_default()
.rfind('\n')
.map(|i| i + 1)
.unwrap_or(0)
}
/// Get end of line
fn end_of_line(&mut self, window: &mut Window, cx: &mut Context<Self>) -> usize {
if self.is_single_line() {
return self.text.len();
}
let offset = self.next_boundary(self.cursor_offset());
// ignore if offset is "\n"
if self
.text_for_range(
self.range_to_utf16(&(offset - 1..offset)),
&mut None,
window,
cx,
)
.unwrap_or_default()
.eq("\n")
{
return offset;
}
self.text_for_range(
self.range_to_utf16(&(offset..self.text.len())),
&mut None,
window,
cx,
)
.unwrap_or_default()
.find('\n')
.map(|i| i + offset)
.unwrap_or(self.text.len())
}
pub(super) fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
if self.selected_range.is_empty() {
self.select_to(self.previous_boundary(self.cursor_offset()), window, cx)
}
self.replace_text_in_range(None, "", window, cx);
self.pause_blink_cursor(cx);
}
pub(super) fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
if self.selected_range.is_empty() {
self.select_to(self.next_boundary(self.cursor_offset()), window, cx)
}
self.replace_text_in_range(None, "", window, cx);
self.pause_blink_cursor(cx);
}
pub(super) fn delete_to_beginning_of_line(
&mut self,
_: &DeleteToBeginningOfLine,
window: &mut Window,
cx: &mut Context<Self>,
) {
let mut offset = self.start_of_line(window, cx);
if offset == self.cursor_offset() {
offset = offset.saturating_sub(1);
}
self.replace_text_in_range(
Some(self.range_to_utf16(&(offset..self.cursor_offset()))),
"",
window,
cx,
);
self.pause_blink_cursor(cx);
}
pub(super) fn delete_to_end_of_line(
&mut self,
_: &DeleteToEndOfLine,
window: &mut Window,
cx: &mut Context<Self>,
) {
let mut offset = self.end_of_line(window, cx);
if offset == self.cursor_offset() {
offset = (offset + 1).clamp(0, self.text.len());
}
self.replace_text_in_range(
Some(self.range_to_utf16(&(self.cursor_offset()..offset))),
"",
window,
cx,
);
self.pause_blink_cursor(cx);
}
pub(super) fn delete_previous_word(
&mut self,
_: &DeleteToPreviousWordStart,
window: &mut Window,
cx: &mut Context<Self>,
) {
let offset = self.previous_start_of_word();
self.replace_text_in_range(
Some(self.range_to_utf16(&(offset..self.cursor_offset()))),
"",
window,
cx,
);
self.pause_blink_cursor(cx);
}
pub(super) fn delete_next_word(
&mut self,
_: &DeleteToNextWordEnd,
window: &mut Window,
cx: &mut Context<Self>,
) {
let offset = self.next_end_of_word();
self.replace_text_in_range(
Some(self.range_to_utf16(&(self.cursor_offset()..offset))),
"",
window,
cx,
);
self.pause_blink_cursor(cx);
}
pub(super) fn enter(&mut self, action: &Enter, window: &mut Window, cx: &mut Context<Self>) {
if self.is_multi_line() && self.new_line_on_enter {
let is_eof = self.selected_range.end == self.text.len();
self.replace_text_in_range(None, "\n", window, cx);
// Move cursor to the start of the next line
let mut new_offset = self.next_boundary(self.cursor_offset()) - 1;
if is_eof {
new_offset += 1;
}
self.move_to(new_offset, window, cx);
}
cx.emit(InputEvent::PressEnter {
secondary: action.secondary,
});
}
pub(super) fn clean(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.replace_text("", window, cx);
}
pub(super) fn escape(&mut self, _: &Escape, window: &mut Window, cx: &mut Context<Self>) {
if !self.selected_range.is_empty() {
return self.unselect(window, cx);
}
if self.clean_on_escape {
return self.clean(window, cx);
}
cx.propagate();
}
pub(super) fn on_mouse_down(
&mut self,
event: &MouseDownEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.selecting = true;
let offset = self.index_for_mouse_position(event.position, window, cx);
// Double click to select word
if event.button == MouseButton::Left && event.click_count == 2 {
self.select_word(offset, window, cx);
return;
}
if event.modifiers.shift {
self.select_to(offset, window, cx);
} else {
self.move_to(offset, window, cx)
}
}
pub(super) fn on_mouse_up(
&mut self,
_: &MouseUpEvent,
_window: &mut Window,
_cx: &mut Context<Self>,
) {
self.selecting = false;
self.selected_word_range = None;
}
pub(super) fn on_scroll_wheel(
&mut self,
event: &ScrollWheelEvent,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let delta = event.delta.pixel_delta(self.last_line_height);
self.update_scroll_offset(Some(self.scroll_handle.offset() + delta), cx);
}
fn update_scroll_offset(&mut self, offset: Option<Point<Pixels>>, cx: &mut Context<Self>) {
let mut offset = offset.unwrap_or(self.scroll_handle.offset());
let safe_y_range =
(-self.scroll_size.height + self.input_bounds.size.height).min(px(0.0))..px(0.);
let safe_x_range =
(-self.scroll_size.width + self.input_bounds.size.width).min(px(0.0))..px(0.);
offset.y = offset.y.clamp(safe_y_range.start, safe_y_range.end);
offset.x = offset.x.clamp(safe_x_range.start, safe_x_range.end);
self.scroll_handle.set_offset(offset);
cx.notify();
}
pub(super) fn show_character_palette(
&mut self,
_: &ShowCharacterPalette,
window: &mut Window,
_: &mut Context<Self>,
) {
window.show_character_palette();
}
pub(super) fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
if self.selected_range.is_empty() {
return;
}
let selected_text = self.text[self.selected_range.clone()].to_string();
cx.write_to_clipboard(ClipboardItem::new_string(selected_text));
}
pub(super) fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
if self.selected_range.is_empty() {
return;
}
let selected_text = self.text[self.selected_range.clone()].to_string();
cx.write_to_clipboard(ClipboardItem::new_string(selected_text));
self.replace_text_in_range(None, "", window, cx);
}
pub(super) fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
if let Some(clipboard) = cx.read_from_clipboard() {
let mut new_text = clipboard.text().unwrap_or_default();
if !self.is_multi_line() {
new_text = new_text.replace('\n', "");
}
self.replace_text_in_range(None, &new_text, window, cx);
}
}
fn push_history(
&mut self,
range: &Range<usize>,
new_text: &str,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.history.ignore {
return;
}
let old_text = self
.text_for_range(self.range_to_utf16(range), &mut None, window, cx)
.unwrap_or("".to_string());
let new_range = range.start..range.start + new_text.len();
self.history.push(Change::new(
range.clone(),
&old_text,
new_range.clone(),
new_text,
));
}
pub(super) fn undo(&mut self, _: &Undo, window: &mut Window, cx: &mut Context<Self>) {
self.history.ignore = true;
if let Some(changes) = self.history.undo() {
for change in changes {
let range_utf16 = self.range_to_utf16(&change.new_range);
self.replace_text_in_range(Some(range_utf16), &change.old_text, window, cx);
}
}
self.history.ignore = false;
}
pub(super) fn redo(&mut self, _: &Redo, window: &mut Window, cx: &mut Context<Self>) {
self.history.ignore = true;
if let Some(changes) = self.history.redo() {
for change in changes {
let range_utf16 = self.range_to_utf16(&change.old_range);
self.replace_text_in_range(Some(range_utf16), &change.new_text, window, cx);
}
}
self.history.ignore = false;
}
/// Move the cursor to the given offset.
///
/// The offset is the UTF-8 offset.
///
/// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset.
fn move_to(&mut self, offset: usize, _: &mut Window, cx: &mut Context<Self>) {
let offset = offset.clamp(0, self.text.len());
self.selected_range = offset..offset;
self.pause_blink_cursor(cx);
self.update_preferred_x_offset(cx);
cx.notify()
}
pub(super) fn cursor_offset(&self) -> usize {
if self.selection_reversed {
self.selected_range.start
} else {
self.selected_range.end
}
}
fn index_for_mouse_position(
&self,
position: Point<Pixels>,
_window: &Window,
_cx: &App,
) -> usize {
// If the text is empty, always return 0
if self.text.is_empty() {
return 0;
}
let (Some(bounds), Some(lines)) = (self.last_bounds.as_ref(), self.last_layout.as_ref())
else {
return 0;
};
let line_height = self.last_line_height;
// TIP: About the IBeam cursor
//
// If cursor style is IBeam, the mouse mouse position is in the middle of the cursor (This is special in OS)
// The position is relative to the bounds of the text input
//
// bounds.origin:
//
// - included the input padding.
// - included the scroll offset.
let inner_position = position - bounds.origin;
let mut index = 0;
let mut y_offset = px(0.);
for line in lines.iter() {
let line_origin = self.line_origin_with_y_offset(&mut y_offset, line, line_height);
let pos = inner_position - line_origin;
// Return offset by use closest_index_for_x if is single line mode.
if self.is_single_line() {
return line.unwrapped_layout.closest_index_for_x(pos.x);
}
let index_result = line.closest_index_for_position(pos, line_height);
if let Ok(v) = index_result {
index += v;
break;
} else if line
.index_for_position(point(px(0.), pos.y), line_height)
.is_ok()
{
// Click in the this line but not in the text, move cursor to the end of the line.
// The fallback index is saved in Err from `index_for_position` method.
index += index_result.unwrap_err();
break;
} else if line.text.trim_end_matches('\r').is_empty() {
// empty line on Windows is `\r`, other is ''
let line_bounds = Bounds {
origin: line_origin,
size: gpui::size(bounds.size.width, line_height),
};
let pos = inner_position;
index += line.len();
if line_bounds.contains(&pos) {
break;
}
} else {
index += line.len();
}
// +1 for revert `lines` split `\n`
index += 1;
}
if index > self.text.len() {
self.text.len()
} else {
index
}
}
/// Returns a y offsetted point for the line origin.
fn line_origin_with_y_offset(
&self,
y_offset: &mut Pixels,
line: &WrappedLine,
line_height: Pixels,
) -> Point<Pixels> {
// NOTE: About line.wrap_boundaries.len()
//
// If only 1 line, the value is 0
// If have 2 line, the value is 1
if self.is_multi_line() {
let p = point(px(0.), *y_offset);
let height = line_height + line.wrap_boundaries.len() as f32 * line_height;
*y_offset += height;
p
} else {
point(px(0.), px(0.))
}
}
/// Select the text from the current cursor position to the given offset.
///
/// The offset is the UTF-8 offset.
///
/// Ensure the offset use self.next_boundary or self.previous_boundary to get the correct offset.
fn select_to(&mut self, offset: usize, _: &mut Window, cx: &mut Context<Self>) {
let offset = offset.clamp(0, self.text.len());
if self.selection_reversed {
self.selected_range.start = offset
} else {
self.selected_range.end = offset
};
if self.selected_range.end < self.selected_range.start {
self.selection_reversed = !self.selection_reversed;
self.selected_range = self.selected_range.end..self.selected_range.start;
}
// Ensure keep word selected range
if let Some(word_range) = self.selected_word_range.as_ref() {
if self.selected_range.start > word_range.start {
self.selected_range.start = word_range.start;
}
if self.selected_range.end < word_range.end {
self.selected_range.end = word_range.end;
}
}
if self.selected_range.is_empty() {
self.update_preferred_x_offset(cx);
}
cx.notify()
}
/// Select the word at the given offset.
///
/// The offset is the UTF-8 offset.
fn select_word(&mut self, offset: usize, window: &mut Window, cx: &mut Context<Self>) {
fn is_word(c: char) -> bool {
c.is_alphanumeric() || matches!(c, '_')
}
let mut start = self.offset_to_utf16(offset);
let mut end = start;
let prev_text = self
.text_for_range(0..start, &mut None, window, cx)
.unwrap_or_default();
let next_text = self
.text_for_range(end..self.text.len(), &mut None, window, cx)
.unwrap_or_default();
let prev_chars = prev_text.chars().rev().peekable();
let next_chars = next_text.chars().peekable();
for c in prev_chars {
if !is_word(c) {
break;
}
start -= c.len_utf16();
}
for c in next_chars {
if !is_word(c) {
break;
}
end += c.len_utf16();
}
self.selected_range = self.range_from_utf16(&(start..end));
self.selected_word_range = Some(self.selected_range.clone());
cx.notify()
}
fn unselect(&mut self, _: &mut Window, cx: &mut Context<Self>) {
let offset = self.next_boundary(self.cursor_offset());
self.selected_range = offset..offset;
cx.notify()
}
fn offset_from_utf16(&self, offset: usize) -> usize {
let mut utf8_offset = 0;
let mut utf16_count = 0;
for ch in self.text.chars() {
if utf16_count >= offset {
break;
}
utf16_count += ch.len_utf16();
utf8_offset += ch.len_utf8();
}
utf8_offset
}
fn offset_to_utf16(&self, offset: usize) -> usize {
let mut utf16_offset = 0;
let mut utf8_count = 0;
for ch in self.text.chars() {
if utf8_count >= offset {
break;
}
utf8_count += ch.len_utf8();
utf16_offset += ch.len_utf16();
}
utf16_offset
}
fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
}
fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
}
fn previous_boundary(&self, offset: usize) -> usize {
self.text
.grapheme_indices(true)
.rev()
.find_map(|(idx, _)| (idx < offset).then_some(idx))
.unwrap_or(0)
}
fn next_boundary(&self, offset: usize) -> usize {
self.text
.grapheme_indices(true)
.find_map(|(idx, _)| (idx > offset).then_some(idx))
.unwrap_or(self.text.len())
}
/// Returns the true to let InputElement to render cursor, when Input is focused and current BlinkCursor is visible.
pub(crate) fn show_cursor(&self, window: &Window, cx: &App) -> bool {
self.focus_handle.is_focused(window) && self.blink_cursor.read(cx).visible()
}
fn on_focus(&mut self, _: &mut Window, cx: &mut Context<Self>) {
self.blink_cursor.update(cx, |cursor, cx| {
cursor.start(cx);
});
cx.emit(InputEvent::Focus);
}
fn on_blur(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.unselect(window, cx);
self.blink_cursor.update(cx, |cursor, cx| {
cursor.stop(cx);
});
Root::update(window, cx, |root, _, _| {
root.focused_input = None;
});
cx.emit(InputEvent::Blur);
}
fn pause_blink_cursor(&mut self, cx: &mut Context<Self>) {
self.blink_cursor.update(cx, |cursor, cx| {
cursor.pause(cx);
});
}
pub(super) fn on_key_down(&mut self, _: &KeyDownEvent, _: &mut Window, cx: &mut Context<Self>) {
self.pause_blink_cursor(cx);
}
pub(super) fn on_drag_move(
&mut self,
event: &MouseMoveEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.text.is_empty() {
return;
}
if self.last_layout.is_none() {
return;
}
if !self.focus_handle.is_focused(window) {
return;
}
if !self.selecting {
return;
}
let offset = self.index_for_mouse_position(event.position, window, cx);
self.select_to(offset, window, cx);
}
fn is_valid_input(&self, new_text: &str) -> bool {
if new_text.is_empty() {
return true;
}
if let Some(validate) = &self.validate {
if !validate(new_text) {
return false;
}
}
if !self.mask_pattern.is_valid(new_text) {
return false;
}
self.pattern
.as_ref()
.map(|p| p.is_match(new_text))
.unwrap_or(true)
}
/// Set the mask pattern for formatting the input text.
///
/// The pattern can contain:
/// - 9: Any digit or dot
/// - A: Any letter
/// - *: Any character
/// - Other characters will be treated as literal mask characters
///
/// Example: "(999)999-999" for phone numbers
pub fn mask_pattern(mut self, pattern: impl Into<MaskPattern>) -> Self {
self.mask_pattern = pattern.into();
if let Some(placeholder) = self.mask_pattern.placeholder() {
self.placeholder = placeholder.into();
}
self
}
pub fn set_mask_pattern(
&mut self,
pattern: impl Into<MaskPattern>,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.mask_pattern = pattern.into();
if let Some(placeholder) = self.mask_pattern.placeholder() {
self.placeholder = placeholder.into();
}
cx.notify();
}
pub(super) fn set_input_bounds(&mut self, new_bounds: Bounds<Pixels>, cx: &mut Context<Self>) {
let wrap_width_changed = self.input_bounds.size.width != new_bounds.size.width;
self.input_bounds = new_bounds;
// Update text_wrapper wrap_width if changed.
if wrap_width_changed {
self.text_wrapper
.set_wrap_width(Some(new_bounds.size.width), cx);
self.mode.update_auto_grow(&self.text_wrapper);
}
}
}
impl EntityInputHandler for InputState {
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
adjusted_range: &mut Option<Range<usize>>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<String> {
let range = self.range_from_utf16(&range_utf16);
adjusted_range.replace(self.range_to_utf16(&range));
Some(self.text[range].to_string())
}
fn selected_text_range(
&mut self,
_ignore_disabled_input: bool,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<UTF16Selection> {
Some(UTF16Selection {
range: self.range_to_utf16(&self.selected_range),
reversed: false,
})
}
fn marked_text_range(
&self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<Range<usize>> {
self.marked_range
.as_ref()
.map(|range| self.range_to_utf16(range))
}
fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
self.marked_range = None;
}
/// Replace text in range.
///
/// - If the new text is invalid, it will not be replaced.
/// - If `range_utf16` is not provided, the current selected range will be used.
fn replace_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.disabled {
return;
}
let range = range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
.or(self.marked_range.clone())
.unwrap_or(self.selected_range.clone());
let pending_text: SharedString =
(self.text[0..range.start].to_owned() + new_text + &self.text[range.end..]).into();
// Check if the new text is valid
if !self.is_valid_input(&pending_text) {
return;
}
let mask_text = self.mask_pattern.mask(&pending_text);
let new_text_len = (new_text.len() + mask_text.len()).saturating_sub(pending_text.len());
let new_pos = (range.start + new_text_len).min(mask_text.len());
self.push_history(&range, new_text, window, cx);
self.text = mask_text;
self.text_wrapper.update(self.text.clone(), cx);
self.selected_range = new_pos..new_pos;
self.marked_range.take();
self.update_preferred_x_offset(cx);
self.update_scroll_offset(None, cx);
self.mode.update_auto_grow(&self.text_wrapper);
cx.emit(InputEvent::Change(self.unmask_value()));
cx.notify();
}
fn replace_and_mark_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
new_selected_range_utf16: Option<Range<usize>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.disabled {
return;
}
let range = range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
.or(self.marked_range.clone())
.unwrap_or(self.selected_range.clone());
let pending_text: SharedString =
(self.text[0..range.start].to_owned() + new_text + &self.text[range.end..]).into();
if !self.is_valid_input(&pending_text) {
return;
}
self.push_history(&range, new_text, window, cx);
self.text = pending_text;
self.marked_range = Some(range.start..range.start + new_text.len());
self.selected_range = new_selected_range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
.map(|new_range| new_range.start + range.start..new_range.end + range.end)
.unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
cx.emit(InputEvent::Change(self.unmask_value()));
cx.notify();
}
/// Used to position IME candidates.
/// TODO: Fix position of IME candidates in multi-line text input.
fn bounds_for_range(
&mut self,
range_utf16: Range<usize>,
bounds: Bounds<Pixels>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<Bounds<Pixels>> {
let line_height = self.last_line_height;
let lines = self.last_layout.as_ref()?;
let range = self.range_from_utf16(&range_utf16);
let mut start_origin = None;
let mut end_origin = None;
let mut y_offset = px(0.);
let mut index_offset = 0;
for line in lines.iter() {
if let Some(p) =
line.position_for_index(range.start.saturating_sub(index_offset), line_height)
{
start_origin = Some(p + point(px(0.), y_offset));
}
if let Some(p) =
line.position_for_index(range.end.saturating_sub(index_offset), line_height)
{
end_origin = Some(p + point(px(0.), y_offset));
}
y_offset += line.size(line_height).height;
if start_origin.is_some() && end_origin.is_some() {
break;
}
index_offset += line.len();
}
let start_origin = start_origin.unwrap_or_default();
let end_origin = end_origin.unwrap_or_default();
Some(Bounds::from_corners(
bounds.origin + start_origin,
// + line_height for show IME panel under the cursor line.
bounds.origin + point(end_origin.x, end_origin.y + line_height),
))
}
fn character_index_for_point(
&mut self,
point: gpui::Point<Pixels>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<usize> {
let line_height = self.last_line_height;
let line_point = self.last_bounds?.localize(&point)?;
let lines = self.last_layout.as_ref()?;
for line in lines.iter() {
if let Ok(utf8_index) = line.index_for_position(line_point, line_height) {
return Some(self.offset_to_utf16(utf8_index));
}
}
None
}
}
impl Focusable for InputState {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for InputState {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.id("text-element")
.flex_1()
.flex_grow()
.when(self.is_multi_line(), |this| this.h_full())
.overflow_x_hidden()
.child(TextElement::new(cx.entity().clone()).placeholder(self.placeholder.clone()))
}
}