use super::{Menu, MenuBuilder, MenuEvent, MenuSettings};
use crate::{
core_editor::Editor,
menu_functions::{
can_partially_complete, completer_input, floor_char_boundary, get_match_indices,
replace_in_buffer, style_suggestion, truncate_with_ansi,
},
painting::Painter,
Completer, Suggestion,
};
use nu_ansi_term::ansi::RESET;
use unicode_width::UnicodeWidthStr;
#[derive(Debug, PartialEq, Eq)]
pub enum TraversalDirection {
Horizontal,
Vertical,
}
struct DefaultColumnDetails {
pub columns: u16,
pub col_width: Option<usize>,
pub col_padding: usize,
pub traversal_dir: TraversalDirection,
}
impl Default for DefaultColumnDetails {
fn default() -> Self {
Self {
columns: 4,
col_width: None,
col_padding: 2,
traversal_dir: TraversalDirection::Horizontal,
}
}
}
#[derive(Default)]
struct ColumnDetails {
pub columns: u16,
pub col_width: usize,
pub shortest_base_string: String,
}
pub struct ColumnarMenu {
settings: MenuSettings,
active: bool,
default_details: DefaultColumnDetails,
min_rows: u16,
working_details: ColumnDetails,
values: Vec<Suggestion>,
display_widths: Vec<usize>,
col_pos: u16,
row_pos: u16,
skip_rows: u16,
event: Option<MenuEvent>,
longest_suggestion: usize,
input: Option<String>,
}
impl Default for ColumnarMenu {
fn default() -> Self {
Self {
settings: MenuSettings::default().with_name("columnar_menu"),
active: false,
default_details: DefaultColumnDetails::default(),
min_rows: 3,
working_details: ColumnDetails::default(),
values: Vec::new(),
display_widths: Vec::new(),
col_pos: 0,
row_pos: 0,
skip_rows: 0,
event: None,
longest_suggestion: 0,
input: None,
}
}
}
impl MenuBuilder for ColumnarMenu {
fn settings_mut(&mut self) -> &mut MenuSettings {
&mut self.settings
}
}
impl ColumnarMenu {
#[must_use]
pub fn with_columns(mut self, columns: u16) -> Self {
self.default_details.columns = columns;
self
}
#[must_use]
pub fn with_column_width(mut self, col_width: Option<usize>) -> Self {
self.default_details.col_width = col_width;
self
}
#[must_use]
pub fn with_column_padding(mut self, col_padding: usize) -> Self {
self.default_details.col_padding = col_padding;
self
}
#[must_use]
pub fn with_traversal_direction(mut self, direction: TraversalDirection) -> Self {
self.default_details.traversal_dir = direction;
self
}
}
impl ColumnarMenu {
fn move_next(&mut self) {
let new_index = self.index() + 1;
let new_index = if new_index >= self.get_values().len() {
0
} else {
new_index
};
(self.row_pos, self.col_pos) = self.position_from_index(new_index);
}
fn move_previous(&mut self) {
let new_index = match self.index().checked_sub(1) {
Some(index) => index,
None => self.values.len().saturating_sub(1),
};
(self.row_pos, self.col_pos) = self.position_from_index(new_index);
}
fn move_up(&mut self) {
self.row_pos = match self.row_pos.checked_sub(1) {
Some(index) => index,
None => self.get_last_row_at_col(self.col_pos),
}
}
fn move_down(&mut self) {
let new_row = self.row_pos + 1;
self.row_pos = if new_row > self.get_last_row_at_col(self.col_pos) {
0
} else {
new_row
}
}
fn move_left(&mut self) {
self.col_pos = if let Some(col) = self.col_pos.checked_sub(1) {
col
} else {
self.get_last_col_at_row(self.row_pos)
}
}
fn move_right(&mut self) {
let new_col = self.col_pos + 1;
self.col_pos = if new_col > self.get_last_col_at_row(self.row_pos) {
0
} else {
new_col
}
}
fn position_from_index(&self, index: usize) -> (u16, u16) {
match self.default_details.traversal_dir {
TraversalDirection::Vertical => {
let row = index % self.get_rows() as usize;
let col = index / self.get_rows() as usize;
(row as u16, col as u16)
}
TraversalDirection::Horizontal => {
let row = index / self.get_used_cols() as usize;
let col = index % self.get_used_cols() as usize;
(row as u16, col as u16)
}
}
}
fn get_last_row_at_col(&self, col_pos: u16) -> u16 {
let num_values = self.get_values().len() as u16;
match self.default_details.traversal_dir {
TraversalDirection::Vertical => {
if col_pos >= self.get_used_cols() - 1 {
let mod_val = num_values % self.get_rows();
if mod_val == 0 {
self.get_rows().saturating_sub(1)
} else {
mod_val.saturating_sub(1)
}
} else {
self.get_rows().saturating_sub(1)
}
}
TraversalDirection::Horizontal => {
let mod_val = num_values % self.get_used_cols();
if mod_val > 0 && col_pos >= mod_val {
self.get_rows().saturating_sub(2)
} else {
self.get_rows().saturating_sub(1)
}
}
}
}
fn get_last_col_at_row(&self, row_pos: u16) -> u16 {
let num_values = self.get_values().len() as u16;
match self.default_details.traversal_dir {
TraversalDirection::Vertical => {
let mod_val = num_values % self.get_rows();
if mod_val > 0 && row_pos >= mod_val {
self.get_used_cols().saturating_sub(2)
} else {
self.get_used_cols().saturating_sub(1)
}
}
TraversalDirection::Horizontal => {
if row_pos >= self.get_rows() - 1 {
let mod_val = num_values % self.get_used_cols();
if mod_val == 0 {
self.get_used_cols().saturating_sub(1)
} else {
mod_val.saturating_sub(1)
}
} else {
self.get_used_cols().saturating_sub(1)
}
}
}
}
fn index(&self) -> usize {
let index = match self.default_details.traversal_dir {
TraversalDirection::Vertical => self.col_pos * self.get_rows() + self.row_pos,
TraversalDirection::Horizontal => self.row_pos * self.get_used_cols() + self.col_pos,
};
index.into()
}
fn get_value(&self) -> Option<Suggestion> {
self.get_values().get(self.index()).cloned()
}
fn get_rows(&self) -> u16 {
let values = self.get_values().len() as u16;
if values == 0 {
return 1;
}
let rows = values / self.get_cols();
if values % self.get_cols() != 0 {
rows + 1
} else {
rows
}
}
fn get_used_cols(&self) -> u16 {
let values = self.get_values().len() as u16;
if values == 0 {
return 1;
}
match self.default_details.traversal_dir {
TraversalDirection::Vertical => {
let cols = values / self.get_rows();
if values % self.get_rows() != 0 {
cols + 1
} else {
cols
}
}
TraversalDirection::Horizontal => self.get_cols().min(values),
}
}
fn get_width(&self) -> usize {
self.working_details.col_width
}
fn reset_position(&mut self) {
self.col_pos = 0;
self.row_pos = 0;
}
fn no_records_msg(&self, use_ansi_coloring: bool) -> String {
let msg = "NO RECORDS FOUND";
if use_ansi_coloring {
format!(
"{}{}{}",
self.settings.color.selected_text_style.prefix(),
msg,
RESET
)
} else {
msg.to_string()
}
}
fn get_cols(&self) -> u16 {
self.working_details.columns.max(1)
}
fn create_string(
&self,
suggestion: &Suggestion,
index: usize,
use_ansi_coloring: bool,
) -> String {
let selected = index == self.index();
let display_value = suggestion.display_value();
let empty_space = self.get_width().saturating_sub(self.display_widths[index]);
if use_ansi_coloring {
let is_quote = |c: char| "`'\"".contains(c);
let shortest_base = &self.working_details.shortest_base_string;
let shortest_base = shortest_base
.strip_prefix(is_quote)
.unwrap_or(shortest_base);
let match_indices =
get_match_indices(display_value, &suggestion.match_indices, shortest_base);
let left_text_size = self
.get_width()
.min(self.longest_suggestion + self.default_details.col_padding);
let description_size = self.get_width().saturating_sub(left_text_size);
let padding = left_text_size.saturating_sub(self.display_widths[index]);
let text_style = &suggestion.style.unwrap_or(self.settings.color.text_style);
let match_style = if selected {
&self.settings.color.selected_match_style
} else {
&self.settings.color.match_style
};
let value_trunc = truncate_with_ansi(display_value, left_text_size);
let styled_value = style_suggestion(
&value_trunc,
&match_indices,
text_style,
match_style,
selected.then_some(&self.settings.color.selected_text_style),
);
match &suggestion.description {
Some(desc) if description_size > 3 => {
let desc = desc.replace('\n', "");
let desc_trunc = truncate_with_ansi(desc.as_str(), description_size);
if selected {
format!(
"{}{}{}{}{}{}{}",
styled_value,
RESET,
text_style.prefix(),
self.settings.color.selected_text_style.prefix(),
" ".repeat(padding),
self.settings.color.description_style.paint(desc_trunc),
RESET,
)
} else {
format!(
"{}{}{}{}{}",
styled_value,
" ".repeat(padding),
RESET,
self.settings.color.description_style.paint(desc_trunc),
RESET,
)
}
}
_ => {
format!(
"{}{}{:>empty$}",
styled_value,
RESET,
"",
empty = empty_space
)
}
}
} else {
let marker = if index == self.index() { ">" } else { "" };
let line = if let Some(description) = &suggestion.description {
format!(
"{}{:max$}{}",
marker,
display_value,
description
.chars()
.take(empty_space)
.collect::<String>()
.replace('\n', " "),
max = self.longest_suggestion
+ self
.default_details
.col_padding
.saturating_sub(marker.width()),
)
} else {
format!(
"{}{}{:>empty$}",
marker,
display_value,
"",
empty = empty_space.saturating_sub(marker.width()),
)
};
if selected {
line.to_uppercase()
} else {
line
}
}
}
}
impl Menu for ColumnarMenu {
fn settings(&self) -> &MenuSettings {
&self.settings
}
fn is_active(&self) -> bool {
self.active
}
fn can_quick_complete(&self) -> bool {
true
}
fn can_partially_complete(
&mut self,
values_updated: bool,
editor: &mut Editor,
completer: &mut dyn Completer,
) -> bool {
if !values_updated {
self.update_values(editor, completer);
}
if can_partially_complete(self.get_values(), editor) {
self.update_values(editor, completer);
true
} else {
false
}
}
fn menu_event(&mut self, event: MenuEvent) {
match &event {
MenuEvent::Activate(_) => self.active = true,
MenuEvent::Deactivate => {
self.active = false;
self.input = None;
}
_ => {}
}
self.event = Some(event);
}
fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) {
if self.settings.only_buffer_difference && self.input.is_none() {
self.input = Some(editor.get_buffer().to_string());
}
let (input, pos) = completer_input(
editor.get_buffer(),
editor.insertion_point(),
self.input.as_deref(),
self.settings.only_buffer_difference,
);
let (values, base_ranges) = completer.complete_with_base_ranges(&input, pos);
self.values = values;
self.display_widths = self
.values
.iter()
.map(|sugg| strip_ansi_escapes::strip_str(sugg.display_value()).width())
.collect();
self.working_details.shortest_base_string = base_ranges
.iter()
.map(|range| {
let end = floor_char_boundary(editor.get_buffer(), range.end);
let start = floor_char_boundary(editor.get_buffer(), range.start).min(end);
editor.get_buffer()[start..end].to_string()
})
.min_by_key(|s| s.width())
.unwrap_or_default();
self.longest_suggestion = *self.display_widths.iter().max().unwrap_or(&0);
self.reset_position();
}
fn update_working_details(
&mut self,
editor: &mut Editor,
completer: &mut dyn Completer,
painter: &Painter,
) {
if let Some(event) = self.event.take() {
match event {
MenuEvent::Activate(updated) => {
self.reset_position();
if !updated {
self.update_values(editor, completer);
}
}
MenuEvent::Deactivate => {}
MenuEvent::Edit(updated) => {
self.reset_position();
if !updated {
self.update_values(editor, completer);
}
}
MenuEvent::NextElement => self.move_next(),
MenuEvent::PreviousElement => self.move_previous(),
MenuEvent::MoveUp => self.move_up(),
MenuEvent::MoveDown => self.move_down(),
MenuEvent::MoveLeft => self.move_left(),
MenuEvent::MoveRight => self.move_right(),
MenuEvent::PreviousPage | MenuEvent::NextPage => {
}
}
let exist_description = self
.get_values()
.iter()
.any(|suggestion| suggestion.description.is_some());
let screen_width = painter.screen_width() as usize;
if exist_description {
self.working_details.columns = 1;
self.working_details.col_width = screen_width;
} else {
let default_width = if let Some(col_width) = self.default_details.col_width {
col_width
} else {
screen_width / self.default_details.columns as usize
};
self.working_details.col_width = default_width
.max(self.longest_suggestion + self.default_details.col_padding)
.min(screen_width);
let possible_cols = painter.screen_width() / self.working_details.col_width as u16;
if possible_cols > self.default_details.columns {
self.working_details.columns = self.default_details.columns.max(1);
} else {
self.working_details.columns = possible_cols;
}
}
let mut available_lines = painter.remaining_lines_real();
if available_lines == 0 {
available_lines = painter.remaining_lines().min(self.min_rows());
}
self.skip_rows = if self.row_pos < self.skip_rows {
self.row_pos
} else if self.row_pos >= self.skip_rows + available_lines {
self.row_pos - available_lines + 1
} else {
self.skip_rows
};
}
}
fn replace_in_buffer(&self, editor: &mut Editor) {
replace_in_buffer(self.get_value(), editor);
}
fn min_rows(&self) -> u16 {
self.get_rows().min(self.min_rows)
}
fn get_values(&self) -> &[Suggestion] {
&self.values
}
fn menu_required_lines(&self, _terminal_columns: u16) -> u16 {
self.get_rows()
}
fn menu_string(&self, available_lines: u16, use_ansi_coloring: bool) -> String {
if self.get_values().is_empty() {
self.no_records_msg(use_ansi_coloring)
} else {
match self.default_details.traversal_dir {
TraversalDirection::Vertical => {
let num_rows: usize = self.get_rows().into();
let rows_to_draw = num_rows.min(available_lines.into());
let mut menu_string = String::new();
for line in 0..rows_to_draw {
let skip_value = self.skip_rows as usize + line;
let row_string: String = self
.get_values()
.iter()
.enumerate()
.skip(skip_value)
.step_by(num_rows)
.take(self.get_cols().into())
.map(|(index, suggestion)| {
self.create_string(suggestion, index, use_ansi_coloring)
})
.collect();
menu_string.push_str(&row_string);
menu_string.push_str("\r\n");
}
menu_string
}
TraversalDirection::Horizontal => {
let available_values = (available_lines * self.get_cols()) as usize;
let skip_values = (self.skip_rows * self.get_used_cols()) as usize;
self.get_values()
.iter()
.skip(skip_values)
.take(available_values)
.enumerate()
.map(|(index, suggestion)| {
let index = index + skip_values;
let column = index % self.get_cols() as usize;
let end_of_line =
if column == self.get_cols().saturating_sub(1) as usize {
"\r\n"
} else {
""
};
format!(
"{}{}",
self.create_string(suggestion, index, use_ansi_coloring),
end_of_line
)
})
.collect()
}
}
}
}
}
#[cfg(test)]
mod tests {
use std::io::BufWriter;
use crate::{Span, UndoBehavior};
use super::*;
macro_rules! partial_completion_tests {
(name: $test_group_name:ident, completions: $completions:expr, test_cases: $($name:ident: $value:expr,)*) => {
mod $test_group_name {
use crate::{menu::Menu, ColumnarMenu, core_editor::Editor, enums::UndoBehavior};
use super::FakeCompleter;
$(
#[test]
fn $name() {
let (input, expected) = $value;
let mut menu = ColumnarMenu::default();
let mut editor = Editor::default();
editor.set_buffer(input.to_string(), UndoBehavior::CreateUndoPoint);
let mut completer = FakeCompleter::new(&$completions);
menu.can_partially_complete(false, &mut editor, &mut completer);
assert_eq!(editor.get_buffer(), expected);
}
)*
}
}
}
partial_completion_tests! {
name: partial_completion_prefix_matches,
completions: ["build.rs", "build-all.sh"],
test_cases:
empty_completes_prefix: ("", "build"),
partial_completes_shared_prefix: ("bui", "build"),
full_prefix_completes_nothing: ("build", "build"),
}
partial_completion_tests! {
name: partial_completion_fuzzy_matches,
completions: ["build.rs", "build-all.sh", "prepare-build.sh"],
test_cases:
no_shared_prefix_completes_nothing: ("", ""),
shared_prefix_completes_nothing: ("bui", "bui"),
}
partial_completion_tests! {
name: partial_completion_fuzzy_same_prefix_matches,
completions: ["build.rs", "build-all.sh", "build-all-tests.sh"],
test_cases:
completes_no_shared_prefix: ("all", "all"),
}
partial_completion_tests! {
name: partial_completion_with_quotes,
completions: ["`Foo bar`", "`Foo baz`"],
test_cases:
partial_completes_prefix_with_backtick: ("F", "`Foo ba"),
partial_completes_case_insensitive: ("foo", "`Foo ba"),
}
partial_completion_tests! {
name: partial_completion_unicode_case_folding,
completions: ["ßar", "ßaz"],
test_cases:
partial_completes_case_insensitive: ("ss", "ßa"),
}
struct FakeCompleter {
completions: Vec<String>,
}
impl FakeCompleter {
fn new(completions: &[&str]) -> Self {
Self {
completions: completions.iter().map(|c| c.to_string()).collect(),
}
}
}
impl Completer for FakeCompleter {
fn complete(&mut self, _line: &str, pos: usize) -> Vec<Suggestion> {
self.completions
.iter()
.map(|c| fake_suggestion(c, pos))
.collect()
}
}
fn fake_suggestion(name: &str, pos: usize) -> Suggestion {
Suggestion {
value: name.to_string(),
description: None,
style: None,
extra: None,
span: Span { start: 0, end: pos },
append_whitespace: false,
..Default::default()
}
}
fn setup_menu(
menu: &mut ColumnarMenu,
editor: &mut Editor,
completer: &mut dyn Completer,
terminal_size: (u16, u16),
) {
let mut painter = Painter::new(BufWriter::new(std::io::stderr()));
painter.handle_resize(terminal_size.0, terminal_size.1);
menu.menu_event(MenuEvent::Activate(false));
menu.update_working_details(editor, completer, &painter);
}
#[test]
fn test_menu_replace_backtick() {
let mut completer = FakeCompleter::new(&["file1.txt", "file2.txt"]);
let mut menu = ColumnarMenu::default().with_name("testmenu");
let mut editor = Editor::default();
editor.set_buffer("file1.txt`".to_string(), UndoBehavior::CreateUndoPoint);
menu.update_values(&mut editor, &mut completer);
menu.replace_in_buffer(&mut editor);
assert!(
editor.is_cursor_at_buffer_end(),
"cursor should be at the end after completion"
);
}
#[test]
fn test_menu_create_string() {
let mut completer = FakeCompleter::new(&["おはよう", "`おはよう(`"]);
let mut menu = ColumnarMenu::default().with_name("testmenu");
let mut editor = Editor::default();
editor.set_buffer("おは".to_string(), UndoBehavior::CreateUndoPoint);
setup_menu(&mut menu, &mut editor, &mut completer, (10, 10));
assert!(menu.menu_string(2, true).contains("おは"));
}
#[test]
fn test_menu_create_string_starting_with_multibyte_char() {
let mut completer = FakeCompleter::new(&["验abc/"]);
let mut menu = ColumnarMenu::default().with_name("testmenu");
let mut editor = Editor::default();
editor.set_buffer("ac".to_string(), UndoBehavior::CreateUndoPoint);
setup_menu(&mut menu, &mut editor, &mut completer, (10, 10));
assert!(menu.menu_string(2, true).contains("验"));
}
#[test]
fn test_menu_create_string_long_unicode_string() {
let mut completer = FakeCompleter::new(&[&("验".repeat(205) + "abc/")]);
let mut menu = ColumnarMenu::default().with_name("testmenu");
let mut editor = Editor::default();
editor.set_buffer("a".to_string(), UndoBehavior::CreateUndoPoint);
setup_menu(&mut menu, &mut editor, &mut completer, (10, 10));
assert!(menu.menu_string(10, true).contains("验"));
}
#[test]
fn test_horizontal_menu_selection_position() {
let vs: Vec<String> = (0..10).map(|v| v.to_string()).collect();
let vs: Vec<_> = vs.iter().map(|v| v.as_ref()).collect();
let mut completer = FakeCompleter::new(&vs);
let mut menu = ColumnarMenu::default()
.with_traversal_direction(TraversalDirection::Horizontal)
.with_name("testmenu");
menu.working_details.columns = 4;
let mut editor = Editor::default();
editor.set_buffer("a".to_string(), UndoBehavior::CreateUndoPoint);
menu.update_values(&mut editor, &mut completer);
assert!(menu.index() == 0);
assert!(menu.row_pos == 0 && menu.col_pos == 0);
menu.move_previous();
assert!(menu.index() == vs.len() - 1);
assert!(menu.row_pos == 2 && menu.col_pos == 1);
menu.move_next();
assert!(menu.index() == 0);
assert!(menu.row_pos == 0 && menu.col_pos == 0);
menu.move_up();
assert!(menu.row_pos == 2 && menu.col_pos == 0);
menu.move_down();
assert!(menu.row_pos == 0 && menu.col_pos == 0);
menu.move_left();
assert!(menu.row_pos == 0 && menu.col_pos == 3);
menu.move_right();
assert!(menu.row_pos == 0 && menu.col_pos == 0);
menu.move_left();
assert!(menu.row_pos == 0 && menu.col_pos == 3);
menu.move_up();
assert!(menu.row_pos == 1 && menu.col_pos == 3);
menu.move_down();
assert!(menu.row_pos == 0 && menu.col_pos == 3);
menu.move_right();
assert!(menu.row_pos == 0 && menu.col_pos == 0);
menu.move_up();
assert!(menu.row_pos == 2 && menu.col_pos == 0);
menu.move_left();
assert!(menu.row_pos == 2 && menu.col_pos == 1);
menu.move_right();
assert!(menu.row_pos == 2 && menu.col_pos == 0);
}
#[test]
fn test_vertical_menu_selection_position() {
let vs: Vec<String> = (0..11).map(|v| v.to_string()).collect();
let vs: Vec<_> = vs.iter().map(|v| v.as_ref()).collect();
let mut completer = FakeCompleter::new(&vs);
let mut menu = ColumnarMenu::default()
.with_traversal_direction(TraversalDirection::Vertical)
.with_name("testmenu");
menu.working_details.columns = 4;
let mut editor = Editor::default();
editor.set_buffer("a".to_string(), UndoBehavior::CreateUndoPoint);
menu.update_values(&mut editor, &mut completer);
assert!(menu.index() == 0);
assert!(menu.row_pos == 0 && menu.col_pos == 0);
menu.move_previous();
assert!(menu.index() == vs.len() - 1);
assert!(menu.row_pos == 1 && menu.col_pos == 3);
menu.move_next();
assert!(menu.row_pos == 0 && menu.col_pos == 0);
menu.move_up();
assert!(menu.row_pos == 2 && menu.col_pos == 0);
menu.move_down();
assert!(menu.row_pos == 0 && menu.col_pos == 0);
menu.move_left();
assert!(menu.row_pos == 0 && menu.col_pos == 3);
menu.move_right();
assert!(menu.row_pos == 0 && menu.col_pos == 0);
menu.move_left();
assert!(menu.row_pos == 0 && menu.col_pos == 3);
menu.move_up();
assert!(menu.row_pos == 1 && menu.col_pos == 3);
menu.move_down();
assert!(menu.row_pos == 0 && menu.col_pos == 3);
menu.move_right();
assert!(menu.row_pos == 0 && menu.col_pos == 0);
menu.move_up();
assert!(menu.row_pos == 2 && menu.col_pos == 0);
menu.move_left();
assert!(menu.row_pos == 2 && menu.col_pos == 2);
menu.move_right();
assert!(menu.row_pos == 2 && menu.col_pos == 0);
}
#[test]
fn test_small_menu_selection_position() {
let mut vertical_menu = ColumnarMenu::default()
.with_traversal_direction(TraversalDirection::Vertical)
.with_name("testmenu");
vertical_menu.working_details.columns = 4;
let mut horizontal_menu = ColumnarMenu::default()
.with_traversal_direction(TraversalDirection::Horizontal)
.with_name("testmenu");
horizontal_menu.working_details.columns = 4;
let mut editor = Editor::default();
let mut completer = FakeCompleter::new(&["1", "2"]);
for menu in &mut [vertical_menu, horizontal_menu] {
menu.update_values(&mut editor, &mut completer);
assert!(menu.index() == 0);
assert!(menu.row_pos == 0 && menu.col_pos == 0);
menu.move_previous();
assert!(menu.index() == menu.get_values().len() - 1);
assert!(menu.row_pos == 0 && menu.col_pos == 1);
menu.move_next();
assert!(menu.row_pos == 0 && menu.col_pos == 0);
menu.move_next();
assert!(menu.row_pos == 0 && menu.col_pos == 1);
menu.move_right();
assert!(menu.row_pos == 0 && menu.col_pos == 0);
menu.move_left();
assert!(menu.row_pos == 0 && menu.col_pos == 1);
menu.move_up();
assert!(menu.row_pos == 0 && menu.col_pos == 1);
menu.move_down();
assert!(menu.row_pos == 0 && menu.col_pos == 1);
}
}
}