feat: publish HoloLake model-native living system source
This commit is contained in:
parent
6ad10edde1
commit
c395dd3a99
2467 changed files with 615073 additions and 0 deletions
683
product-source/hololake-platform/src-tauri/src/menu.rs
Normal file
683
product-source/hololake-platform/src-tauri/src/menu.rs
Normal file
|
|
@ -0,0 +1,683 @@
|
|||
#[cfg(not(target_os = "macos"))]
|
||||
use crate::window_state::MAIN_WINDOW_LABEL;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{BTreeMap, HashSet},
|
||||
error::Error,
|
||||
sync::OnceLock,
|
||||
};
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
use tauri::{menu::MenuEvent, Manager};
|
||||
use tauri::{
|
||||
menu::{
|
||||
MenuBuilder, MenuItem, MenuItemBuilder, MenuItemKind, Submenu, SubmenuBuilder,
|
||||
WINDOW_SUBMENU_ID,
|
||||
},
|
||||
App, AppHandle, Emitter,
|
||||
};
|
||||
|
||||
const APP_COMMAND_MANIFEST_JSON: &str = include_str!("../../src/shared/appCommandManifest.json");
|
||||
const NOTE_DEPENDENT_GROUP: &str = "noteDependent";
|
||||
const EDITOR_FIND_DEPENDENT_GROUP: &str = "editorFindDependent";
|
||||
const NOTE_LIST_SEARCH_DEPENDENT_GROUP: &str = "noteListSearchDependent";
|
||||
const RESTORE_DELETED_DEPENDENT_GROUP: &str = "restoreDeletedDependent";
|
||||
const GIT_COMMIT_DEPENDENT_GROUP: &str = "gitCommitDependent";
|
||||
const GIT_CONFLICT_DEPENDENT_GROUP: &str = "gitConflictDependent";
|
||||
const GIT_NO_REMOTE_DEPENDENT_GROUP: &str = "gitNoRemoteDependent";
|
||||
|
||||
type MenuResult = Result<Submenu<tauri::Wry>, Box<dyn Error>>;
|
||||
type AppSubmenuBuilder<'a> = SubmenuBuilder<'a, tauri::Wry, App>;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AppCommandManifest {
|
||||
commands: BTreeMap<String, ManifestCommand>,
|
||||
menus: Vec<ManifestMenuSection>,
|
||||
app_menu: Vec<ManifestMenuItem>,
|
||||
menu_state_groups: BTreeMap<String, Vec<MenuStateGroupReference>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ManifestCommand {
|
||||
id: String,
|
||||
shortcut: Option<ManifestShortcut>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ManifestShortcut {
|
||||
accelerator: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ManifestMenuSection {
|
||||
label: String,
|
||||
items: Vec<ManifestMenuItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "kind")]
|
||||
enum ManifestMenuItem {
|
||||
#[serde(rename = "separator")]
|
||||
Separator,
|
||||
#[serde(rename = "command")]
|
||||
Command {
|
||||
command: String,
|
||||
id: Option<String>,
|
||||
label: PlatformLabel,
|
||||
#[serde(default, deserialize_with = "deserialize_accelerator")]
|
||||
accelerator: ManifestAccelerator,
|
||||
enabled: Option<bool>,
|
||||
},
|
||||
#[serde(rename = "menu-event")]
|
||||
MenuEvent {
|
||||
id: String,
|
||||
label: PlatformLabel,
|
||||
#[serde(default, deserialize_with = "deserialize_accelerator")]
|
||||
accelerator: ManifestAccelerator,
|
||||
enabled: Option<bool>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum PlatformLabel {
|
||||
Plain(String),
|
||||
Platform {
|
||||
macos: Option<String>,
|
||||
windows: Option<String>,
|
||||
linux: Option<String>,
|
||||
default: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
enum ManifestAccelerator {
|
||||
#[default]
|
||||
Inherit,
|
||||
Suppressed,
|
||||
Explicit(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum MenuStateGroupReference {
|
||||
Command { command: String },
|
||||
Id { id: String },
|
||||
}
|
||||
|
||||
fn deserialize_accelerator<'de, D>(deserializer: D) -> Result<ManifestAccelerator, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Option::<String>::deserialize(deserializer).map(|accelerator| match accelerator {
|
||||
Some(accelerator) => ManifestAccelerator::Explicit(accelerator),
|
||||
None => ManifestAccelerator::Suppressed,
|
||||
})
|
||||
}
|
||||
|
||||
impl PlatformLabel {
|
||||
fn resolve(&self, target_os: &str) -> &str {
|
||||
match self {
|
||||
Self::Plain(label) => label.as_str(),
|
||||
Self::Platform {
|
||||
macos,
|
||||
windows,
|
||||
linux,
|
||||
default,
|
||||
} => match target_os {
|
||||
"macos" => macos.as_deref().unwrap_or(default.as_str()),
|
||||
"windows" => windows.as_deref().unwrap_or(default.as_str()),
|
||||
"linux" => linux.as_deref().unwrap_or(default.as_str()),
|
||||
_ => default.as_str(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ManifestMenuItem {
|
||||
fn command_id<'a>(&'a self, manifest: &'a AppCommandManifest) -> Option<&'a str> {
|
||||
match self {
|
||||
Self::Command { command, .. } => manifest
|
||||
.commands
|
||||
.get(command)
|
||||
.map(|command| command.id.as_str()),
|
||||
Self::MenuEvent { id, .. } => Some(id.as_str()),
|
||||
Self::Separator => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn menu_item_id<'a>(&'a self, manifest: &'a AppCommandManifest) -> Option<&'a str> {
|
||||
match self {
|
||||
Self::Command { command, id, .. } => id.as_deref().or_else(|| {
|
||||
manifest
|
||||
.commands
|
||||
.get(command)
|
||||
.map(|command| command.id.as_str())
|
||||
}),
|
||||
Self::MenuEvent { id, .. } => Some(id.as_str()),
|
||||
Self::Separator => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn label(&self, target_os: &str) -> Option<&str> {
|
||||
match self {
|
||||
Self::Command { label, .. } | Self::MenuEvent { label, .. } => {
|
||||
Some(label.resolve(target_os))
|
||||
}
|
||||
Self::Separator => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn accelerator<'a>(&'a self, manifest: &'a AppCommandManifest) -> Option<&'a str> {
|
||||
match self {
|
||||
Self::Command {
|
||||
command,
|
||||
accelerator,
|
||||
..
|
||||
} => match accelerator {
|
||||
ManifestAccelerator::Explicit(accelerator) => Some(accelerator.as_str()),
|
||||
ManifestAccelerator::Suppressed => None,
|
||||
ManifestAccelerator::Inherit => manifest
|
||||
.commands
|
||||
.get(command)
|
||||
.and_then(|command| command.shortcut.as_ref())
|
||||
.map(|shortcut| shortcut.accelerator.as_str()),
|
||||
},
|
||||
Self::MenuEvent { accelerator, .. } => match accelerator {
|
||||
ManifestAccelerator::Explicit(accelerator) => Some(accelerator.as_str()),
|
||||
ManifestAccelerator::Suppressed | ManifestAccelerator::Inherit => None,
|
||||
},
|
||||
Self::Separator => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn enabled(&self) -> bool {
|
||||
match self {
|
||||
Self::Command { enabled, .. } | Self::MenuEvent { enabled, .. } => {
|
||||
enabled.unwrap_or(true)
|
||||
}
|
||||
Self::Separator => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static APP_COMMAND_MANIFEST: OnceLock<AppCommandManifest> = OnceLock::new();
|
||||
static CUSTOM_MENU_IDS: OnceLock<HashSet<String>> = OnceLock::new();
|
||||
|
||||
fn manifest() -> &'static AppCommandManifest {
|
||||
APP_COMMAND_MANIFEST.get_or_init(|| {
|
||||
serde_json::from_str(APP_COMMAND_MANIFEST_JSON)
|
||||
.expect("shared app command manifest must be valid JSON")
|
||||
})
|
||||
}
|
||||
|
||||
fn manifest_menu_items() -> impl Iterator<Item = &'static ManifestMenuItem> {
|
||||
let manifest = manifest();
|
||||
manifest
|
||||
.menus
|
||||
.iter()
|
||||
.flat_map(|section| section.items.iter())
|
||||
.chain(manifest.app_menu.iter())
|
||||
}
|
||||
|
||||
fn custom_menu_ids() -> &'static HashSet<String> {
|
||||
CUSTOM_MENU_IDS.get_or_init(|| {
|
||||
manifest_menu_items()
|
||||
.filter_map(|item| item.menu_item_id(manifest()))
|
||||
.map(str::to_owned)
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn manifest_section(label: &str) -> Result<&'static ManifestMenuSection, Box<dyn Error>> {
|
||||
manifest()
|
||||
.menus
|
||||
.iter()
|
||||
.find(|section| section.label == label)
|
||||
.ok_or_else(|| format!("Missing menu section in command manifest: {label}").into())
|
||||
}
|
||||
|
||||
fn app_menu_includes_services(target_os: &str) -> bool {
|
||||
target_os == "macos"
|
||||
}
|
||||
|
||||
fn window_menu_event_handler_required(target_os: &str) -> bool {
|
||||
target_os != "macos"
|
||||
}
|
||||
|
||||
fn native_window_menu_submenu_id(target_os: &str) -> Option<&'static str> {
|
||||
if target_os == "macos" {
|
||||
Some(WINDOW_SUBMENU_ID)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn native_menu_label(label: &str) -> Cow<'_, str> {
|
||||
if label.contains('&') {
|
||||
Cow::Owned(label.replace('&', "&&"))
|
||||
} else {
|
||||
Cow::Borrowed(label)
|
||||
}
|
||||
}
|
||||
|
||||
fn build_manifest_menu_item(
|
||||
app: &App,
|
||||
item: &ManifestMenuItem,
|
||||
) -> Result<Option<MenuItem<tauri::Wry>>, Box<dyn Error>> {
|
||||
let Some(id) = item.menu_item_id(manifest()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(label) = item.label(std::env::consts::OS) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let label = native_menu_label(label);
|
||||
|
||||
let mut builder = MenuItemBuilder::new(label.as_ref())
|
||||
.id(id)
|
||||
.enabled(item.enabled());
|
||||
if let Some(accelerator) = item.accelerator(manifest()) {
|
||||
builder = builder.accelerator(accelerator);
|
||||
}
|
||||
Ok(Some(builder.build(app)?))
|
||||
}
|
||||
|
||||
fn append_manifest_item<'a>(
|
||||
app: &'a App,
|
||||
builder: AppSubmenuBuilder<'a>,
|
||||
item: &ManifestMenuItem,
|
||||
) -> Result<AppSubmenuBuilder<'a>, Box<dyn Error>> {
|
||||
if matches!(item, ManifestMenuItem::Separator) {
|
||||
return Ok(builder.separator());
|
||||
}
|
||||
|
||||
let Some(item) = build_manifest_menu_item(app, item)? else {
|
||||
return Ok(builder);
|
||||
};
|
||||
Ok(builder.item(&item))
|
||||
}
|
||||
|
||||
fn build_manifest_menu(app: &App, label: &str) -> MenuResult {
|
||||
let section = manifest_section(label)?;
|
||||
let mut builder = SubmenuBuilder::new(app, section.label.as_str());
|
||||
for item in §ion.items {
|
||||
builder = append_manifest_item(app, builder, item)?;
|
||||
}
|
||||
Ok(builder.build()?)
|
||||
}
|
||||
|
||||
fn build_app_menu(app: &App) -> MenuResult {
|
||||
let mut builder = SubmenuBuilder::new(app, "Tolaria").about(None).separator();
|
||||
|
||||
for item in &manifest().app_menu {
|
||||
builder = append_manifest_item(app, builder, item)?;
|
||||
}
|
||||
|
||||
builder = builder.separator();
|
||||
|
||||
if app_menu_includes_services(std::env::consts::OS) {
|
||||
builder = builder
|
||||
.services()
|
||||
.separator()
|
||||
.hide()
|
||||
.hide_others()
|
||||
.show_all()
|
||||
.separator();
|
||||
}
|
||||
|
||||
Ok(builder.quit().build()?)
|
||||
}
|
||||
|
||||
fn build_file_menu(app: &App) -> MenuResult {
|
||||
build_manifest_menu(app, "File")
|
||||
}
|
||||
|
||||
fn build_edit_menu(app: &App) -> MenuResult {
|
||||
let section = manifest_section("Edit")?;
|
||||
let mut items = section.items.iter();
|
||||
let mut builder = SubmenuBuilder::new(app, "Edit");
|
||||
|
||||
for item in items.by_ref() {
|
||||
if matches!(item, ManifestMenuItem::Separator) {
|
||||
break;
|
||||
}
|
||||
builder = append_manifest_item(app, builder, item)?;
|
||||
}
|
||||
|
||||
builder = builder.separator().cut().copy().paste();
|
||||
|
||||
if let Some(paste_plain_text) = items.next() {
|
||||
builder = append_manifest_item(app, builder, paste_plain_text)?;
|
||||
}
|
||||
|
||||
builder = builder.separator().select_all().separator();
|
||||
|
||||
if matches!(items.clone().next(), Some(ManifestMenuItem::Separator)) {
|
||||
items.next();
|
||||
}
|
||||
|
||||
for item in items {
|
||||
builder = append_manifest_item(app, builder, item)?;
|
||||
}
|
||||
|
||||
Ok(builder.build()?)
|
||||
}
|
||||
|
||||
fn build_view_menu(app: &App) -> MenuResult {
|
||||
build_manifest_menu(app, "View")
|
||||
}
|
||||
|
||||
fn build_go_menu(app: &App) -> MenuResult {
|
||||
build_manifest_menu(app, "Go")
|
||||
}
|
||||
|
||||
fn build_note_menu(app: &App) -> MenuResult {
|
||||
build_manifest_menu(app, "Note")
|
||||
}
|
||||
|
||||
fn build_vault_menu(app: &App) -> MenuResult {
|
||||
build_manifest_menu(app, "Vault")
|
||||
}
|
||||
|
||||
fn build_window_menu(app: &App) -> MenuResult {
|
||||
let mut builder = SubmenuBuilder::new(app, "Window");
|
||||
if let Some(id) = native_window_menu_submenu_id(std::env::consts::OS) {
|
||||
builder = builder.id(id);
|
||||
}
|
||||
|
||||
Ok(builder
|
||||
.minimize()
|
||||
.maximize()
|
||||
.separator()
|
||||
.close_window()
|
||||
.build()?)
|
||||
}
|
||||
|
||||
pub fn setup_menu(app: &App) -> Result<(), Box<dyn 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 go_menu = build_go_menu(app)?;
|
||||
let note_menu = build_note_menu(app)?;
|
||||
let vault_menu = build_vault_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(&go_menu)
|
||||
.item(¬e_menu)
|
||||
.item(&vault_menu)
|
||||
.item(&window_menu)
|
||||
.build()?;
|
||||
|
||||
app.set_menu(menu)?;
|
||||
|
||||
app.on_menu_event(|app_handle, event| {
|
||||
let id = event.id().0.as_str();
|
||||
let _ = emit_custom_menu_event(app_handle, id);
|
||||
});
|
||||
|
||||
register_window_menu_event_handler(app)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn register_window_menu_event_handler(app: &App) -> Result<(), Box<dyn Error>> {
|
||||
debug_assert!(window_menu_event_handler_required(std::env::consts::OS));
|
||||
let window = app.get_webview_window(MAIN_WINDOW_LABEL).ok_or_else(|| {
|
||||
format!("setup_menu: window '{MAIN_WINDOW_LABEL}' not found; menu events will not fire")
|
||||
})?;
|
||||
let app_handle = app.handle().clone();
|
||||
window.on_menu_event(move |_window, event: MenuEvent| {
|
||||
let id = event.id().0.as_str();
|
||||
let _ = emit_custom_menu_event(&app_handle, id);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn register_window_menu_event_handler(_app: &App) -> Result<(), Box<dyn Error>> {
|
||||
debug_assert!(!window_menu_event_handler_required(std::env::consts::OS));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn emitted_menu_event_id(id: &str) -> Option<&'static str> {
|
||||
manifest_menu_items().find_map(|item| {
|
||||
if item.menu_item_id(manifest()) == Some(id) {
|
||||
item.command_id(manifest())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn emit_custom_menu_event(app_handle: &AppHandle, id: &str) -> Result<(), String> {
|
||||
if !custom_menu_ids().contains(id) {
|
||||
return Err(format!("Unknown custom menu event: {id}"));
|
||||
}
|
||||
let emitted_id = emitted_menu_event_id(id)
|
||||
.ok_or_else(|| format!("Missing emitted command for custom menu event: {id}"))?;
|
||||
app_handle
|
||||
.emit("menu-event", emitted_id)
|
||||
.map_err(|err| format!("Failed to emit menu-event {emitted_id}: {err}"))
|
||||
}
|
||||
|
||||
fn menu_state_group_ids(group_name: &str) -> Vec<&'static str> {
|
||||
manifest()
|
||||
.menu_state_groups
|
||||
.get(group_name)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|reference| match reference {
|
||||
MenuStateGroupReference::Command { command } => manifest()
|
||||
.commands
|
||||
.get(command)
|
||||
.map(|command| command.id.as_str()),
|
||||
MenuStateGroupReference::Id { id } => Some(id.as_str()),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn set_items_enabled<'a>(
|
||||
app_handle: &AppHandle,
|
||||
ids: impl IntoIterator<Item = &'a str>,
|
||||
enabled: bool,
|
||||
) {
|
||||
let Some(menu) = app_handle.menu() else {
|
||||
return;
|
||||
};
|
||||
for id in ids {
|
||||
if let Some(MenuItemKind::MenuItem(mi)) = menu.get(id) {
|
||||
let _ = mi.set_enabled(enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_menu_state_group_enabled(app_handle: &AppHandle, group_name: &str, enabled: bool) {
|
||||
set_items_enabled(app_handle, menu_state_group_ids(group_name), enabled);
|
||||
}
|
||||
|
||||
/// Enable or disable menu items that depend on having an active note tab.
|
||||
pub fn set_note_items_enabled(app_handle: &AppHandle, enabled: bool) {
|
||||
set_menu_state_group_enabled(app_handle, NOTE_DEPENDENT_GROUP, enabled);
|
||||
}
|
||||
|
||||
/// Enable or disable menu items that depend on the editor being the active surface.
|
||||
pub fn set_editor_find_items_enabled(app_handle: &AppHandle, enabled: bool) {
|
||||
set_menu_state_group_enabled(app_handle, EDITOR_FIND_DEPENDENT_GROUP, enabled);
|
||||
}
|
||||
|
||||
/// Enable or disable menu items that depend on the note list being the active surface.
|
||||
pub fn set_note_list_search_items_enabled(app_handle: &AppHandle, enabled: bool) {
|
||||
set_menu_state_group_enabled(app_handle, NOTE_LIST_SEARCH_DEPENDENT_GROUP, enabled);
|
||||
}
|
||||
|
||||
/// Enable or disable menu items that depend on having uncommitted changes.
|
||||
pub fn set_git_commit_items_enabled(app_handle: &AppHandle, enabled: bool) {
|
||||
set_menu_state_group_enabled(app_handle, GIT_COMMIT_DEPENDENT_GROUP, enabled);
|
||||
}
|
||||
|
||||
/// Enable or disable menu items that depend on having merge conflicts.
|
||||
pub fn set_git_conflict_items_enabled(app_handle: &AppHandle, enabled: bool) {
|
||||
set_menu_state_group_enabled(app_handle, GIT_CONFLICT_DEPENDENT_GROUP, enabled);
|
||||
}
|
||||
|
||||
/// Enable or disable menu items that depend on the active vault having no remote.
|
||||
pub fn set_git_no_remote_items_enabled(app_handle: &AppHandle, enabled: bool) {
|
||||
set_menu_state_group_enabled(app_handle, GIT_NO_REMOTE_DEPENDENT_GROUP, enabled);
|
||||
}
|
||||
|
||||
/// Enable or disable menu items that depend on a deleted note preview being active.
|
||||
pub fn set_restore_deleted_item_enabled(app_handle: &AppHandle, enabled: bool) {
|
||||
set_menu_state_group_enabled(app_handle, RESTORE_DELETED_DEPENDENT_GROUP, enabled);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn menu_item_by_id(id: &str) -> &'static ManifestMenuItem {
|
||||
manifest_menu_items()
|
||||
.find(|item| item.menu_item_id(manifest()) == Some(id))
|
||||
.unwrap_or_else(|| panic!("missing menu item {id}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_ids_are_manifest_menu_item_ids() {
|
||||
let expected: HashSet<_> = manifest_menu_items()
|
||||
.filter_map(|item| item.menu_item_id(manifest()))
|
||||
.map(str::to_owned)
|
||||
.collect();
|
||||
|
||||
assert_eq!(custom_menu_ids(), &expected);
|
||||
assert!(custom_menu_ids().contains("file-quick-open-alias"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_command_items_reference_known_commands() {
|
||||
for item in manifest_menu_items() {
|
||||
if let ManifestMenuItem::Command { command, .. } = item {
|
||||
assert!(
|
||||
manifest().commands.contains_key(command),
|
||||
"menu item references missing command key {command}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overridden_menu_item_ids_emit_their_primary_command() {
|
||||
assert_eq!(
|
||||
emitted_menu_event_id("file-quick-open-alias"),
|
||||
Some("file-quick-open")
|
||||
);
|
||||
assert_eq!(
|
||||
emitted_menu_event_id("edit-toggle-note-list-search"),
|
||||
Some("edit-toggle-note-list-search")
|
||||
);
|
||||
assert_eq!(emitted_menu_event_id("file-save"), Some("file-save"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_group_ids_are_manifest_menu_items() {
|
||||
for (group, references) in &manifest().menu_state_groups {
|
||||
for reference in references {
|
||||
let id = match reference {
|
||||
MenuStateGroupReference::Command { command } => manifest()
|
||||
.commands
|
||||
.get(command)
|
||||
.map(|command| command.id.as_str())
|
||||
.unwrap_or_else(|| panic!("state group {group} references {command}")),
|
||||
MenuStateGroupReference::Id { id } => id.as_str(),
|
||||
};
|
||||
assert!(
|
||||
custom_menu_ids().contains(id),
|
||||
"state group {group} references non-menu item {id}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_toggle_properties_keeps_renderer_owned_accelerator() {
|
||||
let item = menu_item_by_id("view-toggle-properties");
|
||||
|
||||
assert_eq!(item.accelerator(manifest()), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_menu_exposes_ai_panel_toggle() {
|
||||
let view_menu = manifest_section("View").expect("view menu exists");
|
||||
let item = view_menu
|
||||
.items
|
||||
.iter()
|
||||
.find(|item| item.command_id(manifest()) == Some("view-toggle-ai-chat"))
|
||||
.expect("View menu exposes the AI panel toggle");
|
||||
|
||||
assert_eq!(item.menu_item_id(manifest()), Some("view-toggle-ai-chat"));
|
||||
assert_eq!(item.label("macos"), Some("Toggle AI Panel"));
|
||||
assert_eq!(item.accelerator(manifest()), Some("CmdOrCtrl+Shift+L"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_duplicate_custom_ids() {
|
||||
let mut seen = HashSet::new();
|
||||
for id in manifest_menu_items().filter_map(|item| item.menu_item_id(manifest())) {
|
||||
assert!(seen.insert(id), "duplicate custom ID: {id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_services_menu_is_macos_only() {
|
||||
assert!(app_menu_includes_services("macos"));
|
||||
assert!(!app_menu_includes_services("windows"));
|
||||
assert!(!app_menu_includes_services("linux"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_menu_event_handler_is_required_off_macos() {
|
||||
assert!(!window_menu_event_handler_required("macos"));
|
||||
assert!(window_menu_event_handler_required("windows"));
|
||||
assert!(window_menu_event_handler_required("linux"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_menu_uses_native_nsapp_integration_on_macos_only() {
|
||||
assert_eq!(
|
||||
native_window_menu_submenu_id("macos"),
|
||||
Some(WINDOW_SUBMENU_ID)
|
||||
);
|
||||
assert_eq!(native_window_menu_submenu_id("windows"), None);
|
||||
assert_eq!(native_window_menu_submenu_id("linux"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_menu_labels_escape_literal_ampersands() {
|
||||
assert_eq!(native_menu_label("Commit & Push"), "Commit && Push");
|
||||
assert_eq!(
|
||||
native_menu_label("Research && Development"),
|
||||
"Research &&&& Development"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_menu_labels_without_ampersands_are_unchanged() {
|
||||
assert_eq!(native_menu_label("Pull from Remote"), "Pull from Remote");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vault_commit_push_menu_label_is_native_menu_safe() {
|
||||
let item = menu_item_by_id("vault-commit-push");
|
||||
let label = item.label("windows").expect("commit push label exists");
|
||||
|
||||
assert_eq!(label, "Commit & Push");
|
||||
assert_eq!(native_menu_label(label), "Commit && Push");
|
||||
assert_eq!(item.accelerator(manifest()), None);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue