using System; using System.Linq; using System.Runtime.InteropServices; using UnityEngine; using UnityEngine.UIElements; namespace UnityEditor { internal class Toast : EditorWindow { private const int DWMWA_WINDOW_CORNER_PREFERENCE = 33; private enum DWM_WINDOW_CORNER_PREFERENCE { DWMWCP_DEFAULT = 0, DWMWCP_DONOTROUND = 1, DWMWCP_ROUND = 2, DWMWCP_ROUNDSMALL = 3 } public enum ToastSeverity { Info, Warning, Error } public enum ToastPosition { TopLeft, TopRight, TopCenter, BottomLeft, BottomRight, BottomCenter } public struct ToastData { public float TimeCreated { get; set; } public ToastArgs ToastArgs { get; set; } } public struct ToastArgs { public string Title { get; set; } public string Message { get; set; } public ToastPosition ToastPosition { get; set; } public float LifeTime { get; set; } public bool NoTimeOut { get; set; } public Func CustomContent { get; set; } public ToastSeverity Severity { get; set; } } [DllImport("user32.dll")] private static extern IntPtr GetActiveWindow(); [DllImport("dwmapi.dll")] private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref DWM_WINDOW_CORNER_PREFERENCE pref, int attrLen); private const float POSITION_SPEED = 10f; private const float POSITION_THRESHOLD = 0.1f; private const float ROOT_CORNER_RADIUS = 9.2f; private const float ROOT_BORDER_WIDTH = 2f; public ToastData Data { get; set; } public ToastArgs Args; public event Action OnClose; private Rect currentPosition; private VisualElement titleBar; private Label titleLabel; private Label messageLabel; private Button closeButton; private VisualElement contentContainer; private ProgressBar lifetimeBar; public void SetupWindow(ToastData toastData) { Data = toastData; Args = toastData.ToastArgs; position = this.GetEditorWindowPosition(Args.ToastPosition); currentPosition = position; titleLabel.text = Args.Title; messageLabel.text = Args.Message; contentContainer.Add(Args.CustomContent?.Invoke()); var backgroundColor = Args.Severity switch { ToastSeverity.Info => new Color(0.27f, 0.38f, 0.49f), ToastSeverity.Warning => new Color(0.69f, 0.5f, 0.02f), _ => new Color(0.49f, 0f, 0f) }; lifetimeBar.lowValue = 0; var datatoastArgs = Data.ToastArgs; lifetimeBar.value = lifetimeBar.highValue = datatoastArgs.NoTimeOut ? 1f : datatoastArgs.LifeTime; var lifetimeBarContainer = lifetimeBar.Children().First(); var lifetimeBarBackground = lifetimeBarContainer.Children().First(); var lifetimeBarFill = lifetimeBarBackground.Children().First(); if (lifetimeBarFill != null) { lifetimeBarFill.style.backgroundColor = backgroundColor; } titleBar.style.backgroundColor = backgroundColor; closeButton.RegisterCallback(evt => { closeButton.style.backgroundColor = backgroundColor; }); closeButton.RegisterCallback(evt => { closeButton.style.backgroundColor = new Color(0.31f, 0.31f, 0.31f); }); EnableRoundedCorners(); } private void EnableRoundedCorners() { IntPtr hwnd = GetActiveWindow(); if (hwnd != IntPtr.Zero) { DWM_WINDOW_CORNER_PREFERENCE pref = DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND; DwmSetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, ref pref, sizeof(int)); } else { Debug.LogWarning("Failed to get window handle."); } } private void CreateGUI() { rootVisualElement.style.borderTopWidth = ROOT_BORDER_WIDTH; rootVisualElement.style.borderBottomWidth = ROOT_BORDER_WIDTH; rootVisualElement.style.borderLeftWidth = ROOT_BORDER_WIDTH; rootVisualElement.style.borderRightWidth = ROOT_BORDER_WIDTH; rootVisualElement.style.borderTopColor = Color.black; rootVisualElement.style.borderBottomColor = Color.black; rootVisualElement.style.borderLeftColor = Color.black; rootVisualElement.style.borderRightColor = Color.black; rootVisualElement.style.borderTopLeftRadius = ROOT_CORNER_RADIUS; rootVisualElement.style.borderTopRightRadius = ROOT_CORNER_RADIUS; rootVisualElement.style.borderBottomLeftRadius = ROOT_CORNER_RADIUS; rootVisualElement.style.borderBottomRightRadius = ROOT_CORNER_RADIUS; titleBar = new VisualElement { style = { height = 20f, minHeight = 20f, width = new StyleLength(Length.Percent(100)), maxWidth = new StyleLength(Length.Percent(100)), flexDirection = FlexDirection.Row, borderBottomWidth = 1f, borderBottomColor = Color.black } }; rootVisualElement.Add(titleBar); { titleLabel = new Label() { style = { flexGrow = 1f, unityFontStyleAndWeight = FontStyle.Bold, fontSize = 13f, unityTextAlign = TextAnchor.MiddleLeft } }; titleBar.Add(titleLabel); closeButton = new Button(Close) { text = "X", style = { flexGrow = 0f, width = 20f, minWidth = 20f, borderBottomWidth = 0f, borderLeftWidth = 1f, borderRightWidth = 0f, borderTopWidth = 0f, borderTopLeftRadius = 0f, borderTopRightRadius = 0f, borderBottomRightRadius = 0f, borderBottomLeftRadius = 0f, marginRight = 0f, marginLeft = 0f, marginTop = 0f, marginBottom = 0f } }; titleBar.Add(closeButton); } lifetimeBar = new ProgressBar { style = { marginBottom = 0f, marginLeft = 0f, marginRight = 0f, marginTop = 0f, height = 6f, minHeight = 6f, maxHeight = 6f } }; rootVisualElement.Add(lifetimeBar); var messageContainer = new VisualElement(); rootVisualElement.Add(messageContainer); { messageLabel = new Label { style = { marginTop = 2f, marginBottom = 2f, marginLeft = 2f, marginRight = 2f, } }; messageContainer.Add(messageLabel); } contentContainer = new VisualElement(); rootVisualElement.Add(contentContainer); } public float GetHeight() => position.height; public void UpdatePosition(Rect positionRect, float currentHeightOffset, double deltaTime) { var targetPosition = new Rect(positionRect.x, positionRect.y + currentHeightOffset, positionRect.width, positionRect.height); if (Mathf.Abs(currentPosition.y - targetPosition.y) < POSITION_THRESHOLD) { currentPosition.y = targetPosition.y; position = currentPosition; } else { var newY = Mathf.Lerp( currentPosition.y, targetPosition.y, (float)(POSITION_SPEED * deltaTime) ); currentPosition.y = newY; currentPosition.x = targetPosition.x; position = currentPosition; } } public bool IsLifetimeOver() { if (Args.NoTimeOut) return false; var currentLifeTime = Time.time - Data.TimeCreated; lifetimeBar.value = Args.LifeTime - currentLifeTime; return currentLifeTime > Args.LifeTime; } private void OnDestroy() { OnClose?.Invoke(this); } } }