chore: update text input

This commit is contained in:
2025-05-21 18:48:00 +07:00
parent e851063de9
commit b0a6b73801
6 changed files with 249 additions and 80 deletions

View File

@@ -17,6 +17,7 @@ use gpui::{
use super::{
blink_cursor::BlinkCursor, change::Change, element::TextElement, mask_pattern::MaskPattern,
text_wrapper::TextWrapper,
};
use crate::{history::History, scroll::ScrollbarState, Root};
@@ -183,21 +184,99 @@ pub fn init(cx: &mut App) {
]);
}
type Validate = Option<Box<dyn Fn(&str) -> bool + 'static>>;
#[derive(Debug, Default, Clone)]
pub enum InputMode {
#[default]
SingleLine,
MultiLine {
rows: usize,
height: Option<DefiniteLength>,
},
AutoGrow {
rows: usize,
min_rows: usize,
max_rows: usize,
},
}
impl InputMode {
pub(super) fn set_rows(&mut self, new_rows: usize) {
match self {
InputMode::MultiLine { rows, .. } => {
*rows = new_rows;
}
InputMode::AutoGrow {
rows,
min_rows,
max_rows,
} => {
*rows = new_rows.clamp(*min_rows, *max_rows);
}
_ => {}
}
}
pub(super) fn update_auto_grow(&mut self, text_wrapper: &TextWrapper) {
if let Self::AutoGrow { .. } = self {
let wrapped_lines = text_wrapper.wrapped_lines.len();
self.set_rows(wrapped_lines);
}
}
/// At least 1 row be return.
pub(super) fn rows(&self) -> usize {
match self {
InputMode::MultiLine { rows, .. } => *rows,
InputMode::AutoGrow { rows, .. } => *rows,
_ => 1,
}
.max(1)
}
/// At least 1 row be return.
#[allow(unused)]
pub(super) fn min_rows(&self) -> usize {
match self {
InputMode::MultiLine { .. } => 1,
InputMode::AutoGrow { min_rows, .. } => *min_rows,
_ => 1,
}
.max(1)
}
#[allow(unused)]
pub(super) fn max_rows(&self) -> usize {
match self {
InputMode::MultiLine { .. } => usize::MAX,
InputMode::AutoGrow { max_rows, .. } => *max_rows,
_ => 1,
}
}
pub(super) fn set_height(&mut self, new_height: Option<DefiniteLength>) {
if let InputMode::MultiLine { height, .. } = self {
*height = new_height;
}
}
pub(super) fn height(&self) -> Option<DefiniteLength> {
match self {
InputMode::MultiLine { height, .. } => *height,
_ => None,
}
}
}
/// InputState to keep editing state of the [`super::TextInput`].
pub struct InputState {
pub(super) focus_handle: FocusHandle,
pub(super) mode: InputMode,
pub(super) text: SharedString,
pub(super) multi_line: bool,
pub(super) new_line_on_enter: bool,
pub(super) text_wrapper: TextWrapper,
pub(super) history: History<Change>,
pub(super) blink_cursor: Entity<BlinkCursor>,
pub(super) loading: bool,
/// Range in UTF-8 length for the selected text.
///
/// - "Hello 世界💝" = 16
/// - "💝" = 4
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>>,
@@ -216,13 +295,9 @@ pub struct InputState {
pub(super) disabled: bool,
pub(super) masked: bool,
pub(super) clean_on_escape: bool,
pub(super) height: Option<DefiniteLength>,
pub(super) rows: usize,
pub(super) min_rows: usize,
pub(super) max_rows: Option<usize>,
pub(super) auto_grow: bool,
pub(super) pattern: Option<regex::Regex>,
pub(super) validate: Validate,
#[allow(clippy::type_complexity)]
pub(super) validate: Option<Box<dyn Fn(&str) -> bool + 'static>>,
pub(crate) scroll_handle: ScrollHandle,
pub(super) scrollbar_state: Rc<Cell<ScrollbarState>>,
/// The size of the scrollable content.
@@ -240,6 +315,9 @@ pub struct InputState {
impl EventEmitter<InputEvent> for InputState {}
impl InputState {
/// Create a Input state with default [`InputMode::SingleLine`] mode.
///
/// See also: [`Self::multi_line`], [`Self::auto_grow`] to set other mode.
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();
let blink_cursor = cx.new(|_| BlinkCursor::new());
@@ -263,10 +341,16 @@ impl InputState {
cx.on_blur(&focus_handle, window, Self::on_blur),
];
let text_style = window.text_style();
Self {
focus_handle: focus_handle.clone(),
text: "".into(),
multi_line: false,
text_wrapper: TextWrapper::new(
text_style.font(),
text_style.font_size.to_pixels(window.rem_size()),
None,
),
new_line_on_enter: true,
blink_cursor,
history,
@@ -282,11 +366,7 @@ impl InputState {
loading: false,
pattern: None,
validate: None,
rows: 3,
min_rows: 3,
max_rows: None,
auto_grow: false,
height: None,
mode: InputMode::SingleLine,
last_layout: None,
last_bounds: None,
last_selected_range: None,
@@ -302,9 +382,24 @@ impl InputState {
}
}
/// Use the text input field as a multi-line Textarea.
/// Set Input to use [`InputMode::MultiLine`] mode.
///
/// Default rows is 2.
pub fn multi_line(mut self) -> Self {
self.multi_line = true;
self.mode = InputMode::MultiLine {
rows: 2,
height: None,
};
self
}
/// Set Input to use [`InputMode::AutoGrow`] mode with min, max rows limit.
pub fn auto_grow(mut self, min_rows: usize, max_rows: usize) -> Self {
self.mode = InputMode::AutoGrow {
rows: min_rows,
min_rows,
max_rows,
};
self
}
@@ -460,12 +555,20 @@ impl InputState {
#[inline]
pub(super) fn is_multi_line(&self) -> bool {
self.multi_line
matches!(
self.mode,
InputMode::MultiLine { .. } | InputMode::AutoGrow { .. }
)
}
#[inline]
pub(super) fn is_single_line(&self) -> bool {
!self.multi_line
matches!(self.mode, InputMode::SingleLine)
}
#[inline]
pub(super) fn is_auto_grow(&self) -> bool {
matches!(self.mode, InputMode::AutoGrow { .. })
}
/// Set the number of rows for the multi-line Textarea.
@@ -474,26 +577,19 @@ impl InputState {
///
/// default: 2
pub fn rows(mut self, rows: usize) -> Self {
self.rows = rows;
self.min_rows = rows;
self
}
/// Set the maximum number of rows for the multi-line Textarea.
///
/// If max_rows is more than rows, then will enable auto-grow.
///
/// This is only used when `multi_line` is set to true.
///
/// default: None
pub fn max_rows(mut self, max_rows: usize) -> Self {
self.max_rows = Some(max_rows);
self
}
/// Set the auto-grow mode for the multi-line Textarea.
pub fn auto_grow(mut self) -> Self {
self.auto_grow = true;
match self.mode {
InputMode::MultiLine { height, .. } => {
self.mode = InputMode::MultiLine { rows, height };
}
InputMode::AutoGrow { max_rows, .. } => {
self.mode = InputMode::AutoGrow {
rows,
min_rows: rows,
max_rows,
};
}
_ => {}
}
self
}
@@ -1120,7 +1216,7 @@ impl InputState {
pub(super) fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
if let Some(clipboard) = cx.read_from_clipboard() {
let mut new_text = clipboard.text().unwrap_or_default();
if !self.multi_line {
if !self.is_multi_line() {
new_text = new_text.replace('\n', "");
}
@@ -1232,11 +1328,10 @@ impl InputState {
for line in lines.iter() {
let line_origin = self.line_origin_with_y_offset(&mut y_offset, line, line_height);
let pos = inner_position - line_origin;
let closest_index = line.unwrapped_layout.closest_index_for_x(pos.x);
// Return offset by use closest_index_for_x if is single line mode.
if self.is_single_line() {
return closest_index;
return line.unwrapped_layout.closest_index_for_x(pos.x);
}
let index_result = line.closest_index_for_position(pos, line_height);
@@ -1251,13 +1346,14 @@ impl InputState {
// 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
} else if line.text.trim_end_matches('\r').is_empty() {
// empty line on Windows is `\r`, other is ''
let line_bounds = Bounds {
origin: line_origin,
size: gpui::size(bounds.size.width, line_height),
};
let pos = inner_position;
index += line.len();
if line_bounds.contains(&pos) {
break;
}
@@ -1265,7 +1361,7 @@ impl InputState {
index += line.len();
}
// add 1 for \n
// +1 for revert `lines` split `\n`
index += 1;
}
@@ -1539,6 +1635,18 @@ impl InputState {
}
cx.notify();
}
pub(super) fn set_input_bounds(&mut self, new_bounds: Bounds<Pixels>, cx: &mut Context<Self>) {
let wrap_width_changed = self.input_bounds.size.width != new_bounds.size.width;
self.input_bounds = new_bounds;
// Update text_wrapper wrap_width if changed.
if wrap_width_changed {
self.text_wrapper
.set_wrap_width(Some(new_bounds.size.width), cx);
self.mode.update_auto_grow(&self.text_wrapper);
}
}
}
impl EntityInputHandler for InputState {
@@ -1614,14 +1722,13 @@ impl EntityInputHandler for InputState {
let new_pos = (range.start + new_text_len).min(mask_text.len());
self.push_history(&range, new_text, window, cx);
self.text = mask_text;
self.text_wrapper.update(self.text.clone(), cx);
self.selected_range = new_pos..new_pos;
self.marked_range.take();
self.update_preferred_x_offset(cx);
self.update_scroll_offset(None, cx);
self.mode.update_auto_grow(&self.text_wrapper);
cx.emit(InputEvent::Change(self.unmask_value()));
cx.notify();
}