tuiserial_core/
menu_def.rs

1//! Menu definition and structure
2//!
3//! This module provides a centralized, type-safe menu structure that eliminates
4//! hardcoded indices and simplifies menu navigation and interaction.
5//!
6//! Following Linus's principle: "Bad programmers worry about the code.
7//! Good programmers worry about data structures."
8
9use crate::Language;
10
11/// Menu action that can be triggered
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum MenuAction {
14    // File menu
15    SaveConfig,
16    LoadConfig,
17    Exit,
18
19    // Session menu (for multi-session support)
20    NewSession,
21    DuplicateSession,
22    RenameSession,
23    CloseSession,
24
25    // View menu (for layout support)
26    ViewSingle,
27    ViewSplitHorizontal,
28    ViewSplitVertical,
29    ViewGrid2x2,
30    ViewNextPane,
31    ViewPrevPane,
32
33    // Settings menu
34    ToggleLanguage,
35
36    // Help menu
37    ShowShortcuts,
38    ShowAbout,
39
40    // Special
41    Separator, // Not an action, just for display
42}
43
44impl MenuAction {
45    /// Get the translation key for this action
46    pub fn label_key(&self) -> &'static str {
47        match self {
48            MenuAction::SaveConfig => "menu.file.save_config",
49            MenuAction::LoadConfig => "menu.file.load_config",
50            MenuAction::Exit => "menu.file.exit",
51            MenuAction::NewSession => "menu.session.new",
52            MenuAction::DuplicateSession => "menu.session.duplicate",
53            MenuAction::RenameSession => "menu.session.rename",
54            MenuAction::CloseSession => "menu.session.close",
55            MenuAction::ViewSingle => "menu.view.single",
56            MenuAction::ViewSplitHorizontal => "menu.view.split_h",
57            MenuAction::ViewSplitVertical => "menu.view.split_v",
58            MenuAction::ViewGrid2x2 => "menu.view.grid_2x2",
59            MenuAction::ViewNextPane => "menu.view.next_pane",
60            MenuAction::ViewPrevPane => "menu.view.prev_pane",
61            MenuAction::ToggleLanguage => "menu.settings.toggle_language",
62            MenuAction::ShowShortcuts => "menu.help.shortcuts",
63            MenuAction::ShowAbout => "menu.help.about",
64            MenuAction::Separator => "",
65        }
66    }
67
68    /// Check if this is a separator
69    pub fn is_separator(&self) -> bool {
70        matches!(self, MenuAction::Separator)
71    }
72}
73
74/// A single menu with its items
75#[derive(Debug, Clone)]
76pub struct Menu {
77    pub label_key: &'static str,
78    pub items: &'static [MenuAction],
79}
80
81impl Menu {
82    /// Get the number of items (including separators)
83    pub fn item_count(&self) -> usize {
84        self.items.len()
85    }
86
87    /// Get the item at index
88    pub fn get_item(&self, index: usize) -> Option<MenuAction> {
89        self.items.get(index).copied()
90    }
91}
92
93/// All application menus
94pub struct MenuBar {
95    pub menus: &'static [Menu],
96}
97
98impl MenuBar {
99    /// Get the number of menus
100    pub fn menu_count(&self) -> usize {
101        self.menus.len()
102    }
103
104    /// Get a menu by index
105    pub fn get_menu(&self, index: usize) -> Option<&Menu> {
106        self.menus.get(index)
107    }
108
109    /// Get menu label key
110    pub fn get_menu_label_key(&self, index: usize) -> Option<&'static str> {
111        self.menus.get(index).map(|m| m.label_key)
112    }
113
114    /// Get item count for a menu
115    pub fn get_item_count(&self, menu_index: usize) -> usize {
116        self.menus
117            .get(menu_index)
118            .map(|m| m.item_count())
119            .unwrap_or(0)
120    }
121
122    /// Get a specific menu action
123    pub fn get_action(&self, menu_index: usize, item_index: usize) -> Option<MenuAction> {
124        self.menus
125            .get(menu_index)
126            .and_then(|m| m.get_item(item_index))
127    }
128}
129
130// Menu definitions - the single source of truth
131const FILE_MENU_ITEMS: &[MenuAction] = &[
132    MenuAction::SaveConfig,
133    MenuAction::LoadConfig,
134    MenuAction::Separator,
135    MenuAction::Exit,
136];
137
138const SESSION_MENU_ITEMS: &[MenuAction] = &[
139    MenuAction::NewSession,
140    MenuAction::DuplicateSession,
141    MenuAction::RenameSession,
142    MenuAction::Separator,
143    MenuAction::CloseSession,
144];
145
146const VIEW_MENU_ITEMS: &[MenuAction] = &[
147    MenuAction::ViewSingle,
148    MenuAction::ViewSplitHorizontal,
149    MenuAction::ViewSplitVertical,
150    MenuAction::ViewGrid2x2,
151    MenuAction::Separator,
152    MenuAction::ViewNextPane,
153    MenuAction::ViewPrevPane,
154];
155
156const SETTINGS_MENU_ITEMS: &[MenuAction] = &[MenuAction::ToggleLanguage];
157
158const HELP_MENU_ITEMS: &[MenuAction] = &[
159    MenuAction::ShowShortcuts,
160    MenuAction::Separator,
161    MenuAction::ShowAbout,
162];
163
164// All menus in order
165const ALL_MENUS: &[Menu] = &[
166    Menu {
167        label_key: "menu.file",
168        items: FILE_MENU_ITEMS,
169    },
170    Menu {
171        label_key: "menu.session",
172        items: SESSION_MENU_ITEMS,
173    },
174    Menu {
175        label_key: "menu.view",
176        items: VIEW_MENU_ITEMS,
177    },
178    Menu {
179        label_key: "menu.settings",
180        items: SETTINGS_MENU_ITEMS,
181    },
182    Menu {
183        label_key: "menu.help",
184        items: HELP_MENU_ITEMS,
185    },
186];
187
188/// Global menu bar instance
189pub const MENU_BAR: MenuBar = MenuBar { menus: ALL_MENUS };
190
191/// Calculate x offset for a menu in the menu bar
192pub fn calculate_menu_x_offset(menu_index: usize, lang: Language) -> u16 {
193    use crate::i18n::t;
194
195    let mut offset = 0u16;
196    for i in 0..menu_index.min(MENU_BAR.menu_count()) {
197        if let Some(label_key) = MENU_BAR.get_menu_label_key(i) {
198            let label = t(label_key, lang);
199            // Each menu has format " Label " (label + 2 spaces for padding + 2 spaces between menus)
200            offset += label.chars().count() as u16 + 4;
201        }
202    }
203    offset
204}
205
206/// Find which menu was clicked based on x position
207pub fn find_clicked_menu(x: u16, lang: Language) -> Option<usize> {
208    use crate::i18n::t;
209
210    let mut current_x = 0u16;
211    for (i, menu) in MENU_BAR.menus.iter().enumerate() {
212        let label = t(menu.label_key, lang);
213        let menu_width = label.chars().count() as u16 + 2; // " Label "
214
215        if x >= current_x && x < current_x + menu_width {
216            return Some(i);
217        }
218
219        current_x += menu_width + 2; // +2 for spacing between menus
220    }
221    None
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_menu_bar_structure() {
230        assert_eq!(MENU_BAR.menu_count(), 5);
231        assert_eq!(MENU_BAR.get_item_count(0), 4); // File: Save, Load, Sep, Exit
232        assert_eq!(MENU_BAR.get_item_count(1), 5); // Session
233        assert_eq!(MENU_BAR.get_item_count(2), 7); // View
234        assert_eq!(MENU_BAR.get_item_count(3), 1); // Settings
235        assert_eq!(MENU_BAR.get_item_count(4), 3); // Help: Shortcuts, Sep, About
236    }
237
238    #[test]
239    fn test_menu_actions() {
240        assert_eq!(MENU_BAR.get_action(0, 0), Some(MenuAction::SaveConfig));
241        assert_eq!(MENU_BAR.get_action(0, 3), Some(MenuAction::Exit));
242        assert_eq!(MENU_BAR.get_action(4, 0), Some(MenuAction::ShowShortcuts));
243    }
244
245    #[test]
246    fn test_separator_detection() {
247        assert!(MenuAction::Separator.is_separator());
248        assert!(!MenuAction::SaveConfig.is_separator());
249    }
250
251    #[test]
252    fn test_invalid_indices() {
253        assert_eq!(MENU_BAR.get_action(99, 0), None);
254        assert_eq!(MENU_BAR.get_action(0, 99), None);
255    }
256
257    #[test]
258    fn test_menu_label_keys() {
259        assert_eq!(MENU_BAR.get_menu_label_key(0), Some("menu.file"));
260        assert_eq!(MENU_BAR.get_menu_label_key(1), Some("menu.session"));
261        assert_eq!(MENU_BAR.get_menu_label_key(4), Some("menu.help"));
262    }
263}