use crate::menu::{SelectDown, SelectLeft, SelectRight, SelectUp};
use crate::sum_tree::Bias;
use crate::{
Action, App, AppContext, Bounds, ClipboardItem, Context, Edges, ElementSize, 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, TextAlign, UTF16Selection, Window, div, point,
prelude::FluentBuilder as _, px,
};
use regex::Regex;
use ropey::{Rope, RopeSlice};
use serde::Deserialize;
use std::borrow::Cow;
use std::cell::Cell;
use std::ops::Range;
use unicode_segmentation::*;
use super::{
DisplayMap, LastLayout, MASK_CHAR, Position, RopeExt as _, Selection, WrappingIndent,
auto_scroll::AutoScroll,
blink_cursor::{BlinkCursor, CURSOR_WIDTH},
change::Change,
decorations::DecorationCollections,
element::{EditorScrollbarSnapshot, RIGHT_MARGIN, TextElement},
history::History,
mask_pattern::{MaskPattern, normalize_number_input},
mode::InputMode,
movement::MoveDirection,
number_input,
number_input::{NumberStep, StepAction},
};
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
#[action(namespace = input, no_json)]
pub struct Enter {
pub secondary: bool,
pub shift: bool,
}
impl Enter {
pub fn is_primary(action: &dyn Action) -> bool {
action.partial_eq(&Enter {
secondary: false,
shift: false,
}) || action.partial_eq(&Enter {
secondary: false,
shift: true,
})
}
}
actions!(
input,
[
Backspace,
Delete,
DeleteToBeginningOfLine,
DeleteToEndOfLine,
DeleteToPreviousWordStart,
DeleteToNextWordEnd,
Indent,
Outdent,
IndentInline,
OutdentInline,
MoveUp,
MoveDown,
MoveLeft,
MoveRight,
MoveHome,
MoveEnd,
MovePageUp,
MovePageDown,
SelectAll,
SelectToStartOfLine,
SelectToEndOfLine,
SelectToStart,
SelectToEnd,
SelectToPreviousWordStart,
SelectToNextWordEnd,
ShowCharacterPalette,
Copy,
Cut,
Paste,
Undo,
Redo,
MoveToStartOfLine,
MoveToEndOfLine,
MoveToStart,
MoveToEnd,
MoveToPreviousWord,
MoveToNextWord,
Escape,
]
);
#[derive(Clone)]
pub enum InputEvent {
Change,
PressEnter {
secondary: bool,
shift: bool,
},
Focus,
Blur,
}
pub(crate) const CONTEXT: &str = "Input";
pub(crate) fn init(cx: &mut App) {
cx.bind_keys([
KeyBinding::new("backspace", Backspace, Some(CONTEXT)),
KeyBinding::new("shift-backspace", Backspace, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("ctrl-backspace", Backspace, Some(CONTEXT)),
KeyBinding::new("delete", Delete, Some(CONTEXT)),
KeyBinding::new("shift-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,
shift: false,
},
Some(CONTEXT),
),
KeyBinding::new(
"shift-enter",
Enter {
secondary: false,
shift: true,
},
Some(CONTEXT),
),
KeyBinding::new(
"secondary-enter",
Enter {
secondary: true,
shift: false,
},
Some(CONTEXT),
),
KeyBinding::new("escape", Escape, Some(CONTEXT)),
KeyBinding::new("up", MoveUp, Some(CONTEXT)),
KeyBinding::new("down", MoveDown, Some(CONTEXT)),
KeyBinding::new("left", MoveLeft, Some(CONTEXT)),
KeyBinding::new("right", MoveRight, Some(CONTEXT)),
KeyBinding::new("pageup", MovePageUp, Some(CONTEXT)),
KeyBinding::new("pagedown", MovePageDown, Some(CONTEXT)),
KeyBinding::new("tab", IndentInline, Some(CONTEXT)),
KeyBinding::new("shift-tab", OutdentInline, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-]", Indent, Some(CONTEXT)),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-]", Indent, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-[", Outdent, Some(CONTEXT)),
#[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-[", Outdent, 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", MoveHome, Some(CONTEXT)),
KeyBinding::new("end", MoveEnd, 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("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", MoveHome, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-left", MoveHome, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("ctrl-e", MoveEnd, Some(CONTEXT)),
#[cfg(target_os = "macos")]
KeyBinding::new("cmd-right", MoveEnd, 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)),
]);
number_input::init(cx);
}
pub struct InputState {
pub(super) focus_handle: FocusHandle,
pub(super) mode: InputMode,
pub(super) text: Rope,
pub(super) display_map: DisplayMap,
pub(super) history: History<Change>,
pub(super) blink_cursor: Entity<BlinkCursor>,
pub(super) loading: bool,
pub(super) selected_range: Selection,
pub(super) selected_word_range: Option<Selection>,
pub(super) selection_reversed: bool,
pub(super) ime_marked_range: Option<Selection>,
pub(super) last_layout: Option<LastLayout>,
pub(super) last_cursor: Option<usize>,
pub(super) input_bounds: Bounds<Pixels>,
pub(super) last_bounds: Option<Bounds<Pixels>>,
pub(super) last_selected_range: Option<Selection>,
pub(super) selecting: bool,
pub(super) size: ElementSize,
pub(super) disabled: bool,
pub(super) masked: bool,
pub(super) masked_set: bool,
pub(super) clean_on_escape: bool,
pub(super) submit_on_enter: bool,
pub(super) soft_wrap: bool,
pub(super) wrapping_indent: WrappingIndent,
pub(super) scroll_beyond_last_line: Option<usize>,
pub(super) cursor_surrounding_lines: Option<usize>,
pub(super) show_whitespaces: bool,
pub(crate) cursor_line_end_affinity: bool,
pub(super) pattern: Option<Regex>,
pub(super) validate: Option<Box<dyn Fn(&str, &mut Context<Self>) -> bool + 'static>>,
pub(super) number_step: Option<NumberStep>,
pub(super) number_min: Option<f64>,
pub(super) number_max: Option<f64>,
pub(crate) scroll_handle: ScrollHandle,
pub(crate) deferred_scroll_offset: Option<Point<Pixels>>,
pub(crate) scroll_size: crate::Size<Pixels>,
pub(super) editor_scrollbar_paddings: Cell<Edges<Pixels>>,
pub(super) editor_scrollbar_snapshot: Cell<Option<EditorScrollbarSnapshot>>,
pub(super) text_align: TextAlign,
pub(super) decorations: DecorationCollections,
pub(crate) mask_pattern: MaskPattern,
pub(super) mask_pattern_set: bool,
pub(super) placeholder: SharedString,
_pending_update: bool,
pub(super) emit_events: bool,
pub(super) preferred_column: Option<(Pixels, usize)>,
_subscriptions: Vec<Subscription>,
pub(super) auto_scroll: AutoScroll,
}
impl EventEmitter<InputEvent> for InputState {}
impl InputState {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle().tab_stop(true);
let blink_cursor = cx.new(|_| BlinkCursor::new());
let history = History::new().group_interval(std::time::Duration::from_secs(1));
let _subscriptions = vec![
cx.observe(&blink_cursor, |_, _, cx| cx.notify()),
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(),
display_map: DisplayMap::new(
text_style.font(),
text_style.font_size.to_pixels(window.rem_size()),
None,
),
blink_cursor,
history,
selected_range: Selection::default(),
selected_word_range: None,
selection_reversed: false,
ime_marked_range: None,
input_bounds: Bounds::default(),
selecting: false,
disabled: false,
masked: false,
masked_set: false,
clean_on_escape: false,
submit_on_enter: false,
soft_wrap: true,
wrapping_indent: WrappingIndent::default(),
scroll_beyond_last_line: None,
cursor_surrounding_lines: None,
show_whitespaces: false,
loading: false,
pattern: None,
validate: None,
number_step: Some(NumberStep::Fixed(1.)),
number_min: None,
number_max: None,
mode: InputMode::default(),
last_layout: None,
last_bounds: None,
last_selected_range: None,
last_cursor: None,
scroll_handle: ScrollHandle::new(),
scroll_size: crate::size(px(0.), px(0.)),
editor_scrollbar_paddings: Cell::new(Edges {
top: px(0.),
right: px(0.),
bottom: px(0.),
left: px(0.),
}),
editor_scrollbar_snapshot: Cell::new(None),
deferred_scroll_offset: None,
preferred_column: None,
placeholder: SharedString::default(),
mask_pattern: MaskPattern::default(),
mask_pattern_set: false,
text_align: TextAlign::Left,
decorations: DecorationCollections::default(),
emit_events: true,
size: ElementSize::default(),
_subscriptions,
_pending_update: false,
cursor_line_end_affinity: false,
auto_scroll: AutoScroll::default(),
}
}
pub fn multi_line(mut self, multi_line: bool) -> Self {
self.mode = self.mode.multi_line(multi_line);
self
}
pub fn auto_grow(mut self, min_rows: usize, max_rows: usize) -> Self {
self.mode = InputMode::auto_grow(min_rows, max_rows);
self
}
pub fn code_editor(mut self, language: impl Into<SharedString>) -> Self {
let language: SharedString = language.into();
self.mode = InputMode::code_editor(language);
self
}
pub fn language(&self) -> Option<&str> {
self.mode.language()
}
pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
self.placeholder = placeholder.into();
self
}
pub fn folding(mut self, folding: bool) -> Self {
debug_assert!(self.mode.is_code_editor());
if let InputMode::CodeEditor { folding: f, .. } = &mut self.mode {
*f = folding;
}
self
}
pub fn set_folding(&mut self, folding: bool, _: &mut Window, cx: &mut Context<Self>) {
debug_assert!(self.mode.is_code_editor());
if let InputMode::CodeEditor { folding: f, .. } = &mut self.mode {
*f = folding;
}
if !folding {
self.display_map.clear_folds();
}
cx.notify();
}
pub fn line_number(mut self, line_number: bool) -> Self {
debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line());
if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode {
*l = line_number;
}
self
}
pub fn set_line_number(&mut self, line_number: bool, _: &mut Window, cx: &mut Context<Self>) {
debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line());
if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode {
*l = line_number;
}
cx.notify();
}
pub fn rows(mut self, rows: usize) -> Self {
match &mut self.mode {
InputMode::PlainText { rows: r, .. } | InputMode::CodeEditor { rows: r, .. } => {
*r = rows
}
InputMode::AutoGrow {
max_rows: max_r,
rows: r,
..
} => {
*r = rows;
*max_r = rows;
}
}
self
}
pub fn set_placeholder(
&mut self,
placeholder: impl Into<SharedString>,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.placeholder = placeholder.into();
cx.notify();
}
pub(super) fn line_and_position_for_offset(
&self,
offset: usize,
) -> (usize, usize, Option<Point<Pixels>>) {
let Some(last_layout) = &self.last_layout else {
return (0, 0, None);
};
let line_height = last_layout.line_height;
let mut y_offset = last_layout.visible_top;
for (vi, line) in last_layout.lines.iter().enumerate() {
let prev_lines_offset = last_layout.visible_line_byte_offsets[vi];
let local_offset = offset.saturating_sub(prev_lines_offset);
if let Some(pos) = line.position_for_index(local_offset, last_layout, false) {
let sub_line_index = (pos.y / line_height) as usize;
let adjusted_pos = point(pos.x + last_layout.line_number_width, pos.y + y_offset);
return (vi, sub_line_index, Some(adjusted_pos));
}
y_offset += line.size(line_height).height;
}
(0, 0, None)
}
pub fn set_value(
&mut self,
value: impl Into<SharedString>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.history.ignore = true;
self.emit_events = false;
self.replace_text(value, window, cx);
self.history.ignore = false;
self.emit_events = true;
self.reset_selection();
self.reset_lsp_state();
self.reset_scroll_to_start();
self.history.clear();
cx.notify();
}
pub fn replace_all(
&mut self,
text: impl Into<SharedString>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.replace_text(text, window, cx);
self.reset_selection();
self.reset_lsp_state();
self.reset_scroll_to_start();
cx.notify();
}
pub fn insert(
&mut self,
text: impl Into<SharedString>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let was_disabled = self.disabled;
self.disabled = false;
let text: SharedString = text.into();
let range_utf16 = self.range_to_utf16(&(self.cursor()..self.cursor()));
self.replace_text_in_range_silent(Some(range_utf16), &text, window, cx);
self.selected_range = (self.selected_range.end..self.selected_range.end).into();
self.disabled = was_disabled;
}
pub fn replace(
&mut self,
text: impl Into<SharedString>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let was_disabled = self.disabled;
self.disabled = false;
let text: SharedString = text.into();
self.replace_text_in_range_silent(None, &text, window, cx);
self.selected_range = (self.selected_range.end..self.selected_range.end).into();
self.disabled = was_disabled;
}
fn replace_text(
&mut self,
text: impl Into<SharedString>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let was_disabled = self.disabled;
self.disabled = false;
let text: SharedString = text.into();
let range = 0..self.text.chars().map(|c| c.len_utf16()).sum();
self.replace_text_in_range_silent(Some(range), &text, window, cx);
self.disabled = was_disabled;
}
fn reset_selection(&mut self) {
if self.mode.is_single_line() {
let end = self.text.len();
self.selected_range = (end..end).into();
} else {
self.selected_range.clear();
}
}
fn reset_lsp_state(&mut self) {
if self.mode.is_code_editor() {
self._pending_update = true;
}
}
fn reset_scroll_to_start(&mut self) {
self.scroll_handle.set_offset(point(px(0.), px(0.)));
if self.mode.is_single_line() {
self.deferred_scroll_offset = Some(point(px(0.), px(0.)));
}
}
pub fn masked(mut self, masked: bool) -> Self {
debug_assert!(self.mode.is_single_line());
self.masked = masked;
self.masked_set = true;
self
}
pub fn set_masked(&mut self, masked: bool, _: &mut Window, cx: &mut Context<Self>) {
debug_assert!(self.mode.is_single_line());
self.masked = masked;
self.masked_set = true;
cx.notify();
}
pub fn clean_on_escape(mut self) -> Self {
self.clean_on_escape = true;
self
}
pub fn submit_on_enter(mut self, submit: bool) -> Self {
self.submit_on_enter = submit;
self
}
pub fn soft_wrap(mut self, wrap: bool) -> Self {
debug_assert!(self.mode.is_multi_line());
self.soft_wrap = wrap;
self
}
pub fn show_whitespaces(mut self, show: bool) -> Self {
self.show_whitespaces = show;
self
}
pub fn wrapping_indent(mut self, wrapping_indent: WrappingIndent) -> Self {
debug_assert!(self.mode.is_multi_line());
self.wrapping_indent = wrapping_indent;
self
}
pub fn set_soft_wrap(&mut self, wrap: bool, _: &mut Window, cx: &mut Context<Self>) {
debug_assert!(self.mode.is_multi_line());
self.soft_wrap = wrap;
if wrap {
let wrap_width = self
.last_layout
.as_ref()
.and_then(|b| b.wrap_width)
.unwrap_or(self.input_bounds.size.width);
self.display_map.on_layout_changed(Some(wrap_width), cx);
let mut offset = self.scroll_handle.offset();
offset.x = px(0.);
self.scroll_handle.set_offset(offset);
} else {
self.display_map.on_layout_changed(None, cx);
}
cx.notify();
}
pub fn set_show_whitespaces(&mut self, show: bool, _: &mut Window, cx: &mut Context<Self>) {
self.show_whitespaces = show;
cx.notify();
}
pub fn set_wrapping_indent(
&mut self,
wrapping_indent: WrappingIndent,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.wrapping_indent = wrapping_indent;
self.display_map.set_wrapping_indent(wrapping_indent, cx);
cx.notify();
}
pub fn scroll_beyond_last_line(mut self, rows: Option<usize>) -> Self {
self.scroll_beyond_last_line = rows;
self
}
pub fn set_scroll_beyond_last_line(
&mut self,
rows: Option<usize>,
_: &mut Window,
cx: &mut Context<Self>,
) {
if self.scroll_beyond_last_line == rows {
return;
}
self.scroll_beyond_last_line = rows;
cx.notify();
}
pub fn cursor_surrounding_lines(mut self, lines: Option<usize>) -> Self {
self.cursor_surrounding_lines = lines;
self
}
pub fn set_cursor_surrounding_lines(
&mut self,
lines: Option<usize>,
_: &mut Window,
cx: &mut Context<Self>,
) {
if self.cursor_surrounding_lines == lines {
return;
}
self.cursor_surrounding_lines = lines;
cx.notify();
}
pub fn pattern(mut self, pattern: Regex) -> Self {
debug_assert!(self.mode.is_single_line());
self.pattern = Some(pattern);
self
}
pub fn set_pattern(&mut self, pattern: Regex, _window: &mut Window, _cx: &mut Context<Self>) {
debug_assert!(self.mode.is_single_line());
self.pattern = Some(pattern);
}
pub fn validate(mut self, f: impl Fn(&str, &mut Context<Self>) -> bool + 'static) -> Self {
debug_assert!(self.mode.is_single_line());
self.validate = Some(Box::new(f));
self
}
pub fn step(mut self, step: impl Into<NumberStep>) -> Self {
debug_assert!(self.mode.is_single_line());
self.number_step = Some(step.into());
self
}
pub fn step_by(
mut self,
f: impl Fn(f64, StepAction, &mut Context<Self>) -> f64 + 'static,
) -> Self {
debug_assert!(self.mode.is_single_line());
self.number_step = Some(NumberStep::by_value(f));
self
}
pub fn min(mut self, min: f64) -> Self {
debug_assert!(self.mode.is_single_line());
self.number_min = Some(min);
self
}
pub fn max(mut self, max: f64) -> Self {
debug_assert!(self.mode.is_single_line());
self.number_max = Some(max);
self
}
pub fn set_step(
&mut self,
step: impl Into<Option<NumberStep>>,
_: &mut Window,
_: &mut Context<Self>,
) {
debug_assert!(self.mode.is_single_line());
self.number_step = step.into();
}
pub fn set_min(&mut self, min: Option<f64>, _: &mut Window, _: &mut Context<Self>) {
debug_assert!(self.mode.is_single_line());
self.number_min = min;
}
pub fn set_max(&mut self, max: Option<f64>, _: &mut Window, _: &mut Context<Self>) {
debug_assert!(self.mode.is_single_line());
self.number_max = max;
}
pub fn set_loading(&mut self, loading: bool, _: &mut Window, cx: &mut Context<Self>) {
debug_assert!(self.mode.is_single_line());
self.loading = loading;
cx.notify();
}
pub fn default_value(mut self, value: impl Into<SharedString>) -> Self {
let text: SharedString = value.into();
self.text = Rope::from(self.normalize_input(&text).as_ref());
self._pending_update = true;
self
}
pub fn value(&self) -> SharedString {
SharedString::new(self.text.to_string())
}
pub fn selected_value(&self) -> SharedString {
SharedString::new(self.selected_text().to_string())
}
pub fn unmask_value(&self) -> SharedString {
self.mask_pattern.unmask(&self.text.to_string()).into()
}
pub fn text(&self) -> &Rope {
&self.text
}
pub fn cursor_position(&self) -> Position {
let offset = self.cursor();
self.text.offset_to_position(offset)
}
pub fn set_cursor_position(
&mut self,
position: impl Into<Position>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let position: Position = position.into();
let offset = self.text.position_to_offset(&position);
self.move_to(offset, None, cx);
self.update_preferred_column();
self.focus(window, cx);
}
pub fn focus(&self, window: &mut Window, cx: &mut Context<Self>) {
self.focus_handle.focus(window, cx);
self.blink_cursor.update(cx, |cursor, cx| {
cursor.start(cx);
});
}
pub fn refresh(&mut self, cx: &mut Context<Self>) {
self._pending_update = true;
cx.notify();
}
pub(super) fn select_left(&mut self, _: &SelectLeft, _: &mut Window, cx: &mut Context<Self>) {
self.select_to(self.previous_boundary(self.cursor()), cx);
}
pub(super) fn select_right(&mut self, _: &SelectRight, _: &mut Window, cx: &mut Context<Self>) {
self.select_to(self.next_boundary(self.cursor()), cx);
}
pub(super) fn select_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
if self.mode.is_single_line() {
return;
}
let offset = self.start_of_line().saturating_sub(1);
self.select_to(self.previous_boundary(offset), cx);
}
pub(super) fn select_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context<Self>) {
if self.mode.is_single_line() {
return;
}
let offset = (self.end_of_line() + 1).min(self.text.len());
self.select_to(self.next_boundary(offset), cx);
}
pub(super) fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
self.selected_range = (0..self.text.len()).into();
cx.notify();
}
pub(super) fn select_to_start(
&mut self,
_: &SelectToStart,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.select_to(0, cx);
}
pub(super) fn select_to_end(
&mut self,
_: &SelectToEnd,
_: &mut Window,
cx: &mut Context<Self>,
) {
let end = self.text.len();
self.select_to(end, cx);
}
pub(super) fn select_to_start_of_line(
&mut self,
_: &SelectToStartOfLine,
_: &mut Window,
cx: &mut Context<Self>,
) {
let offset = self.start_of_line();
self.select_to(offset, cx);
}
pub(super) fn select_to_end_of_line(
&mut self,
_: &SelectToEndOfLine,
_: &mut Window,
cx: &mut Context<Self>,
) {
let offset = self.end_of_line();
self.select_to(offset, cx);
}
pub(super) fn select_to_previous_word(
&mut self,
_: &SelectToPreviousWordStart,
_: &mut Window,
cx: &mut Context<Self>,
) {
let offset = self.previous_start_of_word();
self.select_to(offset, cx);
}
pub(super) fn select_to_next_word(
&mut self,
_: &SelectToNextWordEnd,
_: &mut Window,
cx: &mut Context<Self>,
) {
let offset = self.next_end_of_word();
self.select_to(offset, cx);
}
pub(super) fn previous_start_of_word(&mut self) -> usize {
let offset = self.selected_range.start;
let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
let left_part = self.text.slice(0..offset).to_string();
UnicodeSegmentation::split_word_bound_indices(left_part.as_str())
.rfind(|(_, s)| !s.trim_start().is_empty())
.map(|(i, _)| i)
.unwrap_or(0)
}
pub(super) fn next_end_of_word(&mut self) -> usize {
let offset = self.cursor();
let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
let right_part = self.text.slice(offset..self.text.len()).to_string();
UnicodeSegmentation::split_word_bound_indices(right_part.as_str())
.find(|(_, s)| !s.trim_start().is_empty())
.map(|(i, s)| offset + i + s.len())
.unwrap_or(self.text.len())
}
pub(super) fn start_of_line(&self) -> usize {
if self.mode.is_single_line() {
return 0;
}
let row = self.text.offset_to_point(self.cursor()).row;
let logical_start = self.text.line_start_offset(row);
if self.soft_wrap && self.mode.is_code_editor() {
let wrap_point = self.display_map.offset_to_wrap_display_point(self.cursor());
if let Some(line) = self.display_map.line(row)
&& let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
{
let visual_start = logical_start + range.start;
if self.cursor() != visual_start {
return visual_start;
}
}
}
logical_start
}
pub(super) fn end_of_line(&self) -> usize {
if self.mode.is_single_line() {
return self.text.len();
}
let row = self.text.offset_to_point(self.cursor()).row;
let logical_start = self.text.line_start_offset(row);
let logical_end = self.text.line_end_offset(row);
if self.soft_wrap && self.mode.is_code_editor() {
let wrap_point = self.display_map.offset_to_wrap_display_point(self.cursor());
if let Some(line) = self.display_map.line(row)
&& let Some(range) = line.wrapped_lines.get(wrap_point.local_row)
{
let visual_end = logical_start + range.end;
if self.cursor() != visual_end {
return visual_end;
}
}
}
logical_end
}
pub(super) fn start_of_line_of_selection(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> usize {
if self.mode.is_single_line() {
return 0;
}
let mut offset =
self.previous_boundary(self.selected_range.start.min(self.selected_range.end));
if self.text.char_at(offset) == Some('\r') {
offset += 1;
}
let line = 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);
line
}
pub(super) fn indent_of_next_line(&mut self) -> String {
if self.mode.is_single_line() {
return "".into();
}
let mut current_indent = String::new();
let mut next_indent = String::new();
let current_line_start_pos = self.start_of_line();
let next_line_start_pos = self.end_of_line();
for c in self.text.slice(current_line_start_pos..).chars() {
if !c.is_whitespace() {
break;
}
if c == '\n' || c == '\r' {
break;
}
current_indent.push(c);
}
for c in self.text.slice(next_line_start_pos..).chars() {
if !c.is_whitespace() {
break;
}
if c == '\n' || c == '\r' {
break;
}
next_indent.push(c);
}
if next_indent.len() > current_indent.len() {
return next_indent;
} else {
return current_indent;
}
}
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()), 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()), 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>,
) {
if !self.selected_range.is_empty() {
self.replace_text_in_range(None, "", window, cx);
self.pause_blink_cursor(cx);
return;
}
let mut offset = self.start_of_line();
if offset == self.cursor() {
offset = offset.saturating_sub(1);
}
self.replace_text_in_range_silent(
Some(self.range_to_utf16(&(offset..self.cursor()))),
"",
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>,
) {
if !self.selected_range.is_empty() {
self.replace_text_in_range(None, "", window, cx);
self.pause_blink_cursor(cx);
return;
}
let mut offset = self.end_of_line();
if offset == self.cursor() {
offset = (offset + 1).clamp(0, self.text.len());
}
self.replace_text_in_range_silent(
Some(self.range_to_utf16(&(self.cursor()..offset))),
"",
window,
cx,
);
self.pause_blink_cursor(cx);
}
pub(super) fn delete_previous_word(
&mut self,
_: &DeleteToPreviousWordStart,
window: &mut Window,
cx: &mut Context<Self>,
) {
if !self.selected_range.is_empty() {
self.replace_text_in_range(None, "", window, cx);
self.pause_blink_cursor(cx);
return;
}
let offset = self.previous_start_of_word();
self.replace_text_in_range_silent(
Some(self.range_to_utf16(&(offset..self.cursor()))),
"",
window,
cx,
);
self.pause_blink_cursor(cx);
}
pub(super) fn delete_next_word(
&mut self,
_: &DeleteToNextWordEnd,
window: &mut Window,
cx: &mut Context<Self>,
) {
if !self.selected_range.is_empty() {
self.replace_text_in_range(None, "", window, cx);
self.pause_blink_cursor(cx);
return;
}
let offset = self.next_end_of_word();
self.replace_text_in_range_silent(
Some(self.range_to_utf16(&(self.cursor()..offset))),
"",
window,
cx,
);
self.pause_blink_cursor(cx);
}
pub(super) fn enter(&mut self, action: &Enter, window: &mut Window, cx: &mut Context<Self>) {
let insert_newline = self.mode.is_multi_line() && (!self.submit_on_enter || action.shift);
if insert_newline {
let indent = if self.mode.is_code_editor() {
self.indent_of_next_line()
} else {
"".to_string()
};
let new_line_text = format!("\n{}", indent);
self.replace_text_in_range_silent(None, &new_line_text, window, cx);
self.pause_blink_cursor(cx);
} else {
cx.propagate();
}
cx.emit(InputEvent::PressEnter {
secondary: action.secondary,
shift: action.shift,
});
}
pub(super) fn clean(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.replace_text("", window, cx);
self.selected_range = (0..0).into();
self.scroll_to(0, None, cx);
}
pub(super) fn escape(&mut self, _: &Escape, window: &mut Window, cx: &mut Context<Self>) {
if self.ime_marked_range.is_some() {
self.unmark_text(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>,
) {
if let Some(ime_marked_range) = &self.ime_marked_range {
if ime_marked_range.len() == 0 {
self.ime_marked_range = None;
}
}
self.selecting = true;
let offset = self.index_for_mouse_position(event.position);
if event.button == MouseButton::Left && event.click_count >= 3 {
self.select_line(offset, window, cx);
return;
}
if event.button == MouseButton::Left && event.click_count == 2 {
self.select_word(offset, window, cx);
return;
}
if event.button == MouseButton::Right {
if !self.selected_range.contains(offset) {
self.move_to(offset, None, cx);
}
return;
}
if event.modifiers.shift {
self.select_to(offset, cx);
} else {
self.move_to(offset, None, cx)
}
}
pub(super) fn on_mouse_up(
&mut self,
event: &MouseUpEvent,
_window: &mut Window,
_cx: &mut Context<Self>,
) {
if event.button == MouseButton::Right {
return;
}
if self.selected_range.is_empty() {
self.selection_reversed = false;
}
self.selecting = false;
self.selected_word_range = None;
self.auto_scroll.stop();
}
pub(super) fn on_mouse_move(
&mut self,
event: &MouseMoveEvent,
_window: &mut Window,
_cx: &mut Context<Self>,
) {
let within_bounds = self
.last_bounds
.as_ref()
.map(|bounds| bounds.contains(&event.position))
.unwrap_or(false);
if !within_bounds {
return;
}
}
pub(super) fn on_scroll_wheel(
&mut self,
event: &ScrollWheelEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
let line_height = self
.last_layout
.as_ref()
.map(|layout| layout.line_height)
.unwrap_or(window.line_height());
let delta = event.delta.pixel_delta(line_height);
let old_offset = self.scroll_handle.offset();
self.update_scroll_offset(Some(old_offset + delta), cx);
if self.scroll_handle.offset() != old_offset {
cx.stop_propagation();
}
}
pub(super) 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_x_offset = if self.text_align == TextAlign::Left {
px(0.)
} else {
-CURSOR_WIDTH
};
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 + safe_x_offset)
.min(safe_x_offset)..px(0.);
offset.y = if self.mode.is_single_line() {
px(0.)
} else {
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(crate) fn scroll_to(
&mut self,
offset: usize,
direction: Option<MoveDirection>,
cx: &mut Context<Self>,
) {
let Some(last_layout) = self.last_layout.as_ref() else {
return;
};
let Some(bounds) = self.last_bounds.as_ref() else {
return;
};
let mut scroll_offset = self.scroll_handle.offset();
let was_offset = scroll_offset;
let line_height = last_layout.line_height;
let point = self.text.offset_to_point(offset);
let row = point.row;
let mut row_offset_y = line_height * self.display_map.buffer_line_to_display_row(row);
let safety_margin = match last_layout.text_align {
TextAlign::Left => RIGHT_MARGIN,
TextAlign::Right => px(0.),
TextAlign::Center => CURSOR_WIDTH,
};
if let Some(line) = last_layout
.lines
.get(row.saturating_sub(last_layout.visible_range.start))
{
if let Some(pos) = line.position_for_index(point.column, last_layout, false) {
let bounds_width = bounds.size.width - last_layout.line_number_width;
let col_offset_x = pos.x;
row_offset_y += pos.y;
if col_offset_x - safety_margin < -scroll_offset.x {
scroll_offset.x = -col_offset_x + safety_margin;
} else if col_offset_x + safety_margin > -scroll_offset.x + bounds_width {
scroll_offset.x = -(col_offset_x - bounds_width + safety_margin);
}
}
}
let edge_height = if direction.is_some() && self.mode.is_code_editor() {
super::element::cursor_surrounding_padding(
self.mode.is_auto_grow(),
self.cursor_surrounding_lines,
last_layout.visible_range.len(),
line_height,
)
} else {
line_height
};
if row_offset_y - edge_height + line_height < -scroll_offset.y {
scroll_offset.y = -row_offset_y + edge_height - line_height;
} else if row_offset_y + edge_height > -scroll_offset.y + bounds.size.height {
scroll_offset.y = -(row_offset_y - bounds.size.height + edge_height);
}
if direction == Some(MoveDirection::Up) {
scroll_offset.y = scroll_offset.y.max(was_offset.y);
} else if direction == Some(MoveDirection::Down) {
scroll_offset.y = scroll_offset.y.min(was_offset.y);
}
let safe_y_min = (-self.scroll_size.height + self.input_bounds.size.height).min(px(0.));
scroll_offset.x = scroll_offset.x.min(px(0.));
scroll_offset.y = scroll_offset.y.clamp(safe_y_min, px(0.));
self.deferred_scroll_offset = Some(scroll_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.slice(self.selected_range).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.slice(self.selected_range).to_string();
cx.write_to_clipboard(ClipboardItem::new_string(selected_text));
self.replace_text_in_range_silent(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 new_text = clipboard.text().unwrap_or_default();
self.replace_text_in_range_silent(None, &new_text, window, cx);
self.scroll_to(self.cursor(), None, cx);
}
}
fn push_history(&mut self, text: &Rope, range: &Range<usize>, new_text: &str) {
if self.history.ignore {
return;
}
let range =
text.clip_offset(range.start, Bias::Left)..text.clip_offset(range.end, Bias::Right);
let old_text = text.slice(range.clone()).to_string();
let new_range = range.start..range.start + new_text.len();
self.history
.push(Change::new(range, &old_text, new_range, 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.into());
self.replace_text_in_range_silent(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.into());
self.replace_text_in_range_silent(Some(range_utf16), &change.new_text, window, cx);
}
}
self.history.ignore = false;
}
pub fn cursor(&self) -> usize {
if let Some(ime_marked_range) = &self.ime_marked_range {
return ime_marked_range.end;
}
if self.selection_reversed {
self.selected_range.start
} else {
self.selected_range.end
}
}
pub fn visible_row_range(&self) -> Option<std::ops::Range<usize>> {
self.last_layout.as_ref().map(|l| l.visible_range.clone())
}
pub fn scroll_offset(&self) -> crate::Point<crate::Pixels> {
self.scroll_handle.offset()
}
pub fn set_scroll_offset(
&mut self,
offset: crate::Point<crate::Pixels>,
cx: &mut Context<Self>,
) {
self.deferred_scroll_offset = Some(offset);
cx.notify();
}
pub fn line_height(&self) -> Option<crate::Pixels> {
self.last_layout.as_ref().map(|l| l.line_height)
}
pub fn selected_range(&self) -> std::ops::Range<usize> {
self.selected_range.into()
}
pub fn set_selected_range(&mut self, range: Range<usize>, cx: &mut Context<Self>) {
let len = self.text.len();
let start = range.start.min(len);
let end = range.end.min(len);
self.move_to(start, None, cx);
self.selection_reversed = false;
self.selected_word_range = None;
self.select_to(end, cx);
}
pub(crate) fn index_for_mouse_position(&self, position: Point<Pixels>) -> usize {
if self.text.len() == 0 {
return 0;
}
let (Some(bounds), Some(last_layout)) =
(self.last_bounds.as_ref(), self.last_layout.as_ref())
else {
return 0;
};
let line_height = last_layout.line_height;
let line_number_width = last_layout.line_number_width;
let inner_position = position - bounds.origin - point(line_number_width, px(0.));
let mut y_offset = last_layout.visible_top;
for (vi, (line_layout, _buffer_line)) in last_layout
.lines
.iter()
.zip(last_layout.visible_buffer_lines.iter())
.enumerate()
{
let line_start_offset = last_layout.visible_line_byte_offsets[vi];
let line_origin = point(px(0.), y_offset);
let pos = inner_position - line_origin;
if self.mode.is_single_line() {
let local_index = line_layout.closest_index_for_x(pos.x, last_layout);
let index = line_start_offset + local_index;
return if self.masked {
self.text.char_index_to_offset(index / MASK_CHAR.len_utf8())
} else {
index.min(self.text.len())
};
}
if let Some(local_index) = line_layout.closest_index_for_position(pos, last_layout) {
let index = line_start_offset + local_index;
return if self.masked {
self.text.char_index_to_offset(index / MASK_CHAR.len_utf8())
} else {
index.min(self.text.len())
};
} else if pos.y < px(0.) {
return if self.masked {
self.text
.char_index_to_offset(line_start_offset / MASK_CHAR.len_utf8())
} else {
line_start_offset
};
}
y_offset += line_layout.size(line_height).height;
}
self.text.len()
}
pub(crate) fn select_to(&mut self, offset: usize, 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).into();
}
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_column();
}
cx.notify()
}
pub fn unselect(&mut self, _: &mut Window, cx: &mut Context<Self>) {
let offset = self.cursor();
self.selected_range = (offset..offset).into();
cx.notify()
}
#[inline]
pub(super) fn offset_from_utf16(&self, offset: usize) -> usize {
self.text.offset_utf16_to_offset(offset)
}
#[inline]
pub(super) fn offset_to_utf16(&self, offset: usize) -> usize {
self.text.offset_to_offset_utf16(offset)
}
#[inline]
pub(super) fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
}
#[inline]
pub(super) 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 clamp_offset_to_visible_backward(&self, offset: usize) -> usize {
let line = self.text.offset_to_point(offset).row;
if self.display_map.is_buffer_line_hidden(line) {
for fold in self.display_map.folded_ranges() {
if line > fold.start_line && line <= fold.end_line {
return self.text.line_end_offset(fold.start_line);
}
}
}
offset
}
fn clamp_offset_to_visible_forward(&self, offset: usize) -> usize {
let line = self.text.offset_to_point(offset).row;
if self.display_map.is_buffer_line_hidden(line) {
for fold in self.display_map.folded_ranges() {
if line > fold.start_line && line <= fold.end_line {
return self.text.line_start_offset(fold.end_line);
}
}
}
offset
}
pub(super) fn previous_boundary(&self, offset: usize) -> usize {
let mut offset = self.text.clip_offset(offset.saturating_sub(1), Bias::Left);
if let Some(ch) = self.text.char_at(offset) {
if ch == '\r' {
offset -= 1;
}
}
self.clamp_offset_to_visible_backward(offset)
}
pub(super) fn next_boundary(&self, offset: usize) -> usize {
let mut offset = self.text.clip_offset(offset + 1, Bias::Right);
if let Some(ch) = self.text.char_at(offset) {
if ch == '\r' {
offset += 1;
}
}
self.clamp_offset_to_visible_forward(offset)
}
pub(crate) fn show_cursor(&self, window: &Window, cx: &App) -> bool {
self.focus_handle.is_focused(window)
&& !self.disabled
&& self.blink_cursor.read(cx).visible()
&& window.is_window_active()
}
pub fn is_focused(&self, window: &Window) -> bool {
self.focus_handle.is_focused(window)
}
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.blink_cursor.update(cx, |cursor, cx| {
cursor.stop(cx);
});
self.clamp_number_value(window, cx);
cx.emit(InputEvent::Blur);
cx.notify();
}
fn clamp_number_value(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if !self.mode.is_single_line() {
return;
}
if !matches!(self.mask_pattern, MaskPattern::Number { .. }) {
return;
}
if self.number_min.is_none() && self.number_max.is_none() {
return;
}
let Ok(value) = self.unmask_value().parse::<f64>() else {
return;
};
let clamped = match (self.number_min, self.number_max) {
(Some(min), _) if value < min => min,
(_, Some(max)) if value > max => max,
_ => return,
};
let new_text = clamped.to_string();
if !self.is_valid_input(&new_text, cx) {
return;
}
let range = self.range_to_utf16(&(0..self.text.len()));
self.replace_text_in_range_silent(Some(range), &new_text, window, cx);
}
pub(super) 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.len() == 0 {
return;
}
if self.last_layout.is_none() {
return;
}
if !self.focus_handle.is_focused(window) {
return;
}
if !self.selecting {
return;
}
self.auto_scroll.last_drag_position = Some(event.position);
let offset = self.index_for_mouse_position(event.position);
self.select_to(offset, cx);
if !self.mode.is_single_line() {
let pad = self.editor_scrollbar_paddings.get();
let scroll_bounds = crate::Bounds::new(
point(
self.input_bounds.origin.x - pad.left,
self.input_bounds.origin.y - pad.top,
),
crate::size(
self.input_bounds.size.width + pad.left + pad.right,
self.input_bounds.size.height + pad.top + pad.bottom,
),
);
let delta = AutoScroll::compute_delta(event.position.y, scroll_bounds);
let scroll_delta = delta.map(|d| -d);
self.auto_scroll.set(scroll_delta, cx, |delta, state, cx| {
let current = state.scroll_handle.offset();
state.update_scroll_offset(Some(point(current.x, current.y + delta)), cx);
if let Some(pos) = state.auto_scroll.last_drag_position {
let offset = state.index_for_mouse_position(pos);
state.select_to(offset, cx);
}
});
}
}
fn normalize_input<'a>(&self, new_text: &'a str) -> Cow<'a, str> {
let normalized = if matches!(self.mask_pattern, MaskPattern::Number { .. }) {
normalize_number_input(new_text)
} else {
Cow::Borrowed(new_text)
};
if self.mode.is_single_line() && normalized.contains(['\n', '\r']) {
Cow::Owned(normalized.replace(['\n', '\r'], ""))
} else {
normalized
}
}
pub(super) fn is_valid_input(&self, new_text: &str, cx: &mut Context<Self>) -> bool {
if new_text.is_empty() {
return true;
}
if let Some(validate) = &self.validate {
if !validate(new_text, cx) {
return false;
}
}
if !self.mask_pattern.is_valid(new_text) {
return false;
}
let Some(pattern) = &self.pattern else {
return true;
};
pattern.is_match(new_text)
}
pub fn mask_pattern(mut self, pattern: impl Into<MaskPattern>) -> Self {
self.mask_pattern = pattern.into();
self.mask_pattern_set = true;
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();
self.mask_pattern_set = true;
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;
if let Some(last_layout) = self.last_layout.as_ref() {
if wrap_width_changed {
let wrap_width = if !self.soft_wrap {
None
} else {
last_layout.wrap_width
};
self.display_map.on_layout_changed(wrap_width, cx);
self.mode.update_auto_grow(&self.display_map);
cx.notify();
}
}
}
pub(super) fn selected_text(&self) -> RopeSlice<'_> {
let range_utf16 = self.range_to_utf16(&self.selected_range.into());
let range = self.range_from_utf16(&range_utf16);
self.text.slice(range)
}
pub fn range_to_bounds(&self, range: &Range<usize>) -> Option<Bounds<Pixels>> {
let Some(last_layout) = self.last_layout.as_ref() else {
return None;
};
let Some(last_bounds) = self.last_bounds else {
return None;
};
let (_, _, start_pos) = self.line_and_position_for_offset(range.start);
let (_, _, end_pos) = self.line_and_position_for_offset(range.end);
let Some(start_pos) = start_pos else {
return None;
};
let Some(end_pos) = end_pos else {
return None;
};
Some(Bounds::from_corners(
last_bounds.origin + start_pos,
last_bounds.origin + end_pos + point(px(0.), last_layout.line_height),
))
}
pub(crate) fn replace_text_in_range_silent(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.replace_text_in_range(range_utf16, new_text, window, cx);
}
}
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.slice(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.into()),
reversed: false,
})
}
fn marked_text_range(
&self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<Range<usize>> {
self.ime_marked_range
.map(|range| self.range_to_utf16(&range.into()))
}
fn unmark_text(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
self.ime_marked_range = None;
}
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;
}
if self.blink_cursor.read(cx).visible() {
self.pause_blink_cursor(cx);
}
let new_text = self.normalize_input(new_text);
let new_text: &str = &new_text;
let range = range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
.or(self.ime_marked_range.map(|range| {
let range = self.range_to_utf16(&(range.start..range.end));
self.range_from_utf16(&range)
}))
.unwrap_or(self.selected_range.into());
let old_text = self.text.clone();
self.text.replace(range.clone(), new_text);
let mut new_offset = (range.start + new_text.len()).min(self.text.len());
let mut mask_changed = false;
if self.mode.is_single_line() {
let pending_text = self.text.to_string();
if !self.is_valid_input(&pending_text, cx)
&& self.is_valid_input(&old_text.to_string(), cx)
{
self.text = old_text;
return;
}
if !self.mask_pattern.is_none() {
let mask_text = self.mask_pattern.mask(&pending_text);
mask_changed = mask_text.as_str() != pending_text;
self.text = Rope::from(mask_text.as_str());
let new_text_len =
(new_text.len() + mask_text.len()).saturating_sub(pending_text.len());
new_offset = (range.start + new_text_len).min(mask_text.len());
}
}
if mask_changed {
self.decorations.clear();
} else {
self.decorations.adjust_for_edit(&range, new_text.len());
}
if mask_changed {
self.push_history(&old_text, &(0..old_text.len()), &self.text.to_string());
} else {
self.push_history(&old_text, &range, &new_text);
}
self.history.end_grouping();
self.display_map
.adjust_folds_for_edit(&old_text, &range, new_text);
self.display_map
.on_text_changed(&self.text, &range, &Rope::from(new_text), cx);
self.selected_range = (new_offset..new_offset).into();
self.ime_marked_range.take();
self.update_preferred_column();
self.mode.update_auto_grow(&self.display_map);
if self.emit_events {
cx.emit(InputEvent::Change);
}
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 new_text = self.normalize_input(new_text);
let new_text: &str = &new_text;
let range = range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
.or(self.ime_marked_range.map(|range| {
let range = self.range_to_utf16(&(range.start..range.end));
self.range_from_utf16(&range)
}))
.unwrap_or(self.selected_range.into());
let old_text = self.text.clone();
self.text.replace(range.clone(), new_text);
if self.mode.is_single_line() {
let pending_text = self.text.to_string();
if !self.is_valid_input(&pending_text, cx)
&& self.is_valid_input(&old_text.to_string(), cx)
{
self.text = old_text;
return;
}
}
self.decorations.adjust_for_edit(&range, new_text.len());
self.display_map
.adjust_folds_for_edit(&old_text, &range, new_text);
self.display_map
.on_text_changed(&self.text, &range, &Rope::from(new_text), cx);
if new_text.is_empty() {
self.selected_range = (range.start..range.start).into();
self.ime_marked_range = None;
} else {
self.ime_marked_range = Some((range.start..range.start + new_text.len()).into());
self.selected_range = new_selected_range_utf16
.as_ref()
.map(|range_utf16| {
let new_text = Rope::from(new_text);
range.start + new_text.offset_utf16_to_offset(range_utf16.start)
..range.start + new_text.offset_utf16_to_offset(range_utf16.end)
})
.unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len())
.into();
}
self.mode.update_auto_grow(&self.display_map);
self.history.start_grouping();
self.push_history(&old_text, &range, new_text);
cx.notify();
}
fn bounds_for_range(
&mut self,
range_utf16: Range<usize>,
bounds: Bounds<Pixels>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<Bounds<Pixels>> {
let last_layout = self.last_layout.as_ref()?;
let line_height = last_layout.line_height;
let line_number_width = last_layout.line_number_width;
let range = self.range_from_utf16(&range_utf16);
let mut start_origin = None;
let mut end_origin = None;
let line_number_origin = point(line_number_width, px(0.));
let mut y_offset = last_layout.visible_top;
for (vi, line) in last_layout.lines.iter().enumerate() {
if start_origin.is_some() && end_origin.is_some() {
break;
}
let index_offset = last_layout.visible_line_byte_offsets[vi];
if start_origin.is_none() {
if let Some(p) = line.position_for_index(
range.start.saturating_sub(index_offset),
last_layout,
false,
) {
start_origin = Some(p + point(px(0.), y_offset));
}
}
if end_origin.is_none() {
if let Some(p) = line.position_for_index(
range.end.saturating_sub(index_offset),
last_layout,
false,
) {
end_origin = Some(p + point(px(0.), y_offset));
}
}
y_offset += line.size(line_height).height;
}
let start_origin = start_origin.unwrap_or_default();
let mut end_origin = end_origin.unwrap_or_default();
end_origin.y = start_origin.y;
Some(Bounds::from_corners(
bounds.origin + line_number_origin + start_origin,
bounds.origin + line_number_origin + point(end_origin.x, end_origin.y + line_height),
))
}
fn character_index_for_point(
&mut self,
point: crate::Point<Pixels>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<usize> {
let last_layout = self.last_layout.as_ref()?;
let line_point = self.last_bounds?.localize(&point)?;
for (vi, line) in last_layout.lines.iter().enumerate() {
let offset = last_layout.visible_line_byte_offsets[vi];
if let Some(utf8_index) = line.index_for_position(line_point, last_layout) {
return Some(self.offset_to_utf16(offset + 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, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self._pending_update {
self.display_map.ensure_text_prepared(&self.text, cx);
self._pending_update = false;
}
div()
.id("input-state")
.flex_1()
.when(self.mode.is_multi_line(), |this| this.h_full())
.flex_grow_1()
.overflow_x_hidden()
.child(TextElement::new(cx.entity()).placeholder(self.placeholder.clone()))
}
}