desktop: tray menu panels and pinnable context menus
The tray's right-click menu is drawn by the app: one tree from tray.rs (stable submenu keys), one frameless always-on-top menu-N window per panel rendered by tray-menu.html with the design system, placed inside the monitor's work area. A pure panel book keeps transient and pinned panels apart; pinned panels (pin button, Ctrl+click, middle click) are draggable, step aside from each other and are saved in state.json (menuPins) and restored at start. tray_update pushes the tree to the open panels, which update in place; closed panels are destroyed. The native menu, rendered from the same tree, stays as the fallback and on Linux. Only menu-* windows get the menu_* commands (menu.json). In-app context menus get the same behaviour through a pure menu stack (ui/menu-stack.ts): pin a menu, tear off a submenu, pinned panels survive the next menu, are draggable and re-read their entries from live sources so data changes update them in place. Also drops a stray diff3 marker line from CHANGELOG.md.
This commit is contained in:
parent
445ab1adbc
commit
5236f002f1
35 changed files with 2308 additions and 123 deletions
|
|
@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};
|
|||
|
||||
use serde_json::{json, Value};
|
||||
use storebound_desktop::policy::*;
|
||||
use storebound_desktop::{CARD_COMMANDS, COMMANDS};
|
||||
use storebound_desktop::{CARD_COMMANDS, COMMANDS, MENU_COMMANDS};
|
||||
use url::Url;
|
||||
|
||||
fn root() -> PathBuf {
|
||||
|
|
@ -26,7 +26,7 @@ fn one_capability_for_the_main_window_with_exactly_the_app_commands() {
|
|||
let dir = root().join("capabilities");
|
||||
let mut files: Vec<PathBuf> = fs::read_dir(&dir).unwrap().map(|e| e.unwrap().path()).collect();
|
||||
files.sort();
|
||||
assert_eq!(files.len(), 2, "main and card capability files expected, found {files:?}");
|
||||
assert_eq!(files.len(), 3, "main, card and menu capability files expected, found {files:?}");
|
||||
let cap = read_json(&dir.join("main.json"));
|
||||
assert_eq!(cap["identifier"], "main-window");
|
||||
assert_eq!(cap["windows"], json!(["main"]));
|
||||
|
|
@ -71,6 +71,45 @@ fn the_card_window_gets_only_the_card_commands() {
|
|||
assert!(require_main("card", &u("tauri://localhost/card.html"), false).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_menu_panels_get_only_the_menu_commands() {
|
||||
let cap = read_json(&root().join("capabilities/menu.json"));
|
||||
assert_eq!(cap["identifier"], "menu-window");
|
||||
assert_eq!(cap["windows"], json!(["menu-*"]));
|
||||
assert!(cap.get("webviews").is_none() && cap.get("remote").is_none());
|
||||
assert_ne!(cap["local"], json!(false));
|
||||
let got: BTreeSet<String> = cap["permissions"].as_array().unwrap().iter().map(|p| p.as_str().unwrap().to_string()).collect();
|
||||
let mut want: BTreeSet<String> = MENU_COMMANDS.iter().map(|c| format!("allow-{}", c.replace('_', "-"))).collect();
|
||||
want.insert("core:event:allow-listen".into());
|
||||
want.insert("core:event:allow-unlisten".into());
|
||||
// a pinned panel is dragged by its header (data-tauri-drag-region): only its own window
|
||||
want.insert("core:window:allow-start-dragging".into());
|
||||
assert_eq!(got, want);
|
||||
// the menu commands never leak into the main or card capability
|
||||
for other in ["main.json", "card.json"] {
|
||||
let cap = read_json(&root().join("capabilities").join(other));
|
||||
let perms = cap["permissions"].to_string();
|
||||
for c in MENU_COMMANDS {
|
||||
assert!(!perms.contains(&format!("\"allow-{}\"", c.replace('_', "-"))), "{c} granted in {other}");
|
||||
}
|
||||
}
|
||||
let cmds = fs::read_to_string(root().join("src/commands.rs")).unwrap();
|
||||
for c in MENU_COMMANDS {
|
||||
let at = cmds.find(&format!("pub fn {c}(")).unwrap_or_else(|| panic!("{c} not in commands.rs"));
|
||||
let body = &cmds[at..];
|
||||
let open = body.find('{').unwrap();
|
||||
assert!(body[open + 1..].trim_start().starts_with("guard_menu(&window)?;"), "{c} must start with guard_menu(&window)?");
|
||||
}
|
||||
assert!(require_menu("menu-1", &u("tauri://localhost/tray-menu.html"), false).is_ok());
|
||||
assert!(require_menu("menu-12", &u("http://tauri.localhost/tray-menu.html"), false).is_ok());
|
||||
for bad in ["main", "card", "menu-", "menu-x", "menu-1a", "menus-1"] {
|
||||
assert!(require_menu(bad, &u("tauri://localhost/tray-menu.html"), false).is_err(), "{bad}");
|
||||
}
|
||||
assert!(require_menu("menu-1", &u("https://example.com/tray-menu.html"), false).is_err());
|
||||
assert!(require_main("menu-1", &u("tauri://localhost/tray-menu.html"), false).is_err());
|
||||
assert!(require_card("menu-1", &u("tauri://localhost/tray-menu.html"), false).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_registered_command_is_in_the_list_and_nothing_else() {
|
||||
let lib = fs::read_to_string(root().join("src/lib.rs")).unwrap();
|
||||
|
|
@ -81,7 +120,7 @@ fn every_registered_command_is_in_the_list_and_nothing_else() {
|
|||
.map(|s| s.trim().trim_start_matches("commands::").to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
assert_eq!(registered, COMMANDS.iter().chain(CARD_COMMANDS).map(|s| s.to_string()).collect::<Vec<_>>());
|
||||
assert_eq!(registered, COMMANDS.iter().chain(CARD_COMMANDS).chain(MENU_COMMANDS).map(|s| s.to_string()).collect::<Vec<_>>());
|
||||
// every command checks the caller first
|
||||
let cmds = fs::read_to_string(root().join("src/commands.rs")).unwrap();
|
||||
for c in COMMANDS {
|
||||
|
|
@ -109,7 +148,7 @@ fn csp_is_strict_and_the_config_has_no_backdoors() {
|
|||
assert!(!v.contains("unsafe-eval") && !v.contains("unsafe-inline") && !v.contains('*'), "{k}: {v}");
|
||||
assert!(!v.contains("https:") && !v.contains("http://") || v == "ipc: http://ipc.localhost", "{k}: no remote sources: {v}");
|
||||
}
|
||||
assert_eq!(conf["app"]["security"]["capabilities"], json!(["main-window", "card-window"]));
|
||||
assert_eq!(conf["app"]["security"]["capabilities"], json!(["main-window", "card-window", "menu-window"]));
|
||||
assert_eq!(conf["app"]["security"]["freezePrototype"], json!(true));
|
||||
assert!(conf["app"].get("withGlobalTauri").is_none() || conf["app"]["withGlobalTauri"] == json!(false));
|
||||
let windows = conf["app"]["windows"].as_array().unwrap();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue