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)), window_bounds: Some(WindowBounds::Windowed(window_bounds)),
titlebar: Some(TitleBar::title_bar_options()), titlebar: Some(TitleBar::title_bar_options()),
window_min_size: Some(gpui::Size { window_min_size: Some(gpui::Size {
width: px(640.), width: px(480.),
height: px(480.), height: px(320.),
}), }),
kind: WindowKind::Normal, kind: WindowKind::Normal,
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]

View file

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

View file

@ -34,7 +34,11 @@ impl DiagnosticPopover {
let Some(range) = self.marker.range.as_ref() else { let Some(range) = self.marker.range.as_ref() else {
return None; 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 let (_, _, start_pos) = self
.state .state

View file

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

View file

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