1use crate::Language;
10
11fn display_width(s: &str) -> usize {
16 s.chars().map(|c| if c.is_ascii() { 1 } else { 2 }).sum()
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum MenuAction {
22 SaveConfig,
24 LoadConfig,
25 Exit,
26
27 NewSession,
29 DuplicateSession,
30 RenameSession,
31 CloseSession,
32
33 ViewSingle,
35 ViewSplitHorizontal,
36 ViewSplitVertical,
37 ViewGrid2x2,
38 ViewNextPane,
39 ViewPrevPane,
40
41 ToggleLanguage,
43
44 ShowShortcuts,
46 ShowAbout,
47
48 Separator, }
51
52impl MenuAction {
53 pub fn label_key(&self) -> &'static str {
55 match self {
56 MenuAction::SaveConfig => "menu.file.save_config",
57 MenuAction::LoadConfig => "menu.file.load_config",
58 MenuAction::Exit => "menu.file.exit",
59 MenuAction::NewSession => "menu.session.new",
60 MenuAction::DuplicateSession => "menu.session.duplicate",
61 MenuAction::RenameSession => "menu.session.rename",
62 MenuAction::CloseSession => "menu.session.close",
63 MenuAction::ViewSingle => "menu.view.single",
64 MenuAction::ViewSplitHorizontal => "menu.view.split_h",
65 MenuAction::ViewSplitVertical => "menu.view.split_v",
66 MenuAction::ViewGrid2x2 => "menu.view.grid_2x2",
67 MenuAction::ViewNextPane => "menu.view.next_pane",
68 MenuAction::ViewPrevPane => "menu.view.prev_pane",
69 MenuAction::ToggleLanguage => "menu.settings.toggle_language",
70 MenuAction::ShowShortcuts => "menu.help.shortcuts",
71 MenuAction::ShowAbout => "menu.help.about",
72 MenuAction::Separator => "",
73 }
74 }
75
76 pub fn is_separator(&self) -> bool {
78 matches!(self, MenuAction::Separator)
79 }
80}
81
82#[derive(Debug, Clone)]
84pub struct Menu {
85 pub label_key: &'static str,
86 pub items: &'static [MenuAction],
87}
88
89impl Menu {
90 pub fn item_count(&self) -> usize {
92 self.items.len()
93 }
94
95 pub fn get_item(&self, index: usize) -> Option<MenuAction> {
97 self.items.get(index).copied()
98 }
99}
100
101pub struct MenuBar {
103 pub menus: &'static [Menu],
104}
105
106impl MenuBar {
107 pub fn menu_count(&self) -> usize {
109 self.menus.len()
110 }
111
112 pub fn get_menu(&self, index: usize) -> Option<&Menu> {
114 self.menus.get(index)
115 }
116
117 pub fn get_menu_label_key(&self, index: usize) -> Option<&'static str> {
119 self.menus.get(index).map(|m| m.label_key)
120 }
121
122 pub fn get_item_count(&self, menu_index: usize) -> usize {
124 self.menus
125 .get(menu_index)
126 .map(|m| m.item_count())
127 .unwrap_or(0)
128 }
129
130 pub fn get_action(&self, menu_index: usize, item_index: usize) -> Option<MenuAction> {
132 self.menus
133 .get(menu_index)
134 .and_then(|m| m.get_item(item_index))
135 }
136}
137
138const FILE_MENU_ITEMS: &[MenuAction] = &[
140 MenuAction::SaveConfig,
141 MenuAction::LoadConfig,
142 MenuAction::Separator,
143 MenuAction::Exit,
144];
145
146const SESSION_MENU_ITEMS: &[MenuAction] = &[
147 MenuAction::NewSession,
148 MenuAction::DuplicateSession,
149 MenuAction::RenameSession,
150 MenuAction::Separator,
151 MenuAction::CloseSession,
152];
153
154const VIEW_MENU_ITEMS: &[MenuAction] = &[
155 MenuAction::ViewSingle,
156 MenuAction::ViewSplitHorizontal,
157 MenuAction::ViewSplitVertical,
158 MenuAction::ViewGrid2x2,
159 MenuAction::Separator,
160 MenuAction::ViewNextPane,
161 MenuAction::ViewPrevPane,
162];
163
164const SETTINGS_MENU_ITEMS: &[MenuAction] = &[MenuAction::ToggleLanguage];
165
166const HELP_MENU_ITEMS: &[MenuAction] = &[
167 MenuAction::ShowShortcuts,
168 MenuAction::Separator,
169 MenuAction::ShowAbout,
170];
171
172const ALL_MENUS: &[Menu] = &[
174 Menu {
175 label_key: "menu.file",
176 items: FILE_MENU_ITEMS,
177 },
178 Menu {
179 label_key: "menu.session",
180 items: SESSION_MENU_ITEMS,
181 },
182 Menu {
183 label_key: "menu.view",
184 items: VIEW_MENU_ITEMS,
185 },
186 Menu {
187 label_key: "menu.settings",
188 items: SETTINGS_MENU_ITEMS,
189 },
190 Menu {
191 label_key: "menu.help",
192 items: HELP_MENU_ITEMS,
193 },
194];
195
196pub const MENU_BAR: MenuBar = MenuBar { menus: ALL_MENUS };
198
199pub fn calculate_menu_x_offset(menu_index: usize, lang: Language) -> u16 {
201 use crate::i18n::t;
202
203 let mut offset = 0u16;
204 for i in 0..menu_index.min(MENU_BAR.menu_count()) {
205 if let Some(label_key) = MENU_BAR.get_menu_label_key(i) {
206 let label = t(label_key, lang);
207 offset += display_width(label) as u16 + 4;
209 }
210 }
211 offset
212}
213
214pub fn find_clicked_menu(x: u16, lang: Language) -> Option<usize> {
216 use crate::i18n::t;
217
218 let mut current_x = 0u16;
219 for (i, menu) in MENU_BAR.menus.iter().enumerate() {
220 let label = t(menu.label_key, lang);
221 let menu_width = display_width(label) as u16 + 2; if x >= current_x && x < current_x + menu_width {
224 return Some(i);
225 }
226
227 current_x += menu_width + 2; }
229 None
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn test_menu_bar_structure() {
238 assert_eq!(MENU_BAR.menu_count(), 5);
239 assert_eq!(MENU_BAR.get_item_count(0), 4); assert_eq!(MENU_BAR.get_item_count(1), 5); assert_eq!(MENU_BAR.get_item_count(2), 7); assert_eq!(MENU_BAR.get_item_count(3), 1); assert_eq!(MENU_BAR.get_item_count(4), 3); }
245
246 #[test]
247 fn test_menu_actions() {
248 assert_eq!(MENU_BAR.get_action(0, 0), Some(MenuAction::SaveConfig));
249 assert_eq!(MENU_BAR.get_action(0, 3), Some(MenuAction::Exit));
250 assert_eq!(MENU_BAR.get_action(4, 0), Some(MenuAction::ShowShortcuts));
251 }
252
253 #[test]
254 fn test_separator_detection() {
255 assert!(MenuAction::Separator.is_separator());
256 assert!(!MenuAction::SaveConfig.is_separator());
257 }
258
259 #[test]
260 fn test_invalid_indices() {
261 assert_eq!(MENU_BAR.get_action(99, 0), None);
262 assert_eq!(MENU_BAR.get_action(0, 99), None);
263 }
264
265 #[test]
266 fn test_menu_label_keys() {
267 assert_eq!(MENU_BAR.get_menu_label_key(0), Some("menu.file"));
268 assert_eq!(MENU_BAR.get_menu_label_key(1), Some("menu.session"));
269 assert_eq!(MENU_BAR.get_menu_label_key(4), Some("menu.help"));
270 }
271}