Skip to content

Commit 7c77f5b

Browse files
authored
Add smooth scrolling to menus (#891)
* implement smooth scrolling for ide menu * implement smooth scrolling for columnar_menu * fmt * fix selection not shown on menu open * change comments * fix bug with capped completion height in ide_menu * use correct prompt size * fix remaining lines and undo prompt_lines change * add prompt_height to remaining lines * fix painting with large prompt
1 parent cf2887a commit 7c77f5b

File tree

3 files changed

+72
-21
lines changed

3 files changed

+72
-21
lines changed

src/menu/columnar_menu.rs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ pub struct ColumnarMenu {
6363
col_pos: u16,
6464
/// row position in the menu. Starts from 0
6565
row_pos: u16,
66+
/// Number of values that are skipped when printing,
67+
/// depending on selected value and terminal height
68+
skip_values: u16,
6669
/// Event sent to the menu
6770
event: Option<MenuEvent>,
6871
/// Longest suggestion found in the values
@@ -82,6 +85,7 @@ impl Default for ColumnarMenu {
8285
values: Vec::new(),
8386
col_pos: 0,
8487
row_pos: 0,
88+
skip_values: 0,
8589
event: None,
8690
longest_suggestion: 0,
8791
input: None,
@@ -647,6 +651,26 @@ impl Menu for ColumnarMenu {
647651
self.working_details.columns = possible_cols;
648652
}
649653
}
654+
655+
let mut available_lines = painter.remaining_lines_real();
656+
// Handle the case where a prompt uses the entire screen.
657+
// Drawing the menu has priority over the drawing the prompt.
658+
if available_lines == 0 {
659+
available_lines = painter.remaining_lines().min(self.min_rows());
660+
}
661+
662+
let first_visible_row = self.skip_values / self.get_cols();
663+
664+
self.skip_values = if self.row_pos <= first_visible_row {
665+
// Selection is above the visible area, scroll up
666+
self.row_pos * self.get_cols()
667+
} else if self.row_pos >= first_visible_row + available_lines {
668+
// Selection is below the visible area, scroll down
669+
(self.row_pos.saturating_sub(available_lines) + 1) * self.get_cols()
670+
} else {
671+
// Selection is within the visible area
672+
self.skip_values
673+
};
650674
}
651675
}
652676

@@ -673,19 +697,12 @@ impl Menu for ColumnarMenu {
673697
if self.get_values().is_empty() {
674698
self.no_records_msg(use_ansi_coloring)
675699
} else {
676-
// The skip values represent the number of lines that should be skipped
677-
// while printing the menu
678-
let skip_values = if self.row_pos >= available_lines {
679-
let skip_lines = self.row_pos.saturating_sub(available_lines) + 1;
680-
(skip_lines * self.get_cols()) as usize
681-
} else {
682-
0
683-
};
684-
685700
// It seems that crossterm prefers to have a complete string ready to be printed
686701
// rather than looping through the values and printing multiple things
687702
// This reduces the flickering when printing the menu
688703
let available_values = (available_lines * self.get_cols()) as usize;
704+
let skip_values = self.skip_values as usize;
705+
689706
self.get_values()
690707
.iter()
691708
.skip(skip_values)

src/menu/ide_menu.rs

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ pub struct IdeMenu {
144144
values: Vec<Suggestion>,
145145
/// Selected value. Starts at 0
146146
selected: u16,
147+
/// Number of values that are skipped when printing,
148+
/// depending on selected value and terminal height
149+
skip_values: u16,
147150
/// Event sent to the menu
148151
event: Option<MenuEvent>,
149152
/// Longest suggestion found in the values
@@ -161,6 +164,7 @@ impl Default for IdeMenu {
161164
working_details: IdeMenuDetails::default(),
162165
values: Vec::new(),
163166
selected: 0,
167+
skip_values: 0,
164168
event: None,
165169
longest_suggestion: 0,
166170
input: None,
@@ -832,6 +836,29 @@ impl Menu for IdeMenu {
832836

833837
self.working_details.space_left = space_left;
834838
self.working_details.space_right = space_right;
839+
840+
let mut available_lines = painter
841+
.remaining_lines_real()
842+
.min(self.default_details.max_completion_height);
843+
844+
// Handle the case where a prompt uses the entire screen.
845+
// Drawing the menu has priority over the drawing the prompt.
846+
if available_lines == 0 {
847+
available_lines = painter.remaining_lines().min(self.min_rows());
848+
}
849+
850+
let visible_items = available_lines.saturating_sub(border_width);
851+
852+
self.skip_values = if self.selected <= self.skip_values {
853+
// Selection is above the visible area
854+
self.selected
855+
} else if self.selected >= self.skip_values + visible_items {
856+
// Selection is below the visible area
857+
self.selected.saturating_sub(visible_items) + 1
858+
} else {
859+
// Selection is within the visible area
860+
self.skip_values
861+
}
835862
}
836863
}
837864

@@ -865,17 +892,7 @@ impl Menu for IdeMenu {
865892
};
866893

867894
let available_lines = available_lines.min(self.default_details.max_completion_height);
868-
// The skip values represent the number of lines that should be skipped
869-
// while printing the menu
870-
let skip_values = if self.selected >= available_lines.saturating_sub(border_width) {
871-
let skip_lines = self
872-
.selected
873-
.saturating_sub(available_lines.saturating_sub(border_width))
874-
+ 1;
875-
skip_lines as usize
876-
} else {
877-
0
878-
};
895+
let skip_values = self.skip_values as usize;
879896

880897
let available_values = available_lines.saturating_sub(border_width) as usize;
881898

src/painting/painter.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ pub struct Painter {
9191
// Stdout
9292
stdout: W,
9393
prompt_start_row: u16,
94+
// The number of lines that the prompt takes up
95+
prompt_height: u16,
9496
terminal_size: (u16, u16),
9597
last_required_lines: u16,
9698
large_buffer: bool,
@@ -103,6 +105,7 @@ impl Painter {
103105
Painter {
104106
stdout,
105107
prompt_start_row: 0,
108+
prompt_height: 0,
106109
terminal_size: (0, 0),
107110
last_required_lines: 0,
108111
large_buffer: false,
@@ -121,7 +124,18 @@ impl Painter {
121124
self.terminal_size.0
122125
}
123126

124-
/// Returns the available lines from the prompt down
127+
/// Returns the empty lines from the prompt down.
128+
pub fn remaining_lines_real(&self) -> u16 {
129+
self.screen_height()
130+
.saturating_sub(self.prompt_start_row)
131+
.saturating_sub(self.prompt_height)
132+
}
133+
134+
/// Returns the number of lines that are available or can be made available by
135+
/// stripping the prompt.
136+
///
137+
/// If you want the number of empty lines below the prompt,
138+
/// use [`Painter::remaining_lines_real`] instead.
125139
pub fn remaining_lines(&self) -> u16 {
126140
self.screen_height().saturating_sub(self.prompt_start_row)
127141
}
@@ -199,6 +213,9 @@ impl Painter {
199213
let screen_width = self.screen_width();
200214
let screen_height = self.screen_height();
201215

216+
// We add one here as [`PromptLines::prompt_lines_with_wrap`] intentionally subtracts 1 from the real value.
217+
self.prompt_height = lines.prompt_lines_with_wrap(screen_width) + 1;
218+
202219
// Handle resize for multi line prompt
203220
if self.just_resized {
204221
self.prompt_start_row = self.prompt_start_row.saturating_sub(

0 commit comments

Comments
 (0)