input: Use shape line for soft wrap lines. (#1293)
This change aim to fix the before version `shape_text` (in Element paint) and `wrap_line` (in TextWrapper cached soft wrap map) may have difference wrap width. Now move to based on TextWrapper's wrap map to paint text by use `shape_line` method.
This commit is contained in:
parent
4983300ddf
commit
172015f147
3 changed files with 432 additions and 65 deletions
|
|
@ -3,14 +3,18 @@ use std::{ops::Range, rc::Rc};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler,
|
fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler,
|
||||||
Entity, GlobalElementId, Half, HighlightStyle, Hitbox, IntoElement, LayoutId, MouseButton,
|
Entity, GlobalElementId, Half, HighlightStyle, Hitbox, IntoElement, LayoutId, MouseButton,
|
||||||
MouseMoveEvent, Path, Pixels, Point, ShapedLine, SharedString, Size, Style, TextAlign, TextRun,
|
MouseMoveEvent, Path, Pixels, Point, ShapedLine, SharedString, Size, Style, TextRun, TextStyle,
|
||||||
TextStyle, UnderlineStyle, Window,
|
UnderlineStyle, Window,
|
||||||
};
|
};
|
||||||
use ropey::Rope;
|
use ropey::Rope;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
input::{blink_cursor::CURSOR_WIDTH, RopeExt as _},
|
input::{
|
||||||
|
blink_cursor::CURSOR_WIDTH,
|
||||||
|
text_wrapper::{LineLayout, TextWrapper},
|
||||||
|
RopeExt as _,
|
||||||
|
},
|
||||||
ActiveTheme as _, Colorize, Root,
|
ActiveTheme as _, Colorize, Root,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -522,6 +526,53 @@ impl TextElement {
|
||||||
(line_number_width, line_number_len)
|
(line_number_width, line_number_len)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn layout_lines(
|
||||||
|
text: &Rope,
|
||||||
|
text_wrapper: &TextWrapper,
|
||||||
|
visible_range: &Range<usize>,
|
||||||
|
font_size: Pixels,
|
||||||
|
runs: &[TextRun],
|
||||||
|
window: &mut Window,
|
||||||
|
) -> Vec<LineLayout> {
|
||||||
|
let visible_text = text
|
||||||
|
.slice_lines(visible_range.start..visible_range.end)
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let mut lines = vec![];
|
||||||
|
let mut offset = 0;
|
||||||
|
for (ix, line) in visible_text.split("\n").enumerate() {
|
||||||
|
let line_item = text_wrapper
|
||||||
|
.lines
|
||||||
|
.get(visible_range.start + ix)
|
||||||
|
.expect("line should exists in text_wrapper");
|
||||||
|
// if line_item.len() != line.len() {
|
||||||
|
// dbg!(&line, &line_item.wrapped_lines);
|
||||||
|
// }
|
||||||
|
debug_assert_eq!(line_item.len(), line.len());
|
||||||
|
|
||||||
|
let mut line_layout = LineLayout::new();
|
||||||
|
let mut wrapped_lines = SmallVec::with_capacity(1);
|
||||||
|
|
||||||
|
for range in &line_item.wrapped_lines {
|
||||||
|
let line_runs = runs_for_range(runs, offset, &range);
|
||||||
|
let sub_line: SharedString = line[range.clone()].to_string().into();
|
||||||
|
let shaped_line = window
|
||||||
|
.text_system()
|
||||||
|
.shape_line(sub_line, font_size, &line_runs, None);
|
||||||
|
|
||||||
|
wrapped_lines.push(shaped_line);
|
||||||
|
}
|
||||||
|
|
||||||
|
line_layout.set_wrapped_lines(wrapped_lines);
|
||||||
|
lines.push(line_layout);
|
||||||
|
|
||||||
|
// +1 for the `\n`
|
||||||
|
offset += line.len() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
lines
|
||||||
|
}
|
||||||
|
|
||||||
/// First usize is the offset of skipped.
|
/// First usize is the offset of skipped.
|
||||||
fn highlight_lines(
|
fn highlight_lines(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -801,17 +852,14 @@ impl Element for TextElement {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// NOTE: Here 50 lines about 150µs
|
let lines = Self::layout_lines(
|
||||||
// let measure = crate::Measure::new("shape_text");
|
&text,
|
||||||
let visible_text = display_text
|
&state.text_wrapper,
|
||||||
.slice_lines(visible_range.start..visible_range.end)
|
&visible_range,
|
||||||
.to_string();
|
font_size,
|
||||||
|
&runs,
|
||||||
let lines = window
|
window,
|
||||||
.text_system()
|
);
|
||||||
.shape_text(visible_text.into(), font_size, &runs, wrap_width, None)
|
|
||||||
.expect("failed to shape text");
|
|
||||||
// measure.end();
|
|
||||||
|
|
||||||
let mut longest_line_width = wrap_width.unwrap_or(px(0.));
|
let mut longest_line_width = wrap_width.unwrap_or(px(0.));
|
||||||
if state.mode.is_multi_line() && !state.soft_wrap && lines.len() > 1 {
|
if state.mode.is_multi_line() && !state.soft_wrap && lines.len() > 1 {
|
||||||
|
|
@ -938,7 +986,7 @@ impl Element for TextElement {
|
||||||
.text_system()
|
.text_system()
|
||||||
.shape_line(line_no, font_size, &runs, None),
|
.shape_line(line_no, font_size, &runs, None),
|
||||||
);
|
);
|
||||||
for _ in 0..line.wrap_boundaries.len() {
|
for _ in 0..line.wrapped_lines.len().saturating_sub(1) {
|
||||||
sub_lines.push(ShapedLine::default());
|
sub_lines.push(ShapedLine::default());
|
||||||
}
|
}
|
||||||
line_numbers.push(sub_lines);
|
line_numbers.push(sub_lines);
|
||||||
|
|
@ -1082,7 +1130,7 @@ impl Element for TextElement {
|
||||||
origin.x + prepaint.last_layout.line_number_width,
|
origin.x + prepaint.last_layout.line_number_width,
|
||||||
origin.y + offset_y,
|
origin.y + offset_y,
|
||||||
);
|
);
|
||||||
_ = line.paint(p, line_height, TextAlign::Left, None, window, cx);
|
_ = line.paint(p, line_height, window, cx);
|
||||||
offset_y += line.size(line_height).height;
|
offset_y += line.size(line_height).height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1162,3 +1210,105 @@ impl Element for TextElement {
|
||||||
self.paint_mouse_listeners(window, cx);
|
self.paint_mouse_listeners(window, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the runs for the given range.
|
||||||
|
///
|
||||||
|
/// The range is the byte range of the wrapped line.
|
||||||
|
pub(super) fn runs_for_range(
|
||||||
|
runs: &[TextRun],
|
||||||
|
line_offset: usize,
|
||||||
|
range: &Range<usize>,
|
||||||
|
) -> Vec<TextRun> {
|
||||||
|
let mut result = vec![];
|
||||||
|
let range = (line_offset + range.start)..(line_offset + range.end);
|
||||||
|
let mut cursor = 0;
|
||||||
|
|
||||||
|
for run in runs {
|
||||||
|
let run_start = cursor;
|
||||||
|
let run_end = cursor + run.len;
|
||||||
|
|
||||||
|
if run_end <= range.start {
|
||||||
|
cursor = run_end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if run_start >= range.end {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = range.start.max(run_start) - run_start;
|
||||||
|
let end = range.end.min(run_end) - run_start;
|
||||||
|
let len = end - start;
|
||||||
|
|
||||||
|
if len > 0 {
|
||||||
|
result.push(TextRun { len, ..run.clone() });
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = run_end;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_runs_for_range() {
|
||||||
|
let run = TextRun {
|
||||||
|
len: 0,
|
||||||
|
font: gpui::font(".SystemUIFont"),
|
||||||
|
color: gpui::black(),
|
||||||
|
background_color: None,
|
||||||
|
underline: None,
|
||||||
|
strikethrough: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// use hello this-is-test
|
||||||
|
let runs = vec![
|
||||||
|
// use
|
||||||
|
TextRun {
|
||||||
|
len: 3,
|
||||||
|
..run.clone()
|
||||||
|
},
|
||||||
|
// \s
|
||||||
|
TextRun {
|
||||||
|
len: 1,
|
||||||
|
..run.clone()
|
||||||
|
},
|
||||||
|
// hello
|
||||||
|
TextRun {
|
||||||
|
len: 5,
|
||||||
|
..run.clone()
|
||||||
|
},
|
||||||
|
// \s
|
||||||
|
TextRun {
|
||||||
|
len: 1,
|
||||||
|
..run.clone()
|
||||||
|
},
|
||||||
|
// this-is-test
|
||||||
|
TextRun {
|
||||||
|
len: 12,
|
||||||
|
..run.clone()
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn assert_runs(actual: Vec<TextRun>, expected: &[usize]) {
|
||||||
|
let left = actual.iter().map(|run| run.len).collect::<Vec<_>>();
|
||||||
|
assert_eq!(left, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_runs(runs_for_range(&runs, 0, &(0..0)), &[]);
|
||||||
|
assert_runs(runs_for_range(&runs, 0, &(0..100)), &[3, 1, 5, 1, 12]);
|
||||||
|
|
||||||
|
assert_runs(runs_for_range(&runs, 0, &(0..6)), &[3, 1, 2]);
|
||||||
|
assert_runs(runs_for_range(&runs, 0, &(1..6)), &[2, 1, 2]);
|
||||||
|
assert_runs(runs_for_range(&runs, 0, &(3..10)), &[1, 5, 1]);
|
||||||
|
assert_runs(runs_for_range(&runs, 0, &(5..8)), &[3]);
|
||||||
|
assert_runs(runs_for_range(&runs, 3, &(0..3)), &[1, 2]);
|
||||||
|
assert_runs(runs_for_range(&runs, 3, &(2..10)), &[4, 1, 3]);
|
||||||
|
assert_runs(runs_for_range(&runs, 9, &(0..8)), &[1, 7]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,9 @@ use gpui::{
|
||||||
InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton, MouseDownEvent,
|
InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton, MouseDownEvent,
|
||||||
MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, ScrollHandle,
|
MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, ScrollHandle,
|
||||||
ScrollWheelEvent, SharedString, Styled as _, Subscription, Task, UTF16Selection, Window,
|
ScrollWheelEvent, SharedString, Styled as _, Subscription, Task, UTF16Selection, Window,
|
||||||
WrappedLine,
|
|
||||||
};
|
};
|
||||||
use ropey::{Rope, RopeSlice};
|
use ropey::{Rope, RopeSlice};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use smallvec::SmallVec;
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
@ -33,6 +31,7 @@ use crate::input::{
|
||||||
element::RIGHT_MARGIN,
|
element::RIGHT_MARGIN,
|
||||||
popovers::{ContextMenu, DiagnosticPopover, HoverPopover, MouseContextMenu},
|
popovers::{ContextMenu, DiagnosticPopover, HoverPopover, MouseContextMenu},
|
||||||
search::{self, SearchPanel},
|
search::{self, SearchPanel},
|
||||||
|
text_wrapper::LineLayout,
|
||||||
HoverDefinition, Lsp, Position,
|
HoverDefinition, Lsp, Position,
|
||||||
};
|
};
|
||||||
use crate::input::{RopeExt as _, Selection};
|
use crate::input::{RopeExt as _, Selection};
|
||||||
|
|
@ -239,7 +238,7 @@ pub(super) struct LastLayout {
|
||||||
/// The range of byte offset of the visible lines.
|
/// The range of byte offset of the visible lines.
|
||||||
pub(super) visible_range_offset: Range<usize>,
|
pub(super) visible_range_offset: Range<usize>,
|
||||||
/// The last layout lines (Only have visible lines).
|
/// The last layout lines (Only have visible lines).
|
||||||
pub(super) lines: Rc<SmallVec<[WrappedLine; 1]>>,
|
pub(super) lines: Rc<Vec<LineLayout>>,
|
||||||
/// The line_height of text layout, this will change will InputElement painted.
|
/// The line_height of text layout, this will change will InputElement painted.
|
||||||
pub(super) line_height: Pixels,
|
pub(super) line_height: Pixels,
|
||||||
/// The wrap width of text layout, this will change will InputElement painted.
|
/// The wrap width of text layout, this will change will InputElement painted.
|
||||||
|
|
@ -659,7 +658,7 @@ impl InputState {
|
||||||
if new_row >= last_layout.visible_range.start {
|
if new_row >= last_layout.visible_range.start {
|
||||||
let visible_row = new_row.saturating_sub(last_layout.visible_range.start);
|
let visible_row = new_row.saturating_sub(last_layout.visible_range.start);
|
||||||
if let Some(line) = last_layout.lines.get(visible_row) {
|
if let Some(line) = last_layout.lines.get(visible_row) {
|
||||||
if let Ok(x) = line.closest_index_for_position(
|
if let Some(x) = line.closest_index_for_position(
|
||||||
Point {
|
Point {
|
||||||
x: preferred_x,
|
x: preferred_x,
|
||||||
y: px(0.),
|
y: px(0.),
|
||||||
|
|
@ -1544,7 +1543,7 @@ impl InputState {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.selecting = true;
|
self.selecting = true;
|
||||||
let offset = self.index_for_mouse_position(event.position, window, cx);
|
let offset = self.index_for_mouse_position(event.position);
|
||||||
|
|
||||||
if self.handle_click_hover_definition(event, offset, window, cx) {
|
if self.handle_click_hover_definition(event, offset, window, cx) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1586,7 +1585,7 @@ impl InputState {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
// Show diagnostic popover on mouse move
|
// Show diagnostic popover on mouse move
|
||||||
let offset = self.index_for_mouse_position(event.position, window, cx);
|
let offset = self.index_for_mouse_position(event.position);
|
||||||
self.handle_mouse_move(offset, event, window, cx);
|
self.handle_mouse_move(offset, event, window, cx);
|
||||||
|
|
||||||
if self.mode.is_code_editor() {
|
if self.mode.is_code_editor() {
|
||||||
|
|
@ -1814,12 +1813,7 @@ impl InputState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn index_for_mouse_position(
|
pub(crate) fn index_for_mouse_position(&self, position: Point<Pixels>) -> usize {
|
||||||
&self,
|
|
||||||
position: Point<Pixels>,
|
|
||||||
_window: &Window,
|
|
||||||
_cx: &App,
|
|
||||||
) -> usize {
|
|
||||||
// If the text is empty, always return 0
|
// If the text is empty, always return 0
|
||||||
if self.text.len() == 0 {
|
if self.text.len() == 0 {
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -1858,7 +1852,7 @@ impl InputState {
|
||||||
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 Some(rendered_line) = last_layout.lines.get(ix) else {
|
let Some(line_layout) = last_layout.lines.get(ix) else {
|
||||||
if pos.y < line_origin.y + line_height {
|
if pos.y < line_origin.y + line_height {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -1868,37 +1862,18 @@ impl InputState {
|
||||||
|
|
||||||
// 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.mode.is_single_line() {
|
if self.mode.is_single_line() {
|
||||||
return rendered_line.unwrapped_layout.closest_index_for_x(pos.x);
|
return line_layout.closest_index_for_x(pos.x);
|
||||||
}
|
}
|
||||||
|
|
||||||
let index_result = rendered_line.closest_index_for_position(pos, line_height);
|
if let Some(v) = line_layout.closest_index_for_position(pos, line_height) {
|
||||||
if let Ok(v) = index_result {
|
|
||||||
index += v;
|
index += v;
|
||||||
break;
|
break;
|
||||||
} else if let Ok(_) =
|
} else if pos.y < px(0.) {
|
||||||
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;
|
break;
|
||||||
} 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 += rendered_line.len();
|
|
||||||
if line_bounds.contains(&pos) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
index += rendered_line.len();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// +1 for revert `lines` split `\n`
|
// +1 for `\n`
|
||||||
index += 1;
|
index += line_layout.len() + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if index > self.text.len() {
|
if index > self.text.len() {
|
||||||
|
|
@ -2126,7 +2101,7 @@ impl InputState {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let offset = self.index_for_mouse_position(event.position, window, cx);
|
let offset = self.index_for_mouse_position(event.position);
|
||||||
self.select_to(offset, cx);
|
self.select_to(offset, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2504,7 +2479,7 @@ impl EntityInputHandler for InputState {
|
||||||
let offset = last_layout.visible_range_offset.start;
|
let offset = last_layout.visible_range_offset.start;
|
||||||
|
|
||||||
for line in last_layout.lines.iter() {
|
for line in last_layout.lines.iter() {
|
||||||
if let Ok(utf8_index) = line.index_for_position(line_point, line_height) {
|
if let Some(utf8_index) = line.index_for_position(line_point, line_height) {
|
||||||
return Some(self.offset_to_utf16(offset + utf8_index));
|
return Some(self.offset_to_utf16(offset + utf8_index));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use gpui::{App, Font, LineFragment, Pixels};
|
use gpui::{point, px, size, App, Font, LineFragment, Pixels, Point, ShapedLine, Size, Window};
|
||||||
use ropey::Rope;
|
use ropey::Rope;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use crate::input::RopeExt;
|
use crate::input::RopeExt;
|
||||||
|
|
||||||
/// A line with soft wrapped lines info.
|
/// A line with soft wrapped lines info.
|
||||||
#[derive(Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(super) struct LineItem {
|
pub(super) struct LineItem {
|
||||||
/// The original line text.
|
/// The original line text.
|
||||||
line: Rope,
|
line: Rope,
|
||||||
|
|
@ -15,7 +16,7 @@ pub(super) struct LineItem {
|
||||||
/// FIXME: Here in somecase, the `line_wrapper.wrap_line` has returned different
|
/// FIXME: Here in somecase, the `line_wrapper.wrap_line` has returned different
|
||||||
/// like the `window.text_system().shape_text`. So, this value may not equal
|
/// like the `window.text_system().shape_text`. So, this value may not equal
|
||||||
/// the actual rendered lines.
|
/// the actual rendered lines.
|
||||||
wrapped_lines: Vec<Range<usize>>,
|
pub(super) wrapped_lines: Vec<Range<usize>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LineItem {
|
impl LineItem {
|
||||||
|
|
@ -176,9 +177,9 @@ impl TextWrapper {
|
||||||
let new_range = new_start_offset..new_end_offset;
|
let new_range = new_start_offset..new_end_offset;
|
||||||
|
|
||||||
let mut new_lines = vec![];
|
let mut new_lines = vec![];
|
||||||
|
|
||||||
let wrap_width = self.wrap_width;
|
let wrap_width = self.wrap_width;
|
||||||
|
|
||||||
|
// line not contains `\n`.
|
||||||
for (ix, line) in Rope::from(changed_text.slice(new_range))
|
for (ix, line) in Rope::from(changed_text.slice(new_range))
|
||||||
.iter_lines()
|
.iter_lines()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
|
|
@ -237,6 +238,145 @@ impl TextWrapper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct LineLayout {
|
||||||
|
/// Total bytes length of this line.
|
||||||
|
len: usize,
|
||||||
|
/// The soft wrapped lines of this line (Include the first line).
|
||||||
|
pub(crate) wrapped_lines: SmallVec<[ShapedLine; 1]>,
|
||||||
|
pub(crate) longest_width: Pixels,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LineLayout {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
len: 0,
|
||||||
|
longest_width: px(0.),
|
||||||
|
wrapped_lines: SmallVec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_wrapped_lines(&mut self, wrapped_lines: SmallVec<[ShapedLine; 1]>) {
|
||||||
|
self.len = wrapped_lines.iter().map(|l| l.len).sum();
|
||||||
|
let width = wrapped_lines
|
||||||
|
.iter()
|
||||||
|
.map(|l| l.width)
|
||||||
|
.max()
|
||||||
|
.unwrap_or_default();
|
||||||
|
self.longest_width = width;
|
||||||
|
self.wrapped_lines = wrapped_lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn len(&self) -> usize {
|
||||||
|
self.len
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the position (x, y) for the given index in this line layout.
|
||||||
|
///
|
||||||
|
/// - The `offset` is a local byte index in this line layout.
|
||||||
|
/// - The return value is relative to the top-left corner of this line layout, start from (0, 0)
|
||||||
|
pub(crate) fn position_for_index(
|
||||||
|
&self,
|
||||||
|
offset: usize,
|
||||||
|
line_height: Pixels,
|
||||||
|
) -> Option<Point<Pixels>> {
|
||||||
|
let mut acc_len = 0;
|
||||||
|
let mut offset_y = px(0.);
|
||||||
|
|
||||||
|
for line in self.wrapped_lines.iter() {
|
||||||
|
let range = acc_len..=(acc_len + line.len());
|
||||||
|
if range.contains(&offset) {
|
||||||
|
let x = line.x_for_index(offset.saturating_sub(acc_len));
|
||||||
|
return Some(point(x, offset_y));
|
||||||
|
}
|
||||||
|
acc_len += line.text.len();
|
||||||
|
offset_y += line_height;
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn closest_index_for_x(&self, x: Pixels) -> usize {
|
||||||
|
let mut acc_len = 0;
|
||||||
|
for line in self.wrapped_lines.iter() {
|
||||||
|
if x <= line.width {
|
||||||
|
let ix = line.closest_index_for_x(x);
|
||||||
|
return acc_len + ix;
|
||||||
|
}
|
||||||
|
acc_len += line.text.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
acc_len
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the index for the given position (x, y) in this line layout.
|
||||||
|
///
|
||||||
|
/// The `pos` is relative to the top-left corner of this line layout, start from (0, 0)
|
||||||
|
/// The return value is a local byte index in this line layout, start from 0.
|
||||||
|
pub(super) fn closest_index_for_position(
|
||||||
|
&self,
|
||||||
|
pos: Point<Pixels>,
|
||||||
|
line_height: Pixels,
|
||||||
|
) -> Option<usize> {
|
||||||
|
let mut offset = 0;
|
||||||
|
let mut line_top = px(0.);
|
||||||
|
for line in self.wrapped_lines.iter() {
|
||||||
|
let line_bottom = line_top + line_height;
|
||||||
|
if pos.y >= line_top && pos.y < line_bottom {
|
||||||
|
let ix = line.closest_index_for_x(pos.x);
|
||||||
|
return Some(offset + ix);
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += line.text.len();
|
||||||
|
line_top = line_bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn index_for_position(
|
||||||
|
&self,
|
||||||
|
pos: Point<Pixels>,
|
||||||
|
line_height: Pixels,
|
||||||
|
) -> Option<usize> {
|
||||||
|
let mut offset = 0;
|
||||||
|
let mut line_top = px(0.);
|
||||||
|
for line in self.wrapped_lines.iter() {
|
||||||
|
let line_bottom = line_top + line_height;
|
||||||
|
if pos.y >= line_top && pos.y < line_bottom {
|
||||||
|
let ix = line.index_for_x(pos.x)?;
|
||||||
|
return Some(offset + ix);
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += line.text.len();
|
||||||
|
line_top = line_bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn size(&self, line_height: Pixels) -> Size<Pixels> {
|
||||||
|
size(self.longest_width, self.wrapped_lines.len() * line_height)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn paint(
|
||||||
|
&self,
|
||||||
|
pos: Point<Pixels>,
|
||||||
|
line_height: Pixels,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
for (ix, line) in self.wrapped_lines.iter().enumerate() {
|
||||||
|
_ = line.paint(
|
||||||
|
pos + point(px(0.), ix * line_height),
|
||||||
|
line_height,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -253,15 +393,43 @@ mod tests {
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut wrapper = TextWrapper::new(font, px(14.), None);
|
let mut wrapper = TextWrapper::new(font, px(14.), None);
|
||||||
let mut text =
|
let mut text = Rope::from(
|
||||||
Rope::from("Hello, 世界!\nThis is second line.\nThis is third line.\n这里是第 4 行。");
|
"Hello, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。",
|
||||||
|
);
|
||||||
|
|
||||||
fn fake_wrap_line(_line_str: &str, _wrap_width: Pixels) -> Vec<Boundary> {
|
fn fake_wrap_line(_line: &str, _wrap_width: Pixels) -> Vec<Boundary> {
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn assert_wrapper_lines(text: &Rope, wrapper: &TextWrapper, expected_lines: &[&[&str]]) {
|
||||||
|
let mut actual_lines = vec![];
|
||||||
|
let mut offset = 0;
|
||||||
|
for line in wrapper.lines.iter() {
|
||||||
|
actual_lines.push(
|
||||||
|
line.wrapped_lines
|
||||||
|
.iter()
|
||||||
|
.map(|range| text.slice(offset + range.start..offset + range.end))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
);
|
||||||
|
// +1 \n
|
||||||
|
offset += line.len() + 1;
|
||||||
|
}
|
||||||
|
assert_eq!(actual_lines, expected_lines);
|
||||||
|
}
|
||||||
|
|
||||||
wrapper._update(&text, &(0..text.len()), &text, &mut fake_wrap_line);
|
wrapper._update(&text, &(0..text.len()), &text, &mut fake_wrap_line);
|
||||||
assert_eq!(wrapper.lines.len(), 4);
|
assert_eq!(wrapper.lines.len(), 4);
|
||||||
|
assert_wrapper_lines(
|
||||||
|
&text,
|
||||||
|
&wrapper,
|
||||||
|
&[
|
||||||
|
&["Hello, 世界!\r"],
|
||||||
|
&["This is second line."],
|
||||||
|
&["This is third line."],
|
||||||
|
&["这里是第 4 行。"],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
// Add a new text to end
|
// Add a new text to end
|
||||||
let range = text.len()..text.len();
|
let range = text.len()..text.len();
|
||||||
|
|
@ -270,9 +438,20 @@ mod tests {
|
||||||
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
|
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
text.to_string(),
|
text.to_string(),
|
||||||
"Hello, 世界!\nThis is second line.\nThis is third line.\n这里是第 4 行。New text"
|
"Hello, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。New text"
|
||||||
);
|
);
|
||||||
assert_eq!(wrapper.lines.len(), 4);
|
assert_eq!(wrapper.lines.len(), 4);
|
||||||
|
assert_eq!(wrapper.lines.len(), 4);
|
||||||
|
assert_wrapper_lines(
|
||||||
|
&text,
|
||||||
|
&wrapper,
|
||||||
|
&[
|
||||||
|
&["Hello, 世界!\r"],
|
||||||
|
&["This is second line."],
|
||||||
|
&["This is third line."],
|
||||||
|
&["这里是第 4 行。New text"],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
// Replace first line `Hello` to `AAA`
|
// Replace first line `Hello` to `AAA`
|
||||||
let range = 0..5;
|
let range = 0..5;
|
||||||
|
|
@ -281,9 +460,20 @@ mod tests {
|
||||||
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
|
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
text.to_string(),
|
text.to_string(),
|
||||||
"AAA, 世界!\nThis is second line.\nThis is third line.\n这里是第 4 行。New text"
|
"AAA, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。New text"
|
||||||
);
|
);
|
||||||
|
dbg!(&wrapper.lines);
|
||||||
assert_eq!(wrapper.lines.len(), 4);
|
assert_eq!(wrapper.lines.len(), 4);
|
||||||
|
assert_wrapper_lines(
|
||||||
|
&text,
|
||||||
|
&wrapper,
|
||||||
|
&[
|
||||||
|
&["AAA, 世界!\r"],
|
||||||
|
&["This is second line."],
|
||||||
|
&["This is third line."],
|
||||||
|
&["这里是第 4 行。New text"],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
// Remove the second line
|
// Remove the second line
|
||||||
let start_offset = text.line_start_offset(1);
|
let start_offset = text.line_start_offset(1);
|
||||||
|
|
@ -293,9 +483,18 @@ mod tests {
|
||||||
wrapper._update(&text, &range, &Rope::from(""), &mut fake_wrap_line);
|
wrapper._update(&text, &range, &Rope::from(""), &mut fake_wrap_line);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
text.to_string(),
|
text.to_string(),
|
||||||
"AAA, 世界!\nThis is third line.\n这里是第 4 行。New text"
|
"AAA, 世界!\r\nThis is third line.\n这里是第 4 行。New text"
|
||||||
);
|
);
|
||||||
assert_eq!(wrapper.lines.len(), 3);
|
assert_eq!(wrapper.lines.len(), 3);
|
||||||
|
assert_wrapper_lines(
|
||||||
|
&text,
|
||||||
|
&wrapper,
|
||||||
|
&[
|
||||||
|
&["AAA, 世界!\r"],
|
||||||
|
&["This is third line."],
|
||||||
|
&["这里是第 4 行。New text"],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
// Replace the first 2 lines to "This is a new line."
|
// Replace the first 2 lines to "This is a new line."
|
||||||
let range = text.line_start_offset(0)..text.line_end_offset(1) + 1;
|
let range = text.line_start_offset(0)..text.line_end_offset(1) + 1;
|
||||||
|
|
@ -307,6 +506,15 @@ mod tests {
|
||||||
"This is a new line.\nThis is new line 2.\n这里是第 4 行。New text"
|
"This is a new line.\nThis is new line 2.\n这里是第 4 行。New text"
|
||||||
);
|
);
|
||||||
assert_eq!(wrapper.lines.len(), 3);
|
assert_eq!(wrapper.lines.len(), 3);
|
||||||
|
assert_wrapper_lines(
|
||||||
|
&text,
|
||||||
|
&wrapper,
|
||||||
|
&[
|
||||||
|
&["This is a new line."],
|
||||||
|
&["This is new line 2."],
|
||||||
|
&["这里是第 4 行。New text"],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
// Add a new line at the end
|
// Add a new line at the end
|
||||||
let range = text.len()..text.len();
|
let range = text.len()..text.len();
|
||||||
|
|
@ -318,6 +526,16 @@ mod tests {
|
||||||
"This is a new line.\nThis is new line 2.\n这里是第 4 行。New text\nThis is a new line at the end."
|
"This is a new line.\nThis is new line 2.\n这里是第 4 行。New text\nThis is a new line at the end."
|
||||||
);
|
);
|
||||||
assert_eq!(wrapper.lines.len(), 4);
|
assert_eq!(wrapper.lines.len(), 4);
|
||||||
|
assert_wrapper_lines(
|
||||||
|
&text,
|
||||||
|
&wrapper,
|
||||||
|
&[
|
||||||
|
&["This is a new line."],
|
||||||
|
&["This is new line 2."],
|
||||||
|
&["这里是第 4 行。New text"],
|
||||||
|
&["This is a new line at the end."],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
// Add a new line at the beginning
|
// Add a new line at the beginning
|
||||||
let range = 0..0;
|
let range = 0..0;
|
||||||
|
|
@ -329,6 +547,17 @@ mod tests {
|
||||||
"This is a new line at the beginning.\nThis is a new line.\nThis is new line 2.\n这里是第 4 行。New text\nThis is a new line at the end."
|
"This is a new line at the beginning.\nThis is a new line.\nThis is new line 2.\n这里是第 4 行。New text\nThis is a new line at the end."
|
||||||
);
|
);
|
||||||
assert_eq!(wrapper.lines.len(), 5);
|
assert_eq!(wrapper.lines.len(), 5);
|
||||||
|
assert_wrapper_lines(
|
||||||
|
&text,
|
||||||
|
&wrapper,
|
||||||
|
&[
|
||||||
|
&["This is a new line at the beginning."],
|
||||||
|
&["This is a new line."],
|
||||||
|
&["This is new line 2."],
|
||||||
|
&["这里是第 4 行。New text"],
|
||||||
|
&["This is a new line at the end."],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
// Remove all to at least one line in `lines`.
|
// Remove all to at least one line in `lines`.
|
||||||
let range = 0..text.len();
|
let range = 0..text.len();
|
||||||
|
|
@ -337,6 +566,7 @@ mod tests {
|
||||||
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
|
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
|
||||||
assert_eq!(text.to_string(), "");
|
assert_eq!(text.to_string(), "");
|
||||||
assert_eq!(wrapper.lines.len(), 1);
|
assert_eq!(wrapper.lines.len(), 1);
|
||||||
|
assert_eq!(wrapper.lines[0].wrapped_lines, vec![0..0]);
|
||||||
|
|
||||||
// Test update_all
|
// Test update_all
|
||||||
let range = 0..text.len();
|
let range = 0..text.len();
|
||||||
|
|
@ -349,4 +579,16 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(wrapper.lines.len(), 2);
|
assert_eq!(wrapper.lines.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_line_layout() {
|
||||||
|
let mut line_layout = LineLayout::new();
|
||||||
|
|
||||||
|
let line1 = ShapedLine::default().with_len(100);
|
||||||
|
let line2 = ShapedLine::default().with_len(50);
|
||||||
|
let wrapped_lines = smallvec::smallvec![line1, line2];
|
||||||
|
line_layout.set_wrapped_lines(wrapped_lines);
|
||||||
|
assert_eq!(line_layout.len(), 150);
|
||||||
|
assert_eq!(line_layout.wrapped_lines.len(), 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue