#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
///
/// 카드 컴포넌트.
/// 콘텐츠를 카드 형태로 그룹화하여 표시하는 컨테이너입니다.
///
///
/// Card(카드)란?
///
/// Card는 관련 정보를 시각적으로 그룹화하여 표시하는 UI 컴포넌트입니다.
/// 이미지, 제목, 설명, 액션 버튼 등을 하나의 단위로 묶어 표현합니다.
/// 상품 목록, 게시글, 프로필, 대시보드 위젯 등에서 널리 사용됩니다.
///
///
/// 카드 스타일 (CardVariant):
///
/// - Elevated - 그림자가 있는 떠 있는 스타일 (기본값)
/// - Outlined - 테두리만 있는 평면 스타일
/// - Filled - 배경색으로 채워진 스타일
///
///
/// 카드 구성 요소:
///
/// - 이미지 영역 - 상단 이미지/썸네일
/// - 헤더 - 제목과 부제목
/// - 콘텐츠 - 본문 내용 (Add()로 추가)
/// - 액션 - 하단 버튼 영역 (AddAction()으로 추가)
///
///
/// 클릭 가능 카드:
///
/// IsClickable = true로 설정하면 전체 카드가 클릭 가능해집니다.
/// 호버 효과, 키보드 포커스, Enter/Space 키 지원이 활성화됩니다.
///
///
/// 실제 활용 예시:
///
/// - 상품 목록 - 이미지, 이름, 가격, 구매 버튼
/// - 소셜 피드 - 프로필, 게시글 내용, 좋아요/댓글 버튼
/// - 대시보드 - 통계 카드, KPI 표시
/// - 설정 화면 - 설정 그룹 카드
///
///
///
/// 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, TrickleDown.TrickleDown);
}
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);
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;
UnregisterCallback(OnAttachToPanelForTheme);
UnregisterCallback(OnDetachFromPanelForTheme);
OnClicked = null;
}
#endregion
}
}