Files

432 lines
14 KiB
C#

#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 카드 컴포넌트.
/// 콘텐츠를 카드 형태로 그룹화하여 표시하는 컨테이너입니다.
/// </summary>
/// <remarks>
/// <para><b>Card(카드)란?</b></para>
/// <para>
/// Card는 관련 정보를 시각적으로 그룹화하여 표시하는 UI 컴포넌트입니다.
/// 이미지, 제목, 설명, 액션 버튼 등을 하나의 단위로 묶어 표현합니다.
/// 상품 목록, 게시글, 프로필, 대시보드 위젯 등에서 널리 사용됩니다.
/// </para>
///
/// <para><b>카드 스타일 (CardVariant):</b></para>
/// <list type="bullet">
/// <item><description><c>Elevated</c> - 그림자가 있는 떠 있는 스타일 (기본값)</description></item>
/// <item><description><c>Outlined</c> - 테두리만 있는 평면 스타일</description></item>
/// <item><description><c>Filled</c> - 배경색으로 채워진 스타일</description></item>
/// </list>
///
/// <para><b>카드 구성 요소:</b></para>
/// <list type="bullet">
/// <item><description>이미지 영역 - 상단 이미지/썸네일</description></item>
/// <item><description>헤더 - 제목과 부제목</description></item>
/// <item><description>콘텐츠 - 본문 내용 (Add()로 추가)</description></item>
/// <item><description>액션 - 하단 버튼 영역 (AddAction()으로 추가)</description></item>
/// </list>
///
/// <para><b>클릭 가능 카드:</b></para>
/// <para>
/// <c>IsClickable = true</c>로 설정하면 전체 카드가 클릭 가능해집니다.
/// 호버 효과, 키보드 포커스, Enter/Space 키 지원이 활성화됩니다.
/// </para>
///
/// <para><b>실제 활용 예시:</b></para>
/// <list type="bullet">
/// <item><description>상품 목록 - 이미지, 이름, 가격, 구매 버튼</description></item>
/// <item><description>소셜 피드 - 프로필, 게시글 내용, 좋아요/댓글 버튼</description></item>
/// <item><description>대시보드 - 통계 카드, KPI 표시</description></item>
/// <item><description>설정 화면 - 설정 그룹 카드</description></item>
/// </list>
/// </remarks>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 카드
/// 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("카드 클릭");
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- 기본 카드 -->
/// <utk:UTKCard title="제목" subtitle="부제목" variant="Elevated">
/// <ui:Label text="카드 내용" />
/// </utk:UTKCard>
///
/// <!-- 클릭 가능 카드 -->
/// <utk:UTKCard title="클릭해보세요" is-clickable="true" />
///
/// <!-- 외곽선 카드 -->
/// <utk:UTKCard title="외곽선" variant="Outlined" />
/// </ui:UXML>
/// </code>
/// </example>
[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
/// <summary>카드 클릭 이벤트</summary>
public event Action? OnClicked;
#endregion
#region Properties
/// <summary>카드 제목</summary>
[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();
}
}
/// <summary>카드 부제목</summary>
[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();
}
}
/// <summary>카드 이미지</summary>
public Texture2D? Image
{
get => _image;
set
{
_image = value;
UpdateImage();
}
}
/// <summary>카드 스타일</summary>
[UxmlAttribute("variant")]
public CardVariant Variant
{
get => _variant;
set
{
_variant = value;
UpdateVariant();
}
}
/// <summary>클릭 가능 여부</summary>
[UxmlAttribute("is-clickable")]
public bool IsClickable
{
get => _isClickable;
set
{
_isClickable = value;
EnableInClassList("utk-card--clickable", value);
focusable = value;
}
}
/// <summary>콘텐츠 컨테이너</summary>
public VisualElement? ContentContainer => _content;
/// <summary>액션 컨테이너</summary>
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<StyleSheet>(USS_PATH);
if (uss != null)
{
styleSheets.Add(uss);
}
CreateUI();
SetupEvents();
SubscribeToThemeChanges();
// UXML에서 로드될 때 속성이 설정된 후 UI 갱신
// Unity 6의 소스 생성기는 Deserialize에서 필드에 직접 값을 할당하므로
// AttachToPanelEvent를 사용하여 패널에 연결된 후 UI를 갱신
RegisterCallback<AttachToPanelEvent>(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<ClickEvent>(OnClick);
RegisterCallback<KeyDownEvent>(OnKeyDown, TrickleDown.TrickleDown);
}
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);
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;
}
/// <summary>
/// 콘텐츠 추가
/// </summary>
public new void Add(VisualElement element)
{
_content?.Add(element);
}
/// <summary>
/// 액션 버튼 추가
/// </summary>
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;
}
/// <summary>
/// 액션 영역 표시/숨김
/// </summary>
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<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
OnClicked = null;
}
#endregion
}
}