chore: update text input
This commit is contained in:
@@ -90,8 +90,8 @@ impl Chat {
|
|||||||
.multi_line()
|
.multi_line()
|
||||||
.prevent_new_line_on_enter()
|
.prevent_new_line_on_enter()
|
||||||
.rows(1)
|
.rows(1)
|
||||||
.max_rows(20)
|
.multi_line()
|
||||||
.auto_grow()
|
.auto_grow(1, 20)
|
||||||
.clean_on_escape()
|
.clean_on_escape()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -61,12 +61,11 @@ impl TextElement {
|
|||||||
let mut cursor = None;
|
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.
|
// 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 {
|
let bottom_margin = if input.is_auto_grow() {
|
||||||
px(0.)
|
px(0.) + line_height
|
||||||
} else {
|
} else {
|
||||||
BOTTOM_MARGIN_ROWS * line_height + line_height
|
BOTTOM_MARGIN_ROWS * line_height + line_height
|
||||||
};
|
};
|
||||||
|
|
||||||
// The cursor corresponds to the current cursor position in the text no only the line.
|
// The cursor corresponds to the current cursor position in the text no only the line.
|
||||||
let mut cursor_pos = None;
|
let mut cursor_pos = None;
|
||||||
let mut cursor_start = None;
|
let mut cursor_start = None;
|
||||||
@@ -160,7 +159,7 @@ impl TextElement {
|
|||||||
if input.show_cursor(window, cx) {
|
if input.show_cursor(window, cx) {
|
||||||
// cursor blink
|
// cursor blink
|
||||||
let cursor_height =
|
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(
|
cursor = Some(fill(
|
||||||
Bounds::new(
|
Bounds::new(
|
||||||
point(
|
point(
|
||||||
@@ -224,10 +223,14 @@ impl TextElement {
|
|||||||
(end.y / line_height).ceil() as usize - (start.y / line_height).ceil() as usize;
|
(end.y / line_height).ceil() as usize - (start.y / line_height).ceil() as usize;
|
||||||
|
|
||||||
let mut end_x = end.x;
|
let mut end_x = end.x;
|
||||||
|
|
||||||
if wrapped_lines > 0 {
|
if wrapped_lines > 0 {
|
||||||
end_x = line_wrap_width;
|
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 {
|
line_corners.push(Corners {
|
||||||
top_left: line_origin + point(start.x, start.y),
|
top_left: line_origin + point(start.x, start.y),
|
||||||
top_right: line_origin + point(end_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();
|
let mut style = Style::default();
|
||||||
style.size.width = relative(1.).into();
|
style.size.width = relative(1.).into();
|
||||||
|
|
||||||
if self.input.read(cx).is_multi_line() {
|
if self.input.read(cx).is_multi_line() {
|
||||||
style.flex_grow = 1.0;
|
style.flex_grow = 1.0;
|
||||||
if let Some(h) = input.height {
|
if let Some(h) = input.mode.height() {
|
||||||
style.size.height = h.into();
|
style.size.height = h.into();
|
||||||
style.min_size.height = line_height.into();
|
style.min_size.height = line_height.into();
|
||||||
} else {
|
} 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.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 {
|
} else {
|
||||||
// For single-line inputs, the minimum height should be the line height
|
// 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
|
// Set Root focused_input when self is focused
|
||||||
if focused {
|
if focused {
|
||||||
let state = self.input.clone();
|
let state = self.input.clone();
|
||||||
|
|
||||||
if Root::read(window, cx).focused_input.as_ref() != Some(&state) {
|
if Root::read(window, cx).focused_input.as_ref() != Some(&state) {
|
||||||
Root::update(window, cx, |root, _, cx| {
|
Root::update(window, cx, |root, _, cx| {
|
||||||
root.focused_input = Some(state);
|
root.focused_input = Some(state);
|
||||||
@@ -546,7 +534,6 @@ impl Element for TextElement {
|
|||||||
// And reset focused_input when next_frame start
|
// And reset focused_input when next_frame start
|
||||||
window.on_next_frame({
|
window.on_next_frame({
|
||||||
let state = self.input.clone();
|
let state = self.input.clone();
|
||||||
|
|
||||||
move |window, cx| {
|
move |window, cx| {
|
||||||
if !focused && Root::read(window, cx).focused_input.as_ref() == Some(&state) {
|
if !focused && Root::read(window, cx).focused_input.as_ref() == Some(&state) {
|
||||||
Root::update(window, cx, |root, _, cx| {
|
Root::update(window, cx, |root, _, cx| {
|
||||||
@@ -593,7 +580,6 @@ impl Element for TextElement {
|
|||||||
.map(|l| l.width())
|
.map(|l| l.width())
|
||||||
.max()
|
.max()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let height = offset_y;
|
let height = offset_y;
|
||||||
let scroll_size = size(width, height);
|
let scroll_size = size(width, height);
|
||||||
|
|
||||||
@@ -602,12 +588,15 @@ impl Element for TextElement {
|
|||||||
input.last_bounds = Some(bounds);
|
input.last_bounds = Some(bounds);
|
||||||
input.last_cursor_offset = Some(input.cursor_offset());
|
input.last_cursor_offset = Some(input.cursor_offset());
|
||||||
input.last_line_height = line_height;
|
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.last_selected_range = Some(selected_range);
|
||||||
input.scroll_size = scroll_size;
|
input.scroll_size = scroll_size;
|
||||||
input
|
input
|
||||||
.scroll_handle
|
.scroll_handle
|
||||||
.set_offset(prepaint.cursor_scroll_offset);
|
.set_offset(prepaint.cursor_scroll_offset);
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ mod element;
|
|||||||
mod mask_pattern;
|
mod mask_pattern;
|
||||||
mod state;
|
mod state;
|
||||||
mod text_input;
|
mod text_input;
|
||||||
|
mod text_wrapper;
|
||||||
|
|
||||||
pub(crate) mod clear_button;
|
pub(crate) mod clear_button;
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use gpui::{
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
blink_cursor::BlinkCursor, change::Change, element::TextElement, mask_pattern::MaskPattern,
|
blink_cursor::BlinkCursor, change::Change, element::TextElement, mask_pattern::MaskPattern,
|
||||||
|
text_wrapper::TextWrapper,
|
||||||
};
|
};
|
||||||
use crate::{history::History, scroll::ScrollbarState, Root};
|
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`].
|
/// InputState to keep editing state of the [`super::TextInput`].
|
||||||
pub struct InputState {
|
pub struct InputState {
|
||||||
pub(super) focus_handle: FocusHandle,
|
pub(super) focus_handle: FocusHandle,
|
||||||
|
pub(super) mode: InputMode,
|
||||||
pub(super) text: SharedString,
|
pub(super) text: SharedString,
|
||||||
pub(super) multi_line: bool,
|
|
||||||
pub(super) new_line_on_enter: bool,
|
pub(super) new_line_on_enter: bool,
|
||||||
|
pub(super) text_wrapper: TextWrapper,
|
||||||
pub(super) history: History<Change>,
|
pub(super) history: History<Change>,
|
||||||
pub(super) blink_cursor: Entity<BlinkCursor>,
|
pub(super) blink_cursor: Entity<BlinkCursor>,
|
||||||
pub(super) loading: bool,
|
pub(super) loading: bool,
|
||||||
/// Range in UTF-8 length for the selected text.
|
|
||||||
///
|
|
||||||
/// - "Hello 世界💝" = 16
|
|
||||||
/// - "💝" = 4
|
|
||||||
pub(super) selected_range: Range<usize>,
|
pub(super) selected_range: Range<usize>,
|
||||||
/// Range for save the selected word, use to keep word range when drag move.
|
/// Range for save the selected word, use to keep word range when drag move.
|
||||||
pub(super) selected_word_range: Option<Range<usize>>,
|
pub(super) selected_word_range: Option<Range<usize>>,
|
||||||
@@ -216,13 +295,9 @@ pub struct InputState {
|
|||||||
pub(super) disabled: bool,
|
pub(super) disabled: bool,
|
||||||
pub(super) masked: bool,
|
pub(super) masked: bool,
|
||||||
pub(super) clean_on_escape: 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) 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(crate) scroll_handle: ScrollHandle,
|
||||||
pub(super) scrollbar_state: Rc<Cell<ScrollbarState>>,
|
pub(super) scrollbar_state: Rc<Cell<ScrollbarState>>,
|
||||||
/// The size of the scrollable content.
|
/// The size of the scrollable content.
|
||||||
@@ -240,6 +315,9 @@ pub struct InputState {
|
|||||||
impl EventEmitter<InputEvent> for InputState {}
|
impl EventEmitter<InputEvent> for InputState {}
|
||||||
|
|
||||||
impl 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 {
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let focus_handle = cx.focus_handle();
|
let focus_handle = cx.focus_handle();
|
||||||
let blink_cursor = cx.new(|_| BlinkCursor::new());
|
let blink_cursor = cx.new(|_| BlinkCursor::new());
|
||||||
@@ -263,10 +341,16 @@ impl InputState {
|
|||||||
cx.on_blur(&focus_handle, window, Self::on_blur),
|
cx.on_blur(&focus_handle, window, Self::on_blur),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let text_style = window.text_style();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
focus_handle: focus_handle.clone(),
|
focus_handle: focus_handle.clone(),
|
||||||
text: "".into(),
|
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,
|
new_line_on_enter: true,
|
||||||
blink_cursor,
|
blink_cursor,
|
||||||
history,
|
history,
|
||||||
@@ -282,11 +366,7 @@ impl InputState {
|
|||||||
loading: false,
|
loading: false,
|
||||||
pattern: None,
|
pattern: None,
|
||||||
validate: None,
|
validate: None,
|
||||||
rows: 3,
|
mode: InputMode::SingleLine,
|
||||||
min_rows: 3,
|
|
||||||
max_rows: None,
|
|
||||||
auto_grow: false,
|
|
||||||
height: None,
|
|
||||||
last_layout: None,
|
last_layout: None,
|
||||||
last_bounds: None,
|
last_bounds: None,
|
||||||
last_selected_range: 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 {
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,12 +555,20 @@ impl InputState {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn is_multi_line(&self) -> bool {
|
pub(super) fn is_multi_line(&self) -> bool {
|
||||||
self.multi_line
|
matches!(
|
||||||
|
self.mode,
|
||||||
|
InputMode::MultiLine { .. } | InputMode::AutoGrow { .. }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn is_single_line(&self) -> bool {
|
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.
|
/// Set the number of rows for the multi-line Textarea.
|
||||||
@@ -474,26 +577,19 @@ impl InputState {
|
|||||||
///
|
///
|
||||||
/// default: 2
|
/// default: 2
|
||||||
pub fn rows(mut self, rows: usize) -> Self {
|
pub fn rows(mut self, rows: usize) -> Self {
|
||||||
self.rows = rows;
|
match self.mode {
|
||||||
self.min_rows = rows;
|
InputMode::MultiLine { height, .. } => {
|
||||||
self
|
self.mode = InputMode::MultiLine { rows, height };
|
||||||
}
|
}
|
||||||
|
InputMode::AutoGrow { max_rows, .. } => {
|
||||||
/// Set the maximum number of rows for the multi-line Textarea.
|
self.mode = InputMode::AutoGrow {
|
||||||
///
|
rows,
|
||||||
/// If max_rows is more than rows, then will enable auto-grow.
|
min_rows: rows,
|
||||||
///
|
max_rows,
|
||||||
/// 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;
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1120,7 +1216,7 @@ impl InputState {
|
|||||||
pub(super) fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
pub(super) fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if let Some(clipboard) = cx.read_from_clipboard() {
|
if let Some(clipboard) = cx.read_from_clipboard() {
|
||||||
let mut new_text = clipboard.text().unwrap_or_default();
|
let mut new_text = clipboard.text().unwrap_or_default();
|
||||||
if !self.multi_line {
|
if !self.is_multi_line() {
|
||||||
new_text = new_text.replace('\n', "");
|
new_text = new_text.replace('\n', "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1232,11 +1328,10 @@ impl InputState {
|
|||||||
for line in lines.iter() {
|
for line in lines.iter() {
|
||||||
let line_origin = self.line_origin_with_y_offset(&mut y_offset, line, line_height);
|
let line_origin = self.line_origin_with_y_offset(&mut y_offset, line, line_height);
|
||||||
let pos = inner_position - line_origin;
|
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.
|
// Return offset by use closest_index_for_x if is single line mode.
|
||||||
if self.is_single_line() {
|
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);
|
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.
|
// The fallback index is saved in Err from `index_for_position` method.
|
||||||
index += index_result.unwrap_err();
|
index += index_result.unwrap_err();
|
||||||
break;
|
break;
|
||||||
} else if line.len() == 0 {
|
} else if line.text.trim_end_matches('\r').is_empty() {
|
||||||
// empty line
|
// empty line on Windows is `\r`, other is ''
|
||||||
let line_bounds = Bounds {
|
let line_bounds = Bounds {
|
||||||
origin: line_origin,
|
origin: line_origin,
|
||||||
size: gpui::size(bounds.size.width, line_height),
|
size: gpui::size(bounds.size.width, line_height),
|
||||||
};
|
};
|
||||||
let pos = inner_position;
|
let pos = inner_position;
|
||||||
|
index += line.len();
|
||||||
if line_bounds.contains(&pos) {
|
if line_bounds.contains(&pos) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1265,7 +1361,7 @@ impl InputState {
|
|||||||
index += line.len();
|
index += line.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
// add 1 for \n
|
// +1 for revert `lines` split `\n`
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1539,6 +1635,18 @@ impl InputState {
|
|||||||
}
|
}
|
||||||
cx.notify();
|
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 {
|
impl EntityInputHandler for InputState {
|
||||||
@@ -1614,14 +1722,13 @@ impl EntityInputHandler for InputState {
|
|||||||
let new_pos = (range.start + new_text_len).min(mask_text.len());
|
let new_pos = (range.start + new_text_len).min(mask_text.len());
|
||||||
|
|
||||||
self.push_history(&range, new_text, window, cx);
|
self.push_history(&range, new_text, window, cx);
|
||||||
|
|
||||||
self.text = mask_text;
|
self.text = mask_text;
|
||||||
|
self.text_wrapper.update(self.text.clone(), cx);
|
||||||
self.selected_range = new_pos..new_pos;
|
self.selected_range = new_pos..new_pos;
|
||||||
self.marked_range.take();
|
self.marked_range.take();
|
||||||
|
|
||||||
self.update_preferred_x_offset(cx);
|
self.update_preferred_x_offset(cx);
|
||||||
self.update_scroll_offset(None, cx);
|
self.update_scroll_offset(None, cx);
|
||||||
|
self.mode.update_auto_grow(&self.text_wrapper);
|
||||||
cx.emit(InputEvent::Change(self.unmask_value()));
|
cx.emit(InputEvent::Change(self.unmask_value()));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ impl RenderOnce for TextInput {
|
|||||||
const LINE_HEIGHT: Rems = Rems(1.25);
|
const LINE_HEIGHT: Rems = Rems(1.25);
|
||||||
|
|
||||||
self.state.update(cx, |state, _| {
|
self.state.update(cx, |state, _| {
|
||||||
state.height = self.height;
|
state.mode.set_height(self.height);
|
||||||
state.disabled = self.disabled;
|
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::right))
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_left))
|
.on_action(window.listener_for(&self.state, InputState::select_left))
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_right))
|
.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))
|
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::down))
|
||||||
.on_action(window.listener_for(&self.state, InputState::select_up))
|
.on_action(window.listener_for(&self.state, InputState::select_up))
|
||||||
@@ -228,7 +228,7 @@ impl RenderOnce for TextInput {
|
|||||||
.cursor_text()
|
.cursor_text()
|
||||||
.input_py(self.size)
|
.input_py(self.size)
|
||||||
.input_h(self.size)
|
.input_h(self.size)
|
||||||
.when(state.multi_line, |this| {
|
.when(state.is_multi_line(), |this| {
|
||||||
this.h_auto()
|
this.h_auto()
|
||||||
.when_some(self.height, |this, height| this.h(height))
|
.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