Files

372 lines
12 KiB
C#

#nullable enable
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 토스트 알림 컴포넌트.
/// 화면 하단에 일시적으로 표시되는 비방해 알림입니다.
/// </summary>
/// <remarks>
/// <para><b>Toast(토스트)란?</b></para>
/// <para>
/// Toast는 화면 하단에 잠시 나타났다가 자동으로 사라지는 알림 UI입니다.
/// 이름은 토스터기에서 빵이 올라왔다가 내려가는 모습에서 유래했습니다.
/// 사용자의 작업을 방해하지 않으면서 정보를 전달할 때 사용합니다.
/// </para>
///
/// <para><b>Toast vs Alert 차이:</b></para>
/// <list type="bullet">
/// <item><description><c>Toast</c> - 자동으로 사라짐, 비침투적 (작업 방해 안 함)</description></item>
/// <item><description><c>Alert</c> - 버튼 클릭 필요, 모달 (확인 필요한 중요 정보)</description></item>
/// </list>
///
/// <para><b>Toast 타입 (ToastType):</b></para>
/// <list type="bullet">
/// <item><description><c>Info</c> - 일반 정보 (회색/파란색)</description></item>
/// <item><description><c>Success</c> - 성공 메시지 (녹색)</description></item>
/// <item><description><c>Warning</c> - 경고 메시지 (주황색)</description></item>
/// <item><description><c>Error</c> - 오류 메시지 (빨간색)</description></item>
/// </list>
///
/// <para><b>주요 속성:</b></para>
/// <list type="bullet">
/// <item><description><c>Message</c> - 표시할 메시지</description></item>
/// <item><description><c>Duration</c> - 표시 시간 (밀리초, 기본 1000ms = 1초)</description></item>
/// <item><description><c>ShowCloseButton</c> - 닫기 버튼 표시 여부</description></item>
/// </list>
///
/// <para><b>사용 전 초기화:</b></para>
/// <para>
/// Toast를 표시하기 전에 <c>UTKToast.SetRoot(rootVisualElement)</c>로 루트를 설정해야 합니다.
/// </para>
///
/// <para><b>실제 활용 예시:</b></para>
/// <list type="bullet">
/// <item><description>"저장되었습니다" - 파일 저장 완료</description></item>
/// <item><description>"클립보드에 복사됨" - 복사 완료</description></item>
/// <item><description>"네트워크 연결됨/끊김" - 연결 상태 변경</description></item>
/// <item><description>"새 메시지가 도착했습니다" - 알림</description></item>
/// </list>
/// </remarks>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 초기화 (root 설정 필요)
/// UTKToast.SetRoot(rootVisualElement);
///
/// // 기본 토스트
/// UTKToast.Show("저장되었습니다.");
///
/// // 타입별 토스트
/// UTKToast.ShowSuccess("성공적으로 완료되었습니다.");
/// UTKToast.ShowError("오류가 발생했습니다.");
/// UTKToast.ShowWarning("주의가 필요합니다.");
/// UTKToast.ShowInfo("정보 메시지");
///
/// // 지속시간 설정 (ms)
/// UTKToast.Show("잠시 표시", duration: 2000);
///
/// // 닫기 버튼 표시
/// UTKToast.Show("메시지", showCloseButton: true);
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- 토스트는 주로 C# 코드로 동적 생성합니다 -->
/// <utk:UTKToast Message="저장됨" Type="Success" />
/// </ui:UXML>
/// </code>
/// </example>
[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
/// <summary>닫힘 이벤트</summary>
public event Action? OnClosed;
#endregion
#region Properties
/// <summary>메시지</summary>
[UxmlAttribute("message")]
public string Message
{
get => _message;
set
{
_message = value;
if (_messageLabel != null) _messageLabel.text = value;
}
}
/// <summary>토스트 유형</summary>
[UxmlAttribute("type")]
public ToastType Type
{
get => _type;
set
{
_type = value;
UpdateType();
}
}
/// <summary>표시 시간 (밀리초)</summary>
[UxmlAttribute("duration")]
public int Duration
{
get => _duration;
set => _duration = value;
}
/// <summary>닫기 버튼 표시 여부</summary>
[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<StyleSheet>(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
/// <summary>
/// 기본 루트 요소 설정
/// </summary>
/// <param name="root">Toast를 표시할 기본 루트 요소</param>
public static void SetRoot(VisualElement root)
{
_root = root;
}
/// <summary>
/// 기본 루트 요소 반환
/// </summary>
public static VisualElement? GetRoot() => _root;
private static void ValidateRoot()
{
if (_root == null)
{
throw new InvalidOperationException("UTKToast.SetRoot()를 먼저 호출하여 기본 루트 요소를 설정해야 합니다.");
}
}
#endregion
#region Static Factory
/// <summary>
/// Info 토스트 표시
/// </summary>
public static UTKToast ShowInfo(string message, int duration = DEFAULT_DURATION_MS)
{
return Show(message, ToastType.Info, duration);
}
/// <summary>
/// Success 토스트 표시
/// </summary>
public static UTKToast ShowSuccess(string message, int duration = DEFAULT_DURATION_MS)
{
return Show(message, ToastType.Success, duration);
}
/// <summary>
/// Warning 토스트 표시
/// </summary>
public static UTKToast ShowWarning(string message, int duration = DEFAULT_DURATION_MS)
{
return Show(message, ToastType.Warning, duration);
}
/// <summary>
/// Error 토스트 표시
/// </summary>
public static UTKToast ShowError(string message, int duration = DEFAULT_DURATION_MS)
{
return Show(message, ToastType.Error, duration);
}
/// <summary>
/// 토스트 표시
/// </summary>
public static UTKToast Show(string message, ToastType type = ToastType.Info, int duration = DEFAULT_DURATION_MS)
{
ValidateRoot();
var toast = new UTKToast(message, type);
toast._duration = duration;
// panel.visualTree에 직접 추가
var root = _root!.panel?.visualTree ?? _root!;
root.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<ClickEvent>(_ => Close());
Add(_closeButton);
UpdateType();
}
private void SubscribeToThemeChanges()
{
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
RegisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
RegisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
}
private void OnAttachToPanelForTheme(AttachToPanelEvent evt)
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
private void OnDetachFromPanelForTheme(DetachFromPanelEvent evt)
{
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();
}
}
/// <summary>
/// 토스트 닫기
/// </summary>
public void Close()
{
OnClosed?.Invoke();
RemoveFromHierarchy();
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
OnClosed = null;
}
#endregion
}
}