Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions scripts/tray-popup.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';

test('tray grants only event subscriptions and relies on native main-window command to hide', () => {
const cap = JSON.parse(fs.readFileSync('src-tauri/capabilities/tray.json', 'utf8'));
assert.deepEqual(cap.windows, ['tray-popup']);
assert.deepEqual(cap.permissions, ['core:event:allow-listen', 'core:event:allow-unlisten']);
const source = fs.readFileSync('src/components/TrayPopup.tsx', 'utf8');
assert.ok(source.includes("invoke('show_main_window_cmd')"));
assert.ok(!source.includes('getCurrentWebviewWindow().hide'));
});

test('small work-area popup retains vertical access to action buttons', () => {
const css = fs.readFileSync('src/components/TrayPopup.css', 'utf8');
assert.match(css, /\.tray-popup\s*\{[^}]*overflow-y:\s*auto/s);
assert.match(css, /\.tp-actions\s*\{[^}]*flex-shrink:\s*0/s);
});
7 changes: 7 additions & 0 deletions src-tauri/capabilities/tray.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "tray-events",
"description": "Event subscription for the local tray popup; no filesystem access",
"windows": ["tray-popup"],
"permissions": ["core:event:allow-listen", "core:event:allow-unlisten"]
}
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ mod sse_watchdog;
mod switch_log;
mod token_tracker;
mod tray;
mod tray_position;
mod usage;

use account::{Account, AccountStore};
Expand Down
47 changes: 34 additions & 13 deletions src-tauri/src/tray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ fn toggle_popup(app: &AppHandle, position: tauri::PhysicalPosition<f64>) {
return;
}
// 重新定位并显示
let _ = position_popup(&win, position);
if let Err(error) = position_popup(&win, position) {
eprintln!("[Tray] Position failed: {}", error);
return;
}
let _ = win.show();
let _ = win.set_focus();
return;
Expand Down Expand Up @@ -131,29 +134,47 @@ fn toggle_popup(app: &AppHandle, position: tauri::PhysicalPosition<f64>) {
}
});

let _ = position_popup(&win, position);
if let Err(error) = position_popup(&win, position) {
eprintln!("[Tray] Position failed: {}", error);
return;
}
let _ = win.show();
let _ = win.set_focus();
}
Err(e) => eprintln!("[Tray] 创建 popup 窗口失败: {}", e),
}
}

/// 将 popup 窗口定位到托盘图标附近(macOS 顶部菜单栏下方)
/// Use the clicked monitor's physical work area, including its taskbar and DPI.
fn position_popup(
win: &tauri::WebviewWindow,
tray_pos: tauri::PhysicalPosition<f64>,
) -> Result<(), String> {
let popup_width = 380.0;

let scale = win.scale_factor().unwrap_or(1.0);

let x = (tray_pos.x - popup_width * scale / 2.0).max(0.0) as i32;
let y = (tray_pos.y + 4.0) as i32; // 留一点间距给菜单栏

let _ = win.set_position(tauri::Position::Physical(tauri::PhysicalPosition::new(
x, y,
)));
let monitor = match win
.monitor_from_point(tray_pos.x, tray_pos.y)
.map_err(|e| e.to_string())?
{
Some(monitor) => monitor,
None => match win.current_monitor().map_err(|e| e.to_string())? {
Some(monitor) => monitor,
None => win
.primary_monitor()
.map_err(|e| e.to_string())?
.ok_or("No monitor available for tray popup")?,
},
};
let area = monitor.work_area();
let rect = crate::tray_position::place(
(tray_pos.x, tray_pos.y),
(area.position.x, area.position.y),
(area.size.width, area.size.height),
monitor.scale_factor(),
)
.ok_or("Invalid tray monitor work area")?;
win.set_position(tauri::PhysicalPosition::new(rect.x, rect.y))
.map_err(|e| e.to_string())?;
win.set_size(tauri::PhysicalSize::new(rect.width, rect.height))
.map_err(|e| e.to_string())?;
Ok(())
}

Expand Down
102 changes: 102 additions & 0 deletions src-tauri/src/tray_position.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#[derive(Debug, PartialEq)]
pub struct PopupRect {
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
}

