Skip to main content

brows3r_lib/
menus.rs

1//! Native menu builder for brows3r.
2//!
3//! `build_menu` constructs the full application menu (File, Edit, View, Go,
4//! Help) using Tauri 2's menu API.  Menu item IDs are namespaced with
5//! `menu:` followed by the command path so the frontend event bridge can
6//! map them directly to registered commands:
7//!
8//!   menu:file.new-folder  →  registry command "file.new-folder"
9//!   menu:edit.copy        →  registry command "clipboard.copy"
10//!   …
11//!
12//! The frontend registers a `menu-event` listener in `installMenuBridge` and
13//! dispatches each incoming event to the command registry.
14//!
15//! OCP: adding a new menu item means adding one `MenuItem` + one command
16//! registration on the frontend.  No other code needs to change.
17
18use tauri::menu::{AboutMetadata, Menu, MenuItem, PredefinedMenuItem, Submenu};
19use tauri::{AppHandle, Runtime};
20
21/// Build the full application menu and return it.
22///
23/// Returns an error if any menu item or submenu cannot be constructed, which
24/// would indicate a Tauri API contract violation (should not happen at
25/// runtime).
26pub fn build_menu<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<Menu<R>> {
27    // ------------------------------------------------------------------
28    // File menu
29    // ------------------------------------------------------------------
30
31    let new_folder = MenuItem::with_id(
32        app,
33        "menu:file/new-folder",
34        "New Folder",
35        true,
36        Some("CmdOrCtrl+Shift+N"),
37    )?;
38
39    let open_item = MenuItem::with_id(app, "menu:file/open", "Open", true, Some("Return"))?;
40
41    let save_item = MenuItem::with_id(app, "menu:file/save", "Save", true, Some("CmdOrCtrl+S"))?;
42
43    let sep_file = PredefinedMenuItem::separator(app)?;
44
45    let quit_item = PredefinedMenuItem::quit(app, Some("Quit brows3r"))?;
46
47    let file_menu = Submenu::with_id_and_items(
48        app,
49        "menu:file",
50        "File",
51        true,
52        &[&new_folder, &open_item, &save_item, &sep_file, &quit_item],
53    )?;
54
55    // ------------------------------------------------------------------
56    // Edit menu
57    // ------------------------------------------------------------------
58
59    let undo_item = PredefinedMenuItem::undo(app, Some("Undo"))?;
60    let redo_item = PredefinedMenuItem::redo(app, Some("Redo"))?;
61    let sep_edit1 = PredefinedMenuItem::separator(app)?;
62    let cut_item = PredefinedMenuItem::cut(app, Some("Cut"))?;
63    let copy_item = PredefinedMenuItem::copy(app, Some("Copy"))?;
64    let paste_item = PredefinedMenuItem::paste(app, Some("Paste"))?;
65    let sep_edit2 = PredefinedMenuItem::separator(app)?;
66    let select_all_item = PredefinedMenuItem::select_all(app, Some("Select All"))?;
67    let sep_edit3 = PredefinedMenuItem::separator(app)?;
68
69    let find_item = MenuItem::with_id(app, "menu:edit/find", "Find", true, Some("CmdOrCtrl+F"))?;
70
71    let edit_menu = Submenu::with_id_and_items(
72        app,
73        "menu:edit",
74        "Edit",
75        true,
76        &[
77            &undo_item,
78            &redo_item,
79            &sep_edit1,
80            &cut_item,
81            &copy_item,
82            &paste_item,
83            &sep_edit2,
84            &select_all_item,
85            &sep_edit3,
86            &find_item,
87        ],
88    )?;
89
90    // ------------------------------------------------------------------
91    // View menu
92    // ------------------------------------------------------------------
93
94    let view_details = MenuItem::with_id(
95        app,
96        "menu:view/mode/details",
97        "Details",
98        true,
99        Some("CmdOrCtrl+1"),
100    )?;
101    let view_icon_grid = MenuItem::with_id(
102        app,
103        "menu:view/mode/icon-grid",
104        "Icon Grid",
105        true,
106        Some("CmdOrCtrl+2"),
107    )?;
108    let view_gallery = MenuItem::with_id(
109        app,
110        "menu:view/mode/gallery",
111        "Gallery",
112        true,
113        Some("CmdOrCtrl+3"),
114    )?;
115    let view_column = MenuItem::with_id(
116        app,
117        "menu:view/mode/column",
118        "Column",
119        true,
120        Some("CmdOrCtrl+4"),
121    )?;
122    let view_tree = MenuItem::with_id(
123        app,
124        "menu:view/mode/tree",
125        "Tree",
126        true,
127        Some("CmdOrCtrl+5"),
128    )?;
129    let view_flat_key = MenuItem::with_id(
130        app,
131        "menu:view/mode/flat-key",
132        "Flat Key",
133        true,
134        Some("CmdOrCtrl+6"),
135    )?;
136    let view_dual_pane = MenuItem::with_id(
137        app,
138        "menu:view/mode/dual-pane",
139        "Dual Pane",
140        true,
141        Some("CmdOrCtrl+7"),
142    )?;
143
144    let sep_view1 = PredefinedMenuItem::separator(app)?;
145
146    let refresh_item = MenuItem::with_id(
147        app,
148        "menu:view/refresh",
149        "Refresh",
150        true,
151        Some("CmdOrCtrl+R"),
152    )?;
153
154    let sep_view2 = PredefinedMenuItem::separator(app)?;
155
156    let toggle_sidebar = MenuItem::with_id(
157        app,
158        "menu:view/toggle-sidebar",
159        "Toggle Sidebar",
160        true,
161        None::<&str>,
162    )?;
163
164    let toggle_preview = MenuItem::with_id(
165        app,
166        "menu:view/toggle-preview",
167        "Toggle Preview",
168        true,
169        None::<&str>,
170    )?;
171
172    let view_menu = Submenu::with_id_and_items(
173        app,
174        "menu:view",
175        "View",
176        true,
177        &[
178            &view_details,
179            &view_icon_grid,
180            &view_gallery,
181            &view_column,
182            &view_tree,
183            &view_flat_key,
184            &view_dual_pane,
185            &sep_view1,
186            &refresh_item,
187            &sep_view2,
188            &toggle_sidebar,
189            &toggle_preview,
190        ],
191    )?;
192
193    // ------------------------------------------------------------------
194    // Go menu
195    // ------------------------------------------------------------------
196
197    let back_item = MenuItem::with_id(app, "menu:go/back", "Back", true, Some("CmdOrCtrl+["))?;
198
199    let forward_item =
200        MenuItem::with_id(app, "menu:go/forward", "Forward", true, Some("CmdOrCtrl+]"))?;
201
202    let up_item = MenuItem::with_id(app, "menu:go/up", "Up", true, Some("CmdOrCtrl+Up"))?;
203
204    let sep_go = PredefinedMenuItem::separator(app)?;
205
206    let bookmarks_item =
207        MenuItem::with_id(app, "menu:go/bookmarks", "Bookmarks", true, None::<&str>)?;
208
209    let go_menu = Submenu::with_id_and_items(
210        app,
211        "menu:go",
212        "Go",
213        true,
214        &[
215            &back_item,
216            &forward_item,
217            &up_item,
218            &sep_go,
219            &bookmarks_item,
220        ],
221    )?;
222
223    // ------------------------------------------------------------------
224    // Help menu
225    // ------------------------------------------------------------------
226
227    let about_item =
228        PredefinedMenuItem::about(app, Some("About brows3r"), Some(AboutMetadata::default()))?;
229
230    let sep_help = PredefinedMenuItem::separator(app)?;
231
232    let docs_item = MenuItem::with_id(app, "menu:help/docs", "Documentation", true, None::<&str>)?;
233
234    let report_bug_item = MenuItem::with_id(
235        app,
236        "menu:help/report-bug",
237        "Report a Bug",
238        true,
239        None::<&str>,
240    )?;
241
242    let help_menu = Submenu::with_id_and_items(
243        app,
244        "menu:help",
245        "Help",
246        true,
247        &[&about_item, &sep_help, &docs_item, &report_bug_item],
248    )?;
249
250    // ------------------------------------------------------------------
251    // Assemble top-level menu
252    // ------------------------------------------------------------------
253
254    Menu::with_items(
255        app,
256        &[&file_menu, &edit_menu, &view_menu, &go_menu, &help_menu],
257    )
258}
259
260// ---------------------------------------------------------------------------
261// Tests
262// ---------------------------------------------------------------------------
263
264#[cfg(test)]
265mod tests {
266    // `build_menu` requires a real AppHandle which is not constructable in a
267    // unit-test context without a full Tauri mock runtime.
268    //
269    // The smoke test here verifies the module-level public surface compiles and
270    // that the function signature is stable. A full integration test requires
271    // a Tauri test runtime (tracked as a follow-up); the CI build is the
272    // primary gate for menu compilation correctness.
273
274    #[test]
275    fn build_menu_symbol_is_reachable() {
276        // Verify the symbol resolves at link time — no runtime is needed.
277        let _ = super::build_menu::<tauri::Wry>;
278    }
279}