input: Fix visible range when multi-line text have soft wrap. (#1155)

## Before

<img width="1130" height="783" alt="image"
src="https://github.com/user-attachments/assets/3e0db5f4-bb84-480f-8b38-dbe24e6c6214"
/>

## After


https://github.com/user-attachments/assets/fdaed71e-3ab8-4258-a1eb-b731181ca31c
This commit is contained in:
Jason Lee 2025-08-19 19:21:12 +08:00 committed by GitHub
parent 7fc1544a10
commit fb41e63ea8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 45 additions and 31 deletions

View file

@ -182,8 +182,8 @@ where
window_bounds: Some(WindowBounds::Windowed(window_bounds)),
titlebar: Some(TitleBar::title_bar_options()),
window_min_size: Some(gpui::Size {
width: px(640.),
height: px(480.),
width: px(480.),
height: px(320.),
}),
kind: WindowKind::Normal,
#[cfg(target_os = "linux")]

View file

@ -338,18 +338,23 @@ impl TextElement {
return 0..1;
}
let scroll_top = -state.scroll_handle.offset().y;
let total_lines = state.text_wrapper.lines.len();
let mut visible_range = 0..total_lines;
let mut line_top = px(0.);
for (ix, line) in state.text_wrapper.lines.iter().enumerate() {
line_top += line.height(line_height);
let Some(last_layout) = state.last_layout.as_ref() else {
return 0..1;
};
if line_top < scroll_top {
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;
if line_bottom < -scroll_top {
visible_range.start = ix;
}
if line_top > scroll_top + input_height {
if line_bottom + scroll_top >= input_height {
visible_range.end = (ix + 1).min(total_lines);
break;
}
@ -435,7 +440,6 @@ pub(super) struct PrepaintState {
last_layout: LastLayout,
/// The lines only contains the visible lines in the viewport, based on `visible_range`.
line_numbers: Option<Vec<SmallVec<[WrappedLine; 1]>>>,
line_number_width: Pixels,
/// Size of the scrollable area by entire lines.
scroll_size: Size<Pixels>,
cursor_bounds: Option<Bounds<Pixels>>,
@ -657,7 +661,7 @@ impl Element for TextElement {
};
let wrap_width = if multi_line {
Some(bounds.size.width - line_number_width - RIGHT_MARGIN)
Some(bounds.size.width - line_number_width)
} else {
None
};
@ -796,10 +800,11 @@ impl Element for TextElement {
lines: Rc::new(lines),
line_height,
visible_range,
line_number_width,
wrap_width,
},
scroll_size,
line_numbers,
line_number_width,
cursor_bounds,
cursor_scroll_offset,
current_line_index,
@ -915,7 +920,10 @@ impl Element for TextElement {
.skip(visible_range.start)
.take(visible_range.len())
{
let p = point(origin.x + prepaint.line_number_width, origin.y + offset_y);
let p = point(
origin.x + prepaint.last_layout.line_number_width,
origin.y + offset_y,
);
_ = line.paint(p, line_height, TextAlign::Left, None, window, cx);
offset_y += line.size(line_height).height;
}
@ -934,7 +942,6 @@ impl Element for TextElement {
state.set_input_bounds(input_bounds, cx);
state.last_selected_range = Some(selected_range);
state.scroll_size = prepaint.scroll_size;
state.line_number_width = prepaint.line_number_width;
state
.scroll_handle
.set_offset(prepaint.cursor_scroll_offset);

View file

@ -34,7 +34,11 @@ impl DiagnosticPopover {
let Some(range) = self.marker.range.as_ref() else {
return None;
};
let line_number_width = self.state.read(cx).line_number_width;
let Some(last_layout) = self.state.read(cx).last_layout.as_ref() else {
return None;
};
let line_number_width = last_layout.line_number_width;
let (_, _, start_pos) = self
.state

View file

@ -222,6 +222,10 @@ pub(super) struct LastLayout {
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 width of text layout.
pub(super) line_number_width: Pixels,
}
impl Deref for LastLayout {
@ -268,7 +272,6 @@ pub struct InputState {
pub(super) scroll_state: ScrollbarState,
/// The size of the scrollable content.
pub(crate) scroll_size: gpui::Size<Pixels>,
pub(crate) line_number_width: Pixels,
/// The mask pattern for formatting the input text
pub(crate) mask_pattern: MaskPattern,
@ -344,7 +347,6 @@ impl InputState {
scroll_state: ScrollbarState::default(),
scroll_size: gpui::size(px(0.), px(0.)),
preferred_x_offset: None,
line_number_width: px(0.),
placeholder: SharedString::default(),
mask_pattern: MaskPattern::default(),
diagnostic_popover: None,
@ -1749,6 +1751,7 @@ impl InputState {
};
let line_height = last_layout.line_height;
let line_number_width = last_layout.line_number_width;
// TIP: About the IBeam cursor
//
@ -1760,7 +1763,7 @@ impl InputState {
//
// - included the input padding.
// - included the scroll offset.
let inner_position = position - bounds.origin - point(self.line_number_width, px(0.));
let inner_position = position - bounds.origin - point(line_number_width, px(0.));
let mut index = 0;
let mut y_offset = px(0.);
@ -2095,10 +2098,12 @@ impl InputState {
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);
if let Some(last_layout) = self.last_layout.as_ref() {
if wrap_width_changed {
self.text_wrapper.set_wrap_width(last_layout.wrap_width, cx);
self.mode.update_auto_grow(&self.text_wrapper);
cx.notify();
}
}
}
@ -2256,11 +2261,12 @@ impl EntityInputHandler for InputState {
) -> Option<Bounds<Pixels>> {
let last_layout = self.last_layout.as_ref()?;
let line_height = last_layout.line_height;
let line_number_width = last_layout.line_number_width;
let range = self.range_from_utf16(&range_utf16);
let mut start_origin = None;
let mut end_origin = None;
let line_number_origin = point(self.line_number_width, px(0.));
let line_number_origin = point(line_number_width, px(0.));
let mut y_offset = px(0.);
let mut index_offset = 0;

View file

@ -6,17 +6,15 @@ use gpui::{App, Font, LineFragment, Pixels, SharedString};
#[allow(unused)]
pub(super) struct LineWrap {
/// The number of soft wrapped lines of this line (Not include first line.)
///
/// 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.
pub(super) wrap_lines: usize,
/// The range of the line text in the entire text.
pub(super) range: Range<usize>,
}
impl LineWrap {
pub(super) fn height(&self, line_height: Pixels) -> Pixels {
line_height * (self.wrap_lines + 1)
}
}
/// 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
@ -70,7 +68,6 @@ impl TextWrapper {
let mut line_wrapper = cx
.text_system()
.line_wrapper(self.font.clone(), self.font_size);
let mut prev_line_ix = 0;
for line in text.split('\n') {
let mut line_wraps = vec![];