use crate::scroll::Scrollable;
#[derive(Debug, Clone, Default)]
pub struct SelectionState {
selected_index: Option<usize>,
navigation_active: bool,
scroll_offset: u16,
viewport_height: u16,
suggestion_y_positions: Vec<u16>,
suggestion_heights: Vec<u16>,
hovered_index: Option<usize>,
}
impl SelectionState {
pub fn new() -> Self {
Self {
selected_index: None,
navigation_active: false,
scroll_offset: 0,
viewport_height: 0,
suggestion_y_positions: Vec::new(),
suggestion_heights: Vec::new(),
hovered_index: None,
}
}
#[cfg(test)]
pub fn select_index(&mut self, index: usize) {
self.selected_index = Some(index);
self.navigation_active = false;
}
pub fn clear_selection(&mut self) {
self.selected_index = None;
self.navigation_active = false;
}
pub fn get_selected(&self) -> Option<usize> {
self.selected_index
}
pub fn is_navigation_active(&self) -> bool {
self.navigation_active
}
pub fn navigate_next(&mut self, suggestion_count: usize) {
if suggestion_count == 0 {
return;
}
self.navigation_active = true;
match self.selected_index {
Some(current) => {
if current + 1 < suggestion_count {
self.selected_index = Some(current + 1);
}
}
None => {
self.selected_index = Some(0);
}
}
self.ensure_selected_visible();
}
pub fn navigate_previous(&mut self, suggestion_count: usize) {
if suggestion_count == 0 {
return;
}
self.navigation_active = true;
match self.selected_index {
Some(current) => {
if current > 0 {
self.selected_index = Some(current - 1);
}
}
None => {
self.selected_index = Some(suggestion_count - 1);
}
}
self.ensure_selected_visible();
}
pub fn update_layout(&mut self, heights: Vec<u16>, viewport: u16) {
self.viewport_height = viewport;
self.suggestion_heights = heights;
self.suggestion_y_positions.clear();
let mut current_y = 0u16;
for &height in self.suggestion_heights.iter() {
self.suggestion_y_positions.push(current_y);
current_y = current_y.saturating_add(height);
}
}
pub fn ensure_selected_visible(&mut self) {
let Some(selected_idx) = self.selected_index else {
return;
};
if selected_idx >= self.suggestion_y_positions.len() {
return;
}
let suggestion_start = self.suggestion_y_positions[selected_idx];
let suggestion_height = self
.suggestion_heights
.get(selected_idx)
.copied()
.unwrap_or(1);
let suggestion_end = suggestion_start.saturating_add(suggestion_height);
if suggestion_start < self.scroll_offset {
self.scroll_offset = suggestion_start;
}
else if suggestion_end > self.scroll_offset.saturating_add(self.viewport_height) {
self.scroll_offset = suggestion_end.saturating_sub(self.viewport_height);
}
}
pub fn scroll_offset_u16(&self) -> u16 {
self.scroll_offset
}
pub fn clear_layout(&mut self) {
self.scroll_offset = 0;
self.viewport_height = 0;
self.suggestion_y_positions.clear();
self.suggestion_heights.clear();
}
#[allow(dead_code)]
fn total_content_height(&self) -> u16 {
self.suggestion_heights.iter().copied().sum()
}
pub fn suggestion_at_y(&self, inner_y: u16) -> Option<usize> {
let content_y = inner_y.saturating_add(self.scroll_offset);
for (i, &pos) in self.suggestion_y_positions.iter().enumerate() {
let height = self.suggestion_heights.get(i).copied().unwrap_or(1);
if content_y >= pos && content_y < pos.saturating_add(height) {
return Some(i);
}
}
None
}
pub fn get_hovered(&self) -> Option<usize> {
self.hovered_index
}
pub fn set_hovered(&mut self, index: Option<usize>) {
self.hovered_index = index;
}
pub fn clear_hover(&mut self) {
self.hovered_index = None;
}
}
impl Scrollable for SelectionState {
fn scroll_view_up(&mut self, lines: usize) {
self.scroll_offset = self.scroll_offset.saturating_sub(lines as u16);
}
fn scroll_view_down(&mut self, lines: usize) {
let max = self.max_scroll();
self.scroll_offset = (self.scroll_offset as usize + lines).min(max) as u16;
}
fn scroll_offset(&self) -> usize {
self.scroll_offset as usize
}
fn max_scroll(&self) -> usize {
let total = self.total_content_height() as usize;
total.saturating_sub(self.viewport_height as usize)
}
fn viewport_size(&self) -> usize {
self.viewport_height as usize
}
}
#[cfg(test)]
#[path = "state_tests.rs"]
mod state_tests;