using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Windows.Documents;
using System.Windows.Media.Animation;
using System.Windows.Threading;
namespace NeoUI.Controls;
///
/// 提供静态方法来显示各种类型的轻量级通知,支持屏幕级别和窗口级别的消息提示。
/// 通过调用Screen属性可以获取一个IToastService实例,用于在整个应用范围内显示通知。
/// 使用For方法为特定的窗口创建一个IToastService实例,允许在该窗口上下文中显示通知。
///
public static class Toast
{
///
/// 获取一个IToastService实例,该实例允许在整个应用程序范围内显示屏幕级别的通知。
/// 通过这个属性,可以调用相关的显示方法来发送不同类型的通知消息,如成功、信息、错误和警告等。
///
public static IToastService Screen { get; }
private static readonly ConditionalWeakTable WindowManagers = new();
static Toast() { Screen = new ToastManager(new ScreenHost()); }
///
/// 为指定的窗口创建一个IToastService实例,以便在该窗口上下文中显示轻量级通知。
///
/// 要为其创建通知服务的窗口对象。如果传递null,则会抛出异常。
/// 与给定窗口关联的IToastService实例,用于显示通知。
/// 当提供的window参数为null时抛出。
public static IToastService For(Window window)
{
if (window == null) throw new ArgumentNullException(nameof(window));
return WindowManagers.GetValue(window, w => new ToastManager(new WindowHost(w)));
}
}
///
/// 定义了用于显示轻量级通知的方法。此接口支持多种类型的提示消息,包括成功、错误、警告和信息等,并允许设置消息的显示时长。
/// 通过实现该接口的服务,可以在应用或特定窗口中展示相应的通知。
///
public interface IToastService
{
///
/// 显示一条轻量级通知。
///
/// 要显示的消息内容。
/// 消息的类型,默认为信息类型。可选值包括成功、信息、错误和警告等。
/// 消息的显示时长。如果未指定,则使用默认值。
void Show(string message, ToastType type = ToastType.Info, TimeSpan? duration = null);
///
/// 显示一条成功类型的轻量级通知。
///
/// 要显示的消息内容。
/// 消息的显示时长。如果未指定,则使用默认值。
void ShowSuccess(string message, TimeSpan? duration = null);
///
/// 显示一条错误类型的轻量级通知。
///
/// 要显示的错误消息内容。
/// 消息的显示时长。如果未指定,则使用默认值。
void ShowError(string message, TimeSpan? duration = null);
///
/// 显示一条警告类型的轻量级通知。
///
/// 要显示的消息内容。
/// 消息的显示时长,如果未指定则使用默认值。
void ShowWarning(string message, TimeSpan? duration = null);
///
/// 显示一条信息类型的轻量级通知。
///
/// 要显示的消息内容。
/// 消息的显示时长。如果未指定,则使用默认值。
void ShowInfo(string message, TimeSpan? duration = null);
}
#region Internal Implementation
internal class ToastManager(IToastHost host) : IToastService
{
public void Show(string message, ToastType type, TimeSpan? duration = null)
{
Application.Current.Dispatcher.Invoke(() =>
{
var toast = new ToastControl { Message = message, Type = type };
var fadeInAnim = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(350));
var timer = new DispatcherTimer { Interval = duration ?? TimeSpan.FromSeconds(4) };
timer.Tick += (_, _) =>
{
timer.Stop();
var fadeOutAnim = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(350));
fadeOutAnim.Completed += (_, _) => host.RemoveToast(toast);
toast.BeginAnimation(UIElement.OpacityProperty, fadeOutAnim);
};
toast.Loaded += (_, _) =>
{
toast.BeginAnimation(UIElement.OpacityProperty, fadeInAnim);
timer.Start();
};
host.AddToast(toast);
});
}
public void ShowSuccess(string message, TimeSpan? duration = null) => Show(message, ToastType.Success, duration);
public void ShowError(string message, TimeSpan? duration = null) => Show(message, ToastType.Error, duration);
public void ShowWarning(string message, TimeSpan? duration = null) => Show(message, ToastType.Warning, duration);
public void ShowInfo(string message, TimeSpan? duration = null) => Show(message, ToastType.Info, duration);
}
internal interface IToastHost
{
void AddToast(ToastControl toast);
void RemoveToast(ToastControl toast);
}
internal class WindowHost : IToastHost
{
private readonly StackPanel toastContainer;
public WindowHost(Window window)
{
toastContainer = new StackPanel();
// Use a style to apply margin between toasts, which is a robust approach.
var childStyle = new Style(typeof(ToastControl));
childStyle.Setters.Add(new Setter(FrameworkElement.MarginProperty, new Thickness(0, 8, 0, 8)));
toastContainer.Resources.Add(typeof(ToastControl), childStyle);
var adornerLayer = AdornerLayer.GetAdornerLayer(window.Content as Visual);
if (adornerLayer == null) throw new InvalidOperationException("Cannot find AdornerLayer on the specified window.");
adornerLayer.Add(new ToastAdorner(window.Content as UIElement, toastContainer));
}
public void AddToast(ToastControl toast) => toastContainer.Children.Add(toast);
public void RemoveToast(ToastControl toast) => toastContainer.Children.Remove(toast);
private class ToastAdorner : Adorner
{
private readonly UIElement child; // This is our StackPanel
public ToastAdorner(UIElement? adornedElement, UIElement child) : base(adornedElement)
{
this.child = child;
// Add the child to the visual tree. This is essential.
AddVisualChild(this.child);
}
protected override int VisualChildrenCount => 1;
protected override Visual GetVisualChild(int index) => child;
// We override Measure to tell our child (StackPanel) how much space it has.
protected override Size MeasureOverride(Size constraint)
{
// Give the child all the space it could possibly want.
// It will then measure itself and its children, establishing its DesiredSize.
child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
// The Adorner itself doesn't need to return a specific size, as it's a transparent overlay.
return base.MeasureOverride(constraint);
}
// ArrangeOverride is where we do the actual positioning.
protected override Size ArrangeOverride(Size finalSize)
{
// 'finalSize' is the full size of the element we are adorning (e.g., the window's content area).
// '_child.DesiredSize' is the size our StackPanel reported it needs after being measured.
// Calculate the X coordinate to center the child (StackPanel).
var x = (finalSize.Width - child.DesiredSize.Width) / 2;
// The Y coordinate is 0, placing it at the very top.
double y = 0;
// Create the final rectangle for our child, positioned correctly.
var finalRect = new Rect(new Point(x, y), child.DesiredSize);
// Arrange the child (our StackPanel) in the calculated centered rectangle.
child.Arrange(finalRect);
// The Adorner itself arranges to fill the entire space.
return finalSize;
}
}
}
internal class ScreenHost : IToastHost
{
private readonly List toastWindows = [];
private readonly object @lock = new();
private const double Spacing = 15;
public void AddToast(ToastControl toast)
{
var window = new Window
{
Content = toast,
WindowStyle = WindowStyle.None,
AllowsTransparency = true,
Background = Brushes.Transparent,
ShowInTaskbar = false,
Topmost = true,
Owner = null,
SizeToContent = SizeToContent.WidthAndHeight,
Left = -9999
};
window.Loaded += (_, _) =>
{
lock (@lock)
{
toastWindows.Add(window);
RepositionToasts();
}
};
window.Closed += (_, _) =>
{
lock (@lock)
{
toastWindows.Remove(window);
RepositionToasts();
}
};
window.Show();
}
public void RemoveToast(ToastControl toast)
{
Debug.Assert(toastWindows != null, nameof(toastWindows) + " != null");
var window = toastWindows.FirstOrDefault(w => Equals(w.Content, toast));
window?.Close();
}
private void RepositionToasts()
{
var workArea = SystemParameters.WorkArea;
var currentTop = Spacing; // Start from the top with spacing.
foreach (var window in toastWindows)
{
// Horizontally center the window on the screen.
var left = workArea.Left + (workArea.Width - window.ActualWidth) / 2;
var moveAnim = new DoubleAnimation(currentTop, TimeSpan.FromMilliseconds(350))
{ EasingFunction = new QuinticEase { EasingMode = EasingMode.EaseOut } };
window.Left = left; // Set horizontal position directly.
window.BeginAnimation(Window.TopProperty, moveAnim); // Animate vertical position.
// Update the top position for the next toast.
currentTop += window.ActualHeight + Spacing;
}
}
}
#endregion