//! # Grid Selector State //! //! This module contains the [`GridSelectorState`] for the `GridSelector` widget. //! //! The state is used to keep track of the items, the selected item, and the hovered item and //! encapsulates the navigation logic for the grid selector. use ratatui::text::Text; // If Text has a lifetime parameter, specify it in the implementation. #[derive(Clone, Debug)] pub struct GridItem(String); impl GridItem { // accept both String and &str pub fn new(value: S) -> Self where S: Into, { GridItem(value.into()) } } // convert Label into &str impl AsRef for GridItem { fn as_ref(&self) -> &str { &self.0 } } // convert Label into String impl Into for GridItem { fn into(self) -> String { self.0.clone() } } // Specify the lifetime for the implementation impl<'a> From for Text<'a> { fn from(val: GridItem) -> Self { Text::from(val.0) } } // implement a way to convert String into Label impl From for GridItem { fn from(value: String) -> Self { GridItem(value) } } // implement a way to convert &str into Label impl<'a> From<&'a str> for GridItem { fn from(value: &'a str) -> Self { GridItem(value.to_string()) } } /// State for the [`GridSelector`] widget. /// /// This state is used to keep track of the items, the selected item, and the hovered item. #[derive(Debug, Clone)] pub struct GridSelectorState { pub items: Vec, pub selected: Option, pub hovered: Option, pub(crate) columns: usize, } impl GridSelectorState { /// Create a new [`GridSelectorState`] with the given items. pub fn new(items: I) -> Self where I: IntoIterator, T: Into, // Accept anything that can be converted into Label { let items: Vec = items.into_iter().map(Into::into).collect(); Self { items, selected: None, hovered: Some(0), columns: 5, } } /// builder method to set the number of columns pub fn columns(mut self, columns: usize) -> Self { self.columns = columns; self } /// Get the selected item. pub fn selected(&self) -> Option { self.selected.map(|i| self.items[i].clone()) } /// Get the index of the selected item. pub fn selected_index(&self) -> Option { self.selected } /// Get the hovered item. pub fn hovered(&self) -> Option { self.hovered.map(|i| self.items[i].clone()) } /// Move the hovered item right +1 /// /// Returns `true` if the hovered item was moved, `false` otherwise. pub fn move_right(&mut self) -> bool { self.hovered = if let Some(hovered) = self.hovered { let next = hovered + 1; if next < self.items.len() { Some(next) } else { Some(0) } } else { Some(0) }; true } /// Move the hovered item left -1 /// /// Returns `true` if the hovered item was moved, /// `false` otherwise. pub fn move_left(&mut self) -> bool { self.hovered = if let Some(hovered) = self.hovered { if hovered > 0 { Some(hovered - 1) } else { Some(self.items.len() - 1) } } else { Some(0) }; true } /// Move the hovered item down by one row. /// /// Returns `true` if the hovered item was moved, `false` otherwise. pub fn move_down(&mut self) -> bool { if let Some(hovered) = self.hovered { let items_per_row = self.columns; let num_items = self.items.len(); let current_row = hovered / items_per_row; let next_row_start = (current_row + 1) * items_per_row; let last_item_index = num_items - 1; // If we are in the last row, we can't go down if next_row_start > last_item_index { return false; } let mut next_index = std::cmp::min(hovered + items_per_row, last_item_index); // Handle the case where the next row has fewer items let next_row_count = std::cmp::min(items_per_row, last_item_index - next_row_start + 1); // check if both the next_row_count and the self.columns are odd numbers (3,5,7, etc) // and next_row_count is less than self.columns if next_row_count % 2 != 0 && items_per_row % 2 != 0 && next_row_count < items_per_row { let shift = (items_per_row - next_row_count) / 2; // if we are in the shifted range (left or right) of the row, adjust the hovered // index accordingly if hovered % items_per_row >= shift && hovered % items_per_row < items_per_row - shift { next_index = hovered + items_per_row - shift; } else { // if we are in the left part of the shifted range, set next to the first item // in the last row and if we are in the right part of the shifted range, set // next to the last item in the last row next_index = if hovered % items_per_row < shift { next_row_start } else { last_item_index }; } } self.hovered = Some(std::cmp::min(next_index, last_item_index)); return true; } false } /// Move the hovered item up by one row. /// /// Returns `true` if the hovered item was moved, `false` otherwise. pub fn move_up(&mut self) -> bool { if let Some(hovered) = self.hovered { let row_number = hovered / self.columns; // If we are in the first row, we can't go up if row_number == 0 { return false; } let mut next_index = hovered.saturating_sub(self.columns); // Handle case where the current index is in the last row // let is_last_row = hovered >= self.items.len().saturating_sub(self.columns); let last_row_start = (self.items.len() / self.columns) * self.columns; let is_last_row = hovered >= last_row_start; if is_last_row { let last_row_count = self.items.len() % self.columns; // If the last_row_count and self.columns are odd numbers (3,5,7, etc) // and last_row_count is less than self.columns we need to adjust the next index // to go to the cell just above the current hovered cell if last_row_count % 2 != 0 && self.columns % 2 != 0 && last_row_count < self.columns { let shift = (self.columns - last_row_count) / 2; next_index = hovered.saturating_sub(self.columns - shift); } } // Ensure next_index stays within bounds self.hovered = Some(std::cmp::min(next_index, self.items.len() - 1)); return true; } false } /// Move the hovered item to the first item in the current row. /// /// Returns `true` if the hovered item was moved, `false` otherwise. pub fn move_to_row_start(&mut self) -> bool { if let Some(hovered) = self.hovered { let row_start = (hovered / self.columns) * self.columns; self.hovered = Some(row_start); true } else { false } } /// Move the hovered item to the last item in the current row. /// /// Returns `true` if the hovered item was moved, `false` otherwise. pub fn move_to_row_end(&mut self) -> bool { if let Some(hovered) = self.hovered { let row_end = std::cmp::min( (hovered / self.columns + 1) * self.columns - 1, self.items.len() - 1, ); self.hovered = Some(row_end); true } else { false } } /// Select the hovered item. /// /// Select the hovered item. Returns `true` if the hovered item was selected, `false` otherwise. pub fn select(&mut self) -> bool { if let Some(hovered) = self.hovered { self.selected = Some(hovered); true } else { false } } }