456 lines
15 KiB
C#
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
|
|
}
|
|
}
|