432 lines
14 KiB
C#
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
|
|
}
|
|
}
|