#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TabScope {
Global,
Project,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Tab {
Projects,
Sessions,
Tasks,
Stats,
Settings,
}
impl Tab {
pub const ALL_WITHOUT_TASKS: [Self; 4] =
[Self::Projects, Self::Sessions, Self::Stats, Self::Settings];
pub const ALL_WITH_TASKS: [Self; 5] = [
Self::Projects,
Self::Sessions,
Self::Tasks,
Self::Stats,
Self::Settings,
];
pub const PROJECT_SCOPED_WITHOUT_TASKS: [Self; 3] =
[Self::Sessions, Self::Stats, Self::Settings];
pub const PROJECT_SCOPED_WITH_TASKS: [Self; 4] =
[Self::Sessions, Self::Tasks, Self::Stats, Self::Settings];
pub fn available_tabs(has_tasks_tab: bool) -> &'static [Self] {
if has_tasks_tab {
return &Self::ALL_WITH_TASKS;
}
&Self::ALL_WITHOUT_TASKS
}
pub fn project_scoped_tabs(has_tasks_tab: bool) -> &'static [Self] {
if has_tasks_tab {
return &Self::PROJECT_SCOPED_WITH_TASKS;
}
&Self::PROJECT_SCOPED_WITHOUT_TASKS
}
pub fn title(self) -> &'static str {
match self {
Tab::Projects => "Projects",
Tab::Sessions => "Sessions",
Tab::Tasks => "Tasks",
Tab::Stats => "Stats",
Tab::Settings => "Settings",
}
}
#[must_use]
pub fn scope(self) -> TabScope {
match self {
Tab::Projects => TabScope::Global,
Tab::Sessions | Tab::Tasks | Tab::Stats | Tab::Settings => TabScope::Project,
}
}
#[must_use]
fn next(self, has_tasks_tab: bool) -> Self {
let tabs = Self::available_tabs(has_tasks_tab);
let tab_index = self.index(has_tasks_tab);
let next_index = (tab_index + 1) % tabs.len();
tabs[next_index]
}
#[must_use]
fn previous(self, has_tasks_tab: bool) -> Self {
let tabs = Self::available_tabs(has_tasks_tab);
let tab_index = self.index(has_tasks_tab);
let previous_index = (tab_index + tabs.len() - 1) % tabs.len();
tabs[previous_index]
}
fn index(self, has_tasks_tab: bool) -> usize {
match Self::available_tabs(has_tasks_tab)
.iter()
.position(|tab| *tab == self)
{
Some(tab_index) => tab_index,
None => unreachable!("tab must exist in the display order"),
}
}
}
pub struct TabManager {
current: Tab,
}
impl TabManager {
pub fn new() -> Self {
Self {
current: Tab::Projects,
}
}
#[must_use]
pub fn current(&self) -> Tab {
self.current
}
pub fn next(&mut self, has_tasks_tab: bool) {
self.normalize(has_tasks_tab);
self.current = self.current.next(has_tasks_tab);
}
pub fn previous(&mut self, has_tasks_tab: bool) {
self.normalize(has_tasks_tab);
self.current = self.current.previous(has_tasks_tab);
}
pub fn set(&mut self, tab: Tab) {
self.current = tab;
}
pub fn normalize(&mut self, has_tasks_tab: bool) {
if self.current == Tab::Tasks && !has_tasks_tab {
self.current = Tab::Sessions;
}
}
}
impl Default for TabManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tab_title() {
let titles = Tab::ALL_WITH_TASKS.map(Tab::title);
assert_eq!(
titles,
["Projects", "Sessions", "Tasks", "Stats", "Settings"]
);
}
#[test]
fn test_tab_scope_marks_only_projects_as_global() {
let scopes = Tab::ALL_WITH_TASKS.map(Tab::scope);
assert_eq!(
scopes,
[
TabScope::Global,
TabScope::Project,
TabScope::Project,
TabScope::Project,
TabScope::Project
]
);
}
#[test]
fn test_tab_next_with_tasks() {
let next_tabs = Tab::ALL_WITH_TASKS.map(|tab| tab.next(true));
assert_eq!(
next_tabs,
[
Tab::Sessions,
Tab::Tasks,
Tab::Stats,
Tab::Settings,
Tab::Projects
]
);
}
#[test]
fn test_tab_next_without_tasks_skips_tasks_tab() {
let next_tabs = Tab::ALL_WITHOUT_TASKS.map(|tab| tab.next(false));
assert_eq!(
next_tabs,
[Tab::Sessions, Tab::Stats, Tab::Settings, Tab::Projects]
);
}
#[test]
fn test_tab_previous_with_tasks() {
let previous_tabs = Tab::ALL_WITH_TASKS.map(|tab| tab.previous(true));
assert_eq!(
previous_tabs,
[
Tab::Settings,
Tab::Projects,
Tab::Sessions,
Tab::Tasks,
Tab::Stats
]
);
}
#[test]
fn test_tab_previous_without_tasks_skips_tasks_tab() {
let previous_tabs = Tab::ALL_WITHOUT_TASKS.map(|tab| tab.previous(false));
assert_eq!(
previous_tabs,
[Tab::Settings, Tab::Projects, Tab::Sessions, Tab::Stats]
);
}
#[test]
fn test_tab_project_scoped_order_with_tasks_keeps_project_pages_grouped() {
let project_scoped_tabs = Tab::project_scoped_tabs(true);
assert_eq!(
project_scoped_tabs,
&[Tab::Sessions, Tab::Tasks, Tab::Stats, Tab::Settings]
);
}
#[test]
fn test_tab_project_scoped_order_without_tasks_keeps_project_pages_grouped() {
let project_scoped_tabs = Tab::project_scoped_tabs(false);
assert_eq!(
project_scoped_tabs,
&[Tab::Sessions, Tab::Stats, Tab::Settings]
);
}
#[test]
fn test_tab_manager_new_defaults_to_projects() {
let manager = TabManager::new();
assert_eq!(manager.current(), Tab::Projects);
}
#[test]
fn test_tab_manager_next_cycles_tabs_with_tasks() {
let mut manager = TabManager::new();
let mut observed_tabs = Vec::new();
observed_tabs.push(manager.current());
manager.next(true);
observed_tabs.push(manager.current());
manager.next(true);
observed_tabs.push(manager.current());
manager.next(true);
observed_tabs.push(manager.current());
manager.next(true);
observed_tabs.push(manager.current());
manager.next(true);
observed_tabs.push(manager.current());
assert_eq!(
observed_tabs,
vec![
Tab::Projects,
Tab::Sessions,
Tab::Tasks,
Tab::Stats,
Tab::Settings,
Tab::Projects
]
);
}
#[test]
fn test_tab_manager_previous_cycles_tabs_without_tasks() {
let mut manager = TabManager::new();
let mut observed_tabs = Vec::new();
observed_tabs.push(manager.current());
manager.previous(false);
observed_tabs.push(manager.current());
manager.previous(false);
observed_tabs.push(manager.current());
manager.previous(false);
observed_tabs.push(manager.current());
manager.previous(false);
observed_tabs.push(manager.current());
assert_eq!(
observed_tabs,
vec![
Tab::Projects,
Tab::Settings,
Tab::Stats,
Tab::Sessions,
Tab::Projects
]
);
}
#[test]
fn test_tab_manager_normalize_falls_back_from_hidden_tasks_tab() {
let mut manager = TabManager::new();
manager.set(Tab::Tasks);
manager.normalize(false);
assert_eq!(manager.current(), Tab::Sessions);
}
#[test]
fn test_tab_manager_set_updates_current_tab() {
let mut manager = TabManager::new();
manager.set(Tab::Settings);
assert_eq!(manager.current(), Tab::Settings);
}
}