2026-01-08 20:15:57 +09:00
|
|
|
#nullable enable
|
|
|
|
|
using System;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
using UnityEngine.UIElements;
|
|
|
|
|
|
|
|
|
|
namespace UVC.UIToolkit
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 패널 컨테이너 컴포넌트.
|
|
|
|
|
/// 콘텐츠를 그룹화하고 시각적으로 구분합니다.
|
|
|
|
|
/// </summary>
|
2026-01-13 20:39:45 +09:00
|
|
|
/// <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">
|
|
|
|
|
/// <!-- 기본 패널 -->
|
2026-01-20 20:18:47 +09:00
|
|
|
/// <utk:UTKPanel title="제목">
|
2026-01-13 20:39:45 +09:00
|
|
|
/// <ui:Label text="패널 내용" />
|
|
|
|
|
/// </utk:UTKPanel>
|
2026-01-20 20:18:47 +09:00
|
|
|
///
|
2026-01-13 20:39:45 +09:00
|
|
|
/// <!-- 접을 수 있는 패널 -->
|
2026-01-20 20:18:47 +09:00
|
|
|
/// <utk:UTKPanel title="고급 설정" is-collapsible="true">
|
2026-01-13 20:39:45 +09:00
|
|
|
/// <ui:Label text="내용" />
|
|
|
|
|
/// </utk:UTKPanel>
|
2026-01-20 20:18:47 +09:00
|
|
|
///
|
2026-01-13 20:39:45 +09:00
|
|
|
/// <!-- 외곽선 스타일 -->
|
2026-01-20 20:18:47 +09:00
|
|
|
/// <utk:UTKPanel title="외곽선" variant="Outlined" />
|
2026-01-13 20:39:45 +09:00
|
|
|
/// </ui:UXML>
|
|
|
|
|
/// </code>
|
|
|
|
|
/// </example>
|
2026-01-08 20:15:57 +09:00
|
|
|
[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>
|
2026-01-20 20:18:47 +09:00
|
|
|
[UxmlAttribute("title")]
|
2026-01-08 20:15:57 +09:00
|
|
|
public string Title
|
|
|
|
|
{
|
|
|
|
|
get => _title;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
_title = value;
|
|
|
|
|
if (_titleLabel != null) _titleLabel.text = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>헤더 표시 여부</summary>
|
2026-01-20 20:18:47 +09:00
|
|
|
[UxmlAttribute("show-header")]
|
2026-01-08 20:15:57 +09:00
|
|
|
public bool ShowHeader
|
|
|
|
|
{
|
|
|
|
|
get => _showHeader;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
_showHeader = value;
|
|
|
|
|
if (_header != null)
|
|
|
|
|
{
|
|
|
|
|
_header.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>푸터 표시 여부</summary>
|
2026-01-20 20:18:47 +09:00
|
|
|
[UxmlAttribute("show-footer")]
|
2026-01-08 20:15:57 +09:00
|
|
|
public bool ShowFooter
|
|
|
|
|
{
|
|
|
|
|
get => _showFooter;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
_showFooter = value;
|
|
|
|
|
if (_footer != null)
|
|
|
|
|
{
|
|
|
|
|
_footer.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>패널 스타일</summary>
|
2026-01-20 20:18:47 +09:00
|
|
|
[UxmlAttribute("variant")]
|
2026-01-08 20:15:57 +09:00
|
|
|
public PanelVariant Variant
|
|
|
|
|
{
|
|
|
|
|
get => _variant;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
_variant = value;
|
|
|
|
|
UpdateVariant();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>접기 가능 여부</summary>
|
2026-01-20 20:18:47 +09:00
|
|
|
[UxmlAttribute("is-collapsible")]
|
2026-01-08 20:15:57 +09:00
|
|
|
public bool IsCollapsible
|
|
|
|
|
{
|
|
|
|
|
get => _isCollapsible;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
_isCollapsible = value;
|
|
|
|
|
EnableInClassList("utk-panel--collapsible", value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>접힘 상태</summary>
|
2026-01-20 20:18:47 +09:00
|
|
|
[UxmlAttribute("is-collapsed")]
|
2026-01-08 20:15:57 +09:00
|
|
|
public bool IsCollapsed
|
|
|
|
|
{
|
|
|
|
|
get => _isCollapsed;
|
|
|
|
|
set => SetCollapsed(value, true);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 20:18:47 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// UXML에서 자식 요소가 추가될 컨테이너.
|
|
|
|
|
/// 이 속성을 오버라이드하여 자식 요소들이 _content 영역에 추가되도록 합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public override VisualElement contentContainer => _content ?? this;
|
2026-01-08 20:15:57 +09:00
|
|
|
|
|
|
|
|
/// <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()
|
|
|
|
|
{
|
2026-01-20 20:18:47 +09:00
|
|
|
// 1. 먼저 테마 스타일시트 적용 (변수 정의)
|
2026-01-08 20:15:57 +09:00
|
|
|
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
|
|
|
|
|
2026-01-20 20:18:47 +09:00
|
|
|
// 2. 컴포넌트 USS 적용 (변수 사용)
|
2026-01-08 20:15:57 +09:00
|
|
|
var uss = Resources.Load<StyleSheet>(USS_PATH);
|
|
|
|
|
if (uss != null)
|
|
|
|
|
{
|
|
|
|
|
styleSheets.Add(uss);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CreateUI();
|
|
|
|
|
SetupEvents();
|
|
|
|
|
SubscribeToThemeChanges();
|
2026-01-20 20:18:47 +09:00
|
|
|
|
|
|
|
|
// UXML에서 로드될 때 속성이 설정된 후 UI 갱신
|
|
|
|
|
RegisterCallback<AttachToPanelEvent>(OnAttachToPanel);
|
2026-01-08 20:15:57 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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");
|
2026-01-20 20:18:47 +09:00
|
|
|
_content.RegisterCallback<GeometryChangedEvent>(OnContentGeometryChanged);
|
2026-01-08 20:15:57 +09:00
|
|
|
hierarchy.Add(_content);
|
|
|
|
|
|
|
|
|
|
// Footer
|
|
|
|
|
_footer = new VisualElement { name = "footer" };
|
|
|
|
|
_footer.AddToClassList("utk-panel__footer");
|
|
|
|
|
_footer.style.display = DisplayStyle.None;
|
|
|
|
|
hierarchy.Add(_footer);
|
|
|
|
|
|
|
|
|
|
UpdateVariant();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 20:18:47 +09:00
|
|
|
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");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 20:15:57 +09:00
|
|
|
private void SetupEvents()
|
|
|
|
|
{
|
|
|
|
|
_header?.RegisterCallback<ClickEvent>(OnHeaderClick);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SubscribeToThemeChanges()
|
|
|
|
|
{
|
|
|
|
|
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
|
|
|
|
|
RegisterCallback<DetachFromPanelEvent>(_ =>
|
|
|
|
|
{
|
|
|
|
|
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnThemeChanged(UTKTheme theme)
|
|
|
|
|
{
|
|
|
|
|
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Event Handlers
|
2026-01-20 20:18:47 +09:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 20:15:57 +09:00
|
|
|
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;
|
|
|
|
|
OnCollapsedChanged = null;
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
}
|
|
|
|
|
}
|