implement coarse approach to navigating up and down a large textarea
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user