From 844574befc83c2789fcf8e787e0f839c7c5cad05 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Wed, 10 Sep 2025 13:59:02 +0800 Subject: [PATCH] input: Fix track scroll to selected end on drag move. (#1231) --- Cargo.lock | 30 ++++---- crates/ui/src/input/element.rs | 113 +++++++++++++++++----------- crates/ui/src/input/state.rs | 10 ++- crates/ui/src/input/text_wrapper.rs | 19 ++++- 4 files changed, 106 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed9deeed..8f0fb834 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -709,7 +709,7 @@ dependencies = [ "bitflags 2.9.1", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.12.1", "lazy_static", "lazycell", "log", @@ -732,7 +732,7 @@ dependencies = [ "bitflags 2.9.1", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -1401,7 +1401,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" +source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7" dependencies = [ "indexmap 2.10.0", "rustc-hash 2.1.1", @@ -1870,7 +1870,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" +source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7" dependencies = [ "proc-macro2", "quote", @@ -3049,7 +3049,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" +source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -3232,7 +3232,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" +source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -3501,7 +3501,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" +source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7" dependencies = [ "anyhow", "bytes", @@ -3521,7 +3521,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" +source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7" dependencies = [ "rustls", "rustls-platform-verifier", @@ -4466,7 +4466,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" +source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7" dependencies = [ "anyhow", "bindgen 0.71.1", @@ -6380,7 +6380,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" +source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7" dependencies = [ "derive_refineable", "workspace-hack", @@ -6533,7 +6533,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" +source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7" dependencies = [ "anyhow", "bytes", @@ -6617,7 +6617,7 @@ dependencies = [ [[package]] name = "rope" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" +source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7" dependencies = [ "arrayvec", "log", @@ -7124,7 +7124,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" [[package]] name = "semantic_version" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" +source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7" dependencies = [ "anyhow", "serde", @@ -7747,7 +7747,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" +source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7" dependencies = [ "arrayvec", "log", @@ -9057,7 +9057,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed.git#5c30578c4961d3766ee991b119658b02741b85e5" +source = "git+https://github.com/zed-industries/zed.git#9c548a0ec6d8b84e2b3b1ee2a78add5fae7a8ba7" dependencies = [ "anyhow", "async-fs 2.1.3", diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs index 70dd3ed0..5045903d 100644 --- a/crates/ui/src/input/element.rs +++ b/crates/ui/src/input/element.rs @@ -58,7 +58,9 @@ impl TextElement { /// /// - cursor bounds /// - scroll offset - /// - current line index + /// - current row index (No only the visible lines, but all lines) + /// + /// This method also will update for track scroll to cursor. fn layout_cursor( &self, last_layout: &LastLayout, @@ -66,21 +68,21 @@ impl TextElement { window: &mut Window, cx: &mut App, ) -> (Option>, Point, Option) { + let state = self.state.read(cx); + 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 text_wrapper = &state.text_wrapper; 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 { selected_range = (marked_range.end..marked_range.end).into(); } let cursor = state.cursor(); - let mut current_line_index = None; + let mut current_row = None; let mut scroll_offset = state.scroll_handle.offset(); let mut cursor_bounds = None; @@ -95,49 +97,66 @@ impl TextElement { let mut cursor_start = None; let mut cursor_end = None; - 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; + let mut prev_lines_offset = 0; + let mut offset_y = px(0.); + for (ix, wrap_line) in text_wrapper.lines.iter().enumerate() { + let row = ix; + let line_origin = point(px(0.), offset_y); // break loop if all cursor positions are found if cursor_pos.is_some() && cursor_start.is_some() && cursor_end.is_some() { break; } - let line_origin = point(px(0.), offset_y); - if cursor_pos.is_none() { - let offset = cursor.offset.saturating_sub(prev_lines_offset); - - if let Some(pos) = line.position_for_index(offset, line_height) { - current_line_index = Some(line_ix); - cursor_pos = Some(line_origin + pos); + let in_visible_range = ix >= visible_range.start; + if let Some(line) = in_visible_range + .then(|| lines.get(ix.saturating_sub(visible_range.start))) + .flatten() + { + // If in visible range lines + if cursor_pos.is_none() { + let offset = cursor.offset.saturating_sub(prev_lines_offset); + if let Some(pos) = line.position_for_index(offset, line_height) { + current_row = Some(row); + cursor_pos = Some(line_origin + pos); + } } - } - if cursor_start.is_none() { - let offset = selected_range.start.saturating_sub(prev_lines_offset); - if let Some(pos) = line.position_for_index(offset, line_height) { - cursor_start = Some(line_origin + pos); + if cursor_start.is_none() { + let offset = selected_range.start.saturating_sub(prev_lines_offset); + if let Some(pos) = line.position_for_index(offset, line_height) { + cursor_start = Some(line_origin + pos); + } } - } - if cursor_end.is_none() { - let offset = selected_range.end.saturating_sub(prev_lines_offset); - if let Some(pos) = line.position_for_index(offset, line_height) { - cursor_end = Some(line_origin + pos); + if cursor_end.is_none() { + let offset = selected_range.end.saturating_sub(prev_lines_offset); + if let Some(pos) = line.position_for_index(offset, line_height) { + cursor_end = Some(line_origin + pos); + } } + + offset_y += line.size(line_height).height; + // +1 for the last `\n` + prev_lines_offset += line.len() + 1; + } else { + // If not in the visible range. + + // Just increase the offset_y and prev_lines_offset. + // This will let the scroll_offset to track the cursor position correctly. + if prev_lines_offset >= cursor.offset && cursor_pos.is_none() { + current_row = Some(row); + cursor_pos = Some(line_origin); + } + if prev_lines_offset >= selected_range.start && cursor_start.is_none() { + cursor_start = Some(line_origin); + } + if prev_lines_offset >= selected_range.end && cursor_end.is_none() { + cursor_end = Some(line_origin); + } + + offset_y += wrap_line.height(line_height); + // +1 for the last `\n` + prev_lines_offset += wrap_line.len() + 1; } - - offset_y += line.size(line_height).height; - // +1 for skip the last `\n` - 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)) = @@ -206,7 +225,7 @@ impl TextElement { bounds.origin = bounds.origin + scroll_offset; - (cursor_bounds, scroll_offset, current_line_index) + (cursor_bounds, scroll_offset, current_row) } fn layout_selections( @@ -466,8 +485,8 @@ pub(super) struct PrepaintState { scroll_size: Size, cursor_bounds: Option>, cursor_scroll_offset: Point, - /// line index (zero based), no wrap, same line as the cursor. - current_line_index: Option, + /// row index (zero based), no wrap, same line as the cursor. + current_row: Option, selection_path: Option>, bounds: Bounds, } @@ -755,7 +774,7 @@ impl Element for TextElement { // Calculate the scroll offset to keep the cursor in view - let (cursor_bounds, cursor_scroll_offset, current_line_index) = + let (cursor_bounds, cursor_scroll_offset, current_row) = self.layout_cursor(&last_layout, &mut bounds, window, cx); let selection_path = self.layout_selections(&last_layout, &mut bounds, window, cx); @@ -791,7 +810,7 @@ impl Element for TextElement { line_no_text.push_str(&"\n ".repeat(line.wrap_boundaries.len())); } - let runs = if current_line_index == Some(ix) { + let runs = if current_row == Some(ix) { ¤t_line_runs } else { &other_line_runs @@ -815,7 +834,7 @@ impl Element for TextElement { line_numbers, cursor_bounds, cursor_scroll_offset, - current_line_index, + current_row, selection_path, } } @@ -891,7 +910,8 @@ impl Element for TextElement { // Each item is the normal lines. for (ix, lines) in line_numbers.iter().enumerate() { - let is_active = prepaint.current_line_index == Some(visible_range.start + ix); + let row = visible_range.start + ix; + let is_active = prepaint.current_row == Some(row); for line in lines { let p = point(input_bounds.origin.x, origin.y + offset_y); let line_size = line.size(line_height); @@ -958,10 +978,11 @@ impl Element for TextElement { // Each item is the normal lines. for (ix, lines) in line_numbers.iter().enumerate() { + let row = visible_range.start + ix; for line in lines { let p = point(input_bounds.origin.x, origin.y + offset_y); - let is_active = prepaint.current_line_index == Some(visible_range.start + ix); + let is_active = prepaint.current_row == Some(row); let line_size = line.size(line_height); // paint active line number background diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index b9d6d902..df121552 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -769,15 +769,17 @@ impl InputState { cx: &mut Context, ) { let max_point = self.text.max_point(); - let line_ix = line.saturating_sub(1).min(max_point.row as usize); - let column_ix = column + let row = line.saturating_sub(1).min(max_point.row as usize); + let col = column .unwrap_or(1) .saturating_sub(1) - .min(self.text.line_len(line_ix as u32) as usize); + .min(self.text.line_len(row as u32) as usize); let offset = self .text - .point_to_offset(rope::Point::new(line_ix as u32, column_ix as u32)); + .point_to_offset(rope::Point::new(row as u32, col as u32)); + + // TODO: Scroll to make the row in center of viewport. self.move_to(Cursor::new(offset), window, cx); self.update_preferred_column(); diff --git a/crates/ui/src/input/text_wrapper.rs b/crates/ui/src/input/text_wrapper.rs index ce5a47c3..3e7df6a7 100644 --- a/crates/ui/src/input/text_wrapper.rs +++ b/crates/ui/src/input/text_wrapper.rs @@ -13,10 +13,27 @@ pub(super) struct LineWrap { /// 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. + /// The range of the line text in the entire text (not includes ending `\n`). pub(super) range: Range, } +impl LineWrap { + /// Return the bytes length of this line. + pub(super) fn len(&self) -> usize { + self.range.end - self.range.start + } + + /// Get the total number of lines including wrapped lines. + pub(super) fn lines_len(&self) -> usize { + self.wrap_lines + 1 + } + + /// Get the height of this line including wrapped lines. + pub(super) fn height(&self, line_height: Pixels) -> Pixels { + self.lines_len() * line_height + } +} + /// 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