input: Improve Input performance by only shape visible lines. (#1222)
Close #1193 Ref issue #1220 This PR aim to shape text in visible ranges to reduce `shape_text` time for large document case. - Fix rendering might be delayed and cause blanks when scrolling to follow the selection. - Fix `RopeExt::lines` method to avoid call `line` method on skipped iter.
This commit is contained in:
parent
84b2d2a6f5
commit
03c2327cdb
5 changed files with 266 additions and 144 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,6 +1,7 @@
|
|||
/target
|
||||
.DS_Store
|
||||
/docks.json
|
||||
/profile.json
|
||||
.vscode
|
||||
index.scip
|
||||
*.log
|
||||
*.log
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use gpui::{
|
|||
Path, Pixels, Point, SharedString, Size, Style, TextAlign, TextRun, UnderlineStyle, Window,
|
||||
WrappedLine,
|
||||
};
|
||||
use rope::Rope;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
|
|
@ -60,13 +61,18 @@ impl TextElement {
|
|||
/// - current line index
|
||||
fn layout_cursor(
|
||||
&self,
|
||||
lines: &[WrappedLine],
|
||||
line_height: Pixels,
|
||||
last_layout: &LastLayout,
|
||||
bounds: &mut Bounds<Pixels>,
|
||||
line_number_width: Pixels,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (Option<Bounds<Pixels>>, Point<Pixels>, Option<usize>) {
|
||||
let line_height = last_layout.line_height;
|
||||
let visible_range = &last_layout.visible_range;
|
||||
let visible_top = last_layout.visible_top;
|
||||
let visible_start_offset = last_layout.visible_start_offset;
|
||||
let lines = &last_layout.lines;
|
||||
let line_number_width = last_layout.line_number_width;
|
||||
|
||||
let state = self.state.read(cx);
|
||||
let mut selected_range = state.selected_range;
|
||||
if let Some(marked_range) = &state.marked_range {
|
||||
|
|
@ -89,9 +95,11 @@ impl TextElement {
|
|||
let mut cursor_start = None;
|
||||
let mut cursor_end = None;
|
||||
|
||||
let mut prev_lines_offset = 0;
|
||||
let mut offset_y = px(0.);
|
||||
let mut prev_lines_offset = visible_start_offset;
|
||||
let mut offset_y = visible_top;
|
||||
for (line_ix, line) in lines.iter().enumerate() {
|
||||
let line_ix = visible_range.start + line_ix;
|
||||
|
||||
// break loop if all cursor positions are found
|
||||
if cursor_pos.is_some() && cursor_start.is_some() && cursor_end.is_some() {
|
||||
break;
|
||||
|
|
@ -124,6 +132,14 @@ impl TextElement {
|
|||
prev_lines_offset += line.len() + 1;
|
||||
}
|
||||
|
||||
// If cursor_pos and cursor_end is still None, it means the cursor is at the end of the text.
|
||||
if cursor_pos.is_none() {
|
||||
cursor_pos = Some(point(px(0.), offset_y));
|
||||
}
|
||||
if cursor_end.is_none() {
|
||||
cursor_end = cursor_pos;
|
||||
}
|
||||
|
||||
if let (Some(cursor_pos), Some(cursor_start), Some(cursor_end)) =
|
||||
(cursor_pos, cursor_start, cursor_end)
|
||||
{
|
||||
|
|
@ -195,13 +211,17 @@ impl TextElement {
|
|||
|
||||
fn layout_selections(
|
||||
&self,
|
||||
lines: &[WrappedLine],
|
||||
line_height: Pixels,
|
||||
last_layout: &LastLayout,
|
||||
bounds: &mut Bounds<Pixels>,
|
||||
line_number_width: Pixels,
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Path<Pixels>> {
|
||||
let line_height = last_layout.line_height;
|
||||
let visible_top = last_layout.visible_top;
|
||||
let visible_start_offset = last_layout.visible_start_offset;
|
||||
let lines = &last_layout.lines;
|
||||
let line_number_width = last_layout.line_number_width;
|
||||
|
||||
let state = self.state.read(cx);
|
||||
let mut selected_range = state.selected_range;
|
||||
if let Some(marked_range) = &state.marked_range {
|
||||
|
|
@ -219,10 +239,10 @@ impl TextElement {
|
|||
(selected_range.end, selected_range.start)
|
||||
};
|
||||
|
||||
let mut prev_lines_offset = 0;
|
||||
let mut prev_lines_offset = visible_start_offset;
|
||||
let mut offset_y = visible_top;
|
||||
let mut line_corners = vec![];
|
||||
|
||||
let mut offset_y = px(0.);
|
||||
for line in lines.iter() {
|
||||
let line_size = line.size(line_height);
|
||||
let line_wrap_width = line_size.width;
|
||||
|
|
@ -330,48 +350,54 @@ impl TextElement {
|
|||
|
||||
/// Calculate the visible range of lines in the viewport.
|
||||
///
|
||||
/// The visible range is based on unwrapped lines (Zero based).
|
||||
/// Returns
|
||||
///
|
||||
/// - visible_range: The visible range is based on unwrapped lines (Zero based).
|
||||
/// - visible_top: The top position of the first visible line in the scroll viewport.
|
||||
fn calculate_visible_range(
|
||||
&self,
|
||||
state: &InputState,
|
||||
line_height: Pixels,
|
||||
input_height: Pixels,
|
||||
) -> Range<usize> {
|
||||
) -> (Range<usize>, Pixels) {
|
||||
// Add extra rows to avoid showing empty space when scroll to bottom.
|
||||
let extra_rows = 1;
|
||||
let mut visible_top = px(0.);
|
||||
if state.mode.is_single_line() {
|
||||
return 0..1;
|
||||
return (0..1, visible_top);
|
||||
}
|
||||
|
||||
let Some(last_layout) = state.last_layout.as_ref() else {
|
||||
return 0..1;
|
||||
};
|
||||
|
||||
let total_lines = state.text_wrapper.len();
|
||||
let scroll_top = state.scroll_handle.offset().y;
|
||||
let total_lines = last_layout.lines.len();
|
||||
|
||||
let mut visible_range = 0..total_lines;
|
||||
let mut line_bottom = px(0.);
|
||||
for (ix, line) in last_layout.lines.iter().enumerate() {
|
||||
line_bottom += (line.wrap_boundaries.len() + 1) * line_height;
|
||||
for (ix, line) in state.text_wrapper.lines.iter().enumerate() {
|
||||
let wrapped_height = (line.wrap_lines + 1) * line_height;
|
||||
line_bottom += wrapped_height;
|
||||
|
||||
if line_bottom < -scroll_top {
|
||||
visible_top = line_bottom - wrapped_height;
|
||||
visible_range.start = ix;
|
||||
}
|
||||
|
||||
if line_bottom + scroll_top >= input_height {
|
||||
visible_range.end = (ix + 1).min(total_lines);
|
||||
visible_range.end = (ix + extra_rows).min(total_lines);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
visible_range
|
||||
(visible_range, visible_top)
|
||||
}
|
||||
|
||||
/// First usize is the offset of skipped.
|
||||
fn highlight_lines(
|
||||
&mut self,
|
||||
visible_range: &Range<usize>,
|
||||
_visible_top: Pixels,
|
||||
visible_start_offset: usize,
|
||||
cx: &mut App,
|
||||
) -> Option<(usize, Vec<(Range<usize>, HighlightStyle)>)> {
|
||||
) -> Option<Vec<(Range<usize>, HighlightStyle)>> {
|
||||
let theme = cx.theme().highlight_theme.clone();
|
||||
self.state.update(cx, |state, cx| match &state.mode {
|
||||
InputMode::CodeEditor {
|
||||
|
|
@ -389,22 +415,17 @@ impl TextElement {
|
|||
return None;
|
||||
};
|
||||
|
||||
let mut offset = 0;
|
||||
let mut skipped_offset = 0;
|
||||
let mut offset = visible_start_offset;
|
||||
let mut styles = vec![];
|
||||
|
||||
for (ix, line) in state.text.lines().enumerate() {
|
||||
for line in state
|
||||
.text
|
||||
.lines()
|
||||
.skip(visible_range.start)
|
||||
.take(visible_range.len())
|
||||
{
|
||||
// +1 for `\n`
|
||||
let line_len = line.len() + 1;
|
||||
if ix < visible_range.start {
|
||||
offset += line_len;
|
||||
skipped_offset = offset;
|
||||
continue;
|
||||
}
|
||||
if ix > visible_range.end {
|
||||
break;
|
||||
}
|
||||
|
||||
let range = offset..offset + line_len;
|
||||
let line_styles = highlighter.styles(&range, &theme);
|
||||
styles = gpui::combine_highlights(styles, line_styles).collect();
|
||||
|
|
@ -412,26 +433,24 @@ impl TextElement {
|
|||
offset = range.end;
|
||||
}
|
||||
|
||||
let mut marker_styles = vec![];
|
||||
for marker in markers.iter() {
|
||||
if let Some(range) = &marker.range {
|
||||
if range.start < skipped_offset {
|
||||
continue;
|
||||
}
|
||||
// Combine marker styles
|
||||
if !markers.is_empty() {
|
||||
let mut marker_styles = vec![];
|
||||
for marker in markers.iter() {
|
||||
if let Some(range) = &marker.range {
|
||||
if range.start < visible_start_offset {
|
||||
continue;
|
||||
}
|
||||
|
||||
let node_range = range.start..range.end;
|
||||
if node_range.start >= visible_range.start
|
||||
|| node_range.end <= visible_range.end
|
||||
{
|
||||
marker_styles
|
||||
.push((node_range, marker.severity.highlight_style(&theme, cx)));
|
||||
.push((range.clone(), marker.severity.highlight_style(&theme, cx)));
|
||||
}
|
||||
}
|
||||
|
||||
styles = gpui::combine_highlights(marker_styles, styles).collect();
|
||||
}
|
||||
|
||||
styles = gpui::combine_highlights(marker_styles, styles).collect();
|
||||
|
||||
Some((skipped_offset, styles))
|
||||
Some(styles)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
|
|
@ -543,8 +562,12 @@ impl Element for TextElement {
|
|||
let state = self.state.read(cx);
|
||||
let line_height = window.line_height();
|
||||
|
||||
let visible_range = self.calculate_visible_range(&state, line_height, bounds.size.height);
|
||||
let highlight_styles = self.highlight_lines(&visible_range, cx);
|
||||
let (visible_range, visible_top) =
|
||||
self.calculate_visible_range(&state, line_height, bounds.size.height);
|
||||
let visible_start_offset = state.text.line_start_offset(visible_range.start);
|
||||
|
||||
let highlight_styles =
|
||||
self.highlight_lines(&visible_range, visible_top, visible_start_offset, cx);
|
||||
|
||||
let state = self.state.read(cx);
|
||||
let multi_line = state.mode.is_multi_line();
|
||||
|
|
@ -556,11 +579,17 @@ impl Element for TextElement {
|
|||
let mut bounds = bounds;
|
||||
|
||||
let (display_text, text_color) = if is_empty {
|
||||
(placeholder, cx.theme().muted_foreground)
|
||||
(
|
||||
Rope::from(placeholder.as_str()),
|
||||
cx.theme().muted_foreground,
|
||||
)
|
||||
} else if state.masked {
|
||||
("*".repeat(text.chars_count()).into(), cx.theme().foreground)
|
||||
(
|
||||
Rope::from("*".repeat(text.chars_count())),
|
||||
cx.theme().foreground,
|
||||
)
|
||||
} else {
|
||||
(text.to_string().into(), cx.theme().foreground)
|
||||
(text.clone(), cx.theme().foreground)
|
||||
};
|
||||
|
||||
let text_style = window.text_style();
|
||||
|
|
@ -607,14 +636,8 @@ impl Element for TextElement {
|
|||
};
|
||||
|
||||
let runs = if !is_empty {
|
||||
if let Some((skipped_offset, highlight_styles)) = highlight_styles {
|
||||
if let Some(highlight_styles) = highlight_styles {
|
||||
let mut runs = vec![];
|
||||
if skipped_offset > 0 {
|
||||
runs.push(TextRun {
|
||||
len: skipped_offset,
|
||||
..run.clone()
|
||||
});
|
||||
}
|
||||
|
||||
runs.extend(highlight_styles.iter().map(|(range, style)| {
|
||||
let mut run = text_style.clone().highlight(*style).to_run(range.len());
|
||||
|
|
@ -663,21 +686,23 @@ impl Element for TextElement {
|
|||
None
|
||||
};
|
||||
|
||||
// NOTE: If there have about 10K lines, this will take about 5~6ms.
|
||||
// let measure = Measure::new("shape_text");
|
||||
// NOTE: Here 50 lines about 150µs
|
||||
// let measure = crate::Measure::new("shape_text");
|
||||
let visible_text = display_text
|
||||
.slice_rows(visible_range.start as u32..visible_range.end as u32)
|
||||
.to_string();
|
||||
|
||||
let lines = window
|
||||
.text_system()
|
||||
.shape_text(display_text, font_size, &runs, wrap_width, None)
|
||||
.shape_text(visible_text.into(), font_size, &runs, wrap_width, None)
|
||||
.expect("failed to shape text");
|
||||
// measure.end();
|
||||
|
||||
let mut max_line_width = px(0.);
|
||||
let mut total_wrapped_lines = 0;
|
||||
let total_wrapped_lines = state.text_wrapper.len();
|
||||
for line in lines.iter() {
|
||||
// FIXME: The `shape_text` measured width is not stable, sometime will large, sometime small.
|
||||
max_line_width = max_line_width.max(line.width());
|
||||
// +1 is the first line, `wrap_boundaries` is the wrapped lines after the `\n`.
|
||||
total_wrapped_lines += 1 + line.wrap_boundaries.len();
|
||||
}
|
||||
|
||||
let scroll_size = size(
|
||||
|
|
@ -689,6 +714,16 @@ impl Element for TextElement {
|
|||
(total_wrapped_lines as f32 * line_height).max(bounds.size.height),
|
||||
);
|
||||
|
||||
let last_layout = LastLayout {
|
||||
visible_range,
|
||||
visible_top,
|
||||
visible_start_offset,
|
||||
line_height,
|
||||
wrap_width,
|
||||
line_number_width,
|
||||
lines: Rc::new(lines),
|
||||
};
|
||||
|
||||
// `position_for_index` for example
|
||||
//
|
||||
// #### text
|
||||
|
|
@ -720,23 +755,10 @@ impl Element for TextElement {
|
|||
|
||||
// Calculate the scroll offset to keep the cursor in view
|
||||
|
||||
let (cursor_bounds, cursor_scroll_offset, current_line_index) = self.layout_cursor(
|
||||
&lines,
|
||||
line_height,
|
||||
&mut bounds,
|
||||
line_number_width,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let (cursor_bounds, cursor_scroll_offset, current_line_index) =
|
||||
self.layout_cursor(&last_layout, &mut bounds, window, cx);
|
||||
|
||||
let selection_path = self.layout_selections(
|
||||
&lines,
|
||||
line_height,
|
||||
&mut bounds,
|
||||
line_number_width,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let selection_path = self.layout_selections(&last_layout, &mut bounds, window, cx);
|
||||
|
||||
let state = self.state.read(cx);
|
||||
let line_numbers = if state.mode.line_number() {
|
||||
|
|
@ -760,13 +782,8 @@ impl Element for TextElement {
|
|||
}];
|
||||
|
||||
// build line numbers
|
||||
for (ix, line) in lines
|
||||
.iter()
|
||||
.skip(visible_range.start)
|
||||
.take(visible_range.len())
|
||||
.enumerate()
|
||||
{
|
||||
let ix = ix + visible_range.start;
|
||||
for (ix, line) in last_layout.lines.iter().enumerate() {
|
||||
let ix = last_layout.visible_range.start + ix;
|
||||
let line_no = ix + 1;
|
||||
|
||||
let mut line_no_text = format!("{:>4}", line_no);
|
||||
|
|
@ -793,13 +810,7 @@ impl Element for TextElement {
|
|||
|
||||
PrepaintState {
|
||||
bounds,
|
||||
last_layout: LastLayout {
|
||||
lines: Rc::new(lines),
|
||||
line_height,
|
||||
visible_range,
|
||||
line_number_width,
|
||||
wrap_width,
|
||||
},
|
||||
last_layout,
|
||||
scroll_size,
|
||||
line_numbers,
|
||||
cursor_bounds,
|
||||
|
|
@ -859,10 +870,7 @@ impl Element for TextElement {
|
|||
let line_height = window.line_height();
|
||||
let origin = bounds.origin;
|
||||
|
||||
let mut invisible_top_padding = px(0.);
|
||||
for line in prepaint.last_layout.lines.iter().take(visible_range.start) {
|
||||
invisible_top_padding += line.size(line_height).height;
|
||||
}
|
||||
let invisible_top_padding = prepaint.last_layout.visible_top;
|
||||
|
||||
let mut mask_offset_y = px(0.);
|
||||
if self.state.read(cx).masked {
|
||||
|
|
@ -910,12 +918,7 @@ impl Element for TextElement {
|
|||
|
||||
// Paint text
|
||||
let mut offset_y = mask_offset_y + invisible_top_padding;
|
||||
for line in prepaint
|
||||
.last_layout
|
||||
.iter()
|
||||
.skip(visible_range.start)
|
||||
.take(visible_range.len())
|
||||
{
|
||||
for line in prepaint.last_layout.lines.iter() {
|
||||
let p = point(
|
||||
origin.x + prepaint.last_layout.line_number_width,
|
||||
origin.y + offset_y,
|
||||
|
|
|
|||
|
|
@ -2,18 +2,26 @@ use rope::{Point, Rope};
|
|||
|
||||
/// An extension trait for `Rope` to provide additional utility methods.
|
||||
pub trait RopeExt {
|
||||
/// Get the line at the given row index, including the `\r` at the end, but not `\n`.
|
||||
/// Get the line at the given row (0-based) index, including the `\r` at the end, but not `\n`.
|
||||
///
|
||||
/// Return empty rope if the row is out of bounds.
|
||||
/// Return empty rope if the row (0-based) is out of bounds.
|
||||
fn line(&self, row: usize) -> Rope;
|
||||
|
||||
/// Start offset of the line at the given row (0-based) index.
|
||||
fn line_start_offset(&self, row: usize) -> usize;
|
||||
|
||||
/// Line the end offset (including `\n`) of the line at the given row (0-based) index.
|
||||
///
|
||||
/// Return the end of the rope if the row is out of bounds.
|
||||
fn line_end_offset(&self, row: usize) -> usize;
|
||||
|
||||
/// Return the number of lines in the rope.
|
||||
fn lines_len(&self) -> usize;
|
||||
|
||||
/// Return the lines iterator.
|
||||
///
|
||||
/// Each line is including the `\n` at the end, but not `\n`.
|
||||
fn lines(&self) -> impl Iterator<Item = Rope>;
|
||||
fn lines(&self) -> RopeLines;
|
||||
|
||||
/// Check is equal to another rope.
|
||||
fn eq(&self, other: &Rope) -> bool;
|
||||
|
|
@ -29,6 +37,55 @@ pub trait RopeExt {
|
|||
fn char_at(&self, offset: usize) -> Option<char>;
|
||||
}
|
||||
|
||||
/// An iterator over the lines of a `Rope`.
|
||||
pub struct RopeLines {
|
||||
row: usize,
|
||||
end_row: usize,
|
||||
rope: Rope,
|
||||
}
|
||||
|
||||
impl RopeLines {
|
||||
/// Create a new `RopeLines` iterator.
|
||||
pub fn new(rope: Rope) -> Self {
|
||||
let end_row = rope.lines_len();
|
||||
Self {
|
||||
row: 0,
|
||||
end_row,
|
||||
rope,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for RopeLines {
|
||||
type Item = Rope;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.row >= self.end_row {
|
||||
return None;
|
||||
}
|
||||
|
||||
let line = self.rope.line(self.row);
|
||||
self.row += 1;
|
||||
Some(line)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
||||
self.row = self.row.saturating_add(n);
|
||||
self.next()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let len = self.end_row - self.row;
|
||||
(len, Some(len))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::ExactSizeIterator for RopeLines {}
|
||||
impl std::iter::FusedIterator for RopeLines {}
|
||||
|
||||
impl RopeExt for Rope {
|
||||
fn line(&self, row: usize) -> Rope {
|
||||
let row = row as u32;
|
||||
|
|
@ -37,12 +94,25 @@ impl RopeExt for Rope {
|
|||
self.slice(start..end)
|
||||
}
|
||||
|
||||
fn line_start_offset(&self, row: usize) -> usize {
|
||||
let row = row as u32;
|
||||
self.point_to_offset(Point::new(row, 0))
|
||||
}
|
||||
|
||||
fn line_end_offset(&self, row: usize) -> usize {
|
||||
if row > self.max_point().row as usize {
|
||||
return self.len();
|
||||
}
|
||||
|
||||
self.line_start_offset(row) + self.line_len(row as u32) as usize
|
||||
}
|
||||
|
||||
fn lines_len(&self) -> usize {
|
||||
self.max_point().row as usize + 1
|
||||
}
|
||||
|
||||
fn lines(&self) -> impl Iterator<Item = Rope> {
|
||||
(0..self.lines_len()).map(move |row| self.line(row))
|
||||
fn lines(&self) -> RopeLines {
|
||||
RopeLines::new(self.clone())
|
||||
}
|
||||
|
||||
fn eq(&self, other: &Rope) -> bool {
|
||||
|
|
@ -108,6 +178,25 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_line_start_end_offset() {
|
||||
let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文\nRope");
|
||||
assert_eq!(rope.line_start_offset(0), 0);
|
||||
assert_eq!(rope.line_end_offset(0), 5);
|
||||
|
||||
assert_eq!(rope.line_start_offset(1), 6);
|
||||
assert_eq!(rope.line_end_offset(1), 12);
|
||||
|
||||
assert_eq!(rope.line_start_offset(2), 13);
|
||||
assert_eq!(rope.line_end_offset(2), 34);
|
||||
|
||||
assert_eq!(rope.line_start_offset(3), 35);
|
||||
assert_eq!(rope.line_end_offset(3), 39);
|
||||
|
||||
assert_eq!(rope.line_start_offset(4), 39);
|
||||
assert_eq!(rope.line_end_offset(4), 39);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chars_count() {
|
||||
let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文🎉\nRope");
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use rope::Rope;
|
|||
use serde::Deserialize;
|
||||
use smallvec::SmallVec;
|
||||
use std::cell::RefCell;
|
||||
use std::ops::{Deref, Range};
|
||||
use std::ops::Range;
|
||||
use std::rc::Rc;
|
||||
use sum_tree::Bias;
|
||||
use unicode_segmentation::*;
|
||||
|
|
@ -31,6 +31,7 @@ use super::{
|
|||
};
|
||||
use crate::input::hover_popover::DiagnosticPopover;
|
||||
use crate::input::marker::Marker;
|
||||
use crate::input::text_wrapper::LineWrap;
|
||||
use crate::input::{Cursor, LineColumn, RopeExt as _, Selection};
|
||||
use crate::{history::History, scroll::ScrollbarState, Root};
|
||||
|
||||
|
|
@ -215,26 +216,22 @@ pub fn init(cx: &mut App) {
|
|||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct LastLayout {
|
||||
/// The last layout lines.
|
||||
/// The visible range (no wrap) of lines in the viewport, the value is row (0-based) index.
|
||||
pub(super) visible_range: Range<usize>,
|
||||
/// The first visible line top position in scroll viewport.
|
||||
pub(super) visible_top: Pixels,
|
||||
/// The start byte offset of the first visible line.
|
||||
pub(super) visible_start_offset: usize,
|
||||
/// The last layout lines (Only have visible lines).
|
||||
pub(super) lines: Rc<SmallVec<[WrappedLine; 1]>>,
|
||||
/// The line_height of text layout, this will change will InputElement painted.
|
||||
pub(super) line_height: Pixels,
|
||||
/// The visible range (no wrap) of lines in the viewport.
|
||||
pub(super) visible_range: Range<usize>,
|
||||
/// The wrap width of text layout, this will change will InputElement painted.
|
||||
pub(super) wrap_width: Option<Pixels>,
|
||||
/// The line number area width of text layout, if not line number, this will be 0px.
|
||||
pub(super) line_number_width: Pixels,
|
||||
}
|
||||
|
||||
impl Deref for LastLayout {
|
||||
type Target = Rc<SmallVec<[WrappedLine; 1]>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.lines
|
||||
}
|
||||
}
|
||||
|
||||
/// InputState to keep editing state of the [`super::TextInput`].
|
||||
pub struct InputState {
|
||||
pub(super) focus_handle: FocusHandle,
|
||||
|
|
@ -536,8 +533,8 @@ impl InputState {
|
|||
};
|
||||
let line_height = last_layout.line_height;
|
||||
|
||||
let mut prev_lines_offset = 0;
|
||||
let mut y_offset = px(0.);
|
||||
let mut prev_lines_offset = last_layout.visible_start_offset;
|
||||
let mut y_offset = last_layout.visible_top;
|
||||
for (line_index, line) in last_layout.lines.iter().enumerate() {
|
||||
let local_offset = offset.saturating_sub(prev_lines_offset);
|
||||
if let Some(pos) = line.position_for_index(local_offset, line_height) {
|
||||
|
|
@ -682,6 +679,17 @@ impl InputState {
|
|||
/// Update the soft wrap mode for multi-line input, default is true.
|
||||
pub fn set_soft_wrap(&mut self, wrap: bool, _: &mut Window, cx: &mut Context<Self>) {
|
||||
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.text_wrapper.set_wrap_width(Some(wrap_width), cx);
|
||||
} else {
|
||||
self.text_wrapper.set_wrap_width(None, cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
|
@ -1715,40 +1723,55 @@ impl InputState {
|
|||
// - included the scroll offset.
|
||||
let inner_position = position - bounds.origin - point(line_number_width, px(0.));
|
||||
|
||||
let mut index = 0;
|
||||
let mut y_offset = px(0.);
|
||||
|
||||
for (_, line) in last_layout.lines.iter().enumerate() {
|
||||
let line_origin = self.line_origin_with_y_offset(&mut y_offset, &line, line_height);
|
||||
let mut index = last_layout.visible_start_offset;
|
||||
let mut y_offset = last_layout.visible_top;
|
||||
for (ix, line) in self
|
||||
.text_wrapper
|
||||
.lines
|
||||
.iter()
|
||||
.skip(last_layout.visible_range.start)
|
||||
.enumerate()
|
||||
{
|
||||
let line_origin = self.line_origin_with_y_offset(&mut y_offset, line, line_height);
|
||||
let pos = inner_position - line_origin;
|
||||
|
||||
let Some(rendered_line) = last_layout.lines.get(ix) else {
|
||||
if pos.y < line_origin.y + line_height {
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
};
|
||||
|
||||
// Return offset by use closest_index_for_x if is single line mode.
|
||||
if self.mode.is_single_line() {
|
||||
return line.unwrapped_layout.closest_index_for_x(pos.x);
|
||||
return rendered_line.unwrapped_layout.closest_index_for_x(pos.x);
|
||||
}
|
||||
|
||||
let index_result = line.closest_index_for_position(pos, line_height);
|
||||
let index_result = rendered_line.closest_index_for_position(pos, line_height);
|
||||
if let Ok(v) = index_result {
|
||||
index += v;
|
||||
break;
|
||||
} else if let Ok(_) = line.index_for_position(point(px(0.), pos.y), line_height) {
|
||||
} else if let Ok(_) =
|
||||
rendered_line.index_for_position(point(px(0.), pos.y), line_height)
|
||||
{
|
||||
// Click in the this line but not in the text, move cursor to the end of the line.
|
||||
// The fallback index is saved in Err from `index_for_position` method.
|
||||
index += index_result.unwrap_err();
|
||||
break;
|
||||
} else if line.text.trim_end_matches(|c| c == '\r').len() == 0 {
|
||||
} else if rendered_line.text.trim_end_matches(|c| c == '\r').len() == 0 {
|
||||
// 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();
|
||||
index += rendered_line.len();
|
||||
if line_bounds.contains(&pos) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
index += line.len();
|
||||
index += rendered_line.len();
|
||||
}
|
||||
|
||||
// +1 for revert `lines` split `\n`
|
||||
|
|
@ -1766,7 +1789,7 @@ impl InputState {
|
|||
fn line_origin_with_y_offset(
|
||||
&self,
|
||||
y_offset: &mut Pixels,
|
||||
line: &WrappedLine,
|
||||
line: &LineWrap,
|
||||
line_height: Pixels,
|
||||
) -> Point<Pixels> {
|
||||
// NOTE: About line.wrap_boundaries.len()
|
||||
|
|
@ -1775,7 +1798,7 @@ impl InputState {
|
|||
// If have 2 line, the value is 1
|
||||
if self.mode.is_multi_line() {
|
||||
let p = point(px(0.), *y_offset);
|
||||
let height = line_height + line.wrap_boundaries.len() as f32 * line_height;
|
||||
let height = line_height + line.wrap_lines as f32 * line_height;
|
||||
*y_offset = *y_offset + height;
|
||||
p
|
||||
} else {
|
||||
|
|
@ -2233,8 +2256,8 @@ impl EntityInputHandler for InputState {
|
|||
let mut start_origin = None;
|
||||
let mut end_origin = None;
|
||||
let line_number_origin = point(line_number_width, px(0.));
|
||||
let mut y_offset = px(0.);
|
||||
let mut index_offset = 0;
|
||||
let mut y_offset = last_layout.visible_top;
|
||||
let mut index_offset = last_layout.visible_start_offset;
|
||||
|
||||
for line in last_layout.lines.iter() {
|
||||
if start_origin.is_some() && end_origin.is_some() {
|
||||
|
|
@ -2282,10 +2305,11 @@ impl EntityInputHandler for InputState {
|
|||
let last_layout = self.last_layout.as_ref()?;
|
||||
let line_height = last_layout.line_height;
|
||||
let line_point = self.last_bounds?.localize(&point)?;
|
||||
let offset = last_layout.visible_start_offset;
|
||||
|
||||
for line in last_layout.lines.iter() {
|
||||
if let Ok(utf8_index) = line.index_for_position(line_point, line_height) {
|
||||
return Some(self.offset_to_utf16(utf8_index));
|
||||
return Some(self.offset_to_utf16(offset + utf8_index));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ pub(super) struct LineWrap {
|
|||
/// After use lines to calculate the scroll size of the TextArea
|
||||
pub(super) struct TextWrapper {
|
||||
pub(super) text: Rope,
|
||||
/// The wrapped lines, value is start and end index of the line.
|
||||
/// The wrapped lines (Inlucde the first line), value is start and end index of the line.
|
||||
pub(super) wrapped_lines: Vec<Range<usize>>,
|
||||
/// The lines by split \n
|
||||
pub(super) lines: Vec<LineWrap>,
|
||||
|
|
@ -45,6 +45,11 @@ impl TextWrapper {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get the total number of lines including wrapped lines.
|
||||
pub(super) fn len(&self) -> usize {
|
||||
self.wrapped_lines.len()
|
||||
}
|
||||
|
||||
pub(super) fn set_wrap_width(&mut self, wrap_width: Option<Pixels>, cx: &mut App) {
|
||||
if wrap_width == self.wrap_width {
|
||||
return;
|
||||
|
|
|
|||
Loading…
Reference in a new issue