#nullable enable using System; using UnityEngine; using UnityEngine.UIElements; namespace UVC.UIToolkit { /// /// 패널 컨테이너 컴포넌트. /// 콘텐츠를 그룹화하고 시각적으로 구분하는 섹션 컨테이너입니다. /// /// /// Panel(패널)이란? /// /// Panel은 관련 UI 요소들을 묶어 하나의 섹션으로 표시하는 컨테이너입니다. /// 설정 페이지, 폼, 대시보드 등에서 내용을 구분하고 정리하는 데 사용됩니다. /// 선택적으로 접기/펼치기 기능을 제공합니다. /// /// /// Panel vs Card 차이: /// /// Panel - 기능적 그룹화, 접기/펼치기, 넓은 영역 /// Card - 시각적 그룹화, 독립적 아이템 단위, 클릭 가능 /// /// /// 패널 스타일 (PanelVariant): /// /// Default - 기본 스타일 /// Elevated - 그림자가 있는 떠 있는 스타일 /// Outlined - 테두리만 있는 스타일 /// Flat - 배경/테두리 없는 평면 스타일 /// /// /// 패널 구성 요소: /// /// 헤더 - 제목과 헤더 액션 버튼 /// 콘텐츠 - 본문 내용 (UXML 자식 또는 Add()로 추가) /// 푸터 - 하단 버튼/정보 영역 (ShowFooter로 표시) /// /// /// 접기/펼치기 기능: /// /// IsCollapsible = true로 설정하면 헤더 클릭으로 패널을 접거나 펼 수 있습니다. /// IsCollapsed 속성으로 현재 접힘 상태를 확인/변경할 수 있습니다. /// /// /// 실제 활용 예시: /// /// 설정 페이지 - "일반", "고급", "보안" 등 설정 그룹 /// 인스펙터 - Transform, Renderer 등 컴포넌트 섹션 /// 대시보드 - 통계, 차트, 목록 등 위젯 영역 /// 폼 - 개인정보, 결제정보 등 입력 그룹 /// /// /// /// C# 코드에서 사용: /// /// // 기본 패널 /// 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; /// /// UXML에서 사용: /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// [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 /// 접힘 상태 변경 이벤트 public event Action? OnCollapsedChanged; #endregion #region Properties /// 패널 제목 [UxmlAttribute("title")] public string Title { get => _title; set { _title = value; if (_titleLabel != null) _titleLabel.text = value; } } /// 헤더 표시 여부 [UxmlAttribute("show-header")] public bool ShowHeader { get => _showHeader; set { _showHeader = value; if (_header != null) { _header.style.display = value ? DisplayStyle.Flex : DisplayStyle.None; } } } /// 푸터 표시 여부 [UxmlAttribute("show-footer")] public bool ShowFooter { get => _showFooter; set { _showFooter = value; if (_footer != null) { _footer.style.display = value ? DisplayStyle.Flex : DisplayStyle.None; } } } /// 패널 스타일 [UxmlAttribute("variant")] public PanelVariant Variant { get => _variant; set { _variant = value; UpdateVariant(); } } /// 접기 가능 여부 [UxmlAttribute("is-collapsible")] public bool IsCollapsible { get => _isCollapsible; set { _isCollapsible = value; EnableInClassList("utk-panel--collapsible", value); } } /// 접힘 상태 [UxmlAttribute("is-collapsed")] public bool IsCollapsed { get => _isCollapsed; set => SetCollapsed(value, true); } /// /// UXML에서 자식 요소가 추가될 컨테이너. /// 이 속성을 오버라이드하여 자식 요소들이 _content 영역에 추가되도록 합니다. /// public override VisualElement contentContainer => _content ?? this; /// 푸터 영역 public VisualElement? FooterContainer => _footer; /// 헤더 액션 영역 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(USS_PATH); if (uss != null) { styleSheets.Add(uss); } CreateUI(); SetupEvents(); SubscribeToThemeChanges(); // UXML에서 로드될 때 속성이 설정된 후 UI 갱신 RegisterCallback(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(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(OnHeaderClick); } private void SubscribeToThemeChanges() { UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged; RegisterCallback(OnAttachToPanelForTheme); RegisterCallback(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(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 /// /// 접힘 상태 설정 /// 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); } /// /// 헤더 액션 추가 /// public void AddHeaderAction(VisualElement element) { _headerActions?.Add(element); } /// /// 푸터에 요소 추가 /// public void AddToFooter(VisualElement element) { _footer?.Add(element); } #endregion #region IDisposable public void Dispose() { if (_disposed) return; _disposed = true; UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged; UnregisterCallback(OnAttachToPanelForTheme); UnregisterCallback(OnDetachFromPanelForTheme); OnCollapsedChanged = null; } #endregion } }