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
|
|
@ -1,7 +1,7 @@
|
|||
//! The tray: tooltip, attention icon, context menu and the "where to go" memory of the last notification.
|
||||
//!
|
||||
//! The window pushes a small summary after every store read (`tray_update`); this module turns it into a menu model
|
||||
//! (pure, tested in tests/tray.rs), rebuilds the menu, sets the tooltip and swaps the icon for one with a red dot while
|
||||
//! (pure, tested in tests/tray.rs), pushes it to the menu panels (menu_panel.rs), sets the tooltip and swaps the icon for one with a red dot while
|
||||
//! something needs the owner. Menu clicks come back as events to the window: `tray-route` (a route as JSON) and
|
||||
//! `tray-action` (`read`, `mute-toggle`).
|
||||
//!
|
||||
|
|
@ -14,7 +14,9 @@ use std::time::{Duration, Instant};
|
|||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use tauri::image::Image;
|
||||
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem, Submenu};
|
||||
use tauri::menu::{IsMenuItem, Menu, MenuItem, PredefinedMenuItem, Submenu};
|
||||
|
||||
use crate::menu_panel::{from_entries, Node};
|
||||
use tauri::{AppHandle, Runtime};
|
||||
|
||||
pub const ID_OPEN: &str = "tray-open";
|
||||
|
|
@ -88,7 +90,8 @@ pub struct TraySummary {
|
|||
pub enum Entry {
|
||||
Item { id: String, label: String, enabled: bool },
|
||||
Separator,
|
||||
Sub { label: String, items: Vec<Entry> },
|
||||
/// `key`: the submenu's fixed key (menu_panel::sub_key), the same in every language.
|
||||
Sub { key: String, label: String, items: Vec<Entry> },
|
||||
}
|
||||
|
||||
fn clean(s: &str, max: usize) -> String {
|
||||
|
|
@ -132,7 +135,7 @@ pub fn menu_model(s: &TraySummary) -> Vec<Entry> {
|
|||
item(ID_OPEN, &l.open),
|
||||
item(ID_READ, &l.read_now),
|
||||
Entry::Separator,
|
||||
Entry::Sub { label: clean(&l.apps, 40), items: apps },
|
||||
Entry::Sub { key: "apps".into(), label: clean(&l.apps, 40), items: apps },
|
||||
Entry::Separator,
|
||||
item(ID_MUTE, if s.muted { &l.unmute } else { &l.mute }),
|
||||
item(ID_UPDATE, &l.update),
|
||||
|
|
@ -233,40 +236,45 @@ pub fn take_pending() -> Option<String> {
|
|||
|
||||
static LAST: Mutex<Option<String>> = Mutex::new(None);
|
||||
|
||||
fn build_menu<R: Runtime>(app: &AppHandle<R>, model: &[Entry]) -> tauri::Result<Menu<R>> {
|
||||
let menu = Menu::new(app)?;
|
||||
for e in model {
|
||||
match e {
|
||||
Entry::Item { id, label, enabled } => menu.append(&MenuItem::with_id(app, id, label, *enabled, None::<&str>)?)?,
|
||||
Entry::Separator => menu.append(&PredefinedMenuItem::separator(app)?)?,
|
||||
Entry::Sub { label, items } => {
|
||||
let sub = Submenu::new(app, label, true)?;
|
||||
for it in items {
|
||||
if let Entry::Item { id, label, enabled } = it {
|
||||
sub.append(&MenuItem::with_id(app, id, label, *enabled, None::<&str>)?)?;
|
||||
}
|
||||
}
|
||||
menu.append(&sub)?;
|
||||
/// The native menu items for a panel tree (the fallback, and Linux), nested to any depth.
|
||||
fn native_items<R: Runtime>(app: &AppHandle<R>, nodes: &[Node]) -> tauri::Result<Vec<Box<dyn IsMenuItem<R>>>> {
|
||||
let mut out: Vec<Box<dyn IsMenuItem<R>>> = Vec::new();
|
||||
for n in nodes {
|
||||
match n {
|
||||
Node::Item { id, label, enabled } => out.push(Box::new(MenuItem::with_id(app, id, label, *enabled, None::<&str>)?)),
|
||||
Node::Sep => out.push(Box::new(PredefinedMenuItem::separator(app)?)),
|
||||
Node::Sub { label, items, .. } => {
|
||||
let children = native_items(app, items)?;
|
||||
let refs: Vec<&dyn IsMenuItem<R>> = children.iter().map(|b| b.as_ref()).collect();
|
||||
out.push(Box::new(Submenu::with_items(app, label, true, &refs)?));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(menu)
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The menu shown before the window has sent anything (English words).
|
||||
pub fn initial_menu<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<Menu<R>> {
|
||||
build_menu(app, &menu_model(&TraySummary::default()))
|
||||
/// The native tray menu, rendered from the same tree as the panels.
|
||||
pub fn native_menu<R: Runtime>(app: &AppHandle<R>, nodes: &[Node]) -> tauri::Result<Menu<R>> {
|
||||
let items = native_items(app, nodes)?;
|
||||
let refs: Vec<&dyn IsMenuItem<R>> = items.iter().map(|b| b.as_ref()).collect();
|
||||
Menu::with_items(app, &refs)
|
||||
}
|
||||
|
||||
/// Applies a summary: tooltip, menu and icon of the tray with id `main`. Skips the work when nothing changed.
|
||||
pub fn apply<R: Runtime>(app: &AppHandle<R>, s: &TraySummary) -> tauri::Result<()> {
|
||||
/// The tree before the window has sent anything (English words).
|
||||
pub fn initial_nodes() -> Vec<Node> {
|
||||
from_entries(&menu_model(&TraySummary::default()))
|
||||
}
|
||||
|
||||
/// Applies a summary: tooltip and icon of the tray with id `main`, and the menu tree, which goes live to the open menu
|
||||
/// panels (and to the native menu when that is in use). Skips the work when nothing changed.
|
||||
pub fn apply(app: &AppHandle, s: &TraySummary) -> tauri::Result<()> {
|
||||
let Some(tray) = app.tray_by_id("main") else { return Ok(()) };
|
||||
let key = format!("{s:?}");
|
||||
if LAST.lock().ok().is_some_and(|l| l.as_deref() == Some(key.as_str())) {
|
||||
return Ok(());
|
||||
}
|
||||
tray.set_tooltip(Some(tooltip_text(s)))?;
|
||||
tray.set_menu(Some(build_menu(app, &menu_model(s))?))?;
|
||||
crate::menu_panel::set_nodes(app, from_entries(&menu_model(s)));
|
||||
if let Some(icon) = app.default_window_icon() {
|
||||
let img = if s.attention > 0 { Image::new_owned(with_attention_dot(icon.rgba(), icon.width(), icon.height()), icon.width(), icon.height()) } else { icon.clone() };
|
||||
tray.set_icon(Some(img))?;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue