using System; using System.Collections.Generic; using UnityEngine; using static UnityEditor.Toast; namespace UnityEditor { internal static class ToastManager { private const float NOTIFICATION_MARGIN = 5f; private const double TARGET_FRAME_TIME = 1.0 / 30.0; private static readonly List topLeftNotifications = new(); private static readonly List topRightNotifications = new(); private static readonly List topCenterNotifications = new(); private static readonly List bottomLeftNotifications = new(); private static readonly List bottomRightNotifications = new(); private static readonly List bottomCenterNotifications = new(); private static int notificationCount = 0; [InitializeOnLoadMethod] private static void Init() { AssemblyReloadEvents.beforeAssemblyReload += OnBeforeDomainReload; EnsureUpdateHook(); EditorApplication.QueuePlayerLoopUpdate(); } private static void EnsureUpdateHook() { EditorApplication.update -= CustomUpdateLoop; EditorApplication.update += CustomUpdateLoop; } private static void OnBeforeDomainReload() { //close all notifications on domain reload EditorApplication.update -= CustomUpdateLoop; for (var index = topLeftNotifications.Count - 1; index >= 0; index--) { var notification = topLeftNotifications[index]; RemoveNotification(notification); notification.Close(); } for (var index = topRightNotifications.Count - 1; index >= 0; index--) { var notification = topRightNotifications[index]; RemoveNotification(notification); notification.Close(); } for (var index = topCenterNotifications.Count - 1; index >= 0; index--) { var notification = topCenterNotifications[index]; RemoveNotification(notification); notification.Close(); } for (var index = bottomLeftNotifications.Count - 1; index >= 0; index--) { var notification = bottomLeftNotifications[index]; RemoveNotification(notification); notification.Close(); } for (var index = bottomRightNotifications.Count - 1; index >= 0; index--) { var notification = bottomRightNotifications[index]; RemoveNotification(notification); notification.Close(); } for (var index = bottomCenterNotifications.Count - 1; index >= 0; index--) { var notification = bottomCenterNotifications[index]; RemoveNotification(notification); notification.Close(); } } private static void CustomUpdateLoop() { if (notificationCount <= 0) return; UpdateNotifications(TARGET_FRAME_TIME); if (!EditorApplication.isPlaying || notificationCount > 0) { EditorApplication.delayCall += () => { EditorWindow.focusedWindow?.Repaint(); SceneView.RepaintAll(); }; } } private static void UpdateNotifications(double deltaTime) { CheckNotificationLifetimes(); UpdateNotificationPositions(deltaTime); } private static void CheckNotificationLifetimes() { for (var index = topLeftNotifications.Count - 1; index >= 0; index--) { var notification = topLeftNotifications[index]; if (!notification.IsLifetimeOver()) continue; RemoveNotification(notification); notification.Close(); } for (var index = topRightNotifications.Count - 1; index >= 0; index--) { var notification = topRightNotifications[index]; if (!notification.IsLifetimeOver()) continue; RemoveNotification(notification); notification.Close(); } for (var index = topCenterNotifications.Count - 1; index >= 0; index--) { var notification = topCenterNotifications[index]; if (!notification.IsLifetimeOver()) continue; RemoveNotification(notification); notification.Close(); } for (var index = bottomLeftNotifications.Count - 1; index >= 0; index--) { var notification = bottomLeftNotifications[index]; if (!notification.IsLifetimeOver()) continue; RemoveNotification(notification); notification.Close(); } for (var index = bottomRightNotifications.Count - 1; index >= 0; index--) { var notification = bottomRightNotifications[index]; if (!notification.IsLifetimeOver()) continue; RemoveNotification(notification); notification.Close(); } for (var index = bottomCenterNotifications.Count - 1; index >= 0; index--) { var notification = bottomCenterNotifications[index]; if (!notification.IsLifetimeOver()) continue; RemoveNotification(notification); notification.Close(); } } private static void UpdateNotificationPositions(double deltaTime) { UpdateNotificationPositions(ToastPosition.TopLeft, deltaTime); UpdateNotificationPositions(ToastPosition.TopRight, deltaTime); UpdateNotificationPositions(ToastPosition.TopCenter, deltaTime); UpdateNotificationPositions(ToastPosition.BottomLeft, deltaTime); UpdateNotificationPositions(ToastPosition.BottomRight, deltaTime); UpdateNotificationPositions(ToastPosition.BottomCenter, deltaTime); } public static void ShowNotification(ToastArgs toastArgs, Vector2 windowSize = default) { EnsureUpdateHook(); //Create the window var notification = ScriptableObject.CreateInstance(); notification.titleContent = new GUIContent($"Notification - {toastArgs.Title}"); notification.minSize = windowSize == default ? new Vector2(250, 100) : windowSize; notification.maxSize = notification.minSize; notification.position = new Rect(0, 0, notification.minSize.x, notification.minSize.y); notification.ShowPopup(); notification.OnClose += RemoveNotification; //Set position notification.SetupWindow(new ToastData { ToastArgs = toastArgs, TimeCreated = Time.time, }); //Update other notifications AddNotification(notification); } private static void AddNotification(Toast toast) { switch (toast.Args.ToastPosition) { case ToastPosition.TopLeft: topLeftNotifications.Insert(0, toast); break; case ToastPosition.TopRight: topRightNotifications.Insert(0, toast); break; case ToastPosition.TopCenter: topCenterNotifications.Insert(0, toast); break; case ToastPosition.BottomLeft: bottomLeftNotifications.Insert(0, toast); break; case ToastPosition.BottomRight: bottomRightNotifications.Insert(0, toast); break; case ToastPosition.BottomCenter: bottomCenterNotifications.Insert(0, toast); break; default: throw new ArgumentOutOfRangeException(null); } notificationCount++; } private static void RemoveNotification(Toast toast) { toast.OnClose -= RemoveNotification; switch (toast.Args.ToastPosition) { case ToastPosition.TopLeft: topLeftNotifications.Remove(toast); break; case ToastPosition.TopRight: topRightNotifications.Remove(toast); break; case ToastPosition.TopCenter: topCenterNotifications.Remove(toast); break; case ToastPosition.BottomLeft: bottomLeftNotifications.Remove(toast); break; case ToastPosition.BottomRight: bottomRightNotifications.Remove(toast); break; case ToastPosition.BottomCenter: bottomCenterNotifications.Remove(toast); break; default: throw new ArgumentOutOfRangeException(null); } notificationCount--; } private static void UpdateNotificationPositions(ToastPosition toastPosition, double deltaTime) { var currentHeightOffset = 0f; switch (toastPosition) { case ToastPosition.TopLeft: foreach (var currentNotification in topLeftNotifications) { var positionRect = currentNotification.GetEditorWindowPosition(toastPosition); currentNotification.UpdatePosition(positionRect, currentHeightOffset, deltaTime); currentHeightOffset += currentNotification.GetHeight() + NOTIFICATION_MARGIN; } break; case ToastPosition.TopRight: foreach (var currentNotification in topRightNotifications) { var positionRect = currentNotification.GetEditorWindowPosition(toastPosition); currentNotification.UpdatePosition(positionRect, currentHeightOffset, deltaTime); currentHeightOffset += currentNotification.GetHeight() + NOTIFICATION_MARGIN; } break; case ToastPosition.TopCenter: foreach (var currentNotification in topCenterNotifications) { var positionRect = currentNotification.GetEditorWindowPosition(toastPosition); currentNotification.UpdatePosition(positionRect, currentHeightOffset, deltaTime); currentHeightOffset += currentNotification.GetHeight() + NOTIFICATION_MARGIN; } break; case ToastPosition.BottomLeft: foreach (var currentNotification in bottomLeftNotifications) { var positionRect = currentNotification.GetEditorWindowPosition(toastPosition); currentNotification.UpdatePosition(positionRect, currentHeightOffset, deltaTime); currentHeightOffset -= currentNotification.GetHeight() + NOTIFICATION_MARGIN; } break; case ToastPosition.BottomRight: foreach (var currentNotification in bottomRightNotifications) { var positionRect = currentNotification.GetEditorWindowPosition(toastPosition); currentNotification.UpdatePosition(positionRect, currentHeightOffset, deltaTime); currentHeightOffset -= currentNotification.GetHeight() + NOTIFICATION_MARGIN; } break; case ToastPosition.BottomCenter: foreach (var currentNotification in bottomCenterNotifications) { var positionRect = currentNotification.GetEditorWindowPosition(toastPosition); currentNotification.UpdatePosition(positionRect, currentHeightOffset, deltaTime); currentHeightOffset -= currentNotification.GetHeight() + NOTIFICATION_MARGIN; } break; default: throw new ArgumentOutOfRangeException(nameof(toastPosition), toastPosition, null); } } } }