Skip to main content

fresh/app/
menu_actions.rs

1//! Menu-related action handlers.
2//!
3//! This module contains handlers for menu navigation, execution, and mouse interaction.
4
5use super::Editor;
6use crate::app::types::HoverTarget;
7use crate::config::{generate_dynamic_items, Menu, MenuExt, MenuItem};
8use crate::input::keybindings::Action;
9use anyhow::Result as AnyhowResult;
10
11impl Editor {
12    /// Get all menus (built-in menus + plugin menus) with DynamicSubmenus expanded.
13    fn all_menus(&self) -> Vec<Menu> {
14        self.menus
15            .menus
16            .iter()
17            .chain(self.menu_state.plugin_menus.iter())
18            .cloned()
19            .map(|mut menu| {
20                menu.expand_dynamic_items(&self.menu_state.themes_dir);
21                menu
22            })
23            .collect()
24    }
25
26    /// Handle MenuActivate action - opens the first menu.
27    /// If the menu bar is hidden, it will be temporarily shown.
28    pub fn handle_menu_activate(&mut self) {
29        // Auto-show menu bar if hidden
30        if !self.menu_bar_visible {
31            self.menu_bar_visible = true;
32            self.menu_bar_auto_shown = true;
33        }
34        self.on_editor_focus_lost();
35        self.menu_state.open_menu(0);
36    }
37
38    /// Close the menu and auto-hide the menu bar if it was temporarily shown.
39    /// Use this method instead of `menu_state.close_menu()` to ensure auto-hide works.
40    pub fn close_menu_with_auto_hide(&mut self) {
41        self.menu_state.close_menu();
42        if self.menu_bar_auto_shown {
43            self.menu_bar_visible = false;
44            self.menu_bar_auto_shown = false;
45        }
46    }
47
48    /// Handle MenuClose action - closes the active menu.
49    /// If the menu bar was auto-shown, it will be hidden again.
50    pub fn handle_menu_close(&mut self) {
51        self.close_menu_with_auto_hide();
52    }
53
54    /// Handle MenuLeft action - close submenu or go to previous menu.
55    pub fn handle_menu_left(&mut self) {
56        if !self.menu_state.close_submenu() {
57            let all_menus = self.all_menus();
58            self.menu_state.prev_menu(&all_menus);
59        }
60    }
61
62    /// Handle MenuRight action - open submenu or go to next menu.
63    pub fn handle_menu_right(&mut self) {
64        let all_menus = self.all_menus();
65        if !self.menu_state.open_submenu(&all_menus) {
66            self.menu_state.next_menu(&all_menus);
67        }
68    }
69
70    /// Handle MenuUp action - select previous item in menu.
71    pub fn handle_menu_up(&mut self) {
72        if let Some(active_idx) = self.menu_state.active_menu {
73            let all_menus = self.all_menus();
74            if let Some(menu) = all_menus.get(active_idx) {
75                self.menu_state.prev_item(menu);
76            }
77        }
78    }
79
80    /// Handle MenuDown action - select next item in menu.
81    pub fn handle_menu_down(&mut self) {
82        if let Some(active_idx) = self.menu_state.active_menu {
83            let all_menus = self.all_menus();
84            if let Some(menu) = all_menus.get(active_idx) {
85                self.menu_state.next_item(menu);
86            }
87        }
88    }
89
90    /// Handle MenuExecute action - execute highlighted item or open submenu.
91    ///
92    /// Returns `Some(action)` if an action should be executed after this call.
93    pub fn handle_menu_execute(&mut self) -> Option<Action> {
94        let all_menus = self.all_menus();
95
96        // Check if highlighted item is a submenu - if so, open it
97        if self.menu_state.is_highlighted_submenu(&all_menus) {
98            self.menu_state.open_submenu(&all_menus);
99            return None;
100        }
101
102        // Update context before checking if action is enabled
103        use crate::view::ui::context_keys;
104        self.menu_state
105            .context
106            .set(context_keys::HAS_SELECTION, self.has_active_selection())
107            .set(
108                context_keys::FILE_EXPLORER_FOCUSED,
109                self.key_context == crate::input::keybindings::KeyContext::FileExplorer,
110            );
111
112        if let Some((action_name, args)) = self.menu_state.get_highlighted_action(&all_menus) {
113            // Close the menu with auto-hide support
114            self.close_menu_with_auto_hide();
115
116            // Parse and return the action
117            if let Some(action) = Action::from_str(&action_name, &args) {
118                Some(action)
119            } else {
120                // Treat as a plugin action (global Lua function)
121                Some(Action::PluginAction(action_name))
122            }
123        } else {
124            None
125        }
126    }
127
128    /// Handle MenuOpen action - open a specific menu by name.
129    /// If the menu bar is hidden, it will be temporarily shown.
130    pub fn handle_menu_open(&mut self, menu_name: &str) {
131        // Auto-show menu bar if hidden
132        if !self.menu_bar_visible {
133            self.menu_bar_visible = true;
134            self.menu_bar_auto_shown = true;
135        }
136        self.on_editor_focus_lost();
137
138        let all_menus = self.all_menus();
139        for (idx, menu) in all_menus.iter().enumerate() {
140            // Match by id (locale-independent) rather than label (translated)
141            if menu.match_id().eq_ignore_ascii_case(menu_name) {
142                self.menu_state.open_menu(idx);
143                break;
144            }
145        }
146    }
147
148    /// Compute hover target for menu dropdown chain (main dropdown and submenus).
149    /// Uses the cached menu layout from the previous render frame.
150    pub(crate) fn compute_menu_dropdown_hover(
151        &self,
152        col: u16,
153        row: u16,
154        menu_index: usize,
155    ) -> Option<HoverTarget> {
156        let menu_layout = self.cached_layout.menu_layout.as_ref()?;
157
158        // Check submenu items first (they're rendered on top)
159        if let Some((depth, item_idx)) = menu_layout.submenu_item_at(col, row) {
160            return Some(HoverTarget::SubmenuItem(depth, item_idx));
161        }
162
163        // Check main dropdown items
164        if let Some(item_idx) = menu_layout.item_at(col, row) {
165            return Some(HoverTarget::MenuDropdownItem(menu_index, item_idx));
166        }
167
168        None
169    }
170
171    /// Handle click on menu dropdown chain (main dropdown and any open submenus).
172    /// Returns Some(Ok(())) if click was handled, None if click was outside all dropdowns.
173    /// Uses the cached menu layout from the previous render frame for hit testing.
174    pub(crate) fn handle_menu_dropdown_click(
175        &mut self,
176        col: u16,
177        row: u16,
178        menu: &Menu,
179    ) -> AnyhowResult<Option<AnyhowResult<()>>> {
180        use crate::view::ui::menu::MenuHit;
181
182        let menu_layout = match &self.cached_layout.menu_layout {
183            Some(layout) => layout.clone(),
184            None => return Ok(None),
185        };
186
187        // Use the layout to determine what was clicked
188        let hit = match menu_layout.hit_test(col, row) {
189            Some(MenuHit::DropdownItem(item_idx)) => (0, item_idx),
190            Some(MenuHit::SubmenuItem { depth, index }) => (depth, index),
191            _ => return Ok(None), // Click outside dropdown areas
192        };
193
194        let (depth, item_idx) = hit;
195
196        // Navigate to the clicked item in the menu structure
197        let items = if depth == 0 {
198            // Main dropdown items
199            menu.items.clone()
200        } else {
201            // Navigate through submenu path to find items at this depth
202            let mut current_items = menu.items.clone();
203            for d in 0..depth {
204                if d < self.menu_state.submenu_path.len() {
205                    let submenu_idx = self.menu_state.submenu_path[d];
206                    match current_items.get(submenu_idx) {
207                        Some(MenuItem::Submenu { items, .. }) => {
208                            current_items = items.clone();
209                        }
210                        Some(MenuItem::DynamicSubmenu { source, .. }) => {
211                            current_items =
212                                generate_dynamic_items(source, &self.menu_state.themes_dir);
213                        }
214                        _ => return Ok(Some(Ok(()))),
215                    }
216                } else {
217                    return Ok(Some(Ok(())));
218                }
219            }
220            current_items
221        };
222
223        let Some(item) = items.get(item_idx) else {
224            return Ok(Some(Ok(())));
225        };
226
227        // Handle the clicked item
228        match item {
229            MenuItem::Separator { .. } | MenuItem::Label { .. } => {
230                // Clicked on separator or label - do nothing but consume the click
231                Ok(Some(Ok(())))
232            }
233            MenuItem::Submenu {
234                items: submenu_items,
235                ..
236            } => {
237                // Clicked on submenu - open it
238                self.menu_state.submenu_path.truncate(depth);
239                if !submenu_items.is_empty() {
240                    self.menu_state.submenu_path.push(item_idx);
241                    self.menu_state.highlighted_item = Some(0);
242                }
243                Ok(Some(Ok(())))
244            }
245            MenuItem::DynamicSubmenu { source, .. } => {
246                // Clicked on dynamic submenu - open it
247                self.menu_state.submenu_path.truncate(depth);
248                let generated = generate_dynamic_items(source, &self.menu_state.themes_dir);
249                if !generated.is_empty() {
250                    self.menu_state.submenu_path.push(item_idx);
251                    self.menu_state.highlighted_item = Some(0);
252                }
253                Ok(Some(Ok(())))
254            }
255            MenuItem::Action { action, args, .. } => {
256                // Clicked on action - execute it
257                let action_name = action.clone();
258                let action_args = args.clone();
259
260                self.close_menu_with_auto_hide();
261
262                if let Some(action) = Action::from_str(&action_name, &action_args) {
263                    return Ok(Some(self.handle_action(action)));
264                }
265                Ok(Some(Ok(())))
266            }
267        }
268    }
269}