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:
Jason Lee 2025-09-28 10:31:03 +08:00 committed by GitHub
parent 4983300ddf
commit 172015f147
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 432 additions and 65 deletions

View file

@ -3,14 +3,18 @@ use std::{ops::Range, rc::Rc};
use gpui::{
fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler,
Entity, GlobalElementId, Half, HighlightStyle, Hitbox, IntoElement, LayoutId, MouseButton,
MouseMoveEvent, Path, Pixels, Point, ShapedLine, SharedString, Size, Style, TextAlign, TextRun,
TextStyle, UnderlineStyle, Window,
MouseMoveEvent, Path, Pixels, Point, ShapedLine, SharedString, Size, Style, TextRun, TextStyle,
UnderlineStyle, Window,
};
use ropey::Rope;
use smallvec::SmallVec;
use crate::{
input::{blink_cursor::CURSOR_WIDTH, RopeExt as _},
input::{
blink_cursor::CURSOR_WIDTH,
text_wrapper::{LineLayout, TextWrapper},
RopeExt as _,
},
ActiveTheme as _, Colorize, Root,
};
@ -522,6 +526,53 @@ impl TextElement {
(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.
fn highlight_lines(
&mut self,
@ -801,17 +852,14 @@ impl Element for TextElement {
None
};
// NOTE: Here 50 lines about 150µs
// let measure = crate::Measure::new("shape_text");
let visible_text = display_text
.slice_lines(visible_range.start..visible_range.end)
.to_string();
let lines = window
.text_system()
.shape_text(visible_text.into(), font_size, &runs, wrap_width, None)
.expect("failed to shape text");
// measure.end();
let lines = Self::layout_lines(
&text,
&state.text_wrapper,
&visible_range,
font_size,
&runs,
window,
);
let mut longest_line_width = wrap_width.unwrap_or(px(0.));
if state.mode.is_multi_line() && !state.soft_wrap && lines.len() > 1 {
@ -938,7 +986,7 @@ impl Element for TextElement {
.text_system()
.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());
}
line_numbers.push(sub_lines);
@ -1082,7 +1130,7 @@ impl Element for TextElement {
origin.x + prepaint.last_layout.line_number_width,
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;
}
@ -1162,3 +1210,105 @@ impl Element for TextElement {
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]);
}
}

View file

@ -9,11 +9,9 @@ use gpui::{
InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, MouseButton, MouseDownEvent,
MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, ScrollHandle,
ScrollWheelEvent, SharedString, Styled as _, Subscription, Task, UTF16Selection, Window,
WrappedLine,
};
use ropey::{Rope, RopeSlice};
use serde::Deserialize;
use smallvec::SmallVec;
use std::cell::RefCell;
use std::ops::Range;
use std::rc::Rc;
@ -33,6 +31,7 @@ use crate::input::{
element::RIGHT_MARGIN,
popovers::{ContextMenu, DiagnosticPopover, HoverPopover, MouseContextMenu},
search::{self, SearchPanel},
text_wrapper::LineLayout,
HoverDefinition, Lsp, Position,
};
use crate::input::{RopeExt as _, Selection};
@ -239,7 +238,7 @@ pub(super) struct LastLayout {
/// The range of byte offset of the visible lines.
pub(super) visible_range_offset: Range<usize>,
/// 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.
pub(super) line_height: Pixels,
/// 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 {
let visible_row = new_row.saturating_sub(last_layout.visible_range.start);
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 {
x: preferred_x,
y: px(0.),
@ -1544,7 +1543,7 @@ impl InputState {
}
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) {
return;
@ -1586,7 +1585,7 @@ impl InputState {
cx: &mut Context<Self>,
) {
// 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);
if self.mode.is_code_editor() {
@ -1814,12 +1813,7 @@ impl InputState {
}
}
pub(crate) fn index_for_mouse_position(
&self,
position: Point<Pixels>,
_window: &Window,
_cx: &App,
) -> usize {
pub(crate) fn index_for_mouse_position(&self, position: Point<Pixels>) -> usize {
// If the text is empty, always return 0
if self.text.len() == 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 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 {
break;
}
@ -1868,37 +1862,18 @@ impl InputState {
// Return offset by use closest_index_for_x if is single line mode.
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 Ok(v) = index_result {
if let Some(v) = line_layout.closest_index_for_position(pos, line_height) {
index += v;
break;
} 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();
} else if pos.y < px(0.) {
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`
index += 1;
// +1 for `\n`
index += line_layout.len() + 1;
}
if index > self.text.len() {
@ -2126,7 +2101,7 @@ impl InputState {
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);
}
@ -2504,7 +2479,7 @@ impl EntityInputHandler for InputState {
let offset = last_layout.visible_range_offset.start;
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));
}
}

View file

@ -1,12 +1,13 @@
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 smallvec::SmallVec;
use crate::input::RopeExt;
/// A line with soft wrapped lines info.
#[derive(Clone)]
#[derive(Debug, Clone)]
pub(super) struct LineItem {
/// The original line text.
line: Rope,
@ -15,7 +16,7 @@ pub(super) struct LineItem {
/// 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
/// the actual rendered lines.
wrapped_lines: Vec<Range<usize>>,
pub(super) wrapped_lines: Vec<Range<usize>>,
}
impl LineItem {
@ -176,9 +177,9 @@ impl TextWrapper {
let new_range = new_start_offset..new_end_offset;
let mut new_lines = vec![];
let wrap_width = self.wrap_width;
// line not contains `\n`.
for (ix, line) in Rope::from(changed_text.slice(new_range))
.iter_lines()
.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)]
mod tests {
use super::*;
@ -253,15 +393,43 @@ mod tests {
};
let mut wrapper = TextWrapper::new(font, px(14.), None);
let mut text =
Rope::from("Hello, 世界!\nThis is second line.\nThis is third line.\n这里是第 4 行。");
let mut text = Rope::from(
"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![]
}
#[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);
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
let range = text.len()..text.len();
@ -270,9 +438,20 @@ mod tests {
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
assert_eq!(
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_wrapper_lines(
&text,
&wrapper,
&[
&["Hello, 世界!\r"],
&["This is second line."],
&["This is third line."],
&["这里是第 4 行。New text"],
],
);
// Replace first line `Hello` to `AAA`
let range = 0..5;
@ -281,9 +460,20 @@ mod tests {
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
assert_eq!(
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_wrapper_lines(
&text,
&wrapper,
&[
&["AAA, 世界!\r"],
&["This is second line."],
&["This is third line."],
&["这里是第 4 行。New text"],
],
);
// Remove the second line
let start_offset = text.line_start_offset(1);
@ -293,9 +483,18 @@ mod tests {
wrapper._update(&text, &range, &Rope::from(""), &mut fake_wrap_line);
assert_eq!(
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_wrapper_lines(
&text,
&wrapper,
&[
&["AAA, 世界!\r"],
&["This is third line."],
&["这里是第 4 行。New text"],
],
);
// Replace the first 2 lines to "This is a new line."
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"
);
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
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."
);
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
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."
);
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`.
let range = 0..text.len();
@ -337,6 +566,7 @@ mod tests {
wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line);
assert_eq!(text.to_string(), "");
assert_eq!(wrapper.lines.len(), 1);
assert_eq!(wrapper.lines[0].wrapped_lines, vec![0..0]);
// Test update_all
let range = 0..text.len();
@ -349,4 +579,16 @@ mod tests {
);
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);
}
}