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
|
|
@ -8,6 +8,7 @@ pub mod commands;
|
|||
pub mod confirm;
|
||||
pub mod downloads;
|
||||
pub mod logs;
|
||||
pub mod menu_panel;
|
||||
pub mod policy;
|
||||
pub mod profiles;
|
||||
pub mod registry;
|
||||
|
|
@ -43,17 +44,53 @@ fn started_hidden() -> bool {
|
|||
std::env::args().any(|a| a == "--hidden")
|
||||
}
|
||||
|
||||
/// Tray icon: a click shows the window; the menu (tray.rs) opens the window, reads the stores, lists the apps with their
|
||||
/// state, mutes notifications, checks for updates, opens Settings and quits (the only real quit on Windows and Linux,
|
||||
/// where closing the window keeps the app running in the tray). The window refines tooltip, menu and icon after every
|
||||
/// read (`tray_update`).
|
||||
/// Tray icon: a left click shows the tray card (card.rs), a right click the app's own menu panels (menu_panel.rs, with
|
||||
/// the tree from tray.rs: open the window, read the stores, the apps with their state, mute, updates, Settings and Quit,
|
||||
/// the only real quit on Windows and Linux, where closing the window keeps the app running in the tray). The window
|
||||
/// refines tooltip, menu and icon after every read (`tray_update`). Linux trays send no clicks: the native menu,
|
||||
/// rendered from the same tree, stays on the icon there (and anywhere the panels fail).
|
||||
fn tray(app: &tauri::App) -> tauri::Result<()> {
|
||||
let menu = tray::initial_menu(app.handle())?;
|
||||
let mut b = TrayIconBuilder::with_id("main").tooltip("storebound").menu(&menu).show_menu_on_left_click(false);
|
||||
let handle = app.handle();
|
||||
let panels = handle.state::<menu_panel::MenuPanels>();
|
||||
if let Ok(mut n) = panels.nodes.lock() {
|
||||
*n = tray::initial_nodes();
|
||||
}
|
||||
let mut b = TrayIconBuilder::with_id("main").tooltip("storebound").show_menu_on_left_click(false);
|
||||
if cfg!(target_os = "linux") {
|
||||
panels.native.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
b = b.menu(&tray::native_menu(handle, &tray::initial_nodes())?);
|
||||
}
|
||||
if let Some(icon) = app.default_window_icon() {
|
||||
b = b.icon(icon.clone());
|
||||
}
|
||||
b.on_menu_event(|app, e| match tray::action_of(e.id().as_ref()) {
|
||||
b.on_menu_event(|app, e| dispatch_menu(app, e.id().as_ref()))
|
||||
.on_tray_icon_event(|t, e| {
|
||||
let TrayIconEvent::Click { button, button_state: MouseButtonState::Up, rect, position, .. } = e else { return };
|
||||
let app = t.app_handle();
|
||||
match button {
|
||||
// right click: the menu panels at the click point
|
||||
MouseButton::Right => menu_panel::open_root(app, position.x, position.y),
|
||||
// left click: the tray card next to the icon
|
||||
MouseButton::Left => {
|
||||
if app.get_webview_window(card::LABEL).is_none() {
|
||||
show_main(app);
|
||||
return;
|
||||
}
|
||||
let scale = app.get_webview_window(policy::MAIN).and_then(|w| w.scale_factor().ok()).unwrap_or(1.0);
|
||||
let p = rect.position.to_physical::<f64>(scale);
|
||||
let s = rect.size.to_physical::<f64>(scale);
|
||||
card::toggle(app, card::Rect { x: p.x, y: p.y, w: s.width, h: s.height });
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.build(app)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A tray menu pick (a menu panel row or the native fallback menu): tray.rs maps the id to what it does.
|
||||
pub fn dispatch_menu<R: Runtime>(app: &AppHandle<R>, id: &str) {
|
||||
match tray::action_of(id) {
|
||||
Some(tray::Action::Open) => show_main(app),
|
||||
Some(tray::Action::Quit) => app.exit(0),
|
||||
Some(tray::Action::Event(word)) => {
|
||||
|
|
@ -64,23 +101,7 @@ fn tray(app: &tauri::App) -> tauri::Result<()> {
|
|||
let _ = app.emit_to(policy::MAIN, "tray-route", route);
|
||||
}
|
||||
None => {}
|
||||
})
|
||||
.on_tray_icon_event(|t, e| {
|
||||
// left click: the tray card next to the icon (right click keeps the native menu)
|
||||
if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, rect, .. } = e {
|
||||
let app = t.app_handle();
|
||||
if app.get_webview_window(card::LABEL).is_none() {
|
||||
show_main(app);
|
||||
return;
|
||||
}
|
||||
let scale = app.get_webview_window(policy::MAIN).and_then(|w| w.scale_factor().ok()).unwrap_or(1.0);
|
||||
let p = rect.position.to_physical::<f64>(scale);
|
||||
let s = rect.size.to_physical::<f64>(scale);
|
||||
card::toggle(app, card::Rect { x: p.x, y: p.y, w: s.width, h: s.height });
|
||||
}
|
||||
})
|
||||
.build(app)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Silent self-update: at start (after a minute) and every 30 minutes, a signed update from storebound.dev is
|
||||
|
|
@ -282,7 +303,7 @@ pub fn run() {
|
|||
.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| show_main(app)))
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
// never restore "visible": a --hidden start would otherwise pop the window up
|
||||
.plugin(tauri_plugin_window_state::Builder::new().with_state_flags(tauri_plugin_window_state::StateFlags::all() & !tauri_plugin_window_state::StateFlags::VISIBLE).build())
|
||||
.plugin(tauri_plugin_window_state::Builder::new().with_filter(|label| !label.starts_with(menu_panel::LABEL_PREFIX)).with_state_flags(tauri_plugin_window_state::StateFlags::all() & !tauri_plugin_window_state::StateFlags::VISIBLE).build())
|
||||
.plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec!["--hidden"])))
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
|
|
@ -337,7 +358,10 @@ pub fn run() {
|
|||
Err(e) => logs::host("warn", &format!("autostart not set: {e}")),
|
||||
}
|
||||
app.manage(state);
|
||||
app.manage(menu_panel::MenuPanels::default());
|
||||
tray(app)?;
|
||||
// pinned tray menu panels of the last session come back
|
||||
menu_panel::restore(app.handle());
|
||||
if let Err(e) = card::create(app.handle()) {
|
||||
logs::host("warn", &format!("tray card not created: {e}"));
|
||||
}
|
||||
|
|
@ -438,6 +462,15 @@ pub fn run() {
|
|||
commands::ai_fix_undo,
|
||||
commands::card_get,
|
||||
commands::card_action,
|
||||
commands::menu_state,
|
||||
commands::menu_ready,
|
||||
commands::menu_open_sub,
|
||||
commands::menu_pin_sub,
|
||||
commands::menu_close_subs,
|
||||
commands::menu_close,
|
||||
commands::menu_dismiss,
|
||||
commands::menu_pin,
|
||||
commands::menu_select,
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("storebound desktop failed to start");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue