input: Fix auto-grow height. (#865)
Fix #864 Updated InputState API to split `single_line`, `multi_line` and `auto_grow`. The `InputState::new` for default single_line, and added `multi_line` and `auto_grow` to change other mode. ### With no wrap https://github.com/user-attachments/assets/22a8f0d6-e264-4f39-bb30-386a41975d2b ### With soft wrap https://github.com/user-attachments/assets/b71f0fb1-5937-470b-a278-8b1de7cfd7d7
This commit is contained in:
parent
769b1887d0
commit
b97abd6394
7 changed files with 290 additions and 82 deletions
|
|
@ -59,9 +59,7 @@ impl FormStory {
|
|||
cx.new(|cx| InputState::new(window, cx).placeholder("Enter text here..."));
|
||||
let bio_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.multi_line()
|
||||
.rows(5)
|
||||
.max_rows(20)
|
||||
.auto_grow(5, 20)
|
||||
.placeholder("Enter text here...")
|
||||
.default_value("Hello 世界,this is GPUI component.")
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use gpui::{
|
||||
actions, App, AppContext as _, ClickEvent, Context, Entity, FocusHandle, Focusable,
|
||||
actions, px, App, AppContext as _, ClickEvent, Context, Entity, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, KeyBinding, ParentElement as _, Render, Styled, Window,
|
||||
};
|
||||
|
||||
|
|
@ -24,6 +24,7 @@ pub fn init(cx: &mut App) {
|
|||
|
||||
pub struct TextareaStory {
|
||||
textarea: Entity<InputState>,
|
||||
textarea_auto_grow: Entity<InputState>,
|
||||
}
|
||||
|
||||
impl super::Story for TextareaStory {
|
||||
|
|
@ -78,7 +79,17 @@ impl TextareaStory {
|
|||
)
|
||||
});
|
||||
|
||||
Self { textarea }
|
||||
let textarea_auto_grow = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.auto_grow(1, 5)
|
||||
.placeholder("Enter text here...")
|
||||
.default_value("Hello 世界,this is GPUI component.")
|
||||
});
|
||||
|
||||
Self {
|
||||
textarea,
|
||||
textarea_auto_grow,
|
||||
}
|
||||
}
|
||||
|
||||
fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
|
||||
|
|
@ -138,7 +149,7 @@ impl Render for TextareaStory {
|
|||
v_flex()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.child(TextInput::new(&self.textarea))
|
||||
.child(TextInput::new(&self.textarea).h(px(320.)))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
|
|
@ -157,5 +168,12 @@ impl Render for TextareaStory {
|
|||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
section("Textarea Auto Grow").child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.child(TextInput::new(&self.textarea_auto_grow)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use crate::{ActiveTheme as _, Root};
|
|||
use super::InputState;
|
||||
|
||||
const RIGHT_MARGIN: Pixels = px(5.);
|
||||
const BOTTOM_MARGIN: Pixels = px(20.);
|
||||
const BOTTOM_MARGIN_ROWS: usize = 1;
|
||||
|
||||
pub(super) struct TextElement {
|
||||
input: Entity<InputState>,
|
||||
|
|
@ -59,6 +59,12 @@ impl TextElement {
|
|||
let mut scroll_offset = input.scroll_handle.offset();
|
||||
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.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;
|
||||
|
|
@ -114,16 +120,17 @@ impl TextElement {
|
|||
} else {
|
||||
scroll_offset.x
|
||||
};
|
||||
scroll_offset.y =
|
||||
if scroll_offset.y + cursor_pos.y > (bounds.size.height - BOTTOM_MARGIN) {
|
||||
// cursor is out of bottom
|
||||
bounds.size.height - BOTTOM_MARGIN - cursor_pos.y
|
||||
} else if scroll_offset.y + cursor_pos.y < px(0.) {
|
||||
// cursor is out of top
|
||||
scroll_offset.y - cursor_pos.y
|
||||
} else {
|
||||
scroll_offset.y
|
||||
};
|
||||
scroll_offset.y = if scroll_offset.y + cursor_pos.y + line_height
|
||||
> bounds.size.height - bottom_margin
|
||||
{
|
||||
// cursor is out of bottom
|
||||
bounds.size.height - bottom_margin - cursor_pos.y
|
||||
} else if scroll_offset.y + cursor_pos.y < px(0.) {
|
||||
// cursor is out of top
|
||||
scroll_offset.y - cursor_pos.y
|
||||
} else {
|
||||
scroll_offset.y
|
||||
};
|
||||
|
||||
if input.selection_reversed {
|
||||
if scroll_offset.x + cursor_start.x < px(0.) {
|
||||
|
|
@ -353,20 +360,22 @@ impl Element for TextElement {
|
|||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let input = self.input.read(cx);
|
||||
let line_height = window.line_height();
|
||||
|
||||
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 = window.line_height().into();
|
||||
style.min_size.height = line_height.into();
|
||||
} else {
|
||||
style.size.height = relative(1.).into();
|
||||
style.min_size.height = (input.rows.max(1) as f32 * window.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
|
||||
style.size.height = window.line_height().into();
|
||||
style.size.height = line_height.into();
|
||||
};
|
||||
|
||||
(window.request_layout(style, [], cx), ())
|
||||
|
|
@ -572,25 +581,21 @@ impl Element for TextElement {
|
|||
.map(|l| l.width())
|
||||
.max()
|
||||
.unwrap_or_default();
|
||||
let height = prepaint
|
||||
.lines
|
||||
.iter()
|
||||
.map(|l| l.size(line_height).height.0)
|
||||
.sum::<f32>();
|
||||
let height = offset_y;
|
||||
let scroll_size = size(width, height);
|
||||
|
||||
let scroll_size = size(width, px(height));
|
||||
|
||||
self.input.update(cx, |input, _cx| {
|
||||
self.input.update(cx, |input, cx| {
|
||||
input.last_layout = Some(prepaint.lines.clone());
|
||||
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);
|
||||
input.scroll_size = scroll_size;
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
self.paint_mouse_listeners(window, cx);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ mod number_input;
|
|||
mod otp_input;
|
||||
mod state;
|
||||
mod text_input;
|
||||
mod text_wrapper;
|
||||
|
||||
pub(crate) use clear_button::*;
|
||||
pub use mask_pattern::MaskPattern;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ use gpui::{
|
|||
|
||||
use super::{
|
||||
blink_cursor::BlinkCursor, change::Change, element::TextElement, mask_pattern::MaskPattern,
|
||||
number_input,
|
||||
number_input, text_wrapper::TextWrapper,
|
||||
};
|
||||
use crate::{history::History, scroll::ScrollbarState, Root};
|
||||
|
||||
|
|
@ -191,11 +191,101 @@ pub fn init(cx: &mut App) {
|
|||
number_input::init(cx);
|
||||
}
|
||||
|
||||
#[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) {
|
||||
match self {
|
||||
Self::AutoGrow { .. } => {
|
||||
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>) {
|
||||
match self {
|
||||
InputMode::MultiLine { height, .. } => {
|
||||
*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) text_wrapper: TextWrapper,
|
||||
pub(super) history: History<Change>,
|
||||
pub(super) blink_cursor: Entity<BlinkCursor>,
|
||||
pub(super) loading: bool,
|
||||
|
|
@ -221,10 +311,6 @@ 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) pattern: Option<regex::Regex>,
|
||||
pub(super) validate: Option<Box<dyn Fn(&str) -> bool + 'static>>,
|
||||
pub(crate) scroll_handle: ScrollHandle,
|
||||
|
|
@ -244,6 +330,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());
|
||||
|
|
@ -267,10 +356,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,
|
||||
),
|
||||
blink_cursor,
|
||||
history,
|
||||
selected_range: 0..0,
|
||||
|
|
@ -285,10 +380,7 @@ impl InputState {
|
|||
loading: false,
|
||||
pattern: None,
|
||||
validate: None,
|
||||
rows: 2,
|
||||
min_rows: 2,
|
||||
max_rows: None,
|
||||
height: None,
|
||||
mode: InputMode::SingleLine,
|
||||
last_layout: None,
|
||||
last_bounds: None,
|
||||
last_selected_range: None,
|
||||
|
|
@ -304,9 +396,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: min_rows,
|
||||
max_rows: max_rows,
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -456,12 +563,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.
|
||||
|
|
@ -470,20 +585,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);
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -992,23 +1106,6 @@ impl InputState {
|
|||
});
|
||||
}
|
||||
|
||||
fn check_to_auto_grow(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.is_multi_line() {
|
||||
return;
|
||||
}
|
||||
let Some(max_rows) = self.max_rows else {
|
||||
return;
|
||||
};
|
||||
|
||||
let changed_rows = ((self.scroll_size.height - self.input_bounds.size.height)
|
||||
/ self.last_line_height) as isize;
|
||||
|
||||
self.rows = (self.rows as isize + changed_rows)
|
||||
.clamp(self.min_rows as isize, max_rows as isize)
|
||||
.max(0) as usize;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(super) fn clean(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.replace_text("", window, cx);
|
||||
}
|
||||
|
|
@ -1111,7 +1208,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', "");
|
||||
}
|
||||
|
||||
|
|
@ -1527,6 +1624,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 {
|
||||
|
|
@ -1568,6 +1677,10 @@ impl EntityInputHandler for InputState {
|
|||
self.marked_range = None;
|
||||
}
|
||||
|
||||
/// Replace text in range.
|
||||
///
|
||||
/// - If the new text is invalid, it will not be replaced.
|
||||
/// - If `range_utf16` is not provided, the current selected range will be used.
|
||||
fn replace_text_in_range(
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
|
|
@ -1598,11 +1711,12 @@ impl EntityInputHandler for InputState {
|
|||
|
||||
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.check_to_auto_grow(window, cx);
|
||||
self.mode.update_auto_grow(&self.text_wrapper);
|
||||
cx.emit(InputEvent::Change(self.unmask_value()));
|
||||
cx.notify();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,7 +136,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;
|
||||
});
|
||||
|
||||
|
|
@ -182,7 +182,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))
|
||||
|
|
@ -222,7 +222,7 @@ impl RenderOnce for TextInput {
|
|||
.input_py(self.size)
|
||||
.input_h(self.size)
|
||||
.cursor_text()
|
||||
.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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue