Files

446 lines
14 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 탭 정렬 방향
/// </summary>
public enum TabAlign
{
/// <summary>탭이 위쪽에 배치</summary>
Top,
/// <summary>탭이 아래쪽에 배치</summary>
Bottom,
/// <summary>탭이 왼쪽에 배치 (세로 정렬)</summary>
Left,
/// <summary>탭이 오른쪽에 배치 (세로 정렬)</summary>
Right
}
/// <summary>
/// 탭 뷰 컴포넌트.
/// Unity TabView를 래핑하여 커스텀 스타일을 적용합니다.
/// 여러 콘텐츠 페이지를 탭으로 전환하여 표시합니다.
/// </summary>
/// <remarks>
/// <para><b>TabView(탭 뷰)란?</b></para>
/// <para>
/// TabView는 여러 페이지의 콘텐츠를 탭 버튼으로 전환하여 표시하는 UI 컴포넌트입니다.
/// 같은 공간에 여러 내용을 담을 수 있어 화면 공간을 효율적으로 사용합니다.
/// 설정 페이지, 에디터 창, 프로필 화면 등에서 널리 사용됩니다.
/// </para>
///
/// <para><b>TabView 구성:</b></para>
/// <list type="bullet">
/// <item><description>탭 헤더 - 탭 버튼들이 나열된 영역</description></item>
/// <item><description>탭 콘텐츠 - 선택된 탭의 내용이 표시되는 영역</description></item>
/// </list>
///
/// <para><b>주요 속성:</b></para>
/// <list type="bullet">
/// <item><description><c>SelectedIndex</c> - 현재 선택된 탭 인덱스</description></item>
/// <item><description><c>UTKTabs</c> - 탭 목록 (읽기 전용)</description></item>
/// <item><description><c>Align</c> - 탭 정렬 방향 (Top, Bottom, Left, Right)</description></item>
/// </list>
///
/// <para><b>주요 메서드:</b></para>
/// <list type="bullet">
/// <item><description><c>AddUTKTab(string, VisualElement)</c> - 탭 추가</description></item>
/// <item><description><c>AddTab(UTKTab)</c> - UTKTab 인스턴스 추가</description></item>
/// <item><description><c>RemoveTab(UTKTab)</c> - 탭 제거</description></item>
/// <item><description><c>ClearTabs()</c> - 모든 탭 제거</description></item>
/// </list>
///
/// <para><b>이벤트:</b></para>
/// <list type="bullet">
/// <item><description><c>OnTabChanged</c> - 탭이 변경될 때 (인덱스, Tab 전달)</description></item>
/// </list>
///
/// <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>
/// // 탭 뷰 생성
/// var tabView = new UTKTabView();
///
/// // 탭 추가
/// var tab1 = tabView.AddUTKTab("일반");
/// tab1.Add(new Label("일반 설정 내용"));
///
/// var tab2 = tabView.AddUTKTab("고급");
/// tab2.Add(new Label("고급 설정 내용"));
///
/// // 탭 변경 이벤트
/// tabView.OnTabChanged += (index, tab) => Debug.Log($"탭 {index} 선택됨");
///
/// // 탭 정렬 방향 설정
/// tabView.Align = TabAlign.Left; // 탭을 왼쪽에 세로로 배치
///
/// // 탭 선택
/// tabView.SelectedIndex = 0;
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- 기본 탭 뷰 (위쪽) -->
/// <utk:UTKTabView>
/// <utk:UTKTab label="일반">
/// <ui:Label text="일반 탭 내용" />
/// </utk:UTKTab>
/// <utk:UTKTab label="고급">
/// <ui:Label text="고급 탭 내용" />
/// </utk:UTKTab>
/// </utk:UTKTabView>
///
/// <!-- 탭을 왼쪽에 배치 -->
/// <utk:UTKTabView align="Left">
/// <utk:UTKTab label="탭 1">
/// <ui:Label text="내용 1" />
/// </utk:UTKTab>
/// <utk:UTKTab label="탭 2">
/// <ui:Label text="내용 2" />
/// </utk:UTKTab>
/// </utk:UTKTabView>
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKTabView : TabView, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/Tab/UTKTabView";
#endregion
#region Fields
private bool _disposed;
private readonly List<UTKTab> _utkTabs = new();
private TabAlign _align = TabAlign.Top;
private float _tabWidth = 0;
private float _tabHeight = 0;
private VisualElement? _contentViewport;
private int _previousTabIndex = -1;
#endregion
#region Events
/// <summary>탭 변경 이벤트</summary>
public event Action<int, Tab?>? OnTabChanged;
#endregion
#region Properties
/// <summary>선택된 탭 인덱스</summary>
public int SelectedIndex
{
get => selectedTabIndex;
set => selectedTabIndex = value;
}
/// <summary>UTK 탭 목록</summary>
public IReadOnlyList<UTKTab> UTKTabs => _utkTabs;
/// <summary>탭 정렬 방향</summary>
[UxmlAttribute("align")]
public TabAlign Align
{
get => _align;
set
{
if (_align == value) return;
_align = value;
ApplyAlignment();
}
}
/// <summary>탭 콘텐츠 영역 너비 (0 이하이면 미설정)</summary>
[UxmlAttribute("tab-width")]
public float TabWidth
{
get => _tabWidth;
set
{
_tabWidth = value;
ApplyContentViewportSize();
}
}
/// <summary>탭 콘텐츠 영역 높이 (0 이하이면 미설정)</summary>
[UxmlAttribute("tab-height")]
public float TabHeight
{
get => _tabHeight;
set
{
_tabHeight = value;
ApplyContentViewportSize();
}
}
#endregion
#region Constructor
public UTKTabView() : base()
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
{
styleSheets.Add(uss);
}
SetupStyles();
SetupEvents();
SubscribeToThemeChanges();
}
#endregion
#region Setup
private void SetupStyles()
{
AddToClassList("utk-tabview");
ApplyAlignment();
}
private void SetupEvents()
{
this.RegisterCallback<ChangeEvent<int>>(OnTabIndexChanged);
activeTabChanged += OnActiveTabChanged;
}
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 Event Handlers
/// <summary>
/// 코드에서 selectedTabIndex를 변경했을 때 호출됩니다.
/// </summary>
private void OnTabIndexChanged(ChangeEvent<int> evt)
{
_previousTabIndex = evt.previousValue;
NotifyTabContent(evt.previousValue, evt.newValue).Forget();
UpdateTabSelection();
OnTabChanged?.Invoke(evt.newValue, activeTab);
}
/// <summary>
/// 마우스 클릭 등으로 탭이 변경되었을 때 호출됩니다.
/// </summary>
private void OnActiveTabChanged(Tab previousTab, Tab newTab)
{
int prevIndex = previousTab != null ? _utkTabs.FindIndex(t => t == previousTab) : -1;
int newIndex = newTab != null ? _utkTabs.FindIndex(t => t == newTab) : -1;
// ChangeEvent<int>와 중복 호출 방지
if (prevIndex == _previousTabIndex && newIndex == selectedTabIndex)
return;
_previousTabIndex = prevIndex;
NotifyTabContent(prevIndex, newIndex).Forget();
UpdateTabSelection();
OnTabChanged?.Invoke(newIndex, newTab);
}
/// <summary>
/// 이전 탭 콘텐츠의 Hide, 새 탭 콘텐츠의 Show를 호출합니다.
/// </summary>
private async UniTaskVoid NotifyTabContent(int previousIndex, int newIndex)
{
// 이전 탭 Hide
if (previousIndex >= 0 && previousIndex < _utkTabs.Count)
{
if (FindTabContent(_utkTabs[previousIndex]) is IUTKTabContent prevContent)
{
await prevContent.Hide();
}
}
// 새 탭 Show
if (newIndex >= 0 && newIndex < _utkTabs.Count)
{
if (FindTabContent(_utkTabs[newIndex]) is IUTKTabContent newContent)
{
newContent.Show(null);
}
}
}
/// <summary>
/// 탭 내부에서 IUTKTabContent를 구현한 자식 요소를 찾습니다.
/// </summary>
public static IUTKTabContent? FindTabContent(Tab tab)
{
for (int i = 0; i < tab.childCount; i++)
{
if (tab[i] is IUTKTabContent content) return content;
}
return null;
}
private void UpdateTabSelection()
{
for (int i = 0; i < _utkTabs.Count; i++)
{
_utkTabs[i].IsSelected = (i == selectedTabIndex);
}
}
#endregion
#region Methods
/// <summary>
/// 탭 정렬 방향 적용
/// </summary>
private void ApplyAlignment()
{
// 기존 align 클래스 제거
RemoveFromClassList("utk-tabview--align-top");
RemoveFromClassList("utk-tabview--align-bottom");
RemoveFromClassList("utk-tabview--align-left");
RemoveFromClassList("utk-tabview--align-right");
// 새로운 align 클래스 추가
switch (_align)
{
case TabAlign.Top:
AddToClassList("utk-tabview--align-top");
break;
case TabAlign.Bottom:
AddToClassList("utk-tabview--align-bottom");
break;
case TabAlign.Left:
AddToClassList("utk-tabview--align-left");
break;
case TabAlign.Right:
AddToClassList("utk-tabview--align-right");
break;
}
}
/// <summary>
/// 탭 콘텐츠 영역 크기 적용
/// </summary>
private void ApplyContentViewportSize()
{
_contentViewport ??= this.Q(className: "unity-tab-view__content-viewport");
if (_contentViewport == null) return;
_contentViewport.style.width = _tabWidth > 0
? new StyleLength(_tabWidth)
: new StyleLength(StyleKeyword.Auto);
_contentViewport.style.height = _tabHeight > 0
? new StyleLength(_tabHeight)
: new StyleLength(StyleKeyword.Auto);
}
/// <summary>
/// UTK 탭 추가
/// </summary>
public UTKTab AddUTKTab(string text, VisualElement? content = null)
{
var tab = new UTKTab(text);
if (content != null)
{
tab.Add(content);
}
AddTab(tab);
return tab;
}
/// <summary>
/// 탭 추가
/// </summary>
public UTKTab AddTab(UTKTab tab)
{
_utkTabs.Add(tab);
Add(tab);
if (_utkTabs.Count == 1)
{
tab.IsSelected = true;
}
return tab;
}
/// <summary>
/// 탭 제거
/// </summary>
public void RemoveTab(UTKTab tab)
{
int index = _utkTabs.IndexOf(tab);
if (index < 0) return;
_utkTabs.RemoveAt(index);
tab.RemoveFromHierarchy();
tab.Dispose();
}
/// <summary>
/// 모든 탭 제거
/// </summary>
public void ClearTabs()
{
foreach (var tab in _utkTabs)
{
tab.RemoveFromHierarchy();
tab.Dispose();
}
_utkTabs.Clear();
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
UnregisterCallback<ChangeEvent<int>>(OnTabIndexChanged);
activeTabChanged -= OnActiveTabChanged;
foreach (var tab in _utkTabs)
{
tab.Dispose();
}
_utkTabs.Clear();
OnTabChanged = null;
}
#endregion
}
}