chore: update text input
This commit is contained in:
@@ -61,12 +61,11 @@ impl TextElement {
|
||||
let mut cursor = None;
|
||||
|
||||
// If the input has a fixed height (Otherwise is auto-grow), we need to add a bottom margin to the input.
|
||||
let bottom_margin = if input.auto_grow {
|
||||
px(0.)
|
||||
let bottom_margin = if input.is_auto_grow() {
|
||||
px(0.) + line_height
|
||||
} else {
|
||||
BOTTOM_MARGIN_ROWS * line_height + line_height
|
||||
};
|
||||
|
||||
// The cursor corresponds to the current cursor position in the text no only the line.
|
||||
let mut cursor_pos = None;
|
||||
let mut cursor_start = None;
|
||||
@@ -160,7 +159,7 @@ impl TextElement {
|
||||
if input.show_cursor(window, cx) {
|
||||
// cursor blink
|
||||
let cursor_height =
|
||||
window.text_style().font_size.to_pixels(window.rem_size()) + px(4.);
|
||||
window.text_style().font_size.to_pixels(window.rem_size()) + px(2.);
|
||||
cursor = Some(fill(
|
||||
Bounds::new(
|
||||
point(
|
||||
@@ -224,10 +223,14 @@ impl TextElement {
|
||||
(end.y / line_height).ceil() as usize - (start.y / line_height).ceil() as usize;
|
||||
|
||||
let mut end_x = end.x;
|
||||
|
||||
if wrapped_lines > 0 {
|
||||
end_x = line_wrap_width;
|
||||
}
|
||||
|
||||
// Ensure at least 6px width for the selection for empty lines.
|
||||
end_x = end_x.max(start.x + px(6.));
|
||||
|
||||
line_corners.push(Corners {
|
||||
top_left: line_origin + point(start.x, start.y),
|
||||
top_right: line_origin + point(end_x, start.y),
|
||||
@@ -363,28 +366,14 @@ impl Element for TextElement {
|
||||
|
||||
let mut style = Style::default();
|
||||
style.size.width = relative(1.).into();
|
||||
|
||||
if self.input.read(cx).is_multi_line() {
|
||||
style.flex_grow = 1.0;
|
||||
if let Some(h) = input.height {
|
||||
if let Some(h) = input.mode.height() {
|
||||
style.size.height = h.into();
|
||||
style.min_size.height = line_height.into();
|
||||
} else {
|
||||
// Check to auto grow
|
||||
let rows = if input.auto_grow {
|
||||
let rows = (input.scroll_size.height / line_height) as usize;
|
||||
let max_rows = input
|
||||
.max_rows
|
||||
.unwrap_or(usize::MAX)
|
||||
.min(rows)
|
||||
.max(input.min_rows);
|
||||
rows.clamp(input.min_rows, max_rows)
|
||||
} else {
|
||||
input.rows
|
||||
};
|
||||
|
||||
style.size.height = relative(1.).into();
|
||||
style.min_size.height = (rows.max(1) as f32 * line_height).into();
|
||||
style.min_size.height = (input.mode.rows() * line_height).into();
|
||||
}
|
||||
} else {
|
||||
// For single-line inputs, the minimum height should be the line height
|
||||
@@ -534,7 +523,6 @@ impl Element for TextElement {
|
||||
// Set Root focused_input when self is focused
|
||||
if focused {
|
||||
let state = self.input.clone();
|
||||
|
||||
if Root::read(window, cx).focused_input.as_ref() != Some(&state) {
|
||||
Root::update(window, cx, |root, _, cx| {
|
||||
root.focused_input = Some(state);
|
||||
@@ -546,7 +534,6 @@ impl Element for TextElement {
|
||||
// And reset focused_input when next_frame start
|
||||
window.on_next_frame({
|
||||
let state = self.input.clone();
|
||||
|
||||
move |window, cx| {
|
||||
if !focused && Root::read(window, cx).focused_input.as_ref() == Some(&state) {
|
||||
Root::update(window, cx, |root, _, cx| {
|
||||
@@ -593,7 +580,6 @@ impl Element for TextElement {
|
||||
.map(|l| l.width())
|
||||
.max()
|
||||
.unwrap_or_default();
|
||||
|
||||
let height = offset_y;
|
||||
let scroll_size = size(width, height);
|
||||
|
||||
@@ -602,12 +588,15 @@ impl Element for TextElement {
|
||||
input.last_bounds = Some(bounds);
|
||||
input.last_cursor_offset = Some(input.cursor_offset());
|
||||
input.last_line_height = line_height;
|
||||
input.input_bounds = input_bounds;
|
||||
|
||||
input.set_input_bounds(input_bounds, cx);
|
||||
|
||||
input.last_selected_range = Some(selected_range);
|
||||
input.scroll_size = scroll_size;
|
||||
input
|
||||
.scroll_handle
|
||||
.set_offset(prepaint.cursor_scroll_offset);
|
||||
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ mod element;
|
||||
mod mask_pattern;
|
||||
mod state;
|
||||
mod text_input;
|
||||
mod text_wrapper;
|
||||
|
||||
pub(crate) mod clear_button;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ impl RenderOnce for TextInput {
|
||||
const LINE_HEIGHT: Rems = Rems(1.25);
|
||||
|
||||
self.state.update(cx, |state, _| {
|
||||
state.height = self.height;
|
||||
state.mode.set_height(self.height);
|
||||
state.disabled = self.disabled;
|
||||
});
|
||||
|
||||
@@ -187,7 +187,7 @@ impl RenderOnce for TextInput {
|
||||
.on_action(window.listener_for(&self.state, InputState::right))
|
||||
.on_action(window.listener_for(&self.state, InputState::select_left))
|
||||
.on_action(window.listener_for(&self.state, InputState::select_right))
|
||||
.when(state.multi_line, |this| {
|
||||
.when(state.is_multi_line(), |this| {
|
||||
this.on_action(window.listener_for(&self.state, InputState::up))
|
||||
.on_action(window.listener_for(&self.state, InputState::down))
|
||||
.on_action(window.listener_for(&self.state, InputState::select_up))
|
||||
@@ -228,7 +228,7 @@ impl RenderOnce for TextInput {
|
||||
.cursor_text()
|
||||
.input_py(self.size)
|
||||
.input_h(self.size)
|
||||
.when(state.multi_line, |this| {
|
||||
.when(state.is_multi_line(), |this| {
|
||||
this.h_auto()
|
||||
.when_some(self.height, |this, height| this.h(height))
|
||||
})
|
||||
|
||||
72
crates/ui/src/input/text_wrapper.rs
Normal file
72
crates/ui/src/input/text_wrapper.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::{App, Font, LineFragment, Pixels, SharedString};
|
||||
|
||||
/// Used to prepare the text with soft_wrap to be get lines to displayed in the TextArea
|
||||
///
|
||||
/// After use lines to calculate the scroll size of the TextArea
|
||||
pub(super) struct TextWrapper {
|
||||
pub(super) text: SharedString,
|
||||
/// The wrapped lines, value is start and end index of the line (by split \n).
|
||||
pub(super) wrapped_lines: Vec<Range<usize>>,
|
||||
pub(super) font: Font,
|
||||
pub(super) font_size: Pixels,
|
||||
/// If is none, it means the text is not wrapped
|
||||
pub(super) wrap_width: Option<Pixels>,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl TextWrapper {
|
||||
pub(super) fn new(font: Font, font_size: Pixels, wrap_width: Option<Pixels>) -> Self {
|
||||
Self {
|
||||
text: SharedString::default(),
|
||||
font,
|
||||
font_size,
|
||||
wrap_width,
|
||||
wrapped_lines: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
||||
if self.wrap_width == wrap_width {
|
||||
return;
|
||||
}
|
||||
|
||||
self.wrap_width = wrap_width;
|
||||
self.update(self.text.clone(), cx);
|
||||
}
|
||||
|
||||
pub(super) fn set_font(&mut self, font: Font, cx: &mut App) {
|
||||
self.font = font;
|
||||
self.update(self.text.clone(), cx);
|
||||
}
|
||||
|
||||
pub(super) fn update(&mut self, text: SharedString, cx: &mut App) {
|
||||
let mut wrapped_lines = vec![];
|
||||
let wrap_width = self.wrap_width.unwrap_or(Pixels::MAX);
|
||||
let mut line_wrapper = cx
|
||||
.text_system()
|
||||
.line_wrapper(self.font.clone(), self.font_size);
|
||||
|
||||
for line in text.lines() {
|
||||
let mut prev_boundary_ix = 0;
|
||||
for boundary in line_wrapper.wrap_line(&[LineFragment::text(line)], wrap_width) {
|
||||
wrapped_lines.push(prev_boundary_ix..boundary.ix);
|
||||
prev_boundary_ix = boundary.ix;
|
||||
}
|
||||
|
||||
// Reset of the line
|
||||
if !line[prev_boundary_ix..].is_empty() || prev_boundary_ix == 0 {
|
||||
wrapped_lines.push(prev_boundary_ix..line.len());
|
||||
}
|
||||
}
|
||||
|
||||
// Add last empty line.
|
||||
if text.chars().last().unwrap_or('\n') == '\n' {
|
||||
wrapped_lines.push(text.len()..text.len());
|
||||
}
|
||||
|
||||
self.text = text;
|
||||
self.wrapped_lines = wrapped_lines;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user