Files
notify/Notify/Services/ToastManager.cs
T
chuan cfb1b99162 feat: Avalonia 常驻通知应用(保活 + 弹窗 + 跨桌面)
- 常驻 Host:无主窗口保活 + 系统托盘菜单
- Toast 弹窗:堆叠 / 自动消失 / 悬停暂停 / 淡入淡出 / 点击关闭
- Sticky 常驻弹窗与 InputMode 边框配色
- 跨虚拟桌面显示(源生成 COM PinView,AOT 友好)
- 设置窗口(Semi + Ursa)+ JSON 源生成持久化
- AOT 准备:IsAotCompatible 0 警告
2026-06-22 14:25:14 +08:00

111 lines
3.0 KiB
C#

using System.Collections.Generic;
using Avalonia;
using Avalonia.Platform;
using Notify.Models;
using Notify.ViewModels;
using Notify.Views;
namespace Notify.Services;
/// <summary>
/// 在常驻进程内管理所有 toast 窗口:创建、按角落堆叠、关闭后重新排布
/// 这是 Rust 版"每条通知一进程 + EnumWindows"的替代——进程内一个列表即可
/// </summary>
public sealed class ToastManager
{
private const int Margin = 12;
private const int Gap = 8;
private readonly SettingsService _settings;
private readonly List<ToastWindow> _active = [];
private readonly Queue<ToastRequest> _pending = new();
public ToastManager(SettingsService settings) => _settings = settings;
public void Show(ToastRequest request)
{
var settings = _settings.Current;
if (_active.Count >= settings.MaxVisible)
{
_pending.Enqueue(request);
return;
}
var vm = new ToastViewModel(request);
var window = new ToastWindow(vm, settings, request.Sticky);
window.Closed += OnToastClosed;
_active.Add(window);
// 先显示(拿到尺寸/屏幕信息),再排布
window.Show();
Arrange();
}
private void OnToastClosed(object? sender, System.EventArgs e)
{
if (sender is ToastWindow w)
{
w.Closed -= OnToastClosed;
_active.Remove(w);
}
Arrange();
if (_pending.Count > 0 && _active.Count < _settings.Current.MaxVisible)
{
Show(_pending.Dequeue());
}
}
/// <summary>
/// 把所有活动 toast 从指定角落沿垂直方向依次堆叠
/// </summary>
private void Arrange()
{
if (_active.Count == 0)
{
return;
}
var anchor = _active[0];
var screen = anchor.Screens.ScreenFromWindow(anchor) ?? anchor.Screens.Primary;
if (screen is null)
{
return;
}
var settings = _settings.Current;
var wa = screen.WorkingArea; // 物理像素
var scale = anchor.RenderScaling;
var margin = (int)(Margin * scale);
var gap = (int)(Gap * scale);
var bottom = settings.Corner is ToastCorner.BottomLeft or ToastCorner.BottomRight;
var right = settings.Corner is ToastCorner.TopRight or ToastCorner.BottomRight;
var cursor = bottom ? wa.Bottom - margin : wa.Y + margin;
foreach (var toast in _active)
{
var wPx = (int)(toast.Width * scale);
var hPx = (int)(toast.Bounds.Height * scale);
var x = right ? wa.Right - margin - wPx : wa.X + margin;
int y;
if (bottom)
{
cursor -= hPx;
y = cursor;
cursor -= gap;
}
else
{
y = cursor;
cursor += hPx + gap;
}
toast.Position = new PixelPoint(x, y);
}
}
}