From 25f922283ab634586850e8a97319ff26351ee843 Mon Sep 17 00:00:00 2001 From: temportalflux Date: Fri, 3 Jul 2026 19:50:09 -0400 Subject: [PATCH] refactor text position queries by factoring out common logic --- .../gpui_elements/src/editable_text/layout.rs | 33 ++++ .../gpui_elements/src/editable_text/state.rs | 164 ++++++++---------- 2 files changed, 103 insertions(+), 94 deletions(-) diff --git a/crates/gpui_elements/src/editable_text/layout.rs b/crates/gpui_elements/src/editable_text/layout.rs index d49a01dfe6..693dbbc3f5 100644 --- a/crates/gpui_elements/src/editable_text/layout.rs +++ b/crates/gpui_elements/src/editable_text/layout.rs @@ -67,4 +67,37 @@ impl TextLineSegment { self.text_range.contains(&pos) } } + + /// Returns the index of the character within this segment that is closest + /// to the provided screen space position. + /// The character index returned is in absolute space; it is not relative to this segment. + pub fn character_index_at_point(&self, point: Point, line_height: Pixels) -> usize { + let mut offset = 0usize; + if !self.text_range.is_empty() + && let Some(wrapped) = &self.wrapped_line + { + offset = wrapped + .closest_index_for_position(point, line_height) + .unwrap_or_else(|closest| closest) + .min(wrapped.text.len()); + } + self.text_range.start + offset + } + + /// Returns the screen space position of the character at the position provided. + /// The position of the character must be absolute to the string this segment + /// partially represents, it is converted to a relative offset internally. + pub fn position_for_index( + &self, + character_index: usize, + line_height: Pixels, + ) -> Option> { + let wrapped = self.wrapped_line.as_ref()?; + // the position in the text relative to this line segment + let relative_text_pos = character_index + .saturating_sub(self.text_range.start) + .min(wrapped.text.len()); + // the screen position of the character in this line segment + wrapped.position_for_index(relative_text_pos, line_height) + } } diff --git a/crates/gpui_elements/src/editable_text/state.rs b/crates/gpui_elements/src/editable_text/state.rs index 94b8ac218c..075f4e228a 100644 --- a/crates/gpui_elements/src/editable_text/state.rs +++ b/crates/gpui_elements/src/editable_text/state.rs @@ -289,49 +289,61 @@ 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 +/// Parameters used to find a desired TextLineSegment from layout data. +enum TextSegmentQuery { + /// Find the line containing a character at a position (relative to the document, not a given line). + CharacterPosition(usize), + /// Find the line that most closely contains the provided screen position. + ScreenPosition { 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()); + }, + /// Find the line containing a visual row of text, which may be wrapped + /// (relative to the document, not a given line). + Row(usize), +} + +// Screen space (text layout engine output) & String space transformers +impl EditableTextState { + /// Attempts to find a text segment based on the provided query parameters. + /// If found, the returned tuple is the segment & the number of preceeding visual rows. + /// If not found, the resulting "err" is the total number of visual rows of all line segments. + fn find_segment(&self, query: TextSegmentQuery) -> Result<(&TextLineSegment, usize), usize> { + let mut row_count = 0; + for segment in &self.layout_data.lines { + let found = match &query { + TextSegmentQuery::CharacterPosition(pos) => segment.contains_position(*pos, false), + TextSegmentQuery::ScreenPosition { point, line_height } => { + let segment_start_pos_y = segment.pos_y * *line_height; + let segment_height = *line_height * segment.row_count() as f32; + point.y >= segment_start_pos_y && point.y < segment_start_pos_y + segment_height + } + TextSegmentQuery::Row(row_index) => *row_index < row_count + segment.row_count(), + }; + if found { + return Ok((segment, row_count)); + } + row_count += segment.row_count(); } - offset + Err(row_count) } /// 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(); + let storage_len_utf8 = self.as_str().len(); if storage_len_utf8 == 0 { return 0; } - for line in &self.layout_data.lines { - 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; - } + let segment = self.find_segment(TextSegmentQuery::ScreenPosition { point, line_height }); + let Ok((segment, _preceeding_row_count)) = segment else { + return storage_len_utf8; + }; - // the screen position of the caret relative to the text segment - let relative_point = gpui::point(point.x, point.y - segment_start_pos_y); + // the screen position of the caret relative to the text segment + let relative_point = point - gpui::point(Pixels::ZERO, segment.pos_y * line_height); - let offset = Self::character_offset_at_point(line, relative_point, line_height); - return line.text_range.start + offset; - } - - storage_len_utf8 + return segment.character_index_at_point(relative_point, line_height); } fn find_position_in_vertical_direction( @@ -342,28 +354,17 @@ impl EditableTextState { 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 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; - } + let segment = self.find_segment(TextSegmentQuery::Row(target_line_index)); + let Ok((segment, preceeding_row_count)) = segment else { + return (direction > 0).then(|| self.as_str().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); + // calculate the screen space position of the row we are navigating to, + // relative to the y-position of the segment. + let row_index = (target_line_index - preceeding_row_count) as f32; + let relative_point = gpui::point(caret_point.x, row_index * line_height); - let offset = Self::character_offset_at_point(line, relative_point, line_height); - return Some(line.text_range.start + offset); - } - - (direction > 0).then(|| self.as_str().len()) + Some(segment.character_index_at_point(relative_point, line_height)) } fn line_index_and_point_at_caret(&self, line_height: Pixels) -> (usize, Point) { @@ -372,58 +373,33 @@ impl EditableTextState { } let caret_pos = self.caret_pos(); + let segment = self.find_segment(TextSegmentQuery::CharacterPosition(caret_pos)); + let (segment, preceeding_row_count) = match segment { + Ok(segment) => segment, + Err(total_row_count) => return (total_row_count.saturating_sub(1), Point::default()), + }; - // 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()); - } + // Find the screen position relative to this segment where the caret is at. + let relative_point = segment.position_for_index(caret_pos, line_height); - 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()) + let point = relative_point.unwrap_or_default(); + // the visual row offset from the start of the segment + let row_offset = (point.y / line_height).floor() as usize; + (preceeding_row_count + row_offset, point) } fn find_point_for_character_position(&self, character_pos: usize) -> Point { + let segment = self.find_segment(TextSegmentQuery::CharacterPosition(character_pos)); + let Ok((segment, preceeding_row_count)) = segment else { + return Point::default(); + }; let line_height = self.layout_data.line_height; - let mut row_count = 0; - for segment in &self.layout_data.lines { - if !segment.contains_position(character_pos, false) { - row_count += segment.row_count(); - continue; - } - // 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) - }); + // Find the screen position relative to this segment where the caret is at. + let relative_point = segment.position_for_index(character_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() + let line_origin = point(Pixels::ZERO, preceeding_row_count as f32 * line_height); + return line_origin + relative_point.unwrap_or_default(); } }