From 540d9adf009a3bb519a9362f6342fcde02c9e4ec Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Wed, 19 Mar 2025 03:10:40 +0100 Subject: [PATCH 01/10] implement smooth scrolling for ide menu --- src/menu/ide_menu.rs | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index 0e8c03bc..3a3f45ea 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -144,6 +144,8 @@ pub struct IdeMenu { values: Vec, /// Selected value. Starts at 0 selected: u16, + /// Number of values that need to be skipped (based on selected & terminal height) + skip_values: u16, /// Event sent to the menu event: Option, /// Longest suggestion found in the values @@ -161,6 +163,7 @@ impl Default for IdeMenu { working_details: IdeMenuDetails::default(), values: Vec::new(), selected: 0, + skip_values: 0, event: None, longest_suggestion: 0, input: None, @@ -807,6 +810,21 @@ impl Menu for IdeMenu { self.working_details.space_left = space_left; self.working_details.space_right = space_right; + + let available_lines = painter.remaining_lines().min(self.default_details.max_completion_height) + .saturating_sub(1); // Not sure why this is 1 less than the `available_lines` from [`Menu::menu_string`] + let visible_items = available_lines.saturating_sub(border_width); + + self.skip_values = if self.selected < self.skip_values { + // Selection is above the visible area, scroll up to make it visible + self.selected + } else if self.selected >= self.skip_values + visible_items { + // Selection is below the visible area, scroll down to make it visible + self.selected.saturating_sub(visible_items) + 1 + } else { + // Selection is within the visible area, maintain current scroll position + self.skip_values + } } } @@ -839,18 +857,9 @@ impl Menu for IdeMenu { 0 }; + let available_lines = available_lines.min(self.default_details.max_completion_height); - // The skip values represent the number of lines that should be skipped - // while printing the menu - let skip_values = if self.selected >= available_lines.saturating_sub(border_width) { - let skip_lines = self - .selected - .saturating_sub(available_lines.saturating_sub(border_width)) - + 1; - skip_lines as usize - } else { - 0 - }; + let skip_values = self.skip_values as usize; let available_values = available_lines.saturating_sub(border_width) as usize; From 148516d8b19d5f99aa1dec8065185eec802d61a5 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Wed, 19 Mar 2025 15:29:22 +0100 Subject: [PATCH 02/10] implement smooth scrolling for columnar_menu --- src/menu/columnar_menu.rs | 37 ++++++++++++++++++++++++++----------- src/menu/ide_menu.rs | 8 +++++--- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index ee357aa3..8ce3191a 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -63,6 +63,8 @@ pub struct ColumnarMenu { col_pos: u16, /// row position in the menu. Starts from 0 row_pos: u16, + /// Number of values that need to be skipped (based on selected & terminal height) + skip_values: u16, /// Event sent to the menu event: Option, /// Longest suggestion found in the values @@ -82,6 +84,7 @@ impl Default for ColumnarMenu { values: Vec::new(), col_pos: 0, row_pos: 0, + skip_values: 0, event: None, longest_suggestion: 0, input: None, @@ -619,6 +622,27 @@ impl Menu for ColumnarMenu { self.working_details.columns = possible_cols; } } + + let available_lines = painter.remaining_lines().saturating_sub(1); // Not sure why this is 1 less than the `available_lines` from [`Menu::menu_string`] + + // The skip values represent the number of lines that should be skipped + // while printing the menu + + // Calculate the current first visible row + let first_visible_row = self.skip_values / self.get_cols(); + + // The skip values represent the number of lines that should be skipped + // while printing the menu + self.skip_values = if self.row_pos < first_visible_row { + // Selection is above the visible area, scroll up to make it visible + self.row_pos * self.get_cols() + } else if self.row_pos >= first_visible_row + available_lines { + // Selection is below the visible area, scroll down to make it visible + (self.row_pos.saturating_sub(available_lines) + 1) * self.get_cols() + } else { + // Selection is within the visible area, maintain current scroll position + self.skip_values + }; } } @@ -645,27 +669,18 @@ impl Menu for ColumnarMenu { if self.get_values().is_empty() { self.no_records_msg(use_ansi_coloring) } else { - // The skip values represent the number of lines that should be skipped - // while printing the menu - let skip_values = if self.row_pos >= available_lines { - let skip_lines = self.row_pos.saturating_sub(available_lines) + 1; - (skip_lines * self.get_cols()) as usize - } else { - 0 - }; - // It seems that crossterm prefers to have a complete string ready to be printed // rather than looping through the values and printing multiple things // This reduces the flickering when printing the menu let available_values = (available_lines * self.get_cols()) as usize; self.get_values() .iter() - .skip(skip_values) + .skip(self.skip_values as usize) .take(available_values) .enumerate() .map(|(index, suggestion)| { // Correcting the enumerate index based on the number of skipped values - let index = index + skip_values; + let index = index + self.skip_values as usize; let column = index as u16 % self.get_cols(); let empty_space = self.get_width().saturating_sub(suggestion.value.width()); diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index 3a3f45ea..576253fb 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -811,8 +811,11 @@ impl Menu for IdeMenu { self.working_details.space_left = space_left; self.working_details.space_right = space_right; - let available_lines = painter.remaining_lines().min(self.default_details.max_completion_height) - .saturating_sub(1); // Not sure why this is 1 less than the `available_lines` from [`Menu::menu_string`] + let available_lines = painter + .remaining_lines() + .min(self.default_details.max_completion_height) + .saturating_sub(1); // Not sure why this is 1 less than the `available_lines` from [`Menu::menu_string`] + let visible_items = available_lines.saturating_sub(border_width); self.skip_values = if self.selected < self.skip_values { @@ -857,7 +860,6 @@ impl Menu for IdeMenu { 0 }; - let available_lines = available_lines.min(self.default_details.max_completion_height); let skip_values = self.skip_values as usize; From 36a5ed8a792c8558ea9d1bd210f330a4c9b5124a Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Wed, 19 Mar 2025 15:36:16 +0100 Subject: [PATCH 03/10] fmt --- src/menu/ide_menu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index 576253fb..e6745c69 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -815,7 +815,7 @@ impl Menu for IdeMenu { .remaining_lines() .min(self.default_details.max_completion_height) .saturating_sub(1); // Not sure why this is 1 less than the `available_lines` from [`Menu::menu_string`] - + let visible_items = available_lines.saturating_sub(border_width); self.skip_values = if self.selected < self.skip_values { From 4a47af60dd4b2090cd23fc292d5a2da615bf723a Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Wed, 19 Mar 2025 22:15:29 +0100 Subject: [PATCH 04/10] fix selection not shown on menu open --- src/menu/columnar_menu.rs | 2 +- src/menu/ide_menu.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index 8ce3191a..a0916d09 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -633,7 +633,7 @@ impl Menu for ColumnarMenu { // The skip values represent the number of lines that should be skipped // while printing the menu - self.skip_values = if self.row_pos < first_visible_row { + self.skip_values = if self.row_pos <= first_visible_row { // Selection is above the visible area, scroll up to make it visible self.row_pos * self.get_cols() } else if self.row_pos >= first_visible_row + available_lines { diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index e6745c69..7ba61a95 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -818,7 +818,7 @@ impl Menu for IdeMenu { let visible_items = available_lines.saturating_sub(border_width); - self.skip_values = if self.selected < self.skip_values { + self.skip_values = if self.selected <= self.skip_values { // Selection is above the visible area, scroll up to make it visible self.selected } else if self.selected >= self.skip_values + visible_items { From b7901bf21ac2368f6ca6f55b5ef42a92465408f2 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Wed, 19 Mar 2025 22:20:10 +0100 Subject: [PATCH 05/10] change comments --- src/menu/columnar_menu.rs | 17 ++++++----------- src/menu/ide_menu.rs | 11 ++++++----- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index a0916d09..3a4df0b7 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -63,7 +63,8 @@ pub struct ColumnarMenu { col_pos: u16, /// row position in the menu. Starts from 0 row_pos: u16, - /// Number of values that need to be skipped (based on selected & terminal height) + /// Number of values that are skipped when printing, + /// depending on selected value and terminal height skip_values: u16, /// Event sent to the menu event: Option, @@ -623,24 +624,18 @@ impl Menu for ColumnarMenu { } } - let available_lines = painter.remaining_lines().saturating_sub(1); // Not sure why this is 1 less than the `available_lines` from [`Menu::menu_string`] + let available_lines = painter.remaining_lines().saturating_sub(1); - // The skip values represent the number of lines that should be skipped - // while printing the menu - - // Calculate the current first visible row let first_visible_row = self.skip_values / self.get_cols(); - // The skip values represent the number of lines that should be skipped - // while printing the menu self.skip_values = if self.row_pos <= first_visible_row { - // Selection is above the visible area, scroll up to make it visible + // Selection is above the visible area, scroll up self.row_pos * self.get_cols() } else if self.row_pos >= first_visible_row + available_lines { - // Selection is below the visible area, scroll down to make it visible + // Selection is below the visible area, scroll down (self.row_pos.saturating_sub(available_lines) + 1) * self.get_cols() } else { - // Selection is within the visible area, maintain current scroll position + // Selection is within the visible area self.skip_values }; } diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index 7ba61a95..2e83c7dd 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -144,7 +144,8 @@ pub struct IdeMenu { values: Vec, /// Selected value. Starts at 0 selected: u16, - /// Number of values that need to be skipped (based on selected & terminal height) + /// Number of values that are skipped when printing, + /// depending on selected value and terminal height skip_values: u16, /// Event sent to the menu event: Option, @@ -814,18 +815,18 @@ impl Menu for IdeMenu { let available_lines = painter .remaining_lines() .min(self.default_details.max_completion_height) - .saturating_sub(1); // Not sure why this is 1 less than the `available_lines` from [`Menu::menu_string`] + .saturating_sub(1); let visible_items = available_lines.saturating_sub(border_width); self.skip_values = if self.selected <= self.skip_values { - // Selection is above the visible area, scroll up to make it visible + // Selection is above the visible area self.selected } else if self.selected >= self.skip_values + visible_items { - // Selection is below the visible area, scroll down to make it visible + // Selection is below the visible area self.selected.saturating_sub(visible_items) + 1 } else { - // Selection is within the visible area, maintain current scroll position + // Selection is within the visible area self.skip_values } } From 784bbe4d1bbb39bb50451fee032f00c383b41d63 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Thu, 20 Mar 2025 18:39:38 +0100 Subject: [PATCH 06/10] fix bug with capped completion height in ide_menu --- src/menu/ide_menu.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index 2e83c7dd..19b5452e 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -814,8 +814,8 @@ impl Menu for IdeMenu { let available_lines = painter .remaining_lines() - .min(self.default_details.max_completion_height) - .saturating_sub(1); + .saturating_sub(1) + .min(self.default_details.max_completion_height); let visible_items = available_lines.saturating_sub(border_width); From d80a1298700e6e26a681734df6dd4abe666e4a42 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Thu, 3 Apr 2025 02:29:49 +0200 Subject: [PATCH 07/10] use correct prompt size --- src/menu/columnar_menu.rs | 2 +- src/menu/ide_menu.rs | 1 - src/painting/painter.rs | 7 ++++++- src/painting/prompt_lines.rs | 4 +++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index 3a4df0b7..c5e60547 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -624,7 +624,7 @@ impl Menu for ColumnarMenu { } } - let available_lines = painter.remaining_lines().saturating_sub(1); + let available_lines = painter.remaining_lines(); let first_visible_row = self.skip_values / self.get_cols(); diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index 19b5452e..8727571b 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -814,7 +814,6 @@ impl Menu for IdeMenu { let available_lines = painter .remaining_lines() - .saturating_sub(1) .min(self.default_details.max_completion_height); let visible_items = available_lines.saturating_sub(border_width); diff --git a/src/painting/painter.rs b/src/painting/painter.rs index bf2917ec..2318d3d0 100644 --- a/src/painting/painter.rs +++ b/src/painting/painter.rs @@ -91,6 +91,8 @@ pub struct Painter { // Stdout stdout: W, prompt_start_row: u16, + // The number of lines that the prompt takes up + prompt_height: u16, terminal_size: (u16, u16), last_required_lines: u16, large_buffer: bool, @@ -103,6 +105,7 @@ impl Painter { Painter { stdout, prompt_start_row: 0, + prompt_height: 0, terminal_size: (0, 0), last_required_lines: 0, large_buffer: false, @@ -123,7 +126,7 @@ impl Painter { /// Returns the available lines from the prompt down pub fn remaining_lines(&self) -> u16 { - self.screen_height().saturating_sub(self.prompt_start_row) + self.screen_height().saturating_sub(self.prompt_height) } /// Returns the state necessary before suspending the painter (to run a host command event). @@ -199,6 +202,8 @@ impl Painter { let screen_width = self.screen_width(); let screen_height = self.screen_height(); + self.prompt_height = lines.prompt_lines_with_wrap(screen_width); + // Handle resize for multi line prompt if self.just_resized { self.prompt_start_row = self.prompt_start_row.saturating_sub( diff --git a/src/painting/prompt_lines.rs b/src/painting/prompt_lines.rs index 06082868..23bdb56b 100644 --- a/src/painting/prompt_lines.rs +++ b/src/painting/prompt_lines.rs @@ -120,7 +120,9 @@ impl<'prompt> PromptLines<'prompt> { pub(crate) fn prompt_lines_with_wrap(&self, screen_width: u16) -> u16 { let complete_prompt = self.prompt_str_left.to_string() + &self.prompt_indicator; let lines = estimate_required_lines(&complete_prompt, screen_width); - lines.saturating_sub(1) as u16 + // TODO: make sure this doesnt cause any problems in other places + // lines.saturating_sub(1) as u16 + lines as u16 } /// Estimated width of the line where right prompt will be rendered From b437b94ab42475ffaad35598cf5226792804436e Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:54:51 +0200 Subject: [PATCH 08/10] fix remaining lines and undo prompt_lines change --- src/painting/painter.rs | 7 +++++-- src/painting/prompt_lines.rs | 4 +--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/painting/painter.rs b/src/painting/painter.rs index 2318d3d0..f038e167 100644 --- a/src/painting/painter.rs +++ b/src/painting/painter.rs @@ -126,7 +126,9 @@ impl Painter { /// Returns the available lines from the prompt down pub fn remaining_lines(&self) -> u16 { - self.screen_height().saturating_sub(self.prompt_height) + self.screen_height() + .saturating_sub(self.prompt_start_row) + .saturating_sub(self.prompt_height) } /// Returns the state necessary before suspending the painter (to run a host command event). @@ -202,7 +204,8 @@ impl Painter { let screen_width = self.screen_width(); let screen_height = self.screen_height(); - self.prompt_height = lines.prompt_lines_with_wrap(screen_width); + // We add one here as [`PromptLines::prompt_lines_with_wrap`] intentionally subtracts 1 from the real value. + self.prompt_height = lines.prompt_lines_with_wrap(screen_width) + 1; // Handle resize for multi line prompt if self.just_resized { diff --git a/src/painting/prompt_lines.rs b/src/painting/prompt_lines.rs index 23bdb56b..06082868 100644 --- a/src/painting/prompt_lines.rs +++ b/src/painting/prompt_lines.rs @@ -120,9 +120,7 @@ impl<'prompt> PromptLines<'prompt> { pub(crate) fn prompt_lines_with_wrap(&self, screen_width: u16) -> u16 { let complete_prompt = self.prompt_str_left.to_string() + &self.prompt_indicator; let lines = estimate_required_lines(&complete_prompt, screen_width); - // TODO: make sure this doesnt cause any problems in other places - // lines.saturating_sub(1) as u16 - lines as u16 + lines.saturating_sub(1) as u16 } /// Estimated width of the line where right prompt will be rendered From 116adca78d63ab657fb5d543de932bafb30cc968 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Fri, 4 Apr 2025 15:03:07 +0200 Subject: [PATCH 09/10] add prompt_height to remaining lines --- src/painting/painter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/painting/painter.rs b/src/painting/painter.rs index f038e167..93392bbb 100644 --- a/src/painting/painter.rs +++ b/src/painting/painter.rs @@ -217,7 +217,7 @@ impl Painter { } // Lines and distance parameters - let remaining_lines = self.remaining_lines(); + let remaining_lines = self.remaining_lines() + self.prompt_height; let required_lines = lines.required_lines(screen_width, menu); // Marking the painter state as larger buffer to avoid animations From deb1d0303011a0726560565d52edc6d8502d3ce8 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Tue, 8 Apr 2025 01:01:00 +0200 Subject: [PATCH 10/10] fix painting with large prompt --- src/menu/columnar_menu.rs | 13 ++++++++++--- src/menu/ide_menu.rs | 10 ++++++++-- src/painting/painter.rs | 15 ++++++++++++--- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/menu/columnar_menu.rs b/src/menu/columnar_menu.rs index c5e60547..1f0f23e3 100644 --- a/src/menu/columnar_menu.rs +++ b/src/menu/columnar_menu.rs @@ -624,7 +624,12 @@ impl Menu for ColumnarMenu { } } - let available_lines = painter.remaining_lines(); + let mut available_lines = painter.remaining_lines_real(); + // Handle the case where a prompt uses the entire screen. + // Drawing the menu has priority over the drawing the prompt. + if available_lines == 0 { + available_lines = painter.remaining_lines().min(self.min_rows()); + } let first_visible_row = self.skip_values / self.get_cols(); @@ -668,14 +673,16 @@ impl Menu for ColumnarMenu { // rather than looping through the values and printing multiple things // This reduces the flickering when printing the menu let available_values = (available_lines * self.get_cols()) as usize; + let skip_values = self.skip_values as usize; + self.get_values() .iter() - .skip(self.skip_values as usize) + .skip(skip_values) .take(available_values) .enumerate() .map(|(index, suggestion)| { // Correcting the enumerate index based on the number of skipped values - let index = index + self.skip_values as usize; + let index = index + skip_values; let column = index as u16 % self.get_cols(); let empty_space = self.get_width().saturating_sub(suggestion.value.width()); diff --git a/src/menu/ide_menu.rs b/src/menu/ide_menu.rs index 8727571b..f4fca0b1 100644 --- a/src/menu/ide_menu.rs +++ b/src/menu/ide_menu.rs @@ -812,10 +812,16 @@ impl Menu for IdeMenu { self.working_details.space_left = space_left; self.working_details.space_right = space_right; - let available_lines = painter - .remaining_lines() + let mut available_lines = painter + .remaining_lines_real() .min(self.default_details.max_completion_height); + // Handle the case where a prompt uses the entire screen. + // Drawing the menu has priority over the drawing the prompt. + if available_lines == 0 { + available_lines = painter.remaining_lines().min(self.min_rows()); + } + let visible_items = available_lines.saturating_sub(border_width); self.skip_values = if self.selected <= self.skip_values { diff --git a/src/painting/painter.rs b/src/painting/painter.rs index 93392bbb..647cf66e 100644 --- a/src/painting/painter.rs +++ b/src/painting/painter.rs @@ -124,13 +124,22 @@ impl Painter { self.terminal_size.0 } - /// Returns the available lines from the prompt down - pub fn remaining_lines(&self) -> u16 { + /// Returns the empty lines from the prompt down. + pub fn remaining_lines_real(&self) -> u16 { self.screen_height() .saturating_sub(self.prompt_start_row) .saturating_sub(self.prompt_height) } + /// Returns the number of lines that are available or can be made available by + /// stripping the prompt. + /// + /// If you want the number of empty lines below the prompt, + /// use [`Painter::remaining_lines_real`] instead. + pub fn remaining_lines(&self) -> u16 { + self.screen_height().saturating_sub(self.prompt_start_row) + } + /// Returns the state necessary before suspending the painter (to run a host command event). /// /// This state will be used to re-initialize the painter to re-use last prompt if possible. @@ -217,7 +226,7 @@ impl Painter { } // Lines and distance parameters - let remaining_lines = self.remaining_lines() + self.prompt_height; + let remaining_lines = self.remaining_lines(); let required_lines = lines.required_lines(screen_width, menu); // Marking the painter state as larger buffer to avoid animations