Files
tolaria/src-tauri/src/menu.rs
Luca Rossi 384cffec48 feat: zoom in/out keyboard shortcuts (Cmd+=, Cmd+-, Cmd+0) (#134)
* feat: zoom in/out keyboard shortcuts (Cmd+=, Cmd+-, Cmd+0)

Add zoom control with keyboard shortcuts, native menu items, command
palette integration, and StatusBar zoom indicator with localStorage
persistence (80-150%, 10% step).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add zoom-shortcuts design file with StatusBar zoom indicator

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* design: zoom-shortcuts — StatusBar zoom indicator + Command Palette entries

---------

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 12:41:57 +01:00

236 lines
6.4 KiB
Rust

use tauri::{
menu::{MenuBuilder, MenuItemBuilder, MenuItemKind, Submenu, SubmenuBuilder},
App, AppHandle, Emitter,
};
// Custom menu item IDs that emit events to the frontend.
const APP_SETTINGS: &str = "app-settings";
const FILE_NEW_NOTE: &str = "file-new-note";
const FILE_QUICK_OPEN: &str = "file-quick-open";
const FILE_SAVE: &str = "file-save";
const FILE_CLOSE_TAB: &str = "file-close-tab";
const VIEW_EDITOR_ONLY: &str = "view-editor-only";
const VIEW_EDITOR_LIST: &str = "view-editor-list";
const VIEW_ALL: &str = "view-all";
const VIEW_TOGGLE_INSPECTOR: &str = "view-toggle-inspector";
const VIEW_COMMAND_PALETTE: &str = "view-command-palette";
const VIEW_ZOOM_IN: &str = "view-zoom-in";
const VIEW_ZOOM_OUT: &str = "view-zoom-out";
const VIEW_ZOOM_RESET: &str = "view-zoom-reset";
const CUSTOM_IDS: &[&str] = &[
APP_SETTINGS,
FILE_NEW_NOTE,
FILE_QUICK_OPEN,
FILE_SAVE,
FILE_CLOSE_TAB,
VIEW_EDITOR_ONLY,
VIEW_EDITOR_LIST,
VIEW_ALL,
VIEW_TOGGLE_INSPECTOR,
VIEW_COMMAND_PALETTE,
VIEW_ZOOM_IN,
VIEW_ZOOM_OUT,
VIEW_ZOOM_RESET,
];
/// IDs of menu items that should be disabled when no note tab is active.
const NOTE_DEPENDENT_IDS: &[&str] = &[FILE_SAVE, FILE_CLOSE_TAB];
type MenuResult = Result<Submenu<tauri::Wry>, Box<dyn std::error::Error>>;
fn build_app_menu(app: &App) -> MenuResult {
let settings_item = MenuItemBuilder::new("Settings...")
.id(APP_SETTINGS)
.accelerator("CmdOrCtrl+,")
.build(app)?;
Ok(SubmenuBuilder::new(app, "Laputa")
.about(None)
.separator()
.item(&settings_item)
.separator()
.services()
.separator()
.hide()
.hide_others()
.show_all()
.separator()
.quit()
.build()?)
}
fn build_file_menu(app: &App) -> MenuResult {
let new_note = MenuItemBuilder::new("New Note")
.id(FILE_NEW_NOTE)
.accelerator("CmdOrCtrl+N")
.build(app)?;
let quick_open = MenuItemBuilder::new("Quick Open")
.id(FILE_QUICK_OPEN)
.accelerator("CmdOrCtrl+P")
.build(app)?;
let save = MenuItemBuilder::new("Save")
.id(FILE_SAVE)
.accelerator("CmdOrCtrl+S")
.build(app)?;
let close_tab = MenuItemBuilder::new("Close Tab")
.id(FILE_CLOSE_TAB)
.accelerator("CmdOrCtrl+W")
.build(app)?;
Ok(SubmenuBuilder::new(app, "File")
.item(&new_note)
.item(&quick_open)
.separator()
.item(&save)
.separator()
.item(&close_tab)
.build()?)
}
fn build_edit_menu(app: &App) -> MenuResult {
Ok(SubmenuBuilder::new(app, "Edit")
.undo()
.redo()
.separator()
.cut()
.copy()
.paste()
.separator()
.select_all()
.build()?)
}
fn build_view_menu(app: &App) -> MenuResult {
let editor_only = MenuItemBuilder::new("Editor Only")
.id(VIEW_EDITOR_ONLY)
.accelerator("CmdOrCtrl+1")
.build(app)?;
let editor_list = MenuItemBuilder::new("Editor + Notes")
.id(VIEW_EDITOR_LIST)
.accelerator("CmdOrCtrl+2")
.build(app)?;
let all_panels = MenuItemBuilder::new("All Panels")
.id(VIEW_ALL)
.accelerator("CmdOrCtrl+3")
.build(app)?;
let toggle_inspector = MenuItemBuilder::new("Toggle Inspector")
.id(VIEW_TOGGLE_INSPECTOR)
.build(app)?;
let command_palette = MenuItemBuilder::new("Command Palette")
.id(VIEW_COMMAND_PALETTE)
.accelerator("CmdOrCtrl+K")
.build(app)?;
let zoom_in = MenuItemBuilder::new("Zoom In")
.id(VIEW_ZOOM_IN)
.accelerator("CmdOrCtrl+=")
.build(app)?;
let zoom_out = MenuItemBuilder::new("Zoom Out")
.id(VIEW_ZOOM_OUT)
.accelerator("CmdOrCtrl+-")
.build(app)?;
let zoom_reset = MenuItemBuilder::new("Actual Size")
.id(VIEW_ZOOM_RESET)
.accelerator("CmdOrCtrl+0")
.build(app)?;
Ok(SubmenuBuilder::new(app, "View")
.item(&editor_only)
.item(&editor_list)
.item(&all_panels)
.separator()
.item(&toggle_inspector)
.separator()
.item(&zoom_in)
.item(&zoom_out)
.item(&zoom_reset)
.separator()
.item(&command_palette)
.build()?)
}
fn build_window_menu(app: &App) -> MenuResult {
Ok(SubmenuBuilder::new(app, "Window")
.minimize()
.maximize()
.separator()
.close_window()
.build()?)
}
pub fn setup_menu(app: &App) -> Result<(), Box<dyn std::error::Error>> {
let app_menu = build_app_menu(app)?;
let file_menu = build_file_menu(app)?;
let edit_menu = build_edit_menu(app)?;
let view_menu = build_view_menu(app)?;
let window_menu = build_window_menu(app)?;
let menu = MenuBuilder::new(app)
.item(&app_menu)
.item(&file_menu)
.item(&edit_menu)
.item(&view_menu)
.item(&window_menu)
.build()?;
app.set_menu(menu)?;
app.on_menu_event(|app_handle, event| {
let id = event.id().0.as_str();
if CUSTOM_IDS.contains(&id) {
let _ = app_handle.emit("menu-event", id);
}
});
Ok(())
}
/// Enable or disable menu items that depend on having an active note tab.
pub fn set_note_items_enabled(app_handle: &AppHandle, enabled: bool) {
let Some(menu) = app_handle.menu() else {
return;
};
for id in NOTE_DEPENDENT_IDS {
if let Some(MenuItemKind::MenuItem(mi)) = menu.get(*id) {
let _ = mi.set_enabled(enabled);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn custom_ids_include_all_expected_items() {
let expected = [
"app-settings",
"file-new-note",
"file-quick-open",
"file-save",
"file-close-tab",
"view-editor-only",
"view-editor-list",
"view-all",
"view-toggle-inspector",
"view-command-palette",
"view-zoom-in",
"view-zoom-out",
"view-zoom-reset",
];
for id in &expected {
assert!(CUSTOM_IDS.contains(id), "missing custom ID: {id}");
}
}
#[test]
fn note_dependent_ids_are_subset_of_custom_ids() {
for id in NOTE_DEPENDENT_IDS {
assert!(
CUSTOM_IDS.contains(id),
"note-dependent ID {id} not in CUSTOM_IDS"
);
}
}
}