446 lines
14 KiB
C#
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
|
|
}
|
|
}
|