implement coarse approach to navigating up and down a large textarea

This commit is contained in:
temportalflux
2026-07-11 09:31:07 -04:00
parent a9d139ba47
commit 3cff5195e2
3 changed files with 143 additions and 11 deletions
@@ -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::<Range<usize>>::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<Pixels> = 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)
@@ -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<Pixels>) {
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<usize> {
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();
@@ -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) {