input: Add MovePageUp, MovePageDown keymap to multi-line mode. (#969)

This commit is contained in:
Jason Lee 2025-06-17 20:57:53 +08:00 committed by GitHub
parent 8762e3482a
commit a478f000d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 91 additions and 50 deletions

View file

@ -432,8 +432,6 @@ impl TextElement {
} }
pub(super) struct PrepaintState { pub(super) struct PrepaintState {
/// The visible range (no wrap) of lines in the viewport.
visible_range: Range<usize>,
/// The lines of entire lines. /// The lines of entire lines.
last_layout: LastLayout, last_layout: LastLayout,
/// The lines only contains the visible lines in the viewport, based on `visible_range`. /// The lines only contains the visible lines in the viewport, based on `visible_range`.
@ -798,6 +796,7 @@ impl Element for TextElement {
last_layout: LastLayout { last_layout: LastLayout {
lines: Rc::new(lines), lines: Rc::new(lines),
line_height, line_height,
visible_range,
}, },
scroll_size, scroll_size,
line_numbers, line_numbers,
@ -806,7 +805,6 @@ impl Element for TextElement {
cursor_scroll_offset, cursor_scroll_offset,
current_line_index, current_line_index,
selection_path, selection_path,
visible_range,
} }
} }
@ -824,7 +822,7 @@ impl Element for TextElement {
let focused = focus_handle.is_focused(window); let focused = focus_handle.is_focused(window);
let bounds = prepaint.bounds; let bounds = prepaint.bounds;
let selected_range = self.input.read(cx).selected_range.clone(); let selected_range = self.input.read(cx).selected_range.clone();
let visible_range = &prepaint.visible_range; let visible_range = &prepaint.last_layout.visible_range;
window.handle_input( window.handle_input(
&focus_handle, &focus_handle,

View file

@ -55,17 +55,19 @@ actions!(
Outdent, Outdent,
IndentInline, IndentInline,
OutdentInline, OutdentInline,
Up, MoveUp,
Down, MoveDown,
Left, MoveLeft,
Right, MoveRight,
MoveHome,
MoveEnd,
MovePageUp,
MovePageDown,
SelectUp, SelectUp,
SelectDown, SelectDown,
SelectLeft, SelectLeft,
SelectRight, SelectRight,
SelectAll, SelectAll,
Home,
End,
SelectToStartOfLine, SelectToStartOfLine,
SelectToEndOfLine, SelectToEndOfLine,
SelectToStart, SelectToStart,
@ -84,7 +86,6 @@ actions!(
MoveToEnd, MoveToEnd,
MoveToPreviousWord, MoveToPreviousWord,
MoveToNextWord, MoveToNextWord,
TextChanged,
Escape Escape
] ]
); );
@ -118,10 +119,12 @@ pub fn init(cx: &mut App) {
KeyBinding::new("enter", Enter { secondary: false }, Some(CONTEXT)), KeyBinding::new("enter", Enter { secondary: false }, Some(CONTEXT)),
KeyBinding::new("secondary-enter", Enter { secondary: true }, Some(CONTEXT)), KeyBinding::new("secondary-enter", Enter { secondary: true }, Some(CONTEXT)),
KeyBinding::new("escape", Escape, Some(CONTEXT)), KeyBinding::new("escape", Escape, Some(CONTEXT)),
KeyBinding::new("up", Up, Some(CONTEXT)), KeyBinding::new("up", MoveUp, Some(CONTEXT)),
KeyBinding::new("down", Down, Some(CONTEXT)), KeyBinding::new("down", MoveDown, Some(CONTEXT)),
KeyBinding::new("left", Left, Some(CONTEXT)), KeyBinding::new("left", MoveLeft, Some(CONTEXT)),
KeyBinding::new("right", Right, Some(CONTEXT)), KeyBinding::new("right", MoveRight, Some(CONTEXT)),
KeyBinding::new("pageup", MovePageUp, Some(CONTEXT)),
KeyBinding::new("pagedown", MovePageDown, Some(CONTEXT)),
KeyBinding::new("tab", IndentInline, Some(CONTEXT)), KeyBinding::new("tab", IndentInline, Some(CONTEXT)),
KeyBinding::new("shift-tab", OutdentInline, Some(CONTEXT)), KeyBinding::new("shift-tab", OutdentInline, Some(CONTEXT)),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@ -136,8 +139,8 @@ pub fn init(cx: &mut App) {
KeyBinding::new("shift-right", SelectRight, Some(CONTEXT)), KeyBinding::new("shift-right", SelectRight, Some(CONTEXT)),
KeyBinding::new("shift-up", SelectUp, Some(CONTEXT)), KeyBinding::new("shift-up", SelectUp, Some(CONTEXT)),
KeyBinding::new("shift-down", SelectDown, Some(CONTEXT)), KeyBinding::new("shift-down", SelectDown, Some(CONTEXT)),
KeyBinding::new("home", Home, Some(CONTEXT)), KeyBinding::new("home", MoveHome, Some(CONTEXT)),
KeyBinding::new("end", End, Some(CONTEXT)), KeyBinding::new("end", MoveEnd, Some(CONTEXT)),
KeyBinding::new("shift-home", SelectToStartOfLine, Some(CONTEXT)), KeyBinding::new("shift-home", SelectToStartOfLine, Some(CONTEXT)),
KeyBinding::new("shift-end", SelectToEndOfLine, Some(CONTEXT)), KeyBinding::new("shift-end", SelectToEndOfLine, Some(CONTEXT)),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@ -175,13 +178,13 @@ pub fn init(cx: &mut App) {
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
KeyBinding::new("ctrl-v", Paste, Some(CONTEXT)), KeyBinding::new("ctrl-v", Paste, Some(CONTEXT)),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
KeyBinding::new("ctrl-a", Home, Some(CONTEXT)), KeyBinding::new("ctrl-a", MoveHome, Some(CONTEXT)),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
KeyBinding::new("cmd-left", Home, Some(CONTEXT)), KeyBinding::new("cmd-left", MoveHome, Some(CONTEXT)),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
KeyBinding::new("ctrl-e", End, Some(CONTEXT)), KeyBinding::new("ctrl-e", MoveEnd, Some(CONTEXT)),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
KeyBinding::new("cmd-right", End, Some(CONTEXT)), KeyBinding::new("cmd-right", MoveEnd, Some(CONTEXT)),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
KeyBinding::new("cmd-z", Undo, Some(CONTEXT)), KeyBinding::new("cmd-z", Undo, Some(CONTEXT)),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@ -217,6 +220,8 @@ pub(super) struct LastLayout {
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. /// The line_height of text layout, this will change will InputElement painted.
pub(super) line_height: Pixels, pub(super) line_height: Pixels,
/// The visible range (no wrap) of lines in the viewport.
pub(super) visible_range: Range<usize>,
} }
impl Deref for LastLayout { impl Deref for LastLayout {
@ -555,8 +560,9 @@ impl InputState {
} }
/// Move the cursor vertically by one line (up or down) while preserving the column if possible. /// Move the cursor vertically by one line (up or down) while preserving the column if possible.
/// direction: -1 for up, +1 for down ///
fn move_vertical(&mut self, direction: i32, window: &mut Window, cx: &mut Context<Self>) { /// move_lines: Number of lines to move vertically (positive for down, negative for up).
fn move_vertical(&mut self, move_lines: isize, window: &mut Window, cx: &mut Context<Self>) {
if self.is_single_line() { if self.is_single_line() {
return; return;
} }
@ -568,7 +574,7 @@ impl InputState {
let offset = self.cursor_offset(); let offset = self.cursor_offset();
let preferred_x_offset = self.preferred_x_offset; let preferred_x_offset = self.preferred_x_offset;
let line_height = last_layout.line_height; let line_height = last_layout.line_height;
let (current_line_index, current_sub_line, current_pos) = let (current_line, current_sub_line, current_pos) =
self.line_and_position_for_offset(offset); self.line_and_position_for_offset(offset);
let Some(current_pos) = current_pos else { let Some(current_pos) = current_pos else {
@ -579,30 +585,32 @@ impl InputState {
.preferred_x_offset .preferred_x_offset
.unwrap_or_else(|| current_pos.x + bounds.origin.x); .unwrap_or_else(|| current_pos.x + bounds.origin.x);
let mut new_line_index = current_line_index; let mut new_line = current_line;
let mut new_sub_line = current_sub_line as i32; let mut new_sub_line = current_sub_line as i32;
new_sub_line += if move_lines > 0 { 1 } else { -1 };
new_sub_line += direction;
// Handle moving above the first line // Handle moving above the first line
if direction == -1 && new_line_index == 0 && new_sub_line < 0 { if move_lines < 0 && current_line == 0 && new_sub_line < 0 {
// Move cursor to the beginning of the text // Move cursor to the beginning of the text
self.move_to(0, window, cx); self.move_to(0, window, cx);
self.preferred_x_offset = preferred_x_offset;
return; return;
} }
if new_sub_line < 0 { if new_sub_line < 0 {
if new_line_index > 0 { new_line = new_line
new_line_index -= 1; .saturating_add_signed(move_lines)
new_sub_line = last_layout.lines[new_line_index].wrap_boundaries.len() as i32; .max(0)
} else { .min(last_layout.lines.len().saturating_sub(1));
new_sub_line = 0; new_sub_line = last_layout.lines[new_line].wrap_boundaries.len() as i32;
}
} else { } else {
let max_sub_line = last_layout.lines[new_line_index].wrap_boundaries.len() as i32; let max_sub_line = last_layout.lines[current_line].wrap_boundaries.len() as i32;
if new_sub_line > max_sub_line { if new_sub_line > max_sub_line {
if new_line_index < last_layout.lines.len() - 1 { if new_line < last_layout.lines.len() - 1 {
new_line_index += 1; new_line = new_line
.saturating_add_signed(move_lines)
.max(0)
.min(last_layout.lines.len().saturating_sub(1));
new_sub_line = 0; new_sub_line = 0;
} else { } else {
new_sub_line = max_sub_line; new_sub_line = max_sub_line;
@ -611,11 +619,11 @@ impl InputState {
} }
// If after adjustment, still at the same position, do not proceed // If after adjustment, still at the same position, do not proceed
if new_line_index == current_line_index && new_sub_line == current_sub_line as i32 { if new_line == current_line && new_sub_line == current_sub_line as i32 {
return; return;
} }
let target_line = &last_layout.lines[new_line_index]; let target_line = &last_layout.lines[new_line];
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;
@ -628,11 +636,13 @@ impl InputState {
}; };
let mut prev_lines_offset = 0; let mut prev_lines_offset = 0;
for (i, l) in last_layout.lines.iter().enumerate() { for (_, line) in last_layout
if i == new_line_index { .lines
break; .iter()
} .enumerate()
prev_lines_offset += l.len() + 1; .take_while(|(i, _)| *i < new_line)
{
prev_lines_offset += line.len() + 1;
} }
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());
@ -815,7 +825,7 @@ impl InputState {
self.focus_handle.focus(window); self.focus_handle.focus(window);
} }
pub(super) fn left(&mut self, _: &Left, window: &mut Window, cx: &mut Context<Self>) { pub(super) fn left(&mut self, _: &MoveLeft, window: &mut Window, cx: &mut Context<Self>) {
self.pause_blink_cursor(cx); self.pause_blink_cursor(cx);
if self.selected_range.is_empty() { if self.selected_range.is_empty() {
self.move_to(self.previous_boundary(self.cursor_offset()), window, cx); self.move_to(self.previous_boundary(self.cursor_offset()), window, cx);
@ -824,7 +834,7 @@ impl InputState {
} }
} }
pub(super) fn right(&mut self, _: &Right, window: &mut Window, cx: &mut Context<Self>) { pub(super) fn right(&mut self, _: &MoveRight, window: &mut Window, cx: &mut Context<Self>) {
self.pause_blink_cursor(cx); self.pause_blink_cursor(cx);
if self.selected_range.is_empty() { if self.selected_range.is_empty() {
self.move_to(self.next_boundary(self.selected_range.end), window, cx); self.move_to(self.next_boundary(self.selected_range.end), window, cx);
@ -833,7 +843,7 @@ impl InputState {
} }
} }
pub(super) fn up(&mut self, _: &Up, window: &mut Window, cx: &mut Context<Self>) { pub(super) fn up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
if self.is_single_line() { if self.is_single_line() {
return; return;
} }
@ -849,7 +859,7 @@ impl InputState {
self.move_vertical(-1, window, cx); self.move_vertical(-1, window, cx);
} }
pub(super) fn down(&mut self, _: &Down, window: &mut Window, cx: &mut Context<Self>) { pub(super) fn down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
if self.is_single_line() { if self.is_single_line() {
return; return;
} }
@ -866,6 +876,37 @@ impl InputState {
self.move_vertical(1, window, cx); self.move_vertical(1, window, cx);
} }
pub(super) fn page_up(&mut self, _: &MovePageUp, window: &mut Window, cx: &mut Context<Self>) {
if self.is_single_line() {
return;
}
let Some(last_layout) = &self.last_layout else {
return;
};
let display_lines = (self.input_bounds.size.height / last_layout.line_height) as isize;
self.move_vertical(-display_lines, window, cx);
}
pub(super) fn page_down(
&mut self,
_: &MovePageDown,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.is_single_line() {
return;
}
let Some(last_layout) = &self.last_layout else {
return;
};
let display_lines = (self.input_bounds.size.height / last_layout.line_height) as isize;
self.move_vertical(display_lines, window, cx);
}
pub(super) fn select_left( pub(super) fn select_left(
&mut self, &mut self,
_: &SelectLeft, _: &SelectLeft,
@ -915,13 +956,13 @@ impl InputState {
self.select_to(self.text.len(), window, cx) self.select_to(self.text.len(), window, cx)
} }
pub(super) fn home(&mut self, _: &Home, window: &mut Window, cx: &mut Context<Self>) { pub(super) fn home(&mut self, _: &MoveHome, window: &mut Window, cx: &mut Context<Self>) {
self.pause_blink_cursor(cx); self.pause_blink_cursor(cx);
let offset = self.start_of_line(window, cx); let offset = self.start_of_line(window, cx);
self.move_to(offset, window, cx); self.move_to(offset, window, cx);
} }
pub(super) fn end(&mut self, _: &End, window: &mut Window, cx: &mut Context<Self>) { pub(super) fn end(&mut self, _: &MoveEnd, window: &mut Window, cx: &mut Context<Self>) {
self.pause_blink_cursor(cx); self.pause_blink_cursor(cx);
let offset = self.end_of_line(window, cx); let offset = self.end_of_line(window, cx);
self.move_to(offset, window, cx); self.move_to(offset, window, cx);

View file

@ -214,6 +214,8 @@ impl RenderOnce for TextInput {
.on_action(window.listener_for(&self.state, InputState::down)) .on_action(window.listener_for(&self.state, InputState::down))
.on_action(window.listener_for(&self.state, InputState::select_up)) .on_action(window.listener_for(&self.state, InputState::select_up))
.on_action(window.listener_for(&self.state, InputState::select_down)) .on_action(window.listener_for(&self.state, InputState::select_down))
.on_action(window.listener_for(&self.state, InputState::page_up))
.on_action(window.listener_for(&self.state, InputState::page_down))
}) })
.on_action(window.listener_for(&self.state, InputState::select_all)) .on_action(window.listener_for(&self.state, InputState::select_all))
.on_action(window.listener_for(&self.state, InputState::select_to_start_of_line)) .on_action(window.listener_for(&self.state, InputState::select_to_start_of_line))