input: Fix move up/down to keep same column. (#968)

Close #908

https://github.com/user-attachments/assets/f050dd1a-501c-4784-a45b-78ba77cdf19c
This commit is contained in:
Jason Lee 2025-06-17 15:23:37 +08:00 committed by GitHub
parent 3d7f1f8a73
commit edf9ed7e5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 46 additions and 41 deletions

View file

@ -797,6 +797,7 @@ impl Element for TextElement {
bounds, bounds,
last_layout: LastLayout { last_layout: LastLayout {
lines: Rc::new(lines), lines: Rc::new(lines),
line_height,
}, },
scroll_size, scroll_size,
line_numbers, line_numbers,
@ -933,7 +934,6 @@ impl Element for TextElement {
input.last_layout = Some(prepaint.last_layout.clone()); input.last_layout = Some(prepaint.last_layout.clone());
input.last_bounds = Some(bounds); input.last_bounds = Some(bounds);
input.last_cursor_offset = Some(input.cursor_offset()); input.last_cursor_offset = Some(input.cursor_offset());
input.last_line_height = line_height;
input.set_input_bounds(input_bounds, cx); input.set_input_bounds(input_bounds, cx);
input.last_selected_range = Some(selected_range); input.last_selected_range = Some(selected_range);
input.scroll_size = prepaint.scroll_size; input.scroll_size = prepaint.scroll_size;

View file

@ -35,13 +35,14 @@ impl DiagnosticPopover {
let Some(range) = self.marker.range.as_ref() else { let Some(range) = self.marker.range.as_ref() else {
return None; return None;
}; };
let line_number_width = self.state.read(cx).line_number_width;
let (_, _, start_pos) = self let (_, _, start_pos) = self
.state .state
.read(cx) .read(cx)
.line_and_position_for_offset(range.start); .line_and_position_for_offset(range.start);
start_pos start_pos.map(|pos| pos + Point::new(line_number_width, px(0.)))
} }
pub(super) fn show(&mut self, cx: &mut Context<Self>) { pub(super) fn show(&mut self, cx: &mut Context<Self>) {

View file

@ -213,7 +213,10 @@ pub fn init(cx: &mut App) {
#[derive(Clone)] #[derive(Clone)]
pub(super) struct LastLayout { pub(super) struct LastLayout {
/// The last layout lines.
pub(super) lines: Rc<SmallVec<[WrappedLine; 1]>>, pub(super) lines: Rc<SmallVec<[WrappedLine; 1]>>,
/// The line_height of text layout, this will change will InputElement painted.
pub(super) line_height: Pixels,
} }
impl Deref for LastLayout { impl Deref for LastLayout {
@ -243,11 +246,8 @@ pub struct InputState {
pub(super) selection_reversed: bool, pub(super) selection_reversed: bool,
/// The marked range is the temporary insert text on IME typing. /// The marked range is the temporary insert text on IME typing.
pub(super) marked_range: Option<Range<usize>>, pub(super) marked_range: Option<Range<usize>>,
/// The last layout lines.
pub(super) last_layout: Option<LastLayout>, pub(super) last_layout: Option<LastLayout>,
pub(super) last_cursor_offset: Option<usize>, pub(super) last_cursor_offset: Option<usize>,
/// The line_height of text layout, this will change will InputElement painted.
pub(super) last_line_height: Pixels,
/// The input container bounds /// The input container bounds
pub(super) input_bounds: Bounds<Pixels>, pub(super) input_bounds: Bounds<Pixels>,
/// The text bounds /// The text bounds
@ -272,7 +272,7 @@ pub struct InputState {
/// Popover /// Popover
diagnostic_popover: Option<Entity<DiagnosticPopover>>, diagnostic_popover: Option<Entity<DiagnosticPopover>>,
/// To remember the horizontal column (x-coordinate) of the cursor position. /// To remember the horizontal column (x-coordinate) of the cursor position for keep column for move up/down.
preferred_x_offset: Option<Pixels>, preferred_x_offset: Option<Pixels>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@ -334,7 +334,6 @@ impl InputState {
last_layout: None, last_layout: None,
last_bounds: None, last_bounds: None,
last_selected_range: None, last_selected_range: None,
last_line_height: px(20.),
last_cursor_offset: None, last_cursor_offset: None,
scroll_handle: ScrollHandle::new(), scroll_handle: ScrollHandle::new(),
scroll_state: ScrollbarState::default(), scroll_state: ScrollbarState::default(),
@ -511,18 +510,15 @@ impl InputState {
/// Called after moving the cursor. Updates preferred_x_offset if we know where the cursor now is. /// Called after moving the cursor. Updates preferred_x_offset if we know where the cursor now is.
fn update_preferred_x_offset(&mut self, _cx: &mut Context<Self>) { fn update_preferred_x_offset(&mut self, _cx: &mut Context<Self>) {
if let (Some(_), Some(bounds)) = (&self.last_layout, &self.last_bounds) { let (Some(_), Some(bounds)) = (&self.last_layout, &self.last_bounds) else {
let offset = self.cursor_offset(); return;
};
// Find which line and sub-line the cursor is on and its position // Find which line and sub-line the cursor is on and its position
let (_line_index, _sub_line_index, cursor_pos) = let (_, _, cursor_pos) = self.line_and_position_for_offset(self.cursor_offset());
self.line_and_position_for_offset(offset);
if let Some(pos) = cursor_pos { if let Some(pos) = cursor_pos {
// Adjust by scroll offset self.preferred_x_offset = Some(pos.x + bounds.origin.x);
let scroll_offset = bounds.origin;
self.preferred_x_offset = Some(pos.x + scroll_offset.x);
}
} }
} }
@ -537,19 +533,18 @@ impl InputState {
&self, &self,
offset: usize, offset: usize,
) -> (usize, usize, Option<Point<Pixels>>) { ) -> (usize, usize, Option<Point<Pixels>>) {
let Some(lines) = &self.last_layout else { let Some(last_layout) = &self.last_layout else {
return (0, 0, None); return (0, 0, None);
}; };
let line_height = self.last_line_height; let line_height = last_layout.line_height;
let line_number_width = self.line_number_width;
let mut prev_lines_offset = 0; let mut prev_lines_offset = 0;
let mut y_offset = px(0.); let mut y_offset = px(0.);
for (line_index, line) in lines.iter().enumerate() { for (line_index, line) in last_layout.lines.iter().enumerate() {
let local_offset = offset.saturating_sub(prev_lines_offset); let local_offset = offset.saturating_sub(prev_lines_offset);
if let Some(pos) = line.position_for_index(local_offset, line_height) { if let Some(pos) = line.position_for_index(local_offset, line_height) {
let sub_line_index = (pos.y.0 / line_height.0) as usize; let sub_line_index = (pos.y.0 / line_height.0) as usize;
let adjusted_pos = point(pos.x + line_number_width, pos.y + y_offset); let adjusted_pos = point(pos.x, pos.y + y_offset);
return (line_index, sub_line_index, Some(adjusted_pos)); return (line_index, sub_line_index, Some(adjusted_pos));
} }
@ -566,12 +561,13 @@ impl InputState {
return; return;
} }
let (Some(lines), Some(bounds)) = (&self.last_layout, &self.last_bounds) else { let (Some(last_layout), Some(bounds)) = (&self.last_layout, &self.last_bounds) else {
return; return;
}; };
let offset = self.cursor_offset(); let offset = self.cursor_offset();
let line_height = self.last_line_height; let preferred_x_offset = self.preferred_x_offset;
let line_height = last_layout.line_height;
let (current_line_index, current_sub_line, current_pos) = let (current_line_index, current_sub_line, current_pos) =
self.line_and_position_for_offset(offset); self.line_and_position_for_offset(offset);
@ -598,14 +594,14 @@ impl InputState {
if new_sub_line < 0 { if new_sub_line < 0 {
if new_line_index > 0 { if new_line_index > 0 {
new_line_index -= 1; new_line_index -= 1;
new_sub_line = lines[new_line_index].wrap_boundaries.len() as i32; new_sub_line = last_layout.lines[new_line_index].wrap_boundaries.len() as i32;
} else { } else {
new_sub_line = 0; new_sub_line = 0;
} }
} else { } else {
let max_sub_line = lines[new_line_index].wrap_boundaries.len() as i32; let max_sub_line = last_layout.lines[new_line_index].wrap_boundaries.len() as i32;
if new_sub_line > max_sub_line { if new_sub_line > max_sub_line {
if new_line_index < lines.len() - 1 { if new_line_index < last_layout.lines.len() - 1 {
new_line_index += 1; new_line_index += 1;
new_sub_line = 0; new_sub_line = 0;
} else { } else {
@ -619,7 +615,7 @@ impl InputState {
return; return;
} }
let target_line = &lines[new_line_index]; let target_line = &last_layout.lines[new_line_index];
let line_x = current_x - bounds.origin.x; let line_x = current_x - bounds.origin.x;
let target_sub_line = new_sub_line as usize; let target_sub_line = new_sub_line as usize;
@ -627,12 +623,12 @@ impl InputState {
let index_res = target_line.index_for_position(approx_pos, line_height); let index_res = target_line.index_for_position(approx_pos, line_height);
let new_local_index = match index_res { let new_local_index = match index_res {
Ok(i) => i + 1, Ok(i) => i,
Err(i) => i, Err(i) => i,
}; };
let mut prev_lines_offset = 0; let mut prev_lines_offset = 0;
for (i, l) in lines.iter().enumerate() { for (i, l) in last_layout.lines.iter().enumerate() {
if i == new_line_index { if i == new_line_index {
break; break;
} }
@ -642,6 +638,8 @@ impl InputState {
let new_offset = (prev_lines_offset + new_local_index).min(self.text.len()); let new_offset = (prev_lines_offset + new_local_index).min(self.text.len());
self.selected_range = new_offset..new_offset; self.selected_range = new_offset..new_offset;
self.pause_blink_cursor(cx); self.pause_blink_cursor(cx);
// Set back the preferred_x_offset
self.preferred_x_offset = preferred_x_offset;
cx.notify(); cx.notify();
} }
@ -1515,10 +1513,15 @@ impl InputState {
pub(super) fn on_scroll_wheel( pub(super) fn on_scroll_wheel(
&mut self, &mut self,
event: &ScrollWheelEvent, event: &ScrollWheelEvent,
_window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let delta = event.delta.pixel_delta(self.last_line_height); let line_height = self
.last_layout
.as_ref()
.map(|layout| layout.line_height)
.unwrap_or(window.line_height());
let delta = event.delta.pixel_delta(line_height);
self.update_scroll_offset(Some(self.scroll_handle.offset() + delta), cx); self.update_scroll_offset(Some(self.scroll_handle.offset() + delta), cx);
self.diagnostic_popover = None; self.diagnostic_popover = None;
} }
@ -1663,12 +1666,13 @@ impl InputState {
return 0; return 0;
} }
let (Some(bounds), Some(lines)) = (self.last_bounds.as_ref(), self.last_layout.as_ref()) let (Some(bounds), Some(last_layout)) =
(self.last_bounds.as_ref(), self.last_layout.as_ref())
else { else {
return 0; return 0;
}; };
let line_height = self.last_line_height; let line_height = last_layout.line_height;
// TIP: About the IBeam cursor // TIP: About the IBeam cursor
// //
@ -1685,7 +1689,7 @@ impl InputState {
let mut index = 0; let mut index = 0;
let mut y_offset = px(0.); let mut y_offset = px(0.);
for (_, line) in lines.iter().enumerate() { for (_, line) in last_layout.lines.iter().enumerate() {
let line_origin = self.line_origin_with_y_offset(&mut y_offset, &line, line_height); let line_origin = self.line_origin_with_y_offset(&mut y_offset, &line, line_height);
let pos = inner_position - line_origin; let pos = inner_position - line_origin;
@ -2172,8 +2176,8 @@ impl EntityInputHandler for InputState {
_window: &mut Window, _window: &mut Window,
_cx: &mut Context<Self>, _cx: &mut Context<Self>,
) -> Option<Bounds<Pixels>> { ) -> Option<Bounds<Pixels>> {
let line_height = self.last_line_height; let last_layout = self.last_layout.as_ref()?;
let lines = self.last_layout.as_ref()?; let line_height = last_layout.line_height;
let range = self.range_from_utf16(&range_utf16); let range = self.range_from_utf16(&range_utf16);
let mut start_origin = None; let mut start_origin = None;
@ -2182,7 +2186,7 @@ impl EntityInputHandler for InputState {
let mut y_offset = px(0.); let mut y_offset = px(0.);
let mut index_offset = 0; let mut index_offset = 0;
for line in lines.iter() { for line in last_layout.lines.iter() {
if start_origin.is_some() && end_origin.is_some() { if start_origin.is_some() && end_origin.is_some() {
break; break;
} }
@ -2225,11 +2229,11 @@ impl EntityInputHandler for InputState {
_window: &mut Window, _window: &mut Window,
_cx: &mut Context<Self>, _cx: &mut Context<Self>,
) -> Option<usize> { ) -> Option<usize> {
let line_height = self.last_line_height; let last_layout = self.last_layout.as_ref()?;
let line_height = last_layout.line_height;
let line_point = self.last_bounds?.localize(&point)?; let line_point = self.last_bounds?.localize(&point)?;
let lines = self.last_layout.as_ref()?;
for line in lines.iter() { for line in last_layout.lines.iter() {
if let Ok(utf8_index) = line.index_for_position(line_point, line_height) { if let Ok(utf8_index) = line.index_for_position(line_point, line_height) {
return Some(self.offset_to_utf16(utf8_index)); return Some(self.offset_to_utf16(utf8_index));
} }