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