Files
XRLib/Assets/Scripts/UVC/UIToolkit/Modal/UTKPanel.cs
2026-02-10 20:48:49 +09:00

456 lines
15 KiB
C#

#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 패널 컨테이너 컴포넌트.
/// 콘텐츠를 그룹화하고 시각적으로 구분하는 섹션 컨테이너입니다.
/// </summary>
/// <remarks>
/// <para><b>Panel(패널)이란?</b></para>
/// <para>
/// Panel은 관련 UI 요소들을 묶어 하나의 섹션으로 표시하는 컨테이너입니다.
/// 설정 페이지, 폼, 대시보드 등에서 내용을 구분하고 정리하는 데 사용됩니다.
/// 선택적으로 접기/펼치기 기능을 제공합니다.
/// </para>
///
/// <para><b>Panel vs Card 차이:</b></para>
/// <list type="bullet">
/// <item><description><c>Panel</c> - 기능적 그룹화, 접기/펼치기, 넓은 영역</description></item>
/// <item><description><c>Card</c> - 시각적 그룹화, 독립적 아이템 단위, 클릭 가능</description></item>
/// </list>
///
/// <para><b>패널 스타일 (PanelVariant):</b></para>
/// <list type="bullet">
/// <item><description><c>Default</c> - 기본 스타일</description></item>
/// <item><description><c>Elevated</c> - 그림자가 있는 떠 있는 스타일</description></item>
/// <item><description><c>Outlined</c> - 테두리만 있는 스타일</description></item>
/// <item><description><c>Flat</c> - 배경/테두리 없는 평면 스타일</description></item>
/// </list>
///
/// <para><b>패널 구성 요소:</b></para>
/// <list type="bullet">
/// <item><description>헤더 - 제목과 헤더 액션 버튼</description></item>
/// <item><description>콘텐츠 - 본문 내용 (UXML 자식 또는 Add()로 추가)</description></item>
/// <item><description>푸터 - 하단 버튼/정보 영역 (ShowFooter로 표시)</description></item>
/// </list>
///
/// <para><b>접기/펼치기 기능:</b></para>
/// <para>
/// <c>IsCollapsible = true</c>로 설정하면 헤더 클릭으로 패널을 접거나 펼 수 있습니다.
/// <c>IsCollapsed</c> 속성으로 현재 접힘 상태를 확인/변경할 수 있습니다.
/// </para>
///
/// <para><b>실제 활용 예시:</b></para>
/// <list type="bullet">
/// <item><description>설정 페이지 - "일반", "고급", "보안" 등 설정 그룹</description></item>
/// <item><description>인스펙터 - Transform, Renderer 등 컴포넌트 섹션</description></item>
/// <item><description>대시보드 - 통계, 차트, 목록 등 위젯 영역</description></item>
/// <item><description>폼 - 개인정보, 결제정보 등 입력 그룹</description></item>
/// </list>
/// </remarks>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 패널
/// var panel = new UTKPanel();
/// panel.Title = "설정";
/// panel.AddContent(new Label("패널 내용"));
///
/// // 접을 수 있는 패널
/// panel.IsCollapsible = true;
/// panel.IsCollapsed = false;
///
/// // 헤더 액션 버튼 추가
/// panel.AddHeaderAction(UTKMaterialIcons.Settings, () => Debug.Log("설정"));
///
/// // 푸터 표시
/// panel.ShowFooter = true;
/// panel.AddFooterContent(new Label("푸터 내용"));
///
/// // 변형 스타일
/// panel.Variant = UTKPanel.PanelVariant.Elevated;
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- 기본 패널 -->
/// <utk:UTKPanel title="제목">
/// <ui:Label text="패널 내용" />
/// </utk:UTKPanel>
///
/// <!-- 접을 수 있는 패널 -->
/// <utk:UTKPanel title="고급 설정" is-collapsible="true">
/// <ui:Label text="내용" />
/// </utk:UTKPanel>
///
/// <!-- 외곽선 스타일 -->
/// <utk:UTKPanel title="외곽선" variant="Outlined" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKPanel : VisualElement, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/Modal/UTKPanel";
#endregion
#region Fields
private bool _disposed;
private VisualElement? _header;
private Label? _titleLabel;
private VisualElement? _headerActions;
private VisualElement? _content;
private VisualElement? _footer;
private string _title = "";
private bool _showHeader = true;
private bool _showFooter;
private PanelVariant _variant = PanelVariant.Default;
private bool _isCollapsible;
private bool _isCollapsed;
#endregion
#region Events
/// <summary>접힘 상태 변경 이벤트</summary>
public event Action<bool>? OnCollapsedChanged;
#endregion
#region Properties
/// <summary>패널 제목</summary>
[UxmlAttribute("title")]
public string Title
{
get => _title;
set
{
_title = value;
if (_titleLabel != null) _titleLabel.text = value;
}
}
/// <summary>헤더 표시 여부</summary>
[UxmlAttribute("show-header")]
public bool ShowHeader
{
get => _showHeader;
set
{
_showHeader = value;
if (_header != null)
{
_header.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
}
}
}
/// <summary>푸터 표시 여부</summary>
[UxmlAttribute("show-footer")]
public bool ShowFooter
{
get => _showFooter;
set
{
_showFooter = value;
if (_footer != null)
{
_footer.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
}
}
}
/// <summary>패널 스타일</summary>
[UxmlAttribute("variant")]
public PanelVariant Variant
{
get => _variant;
set
{
_variant = value;
UpdateVariant();
}
}
/// <summary>접기 가능 여부</summary>
[UxmlAttribute("is-collapsible")]
public bool IsCollapsible
{
get => _isCollapsible;
set
{
_isCollapsible = value;
EnableInClassList("utk-panel--collapsible", value);
}
}
/// <summary>접힘 상태</summary>
[UxmlAttribute("is-collapsed")]
public bool IsCollapsed
{
get => _isCollapsed;
set => SetCollapsed(value, true);
}
/// <summary>
/// UXML에서 자식 요소가 추가될 컨테이너.
/// 이 속성을 오버라이드하여 자식 요소들이 _content 영역에 추가되도록 합니다.
/// </summary>
public override VisualElement contentContainer => _content ?? this;
/// <summary>푸터 영역</summary>
public VisualElement? FooterContainer => _footer;
/// <summary>헤더 액션 영역</summary>
public VisualElement? HeaderActionsContainer => _headerActions;
#endregion
#region Enums
public enum PanelVariant
{
Default,
Elevated,
Outlined,
Flat
}
#endregion
#region Constructor
public UTKPanel()
{
// 1. 먼저 테마 스타일시트 적용 (변수 정의)
UTKThemeManager.Instance.ApplyThemeToElement(this);
// 2. 컴포넌트 USS 적용 (변수 사용)
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
{
styleSheets.Add(uss);
}
CreateUI();
SetupEvents();
SubscribeToThemeChanges();
// UXML에서 로드될 때 속성이 설정된 후 UI 갱신
RegisterCallback<AttachToPanelEvent>(OnAttachToPanel);
}
public UTKPanel(string title) : this()
{
Title = title;
}
#endregion
#region UI Creation
private void CreateUI()
{
AddToClassList("utk-panel");
// Header
_header = new VisualElement { name = "header" };
_header.AddToClassList("utk-panel__header");
hierarchy.Add(_header);
_titleLabel = new Label { name = "title" };
_titleLabel.AddToClassList("utk-panel__title");
_header.Add(_titleLabel);
_headerActions = new VisualElement { name = "header-actions" };
_headerActions.AddToClassList("utk-panel__header-actions");
_header.Add(_headerActions);
// Content
_content = new VisualElement { name = "content" };
_content.AddToClassList("utk-panel__content");
_content.RegisterCallback<GeometryChangedEvent>(OnContentGeometryChanged);
hierarchy.Add(_content);
// Footer
_footer = new VisualElement { name = "footer" };
_footer.AddToClassList("utk-panel__footer");
_footer.style.display = DisplayStyle.None;
hierarchy.Add(_footer);
UpdateVariant();
}
private void OnContentGeometryChanged(GeometryChangedEvent evt)
{
// Content 영역 내 Label 요소에 스타일 클래스 추가
ApplyContentLabelStyles();
}
private void ApplyContentLabelStyles()
{
if (_content == null) return;
foreach (var child in _content.Children())
{
if (child is Label label && !label.ClassListContains("utk-panel__content-label"))
{
label.AddToClassList("utk-panel__content-label");
}
}
}
private void SetupEvents()
{
_header?.RegisterCallback<ClickEvent>(OnHeaderClick);
}
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
private void OnAttachToPanel(AttachToPanelEvent evt)
{
// UXML 속성이 설정된 후 한 번만 UI 갱신
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanel);
// Title 적용
if (_titleLabel != null)
{
_titleLabel.text = _title;
}
// Header 표시 상태 적용
if (_header != null)
{
_header.style.display = _showHeader ? DisplayStyle.Flex : DisplayStyle.None;
}
// Footer 표시 상태 적용
if (_footer != null)
{
_footer.style.display = _showFooter ? DisplayStyle.Flex : DisplayStyle.None;
}
// Variant 적용
UpdateVariant();
// Collapsible 상태 적용
EnableInClassList("utk-panel--collapsible", _isCollapsible);
// Collapsed 상태 적용
if (_isCollapsed)
{
EnableInClassList("utk-panel--collapsed", true);
if (_content != null)
{
_content.style.display = DisplayStyle.None;
}
}
// UXML 자식 요소 스타일 적용 (지연 실행)
schedule.Execute(ApplyContentLabelStyles);
}
private void OnHeaderClick(ClickEvent evt)
{
if (!_isCollapsible) return;
SetCollapsed(!_isCollapsed, true);
evt.StopPropagation();
}
#endregion
#region Methods
/// <summary>
/// 접힘 상태 설정
/// </summary>
public void SetCollapsed(bool collapsed, bool notify)
{
if (_isCollapsed == collapsed) return;
_isCollapsed = collapsed;
EnableInClassList("utk-panel--collapsed", collapsed);
if (_content != null)
{
_content.style.display = collapsed ? DisplayStyle.None : DisplayStyle.Flex;
}
if (_footer != null && _showFooter)
{
_footer.style.display = collapsed ? DisplayStyle.None : DisplayStyle.Flex;
}
if (notify)
{
OnCollapsedChanged?.Invoke(collapsed);
}
}
private void UpdateVariant()
{
RemoveFromClassList("utk-panel--default");
RemoveFromClassList("utk-panel--elevated");
RemoveFromClassList("utk-panel--outlined");
RemoveFromClassList("utk-panel--flat");
var variantClass = _variant switch
{
PanelVariant.Elevated => "utk-panel--elevated",
PanelVariant.Outlined => "utk-panel--outlined",
PanelVariant.Flat => "utk-panel--flat",
_ => "utk-panel--default"
};
AddToClassList(variantClass);
}
/// <summary>
/// 헤더 액션 추가
/// </summary>
public void AddHeaderAction(VisualElement element)
{
_headerActions?.Add(element);
}
/// <summary>
/// 푸터에 요소 추가
/// </summary>
public void AddToFooter(VisualElement element)
{
_footer?.Add(element);
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
OnCollapsedChanged = null;
}
#endregion
}
}