1449 lines
48 KiB
Rust
1449 lines
48 KiB
Rust
//! A text input field that allows the user to enter text.
|
|
//!
|
|
//! Based on the `Input` example from the `gpui` crate.
|
|
//! https://github.com/zed-industries/zed/blob/main/crates/gpui/examples/input.rs
|
|
|
|
use smallvec::SmallVec;
|
|
use std::cell::Cell;
|
|
use std::ops::Range;
|
|
use std::rc::Rc;
|
|
use unicode_segmentation::*;
|
|
|
|
use gpui::prelude::FluentBuilder as _;
|
|
use gpui::{
|
|
actions, div, point, px, AnyElement, AppContext, Bounds, ClickEvent, ClipboardItem,
|
|
Context as _, Entity, EventEmitter, FocusHandle, FocusableView, Half, InteractiveElement as _,
|
|
IntoElement, KeyBinding, KeyDownEvent, Model, MouseButton, MouseDownEvent, MouseMoveEvent,
|
|
MouseUpEvent, ParentElement as _, Pixels, Point, Rems, Render, ScrollHandle, ScrollWheelEvent,
|
|
SharedString, Styled as _, UTF16Selection, ViewContext, ViewInputHandler, WindowContext,
|
|
WrappedLine,
|
|
};
|
|
|
|
// TODO:
|
|
// - Press Up,Down to move cursor up, down line if multi-line
|
|
// - Move cursor to skip line eof empty chars.
|
|
|
|
use super::{blink_cursor::BlinkCursor, change::Change, element::TextElement, ClearButton};
|
|
|
|
use crate::{
|
|
history::History,
|
|
indicator::Indicator,
|
|
scroll::{Scrollbar, ScrollbarAxis, ScrollbarState},
|
|
theme::{scale::ColorScaleStep, ActiveTheme},
|
|
Sizable, Size, StyleSized, StyledExt,
|
|
};
|
|
|
|
actions!(
|
|
input,
|
|
[
|
|
Backspace,
|
|
Delete,
|
|
DeleteToBeginningOfLine,
|
|
DeleteToEndOfLine,
|
|
Enter,
|
|
Up,
|
|
Down,
|
|
Left,
|
|
Right,
|
|
SelectUp,
|
|
SelectDown,
|
|
SelectLeft,
|
|
SelectRight,
|
|
SelectAll,
|
|
Home,
|
|
End,
|
|
SelectToStartOfLine,
|
|
SelectToEndOfLine,
|
|
SelectToStart,
|
|
SelectToEnd,
|
|
ShowCharacterPalette,
|
|
Copy,
|
|
Cut,
|
|
Paste,
|
|
Undo,
|
|
Redo,
|
|
MoveToStartOfLine,
|
|
MoveToEndOfLine,
|
|
MoveToStart,
|
|
MoveToEnd,
|
|
TextChanged,
|
|
]
|
|
);
|
|
|
|
#[derive(Clone)]
|
|
pub enum InputEvent {
|
|
Change(SharedString),
|
|
PressEnter,
|
|
Focus,
|
|
Blur,
|
|
}
|
|
|
|
const CONTEXT: &str = "Input";
|
|
|
|
pub fn init(cx: &mut AppContext) {
|
|
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)),
|
|
KeyBinding::new("enter", Enter, 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)),
|
|
#[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("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("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)),
|
|
]);
|
|
}
|
|
|
|
type TextInputPrefix<T> = Option<Box<dyn Fn(&mut ViewContext<T>) -> AnyElement + 'static>>;
|
|
type TextInputSuffix<T> = Option<Box<dyn Fn(&mut ViewContext<T>) -> AnyElement + 'static>>;
|
|
type Validate = Option<Box<dyn Fn(&str) -> bool + 'static>>;
|
|
|
|
pub struct TextInput {
|
|
pub(super) focus_handle: FocusHandle,
|
|
pub(super) text: SharedString,
|
|
multi_line: bool,
|
|
pub(super) history: History<Change>,
|
|
pub(super) blink_cursor: Model<BlinkCursor>,
|
|
pub(super) prefix: TextInputPrefix<Self>,
|
|
pub(super) suffix: TextInputSuffix<Self>,
|
|
pub(super) loading: bool,
|
|
pub(super) placeholder: SharedString,
|
|
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) is_selecting: bool,
|
|
pub(super) disabled: bool,
|
|
pub(super) masked: bool,
|
|
pub(super) appearance: bool,
|
|
pub(super) cleanable: bool,
|
|
pub(super) size: Size,
|
|
pub(super) text_size: Size,
|
|
pub(super) rows: usize,
|
|
pattern: Option<regex::Regex>,
|
|
validate: Validate,
|
|
pub(crate) scroll_handle: ScrollHandle,
|
|
scrollbar_state: Rc<Cell<ScrollbarState>>,
|
|
/// The size of the scrollable content.
|
|
pub(crate) scroll_size: gpui::Size<Pixels>,
|
|
/// To remember the horizontal column (x-coordinate) of the cursor position.
|
|
preferred_x_offset: Option<Pixels>,
|
|
}
|
|
|
|
impl EventEmitter<InputEvent> for TextInput {}
|
|
|
|
impl TextInput {
|
|
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
|
let focus_handle = cx.focus_handle();
|
|
let blink_cursor = cx.new_model(|_| BlinkCursor::new());
|
|
let history = History::new().group_interval(std::time::Duration::from_secs(1));
|
|
let input = Self {
|
|
focus_handle: focus_handle.clone(),
|
|
text: "".into(),
|
|
multi_line: false,
|
|
blink_cursor,
|
|
history,
|
|
placeholder: "".into(),
|
|
selected_range: 0..0,
|
|
selected_word_range: None,
|
|
selection_reversed: false,
|
|
marked_range: None,
|
|
input_bounds: Bounds::default(),
|
|
is_selecting: false,
|
|
disabled: false,
|
|
masked: false,
|
|
appearance: true,
|
|
cleanable: false,
|
|
loading: false,
|
|
prefix: None,
|
|
suffix: None,
|
|
size: Size::Medium,
|
|
text_size: Size::Medium,
|
|
pattern: None,
|
|
validate: None,
|
|
rows: 2,
|
|
last_layout: None,
|
|
last_bounds: None,
|
|
last_selected_range: None,
|
|
last_line_height: px(20.),
|
|
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,
|
|
};
|
|
|
|
// Observe the blink cursor to repaint the view when it changes.
|
|
cx.observe(&input.blink_cursor, |_, _, cx| cx.notify())
|
|
.detach();
|
|
// Blink the cursor when the window is active, pause when it's not.
|
|
cx.observe_window_activation(|input, cx| {
|
|
if cx.is_window_active() {
|
|
let focus_handle = input.focus_handle.clone();
|
|
if focus_handle.is_focused(cx) {
|
|
input.blink_cursor.update(cx, |blink_cursor, cx| {
|
|
blink_cursor.start(cx);
|
|
});
|
|
}
|
|
}
|
|
})
|
|
.detach();
|
|
|
|
cx.on_focus(&focus_handle, Self::on_focus).detach();
|
|
cx.on_blur(&focus_handle, Self::on_blur).detach();
|
|
|
|
input
|
|
}
|
|
|
|
/// Use the text input field as a multi-line Textarea.
|
|
pub fn multi_line(mut self) -> Self {
|
|
self.multi_line = true;
|
|
self
|
|
}
|
|
|
|
/// 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 ViewContext<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, cx: &mut ViewContext<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, 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 {
|
|
self.multi_line
|
|
}
|
|
|
|
#[inline]
|
|
pub(super) fn is_single_line(&self) -> bool {
|
|
!self.multi_line
|
|
}
|
|
|
|
/// 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 {
|
|
self.rows = rows;
|
|
self
|
|
}
|
|
|
|
/// Set the text of the input field.
|
|
///
|
|
/// And the selection_range will be reset to 0..0.
|
|
pub fn set_text(&mut self, text: impl Into<SharedString>, cx: &mut ViewContext<Self>) {
|
|
self.history.ignore = true;
|
|
self.replace_text(text, cx);
|
|
self.history.ignore = false;
|
|
// Ensure cursor to start when set text
|
|
self.selected_range = 0..0;
|
|
|
|
cx.notify();
|
|
}
|
|
|
|
fn replace_text(&mut self, text: impl Into<SharedString>, cx: &mut ViewContext<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, cx);
|
|
}
|
|
|
|
/// Set the disabled state of the input field.
|
|
pub fn set_disabled(&mut self, disabled: bool, cx: &mut ViewContext<Self>) {
|
|
self.disabled = disabled;
|
|
cx.notify();
|
|
}
|
|
|
|
/// Set the masked state of the input field.
|
|
pub fn set_masked(&mut self, masked: bool, cx: &mut ViewContext<Self>) {
|
|
self.masked = masked;
|
|
cx.notify();
|
|
}
|
|
|
|
/// Set the prefix element of the input field.
|
|
pub fn set_prefix<F, E>(&mut self, builder: F, cx: &mut ViewContext<Self>)
|
|
where
|
|
F: Fn(&ViewContext<Self>) -> E + 'static,
|
|
E: IntoElement,
|
|
{
|
|
self.prefix = Some(Box::new(move |cx| builder(cx).into_any_element()));
|
|
cx.notify();
|
|
}
|
|
|
|
/// Set the suffix element of the input field.
|
|
pub fn set_suffix<F, E>(&mut self, builder: F, cx: &mut ViewContext<Self>)
|
|
where
|
|
F: Fn(&ViewContext<Self>) -> E + 'static,
|
|
E: IntoElement,
|
|
{
|
|
self.suffix = Some(Box::new(move |cx| builder(cx).into_any_element()));
|
|
cx.notify();
|
|
}
|
|
|
|
/// Set the Input size
|
|
pub fn set_size(&mut self, size: Size, cx: &mut ViewContext<Self>) {
|
|
self.size = size;
|
|
cx.notify();
|
|
}
|
|
|
|
/// Set the Input size
|
|
pub fn text_size(mut self, size: Size) -> Self {
|
|
self.text_size = size;
|
|
self
|
|
}
|
|
|
|
/// Set the appearance of the input field.
|
|
pub fn appearance(mut self, appearance: bool) -> Self {
|
|
self.appearance = appearance;
|
|
self
|
|
}
|
|
|
|
/// Set the prefix element of the input field, for example a search Icon.
|
|
pub fn prefix<F, E>(mut self, builder: F) -> Self
|
|
where
|
|
F: Fn(&mut ViewContext<Self>) -> E + 'static,
|
|
E: IntoElement,
|
|
{
|
|
self.prefix = Some(Box::new(move |cx| builder(cx).into_any_element()));
|
|
self
|
|
}
|
|
|
|
/// Set the suffix element of the input field, for example a clear button.
|
|
pub fn suffix<F, E>(mut self, builder: F) -> Self
|
|
where
|
|
F: Fn(&mut ViewContext<Self>) -> E + 'static,
|
|
E: IntoElement,
|
|
{
|
|
self.suffix = Some(Box::new(move |cx| builder(cx).into_any_element()));
|
|
self
|
|
}
|
|
|
|
/// Set the placeholder text of the input field.
|
|
pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
|
|
self.placeholder = placeholder.into();
|
|
self
|
|
}
|
|
|
|
/// Set the placeholder text of the input field with reference.
|
|
pub fn set_placeholder(&mut self, placeholder: impl Into<SharedString>) {
|
|
self.placeholder = placeholder.into();
|
|
}
|
|
|
|
/// Set true to show the clear button when the input field is not empty.
|
|
pub fn cleanable(mut self) -> Self {
|
|
self.cleanable = 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) {
|
|
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 ViewContext<Self>) {
|
|
self.loading = loading;
|
|
cx.notify();
|
|
}
|
|
|
|
/// Return the text of the input field.
|
|
pub fn text(&self) -> SharedString {
|
|
self.text.clone()
|
|
}
|
|
|
|
pub fn disabled(&self) -> bool {
|
|
self.disabled
|
|
}
|
|
|
|
/// Focus the input field.
|
|
pub fn focus(&self, cx: &mut ViewContext<Self>) {
|
|
self.focus_handle.focus(cx);
|
|
}
|
|
|
|
fn left(&mut self, _: &Left, cx: &mut ViewContext<Self>) {
|
|
self.pause_blink_cursor(cx);
|
|
if self.selected_range.is_empty() {
|
|
self.move_to(self.previous_boundary(self.cursor_offset()), cx);
|
|
} else {
|
|
self.move_to(self.selected_range.start, cx)
|
|
}
|
|
}
|
|
|
|
fn right(&mut self, _: &Right, cx: &mut ViewContext<Self>) {
|
|
self.pause_blink_cursor(cx);
|
|
if self.selected_range.is_empty() {
|
|
self.move_to(self.next_boundary(self.selected_range.end), cx);
|
|
} else {
|
|
self.move_to(self.selected_range.end, cx)
|
|
}
|
|
}
|
|
|
|
fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
|
|
if self.is_single_line() {
|
|
return;
|
|
}
|
|
self.pause_blink_cursor(cx);
|
|
self.move_vertical(-1, cx);
|
|
}
|
|
|
|
fn down(&mut self, _: &Down, cx: &mut ViewContext<Self>) {
|
|
if self.is_single_line() {
|
|
return;
|
|
}
|
|
self.pause_blink_cursor(cx);
|
|
self.move_vertical(1, cx);
|
|
}
|
|
|
|
fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext<Self>) {
|
|
self.select_to(self.previous_boundary(self.cursor_offset()), cx);
|
|
}
|
|
|
|
fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext<Self>) {
|
|
self.select_to(self.next_boundary(self.cursor_offset()), cx);
|
|
}
|
|
|
|
fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext<Self>) {
|
|
if self.is_single_line() {
|
|
return;
|
|
}
|
|
let offset = self.start_of_line(cx).saturating_sub(1);
|
|
self.select_to(offset, cx);
|
|
}
|
|
|
|
fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext<Self>) {
|
|
if self.is_single_line() {
|
|
return;
|
|
}
|
|
let offset = (self.end_of_line(cx) + 1).min(self.text.len());
|
|
self.select_to(offset, cx);
|
|
}
|
|
|
|
fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext<Self>) {
|
|
self.move_to(0, cx);
|
|
self.select_to(self.text.len(), cx)
|
|
}
|
|
|
|
fn home(&mut self, _: &Home, cx: &mut ViewContext<Self>) {
|
|
self.pause_blink_cursor(cx);
|
|
let offset = self.start_of_line(cx);
|
|
self.move_to(offset, cx);
|
|
}
|
|
|
|
fn end(&mut self, _: &End, cx: &mut ViewContext<Self>) {
|
|
self.pause_blink_cursor(cx);
|
|
let offset = self.end_of_line(cx);
|
|
self.move_to(offset, cx);
|
|
}
|
|
|
|
fn move_to_start(&mut self, _: &MoveToStart, cx: &mut ViewContext<Self>) {
|
|
self.move_to(0, cx);
|
|
}
|
|
|
|
fn move_to_end(&mut self, _: &MoveToEnd, cx: &mut ViewContext<Self>) {
|
|
let end = self.text.len();
|
|
self.move_to(end, cx);
|
|
}
|
|
|
|
fn select_to_start(&mut self, _: &SelectToStart, cx: &mut ViewContext<Self>) {
|
|
self.select_to(0, cx);
|
|
}
|
|
|
|
fn select_to_end(&mut self, _: &SelectToEnd, cx: &mut ViewContext<Self>) {
|
|
let end = self.text.len();
|
|
self.select_to(end, cx);
|
|
}
|
|
|
|
fn select_to_start_of_line(&mut self, _: &SelectToStartOfLine, cx: &mut ViewContext<Self>) {
|
|
let offset = self.start_of_line(cx);
|
|
self.select_to(offset, cx);
|
|
}
|
|
|
|
fn select_to_end_of_line(&mut self, _: &SelectToEndOfLine, cx: &mut ViewContext<Self>) {
|
|
let offset = self.end_of_line(cx);
|
|
self.select_to(offset, cx);
|
|
}
|
|
|
|
/// Get start of line
|
|
fn start_of_line(&mut self, cx: &mut ViewContext<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, cx)
|
|
.unwrap_or_default()
|
|
.rfind('\n')
|
|
.map(|i| i + 1)
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
/// Get end of line
|
|
fn end_of_line(&mut self, cx: &mut ViewContext<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, cx)
|
|
.unwrap_or_default()
|
|
.eq("\n")
|
|
{
|
|
return offset;
|
|
}
|
|
|
|
self.text_for_range(
|
|
self.range_to_utf16(&(offset..self.text.len())),
|
|
&mut None,
|
|
cx,
|
|
)
|
|
.unwrap_or_default()
|
|
.find('\n')
|
|
.map(|i| i + offset)
|
|
.unwrap_or(self.text.len())
|
|
}
|
|
|
|
fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
|
|
if self.selected_range.is_empty() {
|
|
self.select_to(self.previous_boundary(self.cursor_offset()), cx)
|
|
}
|
|
self.replace_text_in_range(None, "", cx);
|
|
self.pause_blink_cursor(cx);
|
|
}
|
|
|
|
fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
|
|
if self.selected_range.is_empty() {
|
|
self.select_to(self.next_boundary(self.cursor_offset()), cx)
|
|
}
|
|
self.replace_text_in_range(None, "", cx);
|
|
self.pause_blink_cursor(cx);
|
|
}
|
|
|
|
fn delete_to_beginning_of_line(
|
|
&mut self,
|
|
_: &DeleteToBeginningOfLine,
|
|
cx: &mut ViewContext<Self>,
|
|
) {
|
|
let offset = self.start_of_line(cx);
|
|
self.replace_text_in_range(
|
|
Some(self.range_to_utf16(&(offset..self.cursor_offset()))),
|
|
"",
|
|
cx,
|
|
);
|
|
self.pause_blink_cursor(cx);
|
|
}
|
|
|
|
fn delete_to_end_of_line(&mut self, _: &DeleteToEndOfLine, cx: &mut ViewContext<Self>) {
|
|
let offset = self.end_of_line(cx);
|
|
self.replace_text_in_range(
|
|
Some(self.range_to_utf16(&(self.cursor_offset()..offset))),
|
|
"",
|
|
cx,
|
|
);
|
|
self.pause_blink_cursor(cx);
|
|
}
|
|
|
|
fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
|
|
if self.is_multi_line() {
|
|
let is_eof = self.selected_range.end == self.text.len();
|
|
self.replace_text_in_range(None, "\n", 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, cx);
|
|
}
|
|
|
|
cx.emit(InputEvent::PressEnter);
|
|
}
|
|
|
|
fn clean(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
|
|
self.replace_text("", cx);
|
|
}
|
|
|
|
fn on_mouse_down(&mut self, event: &MouseDownEvent, cx: &mut ViewContext<Self>) {
|
|
self.is_selecting = true;
|
|
let offset = self.index_for_mouse_position(event.position, cx);
|
|
// Double click to select word
|
|
if event.button == MouseButton::Left && event.click_count == 2 {
|
|
self.select_word(offset, cx);
|
|
return;
|
|
}
|
|
|
|
if event.modifiers.shift {
|
|
self.select_to(offset, cx);
|
|
} else {
|
|
self.move_to(offset, cx)
|
|
}
|
|
}
|
|
|
|
fn on_mouse_up(&mut self, _: &MouseUpEvent, _: &mut ViewContext<Self>) {
|
|
self.is_selecting = false;
|
|
self.selected_word_range = None;
|
|
}
|
|
|
|
fn on_scroll_wheel(&mut self, event: &ScrollWheelEvent, _: &mut ViewContext<Self>) {
|
|
let delta = event.delta.pixel_delta(self.last_line_height);
|
|
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.);
|
|
|
|
let mut offset = self.scroll_handle.offset() + delta;
|
|
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);
|
|
}
|
|
|
|
fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
|
|
cx.show_character_palette();
|
|
}
|
|
|
|
fn copy(&mut self, _: &Copy, cx: &mut ViewContext<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));
|
|
}
|
|
|
|
fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
|
|
if self.selected_range.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let range = self.range_from_utf16(&self.selected_range);
|
|
let selected_text = self.text[range].to_string();
|
|
cx.write_to_clipboard(ClipboardItem::new_string(selected_text));
|
|
self.replace_text_in_range(None, "", cx);
|
|
}
|
|
|
|
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
|
|
if let Some(clipboard) = cx.read_from_clipboard() {
|
|
let mut new_text = clipboard.text().unwrap_or_default();
|
|
if !self.multi_line {
|
|
new_text = new_text.replace('\n', "");
|
|
}
|
|
|
|
self.replace_text_in_range(None, &new_text, cx);
|
|
}
|
|
}
|
|
|
|
fn push_history(&mut self, range: &Range<usize>, new_text: &str, cx: &mut ViewContext<Self>) {
|
|
if self.history.ignore {
|
|
return;
|
|
}
|
|
|
|
let old_text = self
|
|
.text_for_range(self.range_to_utf16(range), &mut None, 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,
|
|
));
|
|
}
|
|
|
|
fn undo(&mut self, _: &Undo, cx: &mut ViewContext<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, cx);
|
|
}
|
|
}
|
|
self.history.ignore = false;
|
|
}
|
|
|
|
fn redo(&mut self, _: &Redo, cx: &mut ViewContext<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, cx);
|
|
}
|
|
}
|
|
self.history.ignore = false;
|
|
}
|
|
|
|
fn move_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
|
|
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>, _: &WindowContext) -> 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 mut pos = inner_position - line_origin;
|
|
// Ignore the y position in single line mode, only check x position.
|
|
if self.is_single_line() {
|
|
pos.y = line_height.half();
|
|
}
|
|
|
|
let index_result = line.index_for_position(pos, line_height);
|
|
if let Ok(v) = index_result {
|
|
// Add 1 for place cursor after the character.
|
|
index += v + 1;
|
|
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.len() == 0 {
|
|
// empty line
|
|
let line_bounds = Bounds {
|
|
origin: line_origin,
|
|
size: gpui::size(bounds.size.width, line_height),
|
|
};
|
|
let pos = inner_position;
|
|
if line_bounds.contains(&pos) {
|
|
break;
|
|
}
|
|
} else {
|
|
index += line.len();
|
|
}
|
|
|
|
// add 1 for \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.))
|
|
}
|
|
}
|
|
|
|
fn select_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
|
|
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.
|
|
fn select_word(&mut self, offset: usize, cx: &mut ViewContext<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, cx)
|
|
.unwrap_or_default();
|
|
let next_text = self
|
|
.text_for_range(end..self.text.len(), &mut None, 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, cx: &mut ViewContext<Self>) {
|
|
self.selected_range = self.cursor_offset()..self.cursor_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, cx: &WindowContext) -> bool {
|
|
self.focus_handle.is_focused(cx) && self.blink_cursor.read(cx).visible()
|
|
}
|
|
|
|
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
|
|
self.blink_cursor.update(cx, |cursor, cx| {
|
|
cursor.start(cx);
|
|
});
|
|
cx.emit(InputEvent::Focus);
|
|
}
|
|
|
|
fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
|
|
self.unselect(cx);
|
|
self.blink_cursor.update(cx, |cursor, cx| {
|
|
cursor.stop(cx);
|
|
});
|
|
cx.emit(InputEvent::Blur);
|
|
}
|
|
|
|
fn pause_blink_cursor(&mut self, cx: &mut ViewContext<Self>) {
|
|
self.blink_cursor.update(cx, |cursor, cx| {
|
|
cursor.pause(cx);
|
|
});
|
|
}
|
|
|
|
fn on_key_down_for_blink_cursor(&mut self, _: &KeyDownEvent, cx: &mut ViewContext<Self>) {
|
|
self.pause_blink_cursor(cx)
|
|
}
|
|
|
|
pub(super) fn on_drag_move(&mut self, event: &MouseMoveEvent, cx: &mut ViewContext<Self>) {
|
|
if self.text.is_empty() {
|
|
return;
|
|
}
|
|
|
|
if self.last_layout.is_none() {
|
|
return;
|
|
}
|
|
|
|
if !self.focus_handle.is_focused(cx) {
|
|
return;
|
|
}
|
|
|
|
if !self.is_selecting {
|
|
return;
|
|
}
|
|
|
|
let offset = self.index_for_mouse_position(event.position, cx);
|
|
self.select_to(offset, 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;
|
|
}
|
|
}
|
|
|
|
self.pattern
|
|
.as_ref()
|
|
.map(|p| p.is_match(new_text))
|
|
.unwrap_or(true)
|
|
}
|
|
}
|
|
|
|
impl Sizable for TextInput {
|
|
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
|
self.size = size.into();
|
|
self
|
|
}
|
|
}
|
|
|
|
impl ViewInputHandler for TextInput {
|
|
fn text_for_range(
|
|
&mut self,
|
|
range_utf16: Range<usize>,
|
|
adjusted_range: &mut Option<Range<usize>>,
|
|
_cx: &mut ViewContext<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,
|
|
_cx: &mut ViewContext<Self>,
|
|
) -> Option<UTF16Selection> {
|
|
Some(UTF16Selection {
|
|
range: self.range_to_utf16(&self.selected_range),
|
|
reversed: false,
|
|
})
|
|
}
|
|
|
|
fn marked_text_range(&self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
|
|
self.marked_range
|
|
.as_ref()
|
|
.map(|range| self.range_to_utf16(range))
|
|
}
|
|
|
|
fn unmark_text(&mut self, _cx: &mut ViewContext<Self>) {
|
|
self.marked_range = None;
|
|
}
|
|
|
|
fn replace_text_in_range(
|
|
&mut self,
|
|
range_utf16: Option<Range<usize>>,
|
|
new_text: &str,
|
|
cx: &mut ViewContext<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, cx);
|
|
self.text = pending_text;
|
|
self.selected_range = range.start + new_text.len()..range.start + new_text.len();
|
|
self.marked_range.take();
|
|
self.update_preferred_x_offset(cx);
|
|
cx.emit(InputEvent::Change(self.text.clone()));
|
|
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>>,
|
|
cx: &mut ViewContext<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, 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.text.clone()));
|
|
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>,
|
|
_: &mut ViewContext<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 - index_offset, line_height) {
|
|
start_origin = Some(p + point(px(0.), y_offset));
|
|
}
|
|
if let Some(p) = line.position_for_index(range.end - 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();
|
|
}
|
|
|
|
Some(Bounds::from_corners(
|
|
bounds.origin + start_origin.unwrap_or_default(),
|
|
bounds.origin + end_origin.unwrap_or_default(),
|
|
))
|
|
}
|
|
}
|
|
|
|
impl FocusableView for TextInput {
|
|
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
|
self.focus_handle.clone()
|
|
}
|
|
}
|
|
|
|
impl Render for TextInput {
|
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
|
const LINE_HEIGHT: Rems = Rems(1.25);
|
|
let focused = self.focus_handle.is_focused(cx);
|
|
|
|
let prefix = self.prefix.as_ref().map(|build| build(cx));
|
|
let suffix = self.suffix.as_ref().map(|build| build(cx));
|
|
|
|
div()
|
|
.flex()
|
|
.id("input")
|
|
.key_context(CONTEXT)
|
|
.track_focus(&self.focus_handle)
|
|
.when(!self.disabled, |this| {
|
|
this.on_action(cx.listener(Self::backspace))
|
|
.on_action(cx.listener(Self::delete))
|
|
.on_action(cx.listener(Self::delete_to_beginning_of_line))
|
|
.on_action(cx.listener(Self::delete_to_end_of_line))
|
|
.on_action(cx.listener(Self::enter))
|
|
})
|
|
.on_action(cx.listener(Self::left))
|
|
.on_action(cx.listener(Self::right))
|
|
.on_action(cx.listener(Self::select_left))
|
|
.on_action(cx.listener(Self::select_right))
|
|
.when(self.multi_line, |this| {
|
|
this.on_action(cx.listener(Self::up))
|
|
.on_action(cx.listener(Self::down))
|
|
.on_action(cx.listener(Self::select_up))
|
|
.on_action(cx.listener(Self::select_down))
|
|
})
|
|
.on_action(cx.listener(Self::select_all))
|
|
.on_action(cx.listener(Self::select_to_start_of_line))
|
|
.on_action(cx.listener(Self::select_to_end_of_line))
|
|
.on_action(cx.listener(Self::home))
|
|
.on_action(cx.listener(Self::end))
|
|
.on_action(cx.listener(Self::move_to_start))
|
|
.on_action(cx.listener(Self::move_to_end))
|
|
.on_action(cx.listener(Self::select_to_start))
|
|
.on_action(cx.listener(Self::select_to_end))
|
|
.on_action(cx.listener(Self::show_character_palette))
|
|
.on_action(cx.listener(Self::copy))
|
|
.on_action(cx.listener(Self::paste))
|
|
.on_action(cx.listener(Self::cut))
|
|
.on_action(cx.listener(Self::undo))
|
|
.on_action(cx.listener(Self::redo))
|
|
.on_action(cx.listener(Self::redo))
|
|
.on_key_down(cx.listener(Self::on_key_down_for_blink_cursor))
|
|
.on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
|
|
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
|
|
.on_scroll_wheel(cx.listener(Self::on_scroll_wheel))
|
|
.size_full()
|
|
.line_height(LINE_HEIGHT)
|
|
.input_py(self.size)
|
|
.input_h(self.size)
|
|
.input_text_size(self.text_size)
|
|
.cursor_text()
|
|
.when(self.multi_line, |this| this.h_auto())
|
|
.when(self.appearance, |this| {
|
|
this.bg(if self.disabled {
|
|
cx.theme().transparent
|
|
} else {
|
|
cx.theme().base.step(cx, ColorScaleStep::THREE)
|
|
})
|
|
.rounded(px(cx.theme().radius))
|
|
.when(cx.theme().shadow, |this| this.shadow_sm())
|
|
.when(focused, |this| this.outline(cx))
|
|
.when(prefix.is_none(), |this| this.input_pl(self.size))
|
|
.when(suffix.is_none(), |this| this.input_pr(self.size))
|
|
})
|
|
.children(prefix)
|
|
.gap_1()
|
|
.items_center()
|
|
.child(
|
|
div()
|
|
.id("TextElement")
|
|
.flex_grow()
|
|
.overflow_x_hidden()
|
|
.child(TextElement::new(cx.view().clone())),
|
|
)
|
|
.when(self.loading, |this| {
|
|
this.child(Indicator::new().color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)))
|
|
})
|
|
.when(
|
|
self.cleanable && !self.loading && !self.text.is_empty() && self.is_single_line(),
|
|
|this| this.child(ClearButton::new(cx).on_click(cx.listener(Self::clean))),
|
|
)
|
|
.children(suffix)
|
|
.when(self.is_multi_line(), |this| {
|
|
let entity_id = cx.view().entity_id();
|
|
if self.last_layout.is_some() {
|
|
let scroll_size = self.scroll_size;
|
|
|
|
this.relative().child(
|
|
div()
|
|
.absolute()
|
|
.top_0()
|
|
.left_0()
|
|
.right(px(1.))
|
|
.bottom_0()
|
|
.child(
|
|
Scrollbar::vertical(
|
|
entity_id,
|
|
self.scrollbar_state.clone(),
|
|
self.scroll_handle.clone(),
|
|
scroll_size,
|
|
)
|
|
.axis(ScrollbarAxis::Vertical),
|
|
),
|
|
)
|
|
} else {
|
|
this
|
|
}
|
|
})
|
|
}
|
|
}
|