pub fn place(
anchor: (f64, f64),
origin: (i32, i32),
work: (u32, u32),
scale: f64,
) -> Option<PopupRect> {
if work.0 == 0
|| work.1 == 0
|| !scale.is_finite()
|| scale <= 0.0
|| !anchor.0.is_finite()
|| !anchor.1.is_finite()
{
return None;
}
let width = (380.0 * scale).round().clamp(1.0, work.0 as f64);
let height = (410.0 * scale).round().clamp(1.0, work.1 as f64);
let left = origin.0 as f64;
let top = origin.1 as f64;
let right = left + work.0 as f64;
let bottom = top + work.1 as f64;
let gap = 4.0 * scale;
let below = anchor.1 + gap;
let y = if below + height <= bottom {
below
} else {
anchor.1 - height - gap
};
Some(PopupRect {
x: (anchor.0 - width / 2.0).clamp(left, right - width).round() as i32,
y: y.clamp(top, bottom - height).round() as i32,
width: width as u32,
height: height as u32,
})
}

#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bottom_taskbar_popup_stays_above_work_area_edge() {
let r = place((1900.0, 1060.0), (0, 0), (1920, 1040), 1.0).unwrap();
assert_eq!(
r,
PopupRect {
x: 1540,
y: 630,
width: 380,
height: 410
}
);
}
#[test]
fn top_menu_bar_popup_stays_below_menu() {
let r = place((800.0, 12.0), (0, 24), (1440, 876), 1.0).unwrap();
assert_eq!(r.y, 24);
}
#[test]
fn scaled_negative_origin_monitor_is_not_clamped_to_primary() {
let r = place((-20.0, 1400.0), (-2560, -100), (2560, 1480), 1.5).unwrap();
assert_eq!(r.width, 570);
assert_eq!(r.height, 615);
assert_eq!(r.x, -570);
assert!(r.y >= -100 && r.y + r.height as i32 <= 1380);
}
#[test]
fn all_taskbar_edges_and_small_monitors_remain_inside_work_area() {
for scale in [1.0, 1.25, 1.5, 2.0, 3.0] {
for (origin, work) in [
((0, 0), (1920, 1040)),
((40, 0), (1880, 1080)),
((-1600, 40), (1600, 860)),
((100, 100), (300, 240)),
] {
for anchor in [
(0.0, 500.0),
(1910.0, 500.0),
(1900.0, 1070.0),
(-1500.0, 0.0),
] {
let r = place(anchor, origin, work, scale).unwrap();
assert!(r.x >= origin.0 && r.y >= origin.1);
assert!(r.x as i64 + r.width as i64 <= origin.0 as i64 + work.0 as i64);
assert!(r.y as i64 + r.height as i64 <= origin.1 as i64 + work.1 as i64);
}
}
}
}
#[test]
fn invalid_monitor_metadata_fails_without_panicking() {
assert!(place((0.0, 0.0), (0, 0), (0, 10), 1.0).is_none());
assert!(place((0.0, 0.0), (0, 0), (10, 10), f64::NAN).is_none());
}
}
4 changes: 3 additions & 1 deletion src/components/TrayPopup.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ body.is-tray-popup,
padding: 16px 18px;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif;
color: #e8e0f0;
overflow: hidden;
overflow-x: hidden;
overflow-y: auto;
user-select: none;
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -276,6 +277,7 @@ body.is-tray-popup,

/* ===== 按钮栏 ===== */
.tp-actions {
flex-shrink: 0;
display: flex;
gap: 6px;
margin-top: auto;
Expand Down
2 changes: 0 additions & 2 deletions src/components/TrayPopup.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useState, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
// Rust 端 on_window_event(Focused(false)) 负责隐藏弹窗
import './TrayPopup.css';

Expand Down Expand Up @@ -161,7 +160,6 @@ export function TrayPopup() {

const handleOpenDashboard = async () => {
await invoke('show_main_window_cmd');
getCurrentWebviewWindow().hide();
};

const q = data?.account?.cached_quota;
Expand Down