#nullable enable using System; using UnityEngine; using UnityEngine.UIElements; namespace UVC.UIToolkit { /// /// 카드 컴포넌트. /// 콘텐츠를 카드 형태로 표시합니다. /// /// /// C# 코드에서 사용: /// /// // 기본 카드 /// var card = new UTKCard(); /// card.Title = "카드 제목"; /// card.Subtitle = "부제목"; /// card.Variant = UTKCard.CardVariant.Elevated; /// /// // 이미지 설정 /// card.SetImage(myTexture); /// /// // 콘텐츠 추가 /// card.AddContent(new Label("카드 내용")); /// /// // 액션 버튼 추가 /// card.AddActionButton("자세히", () => Debug.Log("클릭")); /// /// // 클릭 가능 카드 /// card.IsClickable = true; /// card.OnClicked += () => Debug.Log("카드 클릭"); /// /// UXML에서 사용: /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// [UxmlElement] public partial class UTKCard : VisualElement, IDisposable { #region Constants private const string USS_PATH = "UIToolkit/Card/UTKCard"; #endregion #region Fields private bool _disposed; private VisualElement? _imageContainer; private VisualElement? _header; private Label? _titleLabel; private Label? _subtitleLabel; private VisualElement? _content; private VisualElement? _actions; private string _title = ""; private string _subtitle = ""; private Texture2D? _image; private CardVariant _variant = CardVariant.Elevated; private bool _isClickable; #endregion #region Events /// 카드 클릭 이벤트 public event Action? OnClicked; #endregion #region Properties /// 카드 제목 [UxmlAttribute("title")] public string Title { get => _title; set { _title = value; if (_titleLabel != null) { _titleLabel.text = value; _titleLabel.style.display = string.IsNullOrEmpty(value) ? DisplayStyle.None : DisplayStyle.Flex; } UpdateHeaderVisibility(); } } /// 카드 부제목 [UxmlAttribute("subtitle")] public string Subtitle { get => _subtitle; set { _subtitle = value; if (_subtitleLabel != null) { _subtitleLabel.text = value; _subtitleLabel.style.display = string.IsNullOrEmpty(value) ? DisplayStyle.None : DisplayStyle.Flex; } UpdateHeaderVisibility(); } } /// 카드 이미지 public Texture2D? Image { get => _image; set { _image = value; UpdateImage(); } } /// 카드 스타일 [UxmlAttribute("variant")] public CardVariant Variant { get => _variant; set { _variant = value; UpdateVariant(); } } /// 클릭 가능 여부 [UxmlAttribute("is-clickable")] public bool IsClickable { get => _isClickable; set { _isClickable = value; EnableInClassList("utk-card--clickable", value); focusable = value; } } /// 콘텐츠 컨테이너 public VisualElement? ContentContainer => _content; /// 액션 컨테이너 public VisualElement? ActionsContainer => _actions; #endregion #region Enums public enum CardVariant { Elevated, Outlined, Filled } #endregion #region Constructor public UTKCard() { UTKThemeManager.Instance.ApplyThemeToElement(this); var uss = Resources.Load(USS_PATH); if (uss != null) { styleSheets.Add(uss); } CreateUI(); SetupEvents(); SubscribeToThemeChanges(); // UXML에서 로드될 때 속성이 설정된 후 UI 갱신 // Unity 6의 소스 생성기는 Deserialize에서 필드에 직접 값을 할당하므로 // AttachToPanelEvent를 사용하여 패널에 연결된 후 UI를 갱신 RegisterCallback(OnAttachToPanel); } public UTKCard(string title, string subtitle = "") : this() { Title = title; Subtitle = subtitle; } #endregion #region UI Creation private void CreateUI() { AddToClassList("utk-card"); // Image Container _imageContainer = new VisualElement { name = "image-container" }; _imageContainer.AddToClassList("utk-card__image"); _imageContainer.style.display = DisplayStyle.None; hierarchy.Add(_imageContainer); // Header _header = new VisualElement { name = "header" }; _header.AddToClassList("utk-card__header"); hierarchy.Add(_header); _titleLabel = new Label { name = "title" }; _titleLabel.AddToClassList("utk-card__title"); _titleLabel.style.display = DisplayStyle.None; _header.Add(_titleLabel); _subtitleLabel = new Label { name = "subtitle" }; _subtitleLabel.AddToClassList("utk-card__subtitle"); _subtitleLabel.style.display = DisplayStyle.None; _header.Add(_subtitleLabel); // Content _content = new VisualElement { name = "content" }; _content.AddToClassList("utk-card__content"); hierarchy.Add(_content); // Actions _actions = new VisualElement { name = "actions" }; _actions.AddToClassList("utk-card__actions"); _actions.style.display = DisplayStyle.None; // 기본적으로 숨김 hierarchy.Add(_actions); UpdateVariant(); UpdateHeaderVisibility(); } private void SetupEvents() { RegisterCallback(OnClick); RegisterCallback(OnKeyDown); } private void SubscribeToThemeChanges() { UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged; RegisterCallback(_ => { 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); if (_titleLabel != null) { _titleLabel.text = _title; _titleLabel.style.display = string.IsNullOrEmpty(_title) ? DisplayStyle.None : DisplayStyle.Flex; } if (_subtitleLabel != null) { _subtitleLabel.text = _subtitle; _subtitleLabel.style.display = string.IsNullOrEmpty(_subtitle) ? DisplayStyle.None : DisplayStyle.Flex; } UpdateVariant(); UpdateHeaderVisibility(); // IsClickable 상태 적용 EnableInClassList("utk-card--clickable", _isClickable); focusable = _isClickable; } private void OnClick(ClickEvent evt) { if (!_isClickable) return; OnClicked?.Invoke(); evt.StopPropagation(); } private void OnKeyDown(KeyDownEvent evt) { if (!_isClickable) return; if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.Space) { OnClicked?.Invoke(); evt.StopPropagation(); } } #endregion #region Methods private void UpdateVariant() { RemoveFromClassList("utk-card--elevated"); RemoveFromClassList("utk-card--outlined"); RemoveFromClassList("utk-card--filled"); var variantClass = _variant switch { CardVariant.Outlined => "utk-card--outlined", CardVariant.Filled => "utk-card--filled", _ => "utk-card--elevated" }; AddToClassList(variantClass); } private void UpdateImage() { if (_imageContainer == null) return; if (_image != null) { _imageContainer.style.backgroundImage = new StyleBackground(_image); _imageContainer.style.display = DisplayStyle.Flex; } else { _imageContainer.style.backgroundImage = StyleKeyword.None; _imageContainer.style.display = DisplayStyle.None; } } private void UpdateHeaderVisibility() { if (_header == null) return; bool hasContent = !string.IsNullOrEmpty(_title) || !string.IsNullOrEmpty(_subtitle); _header.style.display = hasContent ? DisplayStyle.Flex : DisplayStyle.None; } /// /// 콘텐츠 추가 /// public new void Add(VisualElement element) { _content?.Add(element); } /// /// 액션 버튼 추가 /// public void AddAction(VisualElement element) { _actions?.Add(element); UpdateActionsVisibility(); } private void UpdateActionsVisibility() { if (_actions == null) return; _actions.style.display = _actions.childCount > 0 ? DisplayStyle.Flex : DisplayStyle.None; } /// /// 액션 영역 표시/숨김 /// public void SetActionsVisible(bool visible) { if (_actions != null) { _actions.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None; } } #endregion #region IDisposable public void Dispose() { if (_disposed) return; _disposed = true; UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged; OnClicked = null; } #endregion } }