#nullable enable using System; using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.UIElements; namespace UVC.UIToolkit { /// /// 토스트 알림 컴포넌트. /// 화면 하단에 일시적으로 표시되는 비방해 알림입니다. /// /// /// Toast(토스트)란? /// /// Toast는 화면 하단에 잠시 나타났다가 자동으로 사라지는 알림 UI입니다. /// 이름은 토스터기에서 빵이 올라왔다가 내려가는 모습에서 유래했습니다. /// 사용자의 작업을 방해하지 않으면서 정보를 전달할 때 사용합니다. /// /// /// Toast vs Alert 차이: /// /// Toast - 자동으로 사라짐, 비침투적 (작업 방해 안 함) /// Alert - 버튼 클릭 필요, 모달 (확인 필요한 중요 정보) /// /// /// Toast 타입 (ToastType): /// /// Info - 일반 정보 (회색/파란색) /// Success - 성공 메시지 (녹색) /// Warning - 경고 메시지 (주황색) /// Error - 오류 메시지 (빨간색) /// /// /// 주요 속성: /// /// Message - 표시할 메시지 /// Duration - 표시 시간 (밀리초, 기본 1000ms = 1초) /// ShowCloseButton - 닫기 버튼 표시 여부 /// /// /// 사용 전 초기화: /// /// Toast를 표시하기 전에 UTKToast.SetRoot(rootVisualElement)로 루트를 설정해야 합니다. /// 또는 Show(parent, ...)로 부모 요소를 직접 전달할 수 있습니다. /// /// /// 실제 활용 예시: /// /// "저장되었습니다" - 파일 저장 완료 /// "클립보드에 복사됨" - 복사 완료 /// "네트워크 연결됨/끊김" - 연결 상태 변경 /// "새 메시지가 도착했습니다" - 알림 /// /// /// /// C# 코드에서 사용: /// /// // 초기화 (root 설정 필요) /// UTKToast.Initialize(rootVisualElement); /// /// // 기본 토스트 /// UTKToast.Show("저장되었습니다."); /// /// // 타입별 토스트 /// UTKToast.ShowSuccess("성공적으로 완료되었습니다."); /// UTKToast.ShowError("오류가 발생했습니다."); /// UTKToast.ShowWarning("주의가 필요합니다."); /// UTKToast.ShowInfo("정보 메시지"); /// /// // 지속시간 설정 (ms) /// UTKToast.Show("잠시 표시", duration: 2000); /// /// // 닫기 버튼 표시 /// UTKToast.Show("메시지", showCloseButton: true); /// /// UXML에서 사용: /// /// /// /// /// /// /// [UxmlElement] public partial class UTKToast : VisualElement, IDisposable { #region Constants private const string USS_PATH = "UIToolkit/Modal/UTKToast"; private const int DEFAULT_DURATION_MS = 1000; #endregion #region Fields private static VisualElement? _root; private bool _disposed; private Label? _messageLabel; private Button? _closeButton; private string _message = ""; private ToastType _type = ToastType.Info; private int _duration = DEFAULT_DURATION_MS; private bool _showCloseButton; #endregion #region Events /// 닫힘 이벤트 public event Action? OnClosed; #endregion #region Properties /// 메시지 [UxmlAttribute("message")] public string Message { get => _message; set { _message = value; if (_messageLabel != null) _messageLabel.text = value; } } /// 토스트 유형 [UxmlAttribute("type")] public ToastType Type { get => _type; set { _type = value; UpdateType(); } } /// 표시 시간 (밀리초) [UxmlAttribute("duration")] public int Duration { get => _duration; set => _duration = value; } /// 닫기 버튼 표시 여부 [UxmlAttribute("show-close-button")] public bool ShowCloseButton { get => _showCloseButton; set { _showCloseButton = value; if (_closeButton != null) { _closeButton.style.display = value ? DisplayStyle.Flex : DisplayStyle.None; } } } #endregion #region Enums public enum ToastType { Info, Success, Warning, Error } #endregion #region Constructor public UTKToast() { UTKThemeManager.Instance.ApplyThemeToElement(this); var uss = Resources.Load(USS_PATH); if (uss != null) { styleSheets.Add(uss); } CreateUI(); SubscribeToThemeChanges(); } public UTKToast(string message, ToastType type = ToastType.Info) : this() { Message = message; Type = type; } #endregion #region Static Methods /// /// 기본 루트 요소 설정 /// /// Toast를 표시할 기본 루트 요소 public static void SetRoot(VisualElement root) { _root = root; } /// /// 기본 루트 요소 반환 /// public static VisualElement? GetRoot() => _root; private static void ValidateRoot() { if (_root == null) { throw new InvalidOperationException("UTKToast.SetRoot()를 먼저 호출하여 기본 루트 요소를 설정해야 합니다."); } } #endregion #region Static Factory (without parent) /// /// Info 토스트 표시 (SetRoot로 설정된 루트 사용) /// public static UTKToast ShowInfo(string message, int duration = DEFAULT_DURATION_MS) { ValidateRoot(); return Show(_root!, message, ToastType.Info, duration); } /// /// Success 토스트 표시 (SetRoot로 설정된 루트 사용) /// public static UTKToast ShowSuccess(string message, int duration = DEFAULT_DURATION_MS) { ValidateRoot(); return Show(_root!, message, ToastType.Success, duration); } /// /// Warning 토스트 표시 (SetRoot로 설정된 루트 사용) /// public static UTKToast ShowWarning(string message, int duration = DEFAULT_DURATION_MS) { ValidateRoot(); return Show(_root!, message, ToastType.Warning, duration); } /// /// Error 토스트 표시 (SetRoot로 설정된 루트 사용) /// public static UTKToast ShowError(string message, int duration = DEFAULT_DURATION_MS) { ValidateRoot(); return Show(_root!, message, ToastType.Error, duration); } /// /// 토스트 표시 (SetRoot로 설정된 루트 사용) /// public static UTKToast Show(string message, ToastType type = ToastType.Info, int duration = DEFAULT_DURATION_MS) { ValidateRoot(); return Show(_root!, message, type, duration); } #endregion #region Static Factory (with parent) /// /// Info 토스트 표시 /// public static UTKToast ShowInfo(VisualElement parent, string message, int duration = DEFAULT_DURATION_MS) { return Show(parent, message, ToastType.Info, duration); } /// /// Success 토스트 표시 /// public static UTKToast ShowSuccess(VisualElement parent, string message, int duration = DEFAULT_DURATION_MS) { return Show(parent, message, ToastType.Success, duration); } /// /// Warning 토스트 표시 /// public static UTKToast ShowWarning(VisualElement parent, string message, int duration = DEFAULT_DURATION_MS) { return Show(parent, message, ToastType.Warning, duration); } /// /// Error 토스트 표시 /// public static UTKToast ShowError(VisualElement parent, string message, int duration = DEFAULT_DURATION_MS) { return Show(parent, message, ToastType.Error, duration); } /// /// 토스트 표시 /// public static UTKToast Show(VisualElement parent, string message, ToastType type, int duration = DEFAULT_DURATION_MS) { var toast = new UTKToast(message, type); toast._duration = duration; parent.Add(toast); // 하단 중앙 정렬 toast.style.position = Position.Absolute; toast.style.left = Length.Percent(50); toast.style.bottom = 20; toast.style.translate = new Translate(Length.Percent(-50), 0); // 자동 닫기 if (duration > 0) { toast.AutoCloseAsync(duration).Forget(); } return toast; } #endregion #region UI Creation private void CreateUI() { AddToClassList("utk-toast"); _messageLabel = new Label { name = "message" }; _messageLabel.AddToClassList("utk-toast__message"); Add(_messageLabel); _closeButton = new Button { name = "close-btn", text = "✕" }; _closeButton.AddToClassList("utk-toast__close-btn"); _closeButton.style.display = DisplayStyle.None; _closeButton.RegisterCallback(_ => Close()); Add(_closeButton); UpdateType(); } private void SubscribeToThemeChanges() { UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged; RegisterCallback(_ => { UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged; }); } private void OnThemeChanged(UTKTheme theme) { UTKThemeManager.Instance.ApplyThemeToElement(this); } #endregion #region Methods private void UpdateType() { RemoveFromClassList("utk-toast--info"); RemoveFromClassList("utk-toast--success"); RemoveFromClassList("utk-toast--warning"); RemoveFromClassList("utk-toast--error"); var typeClass = _type switch { ToastType.Success => "utk-toast--success", ToastType.Warning => "utk-toast--warning", ToastType.Error => "utk-toast--error", _ => "utk-toast--info" }; AddToClassList(typeClass); } private async UniTaskVoid AutoCloseAsync(int delayMs) { await UniTask.Delay(delayMs); if (!_disposed) { Close(); } } /// /// 토스트 닫기 /// public void Close() { OnClosed?.Invoke(); RemoveFromHierarchy(); } #endregion #region IDisposable public void Dispose() { if (_disposed) return; _disposed = true; UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged; OnClosed = null; } #endregion } }