desktop tray: card as tall as its content, card and menu open from the tray icon's edge (never over the taskbar); menu permission files
This commit is contained in:
parent
6782ac6078
commit
da7ca48ab4
17 changed files with 185 additions and 26 deletions
|
|
@ -62,7 +62,7 @@ pub fn place(tray: Rect, work: Rect, card_w: f64, card_h: f64, gap: f64) -> (f64
|
|||
|
||||
/// Whether a card action word is one the card may send.
|
||||
pub fn valid_action(a: &str) -> bool {
|
||||
matches!(a, "hide" | "open" | "route" | "refresh" | "mute" | "quit" | "external" | "ready")
|
||||
matches!(a, "hide" | "open" | "route" | "refresh" | "mute" | "quit" | "external" | "ready" | "size")
|
||||
}
|
||||
|
||||
// ---- the last snapshot ----
|
||||
|
|
@ -123,6 +123,38 @@ mod window {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// The icon rectangle of the last show, so a size change keeps the card anchored to the icon.
|
||||
static LAST_ICON: Mutex<Option<Rect>> = Mutex::new(None);
|
||||
|
||||
/// Card height in CSS px (what the page measured), clamped; the window keeps WIDTH.
|
||||
fn height_px(h: f64) -> f64 { h.clamp(120.0, HEIGHT) }
|
||||
|
||||
/// Puts the card next to `tray` at its current height: its edge GAP from the icon, inside the work area.
|
||||
fn anchor<R: Runtime>(app: &AppHandle<R>, w: &tauri::WebviewWindow<R>, tray: Rect, css_h: f64) {
|
||||
let monitor = app.monitor_from_point(tray.x + tray.w / 2.0, tray.y + tray.h / 2.0).ok().flatten().or_else(|| w.primary_monitor().ok().flatten());
|
||||
if let Some(m) = monitor {
|
||||
let s = m.scale_factor();
|
||||
let wa = m.work_area();
|
||||
let work = Rect { x: wa.position.x as f64, y: wa.position.y as f64, w: wa.size.width as f64, h: wa.size.height as f64 };
|
||||
let _ = w.set_size(tauri::PhysicalSize::new((WIDTH * s).round() as u32, (css_h * s).round() as u32));
|
||||
let (x, y) = place(tray, work, WIDTH * s, css_h * s, GAP * s);
|
||||
let _ = w.set_position(PhysicalPosition::new(x as i32, y as i32));
|
||||
}
|
||||
}
|
||||
|
||||
/// The page measured its height: resize the window to it and keep it anchored (the bottom edge stays above a bottom
|
||||
/// taskbar, no empty band under the content).
|
||||
pub fn resize<R: Runtime>(app: &AppHandle<R>, css_h: f64) {
|
||||
let Some(w) = app.get_webview_window(LABEL) else { return };
|
||||
let h = height_px(css_h);
|
||||
if let Ok(mut c) = CURRENT_H.lock() { *c = h; }
|
||||
if let Some(tray) = LAST_ICON.lock().ok().and_then(|t| *t) {
|
||||
anchor(app, &w, tray, h);
|
||||
}
|
||||
}
|
||||
|
||||
static CURRENT_H: Mutex<f64> = Mutex::new(HEIGHT);
|
||||
|
||||
/// A left click on the tray icon: shows the card next to the icon, or hides it when it is open.
|
||||
pub fn toggle<R: Runtime>(app: &AppHandle<R>, tray: Rect) {
|
||||
let Some(w) = app.get_webview_window(LABEL) else { return };
|
||||
|
|
@ -134,14 +166,9 @@ mod window {
|
|||
if just_hidden() {
|
||||
return;
|
||||
}
|
||||
let monitor = app.monitor_from_point(tray.x + tray.w / 2.0, tray.y + tray.h / 2.0).ok().flatten().or_else(|| w.primary_monitor().ok().flatten());
|
||||
if let Some(m) = monitor {
|
||||
let s = m.scale_factor();
|
||||
let wa = m.work_area();
|
||||
let work = Rect { x: wa.position.x as f64, y: wa.position.y as f64, w: wa.size.width as f64, h: wa.size.height as f64 };
|
||||
let (x, y) = place(tray, work, WIDTH * s, HEIGHT * s, GAP * s);
|
||||
let _ = w.set_position(PhysicalPosition::new(x as i32, y as i32));
|
||||
}
|
||||
if let Ok(mut t) = LAST_ICON.lock() { *t = Some(tray); }
|
||||
let h = CURRENT_H.lock().map(|c| *c).unwrap_or(HEIGHT);
|
||||
anchor(app, &w, tray, h);
|
||||
let _ = w.show();
|
||||
let _ = w.set_focus();
|
||||
let _ = app.emit_to(LABEL, "card-shown", ());
|
||||
|
|
@ -158,4 +185,4 @@ mod window {
|
|||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
pub use window::{create, hide, toggle};
|
||||
pub use window::{create, hide, resize, toggle};
|
||||
|
|
|
|||
|
|
@ -1355,6 +1355,10 @@ pub fn card_action(window: WebviewWindow, app: AppHandle, action: String, arg: O
|
|||
match action.as_str() {
|
||||
"hide" => crate::card::hide(&app),
|
||||
"ready" => { let _ = app.emit_to(policy::MAIN, "card-request", ()); }
|
||||
"size" => {
|
||||
let h: f64 = arg.as_deref().unwrap_or_default().parse().map_err(|_| Fail::host("bad size"))?;
|
||||
crate::card::resize(&app, h);
|
||||
}
|
||||
"open" => { crate::card::hide(&app); main(); }
|
||||
"refresh" => { let _ = app.emit_to(policy::MAIN, "tray-action", "read"); }
|
||||
"mute" => { let _ = app.emit_to(policy::MAIN, "tray-action", "mute-toggle"); }
|
||||
|
|
|
|||
|
|
@ -67,19 +67,20 @@ fn tray(app: &tauri::App) -> tauri::Result<()> {
|
|||
.on_tray_icon_event(|t, e| {
|
||||
let TrayIconEvent::Click { button, button_state: MouseButtonState::Up, rect, position, .. } = e else { return };
|
||||
let app = t.app_handle();
|
||||
// both open from the icon's rectangle (not the click point): their edge sits next to the icon, above a
|
||||
// bottom taskbar, so nothing starts inside the taskbar
|
||||
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);
|
||||
let icon = if s.width > 0.0 && s.height > 0.0 { card::Rect { x: p.x, y: p.y, w: s.width, h: s.height } } else { card::Rect { x: position.x, y: position.y, w: 1.0, h: 1.0 } };
|
||||
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::Right => menu_panel::open_root(app, menu_panel::Anchor::Tray { x: icon.x, y: icon.y, w: icon.w, h: icon.h }),
|
||||
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 });
|
||||
card::toggle(app, icon);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ pub fn find<'a>(nodes: &'a [Node], path: &[String]) -> Option<(&'a [Node], Optio
|
|||
pub enum Anchor {
|
||||
/// The tray click point (physical).
|
||||
Point { x: f64, y: f64 },
|
||||
/// The tray icon's rectangle (physical): the root opens from the icon's edge that faces the screen, like the card.
|
||||
Tray { x: f64, y: f64, w: f64, h: f64 },
|
||||
/// Beside a parent panel: its left and right edge and the row's top (physical).
|
||||
Beside { left: f64, right: f64, top: f64 },
|
||||
/// A remembered top-left (pinned panels).
|
||||
|
|
@ -129,6 +131,8 @@ pub fn place_menu(anchor: Anchor, size: (f64, f64), work: Rect) -> (f64, f64) {
|
|||
let (wx, wy, ww, wh) = work;
|
||||
let (x, y) = match anchor {
|
||||
Anchor::Point { x, y } => (if x + w > wx + ww { x - w } else { x }, if y + h > wy + wh { y - h } else { y }),
|
||||
// same rule as the card: the panel's edge sits GAP from the icon, never over the taskbar
|
||||
Anchor::Tray { x, y, w: iw, h: ih } => return crate::card::place(crate::card::Rect { x, y, w: iw, h: ih }, crate::card::Rect { x: wx, y: wy, w: ww, h: wh }, w, h, GAP),
|
||||
Anchor::Beside { left, right, top } => (if right + w <= wx + ww - GAP { right - 2.0 } else { left - w + 2.0 }, top - 5.0),
|
||||
Anchor::At { x, y } => (x, y),
|
||||
};
|
||||
|
|
@ -520,12 +524,12 @@ mod runtime {
|
|||
});
|
||||
}
|
||||
|
||||
/// A right click on the tray icon: a fresh root menu at the click point (physical px).
|
||||
pub fn open_root(app: &AppHandle, x: f64, y: f64) {
|
||||
/// A right click on the tray icon: a fresh root menu next to the icon (`anchor`: Anchor::Tray, physical px).
|
||||
pub fn open_root(app: &AppHandle, anchor: Anchor) {
|
||||
if native_fallback(app) {
|
||||
return; // the native menu is on the tray and opens by itself
|
||||
}
|
||||
let (label, closes) = book(app, |b| b.open_root(Anchor::Point { x, y }));
|
||||
let (label, closes) = book(app, |b| b.open_root(anchor));
|
||||
destroy(app, &closes);
|
||||
let handle = app.clone();
|
||||
std::thread::spawn(move || {
|
||||
|
|
@ -571,6 +575,7 @@ mod runtime {
|
|||
fn anchor_point(a: Anchor) -> (f64, f64) {
|
||||
match a {
|
||||
Anchor::Point { x, y } | Anchor::At { x, y } => (x, y),
|
||||
Anchor::Tray { x, y, w, h } => (x + w / 2.0, y + h / 2.0),
|
||||
Anchor::Beside { right, top, .. } => (right, top),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue