diff --git a/crates/gpui_elements/src/editable_text/shared_element.rs b/crates/gpui_elements/src/editable_text/shared_element.rs index 13fbac94b3..7d55fbb2d0 100644 --- a/crates/gpui_elements/src/editable_text/shared_element.rs +++ b/crates/gpui_elements/src/editable_text/shared_element.rs @@ -167,6 +167,7 @@ pub trait EditableTextElement: InteractiveElement + EditableInputActionElement { &runs, cx, ); + let text_len = text.len(); let wrapped_lines = window .text_system() @@ -179,6 +180,17 @@ pub trait EditableTextElement: InteractiveElement + EditableInputActionElement { ) .unwrap_or_default(); + let line_ranges = wrapped_lines.iter().fold( + Vec::>::new(), + |mut ranges, line| { + let prev_end = + ranges.last().map(|range| range.end).unwrap_or_default(); + ranges.push(prev_end..prev_end + line.len()); + ranges + }, + ); + println!("{line_ranges:?}"); + // Build the size of the text and convert the wrapped_lines into // lines that will be cached in state and painted. let mut size: Size = Size::default(); @@ -191,7 +203,13 @@ pub trait EditableTextElement: InteractiveElement + EditableInputActionElement { size.width = size.width.max(line_size.width).ceil(); let num_visual_lines = line.wrap_boundaries().len() + 1; - let line_len = line.len(); + let mut line_len = line.len(); + if line_len < text_len { + // to offset for new-line characters that are + // omitted from WrappedLine range + line_len += 1; + } + lines.push(TextLineSegment { text_range: line_start..line_start + line_len, wrapped_line: Some(Arc::new(line)), @@ -382,6 +400,9 @@ pub trait EditableTextElement: InteractiveElement + EditableInputActionElement { segment.text_range.contains(&caret_pos) || caret_pos == segment.text_range.end }; if is_cursor_in_line && let Some(wrapped) = &segment.wrapped_line { + // TODO: when the cursor is functionally at a character that is on the next line + // (a line that spans multiple rows), the cursor displays at the end of the previous + // row instead of the start of the next row. let local_offset = caret_pos.saturating_sub(segment.text_range.start); let caret_px = wrapped .position_for_index(local_offset, line_height) diff --git a/crates/gpui_elements/src/editable_text/shared_state.rs b/crates/gpui_elements/src/editable_text/shared_state.rs index 97d92392f5..37465ea3be 100644 --- a/crates/gpui_elements/src/editable_text/shared_state.rs +++ b/crates/gpui_elements/src/editable_text/shared_state.rs @@ -73,8 +73,18 @@ pub(super) struct TextLineSegment { pub pos_y: usize, /// The number of segments up to and including this segment in the literal line that has been wrapped. /// There may be other segments after this one with a larger counter. + /// TODO: Deprecated in favor of `wrap_boundaries` pub num_visual_lines: usize, } +impl TextLineSegment { + pub fn wrap_boundaries(&self) -> usize { + let count = self + .wrapped_line + .as_ref() + .map(|line| line.wrap_boundaries().len()); + count.unwrap_or_default() + 1 + } +} impl Focusable for TextInputStateBase { fn focus_handle(&self, _: &App) -> FocusHandle { @@ -346,7 +356,7 @@ impl TextInputStateBase { } impl TextInputStateBase { - fn move_to(&mut self, caret_pos: usize, cx: &mut impl TextStateNotifier) { + pub fn move_to(&mut self, caret_pos: usize, cx: &mut impl TextStateNotifier) { //cx.emit(CursorTrigger::PauseBlinkingForUserAction); let caret_pos = caret_pos.min(self.storage.content_utf8().len()); self.selected_range = caret_pos..caret_pos; @@ -354,7 +364,7 @@ impl TextInputStateBase { cx.notify_changed(); } - fn select_to(&mut self, caret_pos: usize, cx: &mut impl TextStateNotifier) { + pub fn select_to(&mut self, caret_pos: usize, cx: &mut impl TextStateNotifier) { //cx.emit(CursorTrigger::PauseBlinkingForUserAction); let caret_pos = caret_pos.min(self.storage().content_utf8().len()); self.selected_range.start = caret_pos; @@ -403,6 +413,82 @@ impl TextInputStateBase { self.move_to(caret_pos, cx); } + pub 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.wrap_boundaries(); + } + + (segment_index.saturating_sub(1), Point::default()) + } + + pub 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); + println!("{line_index:?} {point:?}"); + let line_index = 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.wrap_boundaries(); + + 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; + println!("{result:?}"); + return Some(result); + } + + return Some(segment.text_range.start); + } + + current_visual_line += wrap_boundary_len; + } + + (direction > 0).then(|| self.storage.content_utf8().len()) + } + pub fn select_all(&mut self, cx: &mut impl TextStateNotifier) { self.selected_range = 0..self.storage.content_utf8().len(); cx.notify_changed(); diff --git a/crates/gpui_elements/src/editable_text/text_area_state.rs b/crates/gpui_elements/src/editable_text/text_area_state.rs index d3f13aa116..9968873014 100644 --- a/crates/gpui_elements/src/editable_text/text_area_state.rs +++ b/crates/gpui_elements/src/editable_text/text_area_state.rs @@ -157,6 +157,7 @@ impl<'app> EditableTextActionHandler<'app> for TextAreaState { } fn insert_enter(&mut self, _: &Enter, window: &mut Window, cx: &mut Self::Context) { + // TODO: Why is the cursor appearing at the start of the entire field instead of on the new line? self.replace_text_in_range(None, "\n", window, cx); } @@ -214,12 +215,22 @@ impl<'app> EditableTextActionHandler<'app> for TextAreaState { .nav_linear(NavigationDirection::Forward, TextBoundary::Graphmeme, cx); } - fn nav_up(&mut self, _: &Up, _w: &mut Window, cx: &mut Self::Context) { - // TODO: implement + fn nav_up(&mut self, _: &Up, window: &mut Window, cx: &mut Self::Context) { + if let Some(caret_pos) = self + .internal + .find_position_in_vertical_direction(-1, window.line_height()) + { + self.internal.move_to(caret_pos, cx); + } } - fn nav_down(&mut self, _: &Down, _w: &mut Window, cx: &mut Self::Context) { - // TODO: implement + fn nav_down(&mut self, _: &Down, window: &mut Window, cx: &mut Self::Context) { + if let Some(caret_pos) = self + .internal + .find_position_in_vertical_direction(1, window.line_height()) + { + self.internal.move_to(caret_pos, cx); + } } fn nav_line_start(&mut self, _: &Home, _w: &mut Window, cx: &mut Self::Context) { @@ -266,12 +277,26 @@ impl<'app> EditableTextActionHandler<'app> for TextAreaState { .select_linear(NavigationDirection::Forward, TextBoundary::Graphmeme, cx); } - fn select_up(&mut self, _: &SelectUp, _w: &mut Window, cx: &mut Self::Context) { - // TODO: implement + fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Self::Context) { + if let Some(caret_pos) = self + .internal + .find_position_in_vertical_direction(-1, window.line_height()) + { + self.internal.select_to(caret_pos, cx); + //self.scroll_to_cursor(); + cx.notify_changed(); + } } - fn select_down(&mut self, _: &SelectDown, _w: &mut Window, cx: &mut Self::Context) { - // TODO: implement + fn select_down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Self::Context) { + if let Some(caret_pos) = self + .internal + .find_position_in_vertical_direction(1, window.line_height()) + { + self.internal.select_to(caret_pos, cx); + //self.scroll_to_cursor(); + cx.notify_changed(); + } } fn select_start(&mut self, _: &SelectToBeginning, _w: &mut Window, cx: &mut Self::Context) {