From 50b01e0532c0dcd2f26cc40ea1bab29de89a3c69 Mon Sep 17 00:00:00 2001 From: temportalflux Date: Fri, 3 Jul 2026 19:39:11 -0400 Subject: [PATCH] first pass at simplifying text position queries --- .../gpui_elements/src/editable_text/state.rs | 189 +++++++++--------- 1 file changed, 97 insertions(+), 92 deletions(-) diff --git a/crates/gpui_elements/src/editable_text/state.rs b/crates/gpui_elements/src/editable_text/state.rs index e69abbb62c..94b8ac218c 100644 --- a/crates/gpui_elements/src/editable_text/state.rs +++ b/crates/gpui_elements/src/editable_text/state.rs @@ -3,7 +3,7 @@ use crate::editable_text::{ actions::EditableTextActionHandler, caret::{Caret, CaretNotify}, history::EditableTextHistory, - layout::EditableTextLayoutResult, + layout::{EditableTextLayoutResult, TextLineSegment}, }; use gpui::{ App, Bounds, ClipboardItem, Context, ElementId, Entity, EntityInputHandler, EventEmitter, @@ -291,6 +291,23 @@ impl EditableTextState { // Screen space (text layout engine output) & String space transformers impl EditableTextState { + fn character_offset_at_point( + line: &TextLineSegment, // TODO: move to be a member of segment + point: Point, + line_height: Pixels, + ) -> usize { + let mut offset = 0usize; + if !line.text_range.is_empty() + && let Some(wrapped) = &line.wrapped_line + { + offset = wrapped + .closest_index_for_position(point, line_height) + .unwrap_or_else(|closest| closest) + .min(wrapped.text.len()); + } + offset + } + /// Returns the utf-8 character position of the start of the line that contains the provided pixel-point. fn index_for_pixel_point(&self, point: Point, line_height: Pixels) -> usize { let storage_len_utf8 = self.storage.content_utf8().len(); @@ -299,104 +316,91 @@ impl EditableTextState { } for line in &self.layout_data.lines { - let y_offset = line.pos_y * line_height; - let line_height_total = line_height * line.row_count() as f32; - - if point.y >= y_offset && point.y < y_offset + line_height_total { - if line.text_range.is_empty() { - return line.text_range.start; - } - let Some(wrapped) = &line.wrapped_line else { - return line.text_range.start; - }; - - let relative_y = point.y - y_offset; - let relative_point = gpui::point(point.x, relative_y); - - let closest_result = - wrapped.closest_index_for_position(relative_point, line_height); - - let local_idx = closest_result.unwrap_or_else(|closest| closest); - let clamped = local_idx.min(wrapped.text.len()); - return line.text_range.start + clamped; + let segment_start_pos_y = line.pos_y * line_height; + let segment_height = line_height * line.row_count() as f32; + let segment_contains_point = + point.y >= segment_start_pos_y && point.y < segment_start_pos_y + segment_height; + if !segment_contains_point { + continue; } + + // the screen position of the caret relative to the text segment + let relative_point = gpui::point(point.x, point.y - segment_start_pos_y); + + let offset = Self::character_offset_at_point(line, relative_point, line_height); + return line.text_range.start + offset; } storage_len_utf8 } - fn line_index_and_point_at_caret(&self, line_height: Pixels) -> (usize, Point) { - if self.layout_data.lines.is_empty() { - return (0, Point::default()); - } - - let pos = self.caret_pos(); - - // accumulated vertical line count (not literal lines, since they can be wrapped) - let mut segment_index = 0; - for segment in &self.layout_data.lines { - if segment.text_range.is_empty() { - if pos == segment.text_range.start { - return (segment_index, Point::default()); - } - } - - if segment.text_range.contains(&pos) { - if let Some(wrapped) = &segment.wrapped_line { - let pos_in_segment = (pos - segment.text_range.start).min(wrapped.text.len()); - if let Some(point) = wrapped.position_for_index(pos_in_segment, line_height) { - let visual_line_within = (point.y / line_height).floor() as usize; - return (segment_index + visual_line_within, point); - } - } - return (segment_index, Point::default()); - } - - segment_index += segment.row_count(); - } - - (segment_index.saturating_sub(1), Point::default()) - } - fn find_position_in_vertical_direction( &self, direction: i32, line_height: Pixels, ) -> Option { - let (line_index, point) = self.line_index_and_point_at_caret(line_height); - let line_index = line_index.saturating_add_signed(direction as isize); + let (caret_line_index, caret_point) = self.line_index_and_point_at_caret(line_height); + let target_line_index = caret_line_index.saturating_add_signed(direction as isize); - let mut current_visual_line = 0; - for segment in &self.layout_data.lines { - let wrap_boundary_len = segment.row_count(); - - if line_index < current_visual_line + wrap_boundary_len { - let visual_line_within_layout = line_index - current_visual_line; - - if segment.text_range.is_empty() { - return Some(segment.text_range.start); - } - - if let Some(wrapped) = &segment.wrapped_line { - let y_within_wrapped = line_height * visual_line_within_layout as f32; - let target_point = gpui::point(point.x, y_within_wrapped); - - let closest_result = - wrapped.closest_index_for_position(target_point, line_height); - - let closest_idx = closest_result.unwrap_or_else(|closest| closest); - let clamped = closest_idx.min(wrapped.text.len()); - let result = segment.text_range.start + clamped; - return Some(result); - } - - return Some(segment.text_range.start); + let mut visual_row_index = 0; + for line in &self.layout_data.lines { + let num_rows_in_segment = line.row_count(); + let segment_contains_target = + target_line_index < visual_row_index + num_rows_in_segment; + if !segment_contains_target { + visual_row_index += num_rows_in_segment; + continue; } - current_visual_line += wrap_boundary_len; + // the index/offset of the row in this segment that we are navigating to + let desired_row_in_segment = target_line_index - visual_row_index; + // the y screen position relative to the segment of the desired row + let row_pos_y = line_height * desired_row_in_segment as f32; + // the position of the caret in the row + let relative_point = gpui::point(caret_point.x, row_pos_y); + + let offset = Self::character_offset_at_point(line, relative_point, line_height); + return Some(line.text_range.start + offset); } - (direction > 0).then(|| self.storage.content_utf8().len()) + (direction > 0).then(|| self.as_str().len()) + } + + fn line_index_and_point_at_caret(&self, line_height: Pixels) -> (usize, Point) { + if self.layout_data.lines.is_empty() { + return (0, Point::default()); + } + + let caret_pos = self.caret_pos(); + + // accumulated vertical line count (not literal lines, since they can be wrapped) + let mut visual_row_index = 0; + for segment in &self.layout_data.lines { + // text segment is empty & and the caret is at the newline + if segment.text_range.is_empty() && caret_pos == segment.text_range.start { + return (visual_row_index, Point::default()); + } + + if !segment.text_range.contains(&caret_pos) { + visual_row_index += segment.row_count(); + continue; + } + + // Find the screen position relative to this segment where the caret is at. + let point = segment.wrapped_line.as_ref().map(|wrapped| { + // the character location of the caret relative to the text segment + let relative_text_pos = + (caret_pos - segment.text_range.start).min(wrapped.text.len()); + // the screen position of the text position (if possible, may be none) + wrapped.position_for_index(relative_text_pos, line_height) + }); + let point = point.flatten().unwrap_or_default(); + // the visual row offset from the start of the segment + let row_offset = (point.y / line_height).floor() as usize; + return (visual_row_index + row_offset, point); + } + + (visual_row_index.saturating_sub(1), Point::default()) } fn find_point_for_character_position(&self, character_pos: usize) -> Point { @@ -408,15 +412,16 @@ impl EditableTextState { continue; } - let line_origin = point(Pixels::ZERO, row_count * line_height); - return match &segment.wrapped_line { - None => line_origin, - Some(wrapped) => { - let local_offset = character_pos.saturating_sub(segment.text_range.start); - let position = wrapped.position_for_index(local_offset, line_height); - position.unwrap_or_default() + line_origin - } - }; + // Find the screen position relative to this segment where the caret is at. + let relative_point = segment.wrapped_line.as_ref().map(|wrapped| { + // the position in the text relative to this line segment + let relative_text_pos = character_pos.saturating_sub(segment.text_range.start); + // the screen position of the character in this line segment + wrapped.position_for_index(relative_text_pos, line_height) + }); + + let line_origin = point(Pixels::ZERO, row_count as f32 * line_height); + return line_origin + relative_point.flatten().unwrap_or_default(); } Point::default() }