Skip to main content

cardinal_core/
workspace.rs

1//! Workspace state and selection movement.
2//!
3//! The workspace is visual state only. Domain actions are represented as
4//! parsed commands and future effect requests.
5
6use crate::command::AgendaRange;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct WorkspaceState {
10    pub view: View,
11    pub selected_index: usize,
12    pub status: Option<String>,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum View {
17    Home,
18    Inboxes,
19    MailList { mailbox: String },
20    MailReader { message_id: String },
21    Calendars,
22    Agenda { range: AgendaRange },
23    EventReader { event_id: String },
24    SearchResults { query: String },
25    Compose,
26}
27
28impl Default for WorkspaceState {
29    fn default() -> Self {
30        Self {
31            view: View::Home,
32            selected_index: 0,
33            status: None,
34        }
35    }
36}
37
38impl WorkspaceState {
39    pub fn select_next(&mut self, len: usize) {
40        if len == 0 {
41            self.selected_index = 0;
42            return;
43        }
44
45        self.selected_index = (self.selected_index + 1).min(len - 1);
46    }
47
48    pub fn select_previous(&mut self) {
49        self.selected_index = self.selected_index.saturating_sub(1);
50    }
51
52    pub fn select_top(&mut self) {
53        self.selected_index = 0;
54    }
55
56    pub fn select_bottom(&mut self, len: usize) {
57        self.selected_index = len.saturating_sub(1);
58    }
59
60    pub fn set_view(&mut self, view: View) {
61        self.view = view;
62        self.selected_index = 0;
63    }
64
65    pub fn set_status(&mut self, status: impl Into<String>) {
66        self.status = Some(status.into());
67    }
68
69    pub fn clear_status(&mut self) {
70        self.status = None;
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn select_next_stops_at_end() {
80        let mut state = WorkspaceState::default();
81        state.select_next(2);
82        state.select_next(2);
83        state.select_next(2);
84        assert_eq!(state.selected_index, 1);
85    }
86
87    #[test]
88    fn select_previous_saturates_at_zero() {
89        let mut state = WorkspaceState::default();
90        state.select_previous();
91        assert_eq!(state.selected_index, 0);
92    }
93
94    #[test]
95    fn select_next_zero_len_resets_to_zero() {
96        let mut state = WorkspaceState {
97            selected_index: 4,
98            ..WorkspaceState::default()
99        };
100        state.select_next(0);
101        assert_eq!(state.selected_index, 0);
102    }
103
104    #[test]
105    fn select_top_resets_selection() {
106        let mut state = WorkspaceState {
107            selected_index: 8,
108            ..WorkspaceState::default()
109        };
110        state.select_top();
111        assert_eq!(state.selected_index, 0);
112    }
113
114    #[test]
115    fn select_bottom_tracks_list_len_and_saturates() {
116        let mut state = WorkspaceState::default();
117        state.select_bottom(3);
118        assert_eq!(state.selected_index, 2);
119
120        state.select_bottom(0);
121        assert_eq!(state.selected_index, 0);
122    }
123
124    #[test]
125    fn status_set_and_clear_round_trip() {
126        let mut state = WorkspaceState::default();
127        state.set_status("sync ok");
128        assert_eq!(state.status, Some("sync ok".to_owned()));
129        state.clear_status();
130        assert_eq!(state.status, None);
131    }
132
133    #[test]
134    fn set_view_resets_selection() {
135        let mut state = WorkspaceState::default();
136        state.select_next(10);
137        state.set_view(View::Inboxes);
138        assert_eq!(state.selected_index, 0);
139        assert_eq!(state.view, View::Inboxes);
140    }
141}