use super::{detail_page::DetailPage, view_mode::ViewMode};
use tui_scrollview::ScrollViewState;
pub use super::nav_actions::{
available_actions, navigate_back, navigate_detail_page, navigate_to_detail,
navigate_to_filter_menu, navigate_to_help, navigate_to_search, navigate_to_sort_menu,
};
pub const TRANSITIONS: &[(ViewMode, ViewMode)] = &[
(ViewMode::List, ViewMode::Detail),
(ViewMode::List, ViewMode::Search),
(ViewMode::List, ViewMode::SortMenu),
(ViewMode::List, ViewMode::FilterMenu),
(ViewMode::List, ViewMode::Help),
(ViewMode::Detail, ViewMode::List),
(ViewMode::Detail, ViewMode::Help),
(ViewMode::Search, ViewMode::List),
(ViewMode::Search, ViewMode::Detail), (ViewMode::SortMenu, ViewMode::List),
(ViewMode::FilterMenu, ViewMode::List),
(ViewMode::Help, ViewMode::List),
(ViewMode::Help, ViewMode::Detail),
(ViewMode::Help, ViewMode::Search),
];
pub fn is_valid_transition(from: ViewMode, to: ViewMode) -> bool {
TRANSITIONS.contains(&(from, to))
}
pub fn valid_destinations(from: ViewMode) -> Vec<ViewMode> {
TRANSITIONS
.iter()
.filter(|(f, _)| *f == from)
.map(|(_, t)| *t)
.collect()
}
#[derive(Debug, Clone)]
pub struct NavigationState {
pub view_mode: ViewMode,
pub detail_page: DetailPage,
pub history: Vec<ViewMode>,
pub detail_scroll: ScrollViewState,
}
impl Default for NavigationState {
fn default() -> Self {
Self::new()
}
}
impl NavigationState {
pub fn new() -> Self {
Self {
view_mode: ViewMode::List,
detail_page: DetailPage::Overview,
history: vec![],
detail_scroll: ScrollViewState::new(),
}
}
pub fn reset_detail_scroll(&mut self) {
self.detail_scroll = ScrollViewState::new();
}
pub fn push_and_set_view(&mut self, new_mode: ViewMode) {
self.history.push(self.view_mode);
self.view_mode = new_mode;
}
pub fn go_back(&mut self) -> Option<ViewMode> {
self.history.pop().inspect(|&mode| {
self.view_mode = mode;
})
}
pub fn clear_history(&mut self) {
self.history.clear();
}
}
pub fn can_enter_detail(current_mode: ViewMode, has_items: bool, has_selection: bool) -> bool {
matches!(current_mode, ViewMode::List | ViewMode::Search) && has_items && has_selection
}
pub fn can_enter_search(current_mode: ViewMode) -> bool {
matches!(current_mode, ViewMode::List)
}
pub fn can_enter_sort_menu(current_mode: ViewMode) -> bool {
matches!(current_mode, ViewMode::List)
}
pub fn can_enter_filter_menu(current_mode: ViewMode) -> bool {
matches!(current_mode, ViewMode::List)
}
pub fn can_enter_help(current_mode: ViewMode) -> bool {
!matches!(current_mode, ViewMode::Help)
}
pub fn can_go_back(current_mode: ViewMode, history_len: usize) -> bool {
history_len > 0 || !matches!(current_mode, ViewMode::List)
}
pub fn can_navigate_detail_pages(current_mode: ViewMode) -> bool {
matches!(current_mode, ViewMode::Detail)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NavigationResult {
Success,
Blocked { reason: &'static str },
Invalid { from: ViewMode, to: ViewMode },
}
impl NavigationResult {
pub fn is_success(&self) -> bool {
matches!(self, NavigationResult::Success)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_list_to_detail_valid() {
assert!(is_valid_transition(ViewMode::List, ViewMode::Detail));
}
#[test]
fn test_detail_to_search_invalid() {
assert!(!is_valid_transition(ViewMode::Detail, ViewMode::Search));
}
#[test]
fn test_valid_destinations_from_list() {
let destinations = valid_destinations(ViewMode::List);
assert!(destinations.contains(&ViewMode::Detail));
assert!(destinations.contains(&ViewMode::Search));
assert!(destinations.contains(&ViewMode::Help));
assert!(destinations.contains(&ViewMode::SortMenu));
assert!(destinations.contains(&ViewMode::FilterMenu));
}
#[test]
fn test_valid_destinations_from_detail() {
let destinations = valid_destinations(ViewMode::Detail);
assert!(destinations.contains(&ViewMode::List));
assert!(destinations.contains(&ViewMode::Help));
assert!(!destinations.contains(&ViewMode::Search));
assert!(!destinations.contains(&ViewMode::SortMenu));
assert!(!destinations.contains(&ViewMode::FilterMenu));
}
#[test]
fn test_can_enter_detail_requires_selection() {
assert!(!can_enter_detail(ViewMode::List, false, false));
assert!(!can_enter_detail(ViewMode::List, true, false));
assert!(can_enter_detail(ViewMode::List, true, true));
assert!(can_enter_detail(ViewMode::Search, true, true));
assert!(!can_enter_detail(ViewMode::Detail, true, true));
}
#[test]
fn test_can_enter_search_only_from_list() {
assert!(can_enter_search(ViewMode::List));
assert!(!can_enter_search(ViewMode::Detail));
assert!(!can_enter_search(ViewMode::Search));
}
#[test]
fn test_can_enter_help_not_from_help() {
assert!(can_enter_help(ViewMode::List));
assert!(can_enter_help(ViewMode::Detail));
assert!(can_enter_help(ViewMode::Search));
assert!(!can_enter_help(ViewMode::Help));
}
#[test]
fn test_can_go_back_with_history() {
assert!(!can_go_back(ViewMode::List, 0));
assert!(can_go_back(ViewMode::Detail, 0));
assert!(can_go_back(ViewMode::List, 1));
assert!(can_go_back(ViewMode::Detail, 1));
}
#[test]
fn test_guards_are_pure() {
let r1 = can_enter_detail(ViewMode::List, true, true);
let r2 = can_enter_detail(ViewMode::List, true, true);
assert_eq!(r1, r2);
}
#[test]
fn test_push_and_set_view() {
let mut state = NavigationState::new();
assert_eq!(state.view_mode, ViewMode::List);
assert!(state.history.is_empty());
state.push_and_set_view(ViewMode::Detail);
assert_eq!(state.view_mode, ViewMode::Detail);
assert_eq!(state.history.len(), 1);
assert_eq!(state.history[0], ViewMode::List);
state.push_and_set_view(ViewMode::Help);
assert_eq!(state.view_mode, ViewMode::Help);
assert_eq!(state.history.len(), 2);
}
#[test]
fn test_go_back_with_history() {
let mut state = NavigationState::new();
state.push_and_set_view(ViewMode::Detail);
state.push_and_set_view(ViewMode::Help);
let result = state.go_back();
assert_eq!(result, Some(ViewMode::Detail));
assert_eq!(state.view_mode, ViewMode::Detail);
let result = state.go_back();
assert_eq!(result, Some(ViewMode::List));
assert_eq!(state.view_mode, ViewMode::List);
let result = state.go_back();
assert_eq!(result, None);
}
#[test]
fn test_clear_history() {
let mut state = NavigationState::new();
state.push_and_set_view(ViewMode::Detail);
state.push_and_set_view(ViewMode::Help);
assert_eq!(state.history.len(), 2);
state.clear_history();
assert!(state.history.is_empty());
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
fn view_mode_strategy() -> impl Strategy<Value = ViewMode> {
prop_oneof![
Just(ViewMode::List),
Just(ViewMode::Detail),
Just(ViewMode::Search),
Just(ViewMode::SortMenu),
Just(ViewMode::FilterMenu),
Just(ViewMode::Help),
]
}
proptest! {
#[test]
fn valid_destinations_consistent(from in view_mode_strategy()) {
let destinations = valid_destinations(from);
for to in destinations {
prop_assert!(
is_valid_transition(from, to),
"valid_destinations({:?}) contains {:?} but is_valid_transition returns false",
from, to
);
}
}
#[test]
fn history_is_lifo(modes in proptest::collection::vec(view_mode_strategy(), 0..10)) {
let mut state = NavigationState::new();
for &mode in &modes {
state.push_and_set_view(mode);
}
for &expected in modes.iter().rev() {
let current = state.view_mode;
prop_assert_eq!(current, expected);
state.go_back();
}
}
#[test]
fn clear_history_empties(
push_count in 0usize..20
) {
let mut state = NavigationState::new();
for _ in 0..push_count {
state.push_and_set_view(ViewMode::Detail);
}
state.clear_history();
prop_assert!(state.history.is_empty());
}
#[test]
fn help_blocked_only_from_help(mode in view_mode_strategy()) {
let can_enter = can_enter_help(mode);
prop_assert_eq!(can_enter, mode != ViewMode::Help);
}
}
}