use ratatui::layout::Rect;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct BreadcrumbState {
pub selected: Option<usize>,
pub hovered: Option<usize>,
pub(crate) item_rects: Vec<(usize, Rect)>,
pub(crate) dropdown_rects: Vec<(usize, Rect)>,
pub(crate) ellipsis_rect: Option<Rect>,
pub(crate) rendered_area: Rect,
}
impl BreadcrumbState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn select(&mut self, index: Option<usize>) {
self.selected = index;
}
#[must_use]
pub fn selected(&self) -> Option<usize> {
self.selected
}
pub fn select_next(&mut self, total_items: usize) {
if total_items == 0 {
self.selected = None;
return;
}
self.selected = match self.selected {
Some(curr) => Some((curr + 1).min(total_items - 1)),
None => Some(0),
};
}
pub fn select_previous(&mut self, total_items: usize) {
if total_items == 0 {
self.selected = None;
return;
}
self.selected = match self.selected {
Some(curr) => Some(curr.saturating_sub(1)),
None => Some(total_items - 1),
};
}
pub fn select_first(&mut self) {
self.selected = Some(0);
}
pub fn select_last(&mut self, total_items: usize) {
if total_items == 0 {
self.selected = None;
} else {
self.selected = Some(total_items - 1);
}
}
#[must_use]
pub fn item_at(&self, column: u16, row: u16) -> Option<usize> {
for &(idx, rect) in &self.item_rects {
if contains(rect, column, row) {
return Some(idx);
}
}
None
}
#[must_use]
pub fn dropdown_at(&self, column: u16, row: u16) -> Option<usize> {
for &(idx, rect) in &self.dropdown_rects {
if contains(rect, column, row) {
return Some(idx);
}
}
None
}
#[must_use]
pub fn is_ellipsis_at(&self, column: u16, row: u16) -> bool {
self.ellipsis_rect
.is_some_and(|rect| contains(rect, column, row))
}
pub(crate) fn reset_cache(&mut self, area: Rect) {
self.item_rects.clear();
self.dropdown_rects.clear();
self.ellipsis_rect = None;
self.rendered_area = area;
}
}
fn contains(rect: Rect, x: u16, y: u16) -> bool {
x >= rect.x
&& x < rect.x.saturating_add(rect.width)
&& y >= rect.y
&& y < rect.y.saturating_add(rect.height)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_selection_navigation() {
let mut state = BreadcrumbState::default();
assert_eq!(state.selected(), None);
state.select_next(3);
assert_eq!(state.selected(), Some(0));
state.select_next(3);
assert_eq!(state.selected(), Some(1));
state.select_next(3);
assert_eq!(state.selected(), Some(2));
state.select_next(3);
assert_eq!(state.selected(), Some(2));
state.select_previous(3);
assert_eq!(state.selected(), Some(1));
state.select_first();
assert_eq!(state.selected(), Some(0));
state.select_last(3);
assert_eq!(state.selected(), Some(2));
}
#[test]
fn test_hit_testing() {
let mut state = BreadcrumbState::default();
state.item_rects.push((0, Rect::new(0, 0, 5, 1)));
state.dropdown_rects.push((0, Rect::new(5, 0, 2, 1)));
state.ellipsis_rect = Some(Rect::new(10, 0, 3, 1));
assert_eq!(state.item_at(2, 0), Some(0));
assert_eq!(state.item_at(6, 0), None);
assert_eq!(state.dropdown_at(5, 0), Some(0));
assert_eq!(state.dropdown_at(2, 0), None);
assert!(state.is_ellipsis_at(11, 0));
assert!(!state.is_ellipsis_at(2, 0));
}
}