Files
XRLib/Assets/Scripts/UVC/UIToolkit/Label/UTKLabel.cs
2026-01-20 20:18:47 +09:00

797 lines
26 KiB
C#

#nullable enable
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 텍스트 라벨 컴포넌트.
/// 다양한 스타일과 크기의 텍스트를 표시합니다.
/// Material Icon 또는 Image Icon을 텍스트와 함께 또는 단독으로 표시할 수 있습니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 기본 라벨
/// var label = new UTKLabel();
/// label.Text = "안녕하세요";
/// label.Size = UTKLabel.LabelSize.Body1;
/// label.Variant = UTKLabel.LabelVariant.Primary;
///
/// // 제목 스타일
/// var title = new UTKLabel();
/// title.Text = "제목";
/// title.Size = UTKLabel.LabelSize.Heading1;
/// title.IsBold = true;
///
/// // 스타일 적용
/// label.IsItalic = true;
/// label.TextAlign = UTKLabel.TextAlign.Center;
///
/// // Material Icon과 텍스트 함께 사용
/// var iconLabel = new UTKLabel("설정", UTKMaterialIcons.Settings);
///
/// // Image Icon과 텍스트 함께 사용
/// var imgLabel = new UTKLabel("닫기", UTKImageIcons.BtnClose16, isImageIcon: true);
///
/// // Material Icon만 사용
/// var iconOnly = new UTKLabel(UTKMaterialIcons.Home);
///
/// // Image Icon만 사용
/// var imgOnly = new UTKLabel(UTKImageIcons.IconSetting22, isImageIcon: true);
///
/// // 메서드로 아이콘 설정
/// label.SetMaterialIcon(UTKMaterialIcons.Search);
/// label.SetImageIcon(UTKImageIcons.IconSetting22);
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- 기본 라벨 -->
/// <utk:UTKLabel Text="일반 텍스트" />
///
/// <!-- 제목 -->
/// <utk:UTKLabel Text="제목" Size="H1" IsBold="true" />
///
/// <!-- 보조 텍스트 -->
/// <utk:UTKLabel Text="설명" Size="Caption" Variant="Secondary" />
///
/// <!-- Material Icon과 텍스트 -->
/// <utk:UTKLabel Text="설정" MaterialIcon="settings" />
///
/// <!-- Image Icon과 텍스트 -->
/// <utk:UTKLabel Text="닫기" ImageIcon="btn_close_16" />
///
/// <!-- 아이콘만 (Material) -->
/// <utk:UTKLabel MaterialIcon="home" IconSize="24" />
///
/// <!-- 아이콘만 (Image) -->
/// <utk:UTKLabel ImageIcon="icon_setting_22" IconSize="22" />
///
/// <!-- 아이콘 오른쪽 배치 -->
/// <utk:UTKLabel Text="다음" MaterialIcon="arrow_forward" IconPlacement="Right" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKLabel : VisualElement, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/Label/UTKLabel";
#endregion
#region Fields
private bool _disposed;
private Label? _label;
private Label? _iconLabel;
private VisualElement? _imageIcon;
private string _text = "";
private LabelSize _size = LabelSize.Body1;
private LabelVariant _variant = LabelVariant.Primary;
private bool _isBold;
private bool _isItalic;
private TextAlign _textAlign = TextAlign.Left;
private bool _isSelectable;
private IconPosition _iconPosition = IconPosition.Left;
private int _iconSize;
private int _gap = 4;
private string _materialIconName = "";
private string _imageIconName = "";
#endregion
#region Properties
/// <summary>텍스트</summary>
[UxmlAttribute("text")]
public string Text
{
get => _text;
set
{
_text = value;
if (_label != null) _label.text = value;
}
}
/// <summary>텍스트 크기</summary>
[UxmlAttribute("size")]
public LabelSize Size
{
get => _size;
set
{
_size = value;
UpdateSize();
}
}
/// <summary>텍스트 변형</summary>
[UxmlAttribute("variant")]
public LabelVariant Variant
{
get => _variant;
set
{
_variant = value;
UpdateVariant();
}
}
/// <summary>굵은 글꼴</summary>
[UxmlAttribute("is-bold")]
public bool IsBold
{
get => _isBold;
set
{
_isBold = value;
EnableInClassList("utk-label--bold", value);
}
}
/// <summary>기울임 글꼴</summary>
[UxmlAttribute("is-italic")]
public bool IsItalic
{
get => _isItalic;
set
{
_isItalic = value;
EnableInClassList("utk-label--italic", value);
}
}
/// <summary>텍스트 정렬</summary>
[UxmlAttribute("text-alignment")]
public TextAlign TextAlignment
{
get => _textAlign;
set
{
_textAlign = value;
UpdateTextAlign();
}
}
/// <summary>텍스트 선택 가능 여부</summary>
[UxmlAttribute("is-selectable")]
public bool IsSelectable
{
get => _isSelectable;
set
{
_isSelectable = value;
if (_label != null)
{
_label.selection.isSelectable = value;
}
}
}
/// <summary>아이콘 위치 (텍스트 기준 왼쪽/오른쪽)</summary>
[UxmlAttribute("icon-placement")]
public IconPosition IconPlacement
{
get => _iconPosition;
set
{
_iconPosition = value;
UpdateIconPosition();
}
}
/// <summary>아이콘 크기 (0이면 텍스트 크기에 맞춤)</summary>
[UxmlAttribute("icon-size")]
public int IconSize
{
get => _iconSize;
set
{
_iconSize = value;
UpdateIconSize();
}
}
/// <summary>아이콘과 텍스트 사이 간격 (px)</summary>
[UxmlAttribute("gap")]
public int Gap
{
get => _gap;
set
{
_gap = value;
UpdateGap();
}
}
/// <summary>Material Icon 이름 (예: "settings", "home")</summary>
[UxmlAttribute("material-icon")]
public string MaterialIcon
{
get => _materialIconName;
set
{
_materialIconName = value;
if (!string.IsNullOrEmpty(value))
{
SetMaterialIconByName(value);
}
else
{
ClearIcon();
}
}
}
/// <summary>Image Icon 이름 (예: "icon_setting_22", "btn_close_16")</summary>
[UxmlAttribute("image-icon")]
public string ImageIcon
{
get => _imageIconName;
set
{
_imageIconName = value;
if (!string.IsNullOrEmpty(value))
{
SetImageIconByName(value);
}
else
{
ClearIcon();
}
}
}
#endregion
#region Enums
public enum LabelSize
{
H1,
H2,
H3,
Body1,
Body2,
Label1,
Label2,
Label3,
Caption
}
public enum LabelVariant
{
Primary,
Secondary,
Disabled,
Success,
Warning,
Error,
Info
}
public enum TextAlign
{
Left,
Center,
Right
}
public enum IconPosition
{
Left,
Right
}
#endregion
#region Constructor
public UTKLabel()
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
{
styleSheets.Add(uss);
}
CreateUI();
SubscribeToThemeChanges();
// UXML에서 로드될 때 속성이 설정된 후 UI 갱신
// Unity 6의 소스 생성기는 Deserialize에서 필드에 직접 값을 할당하므로
// AttachToPanelEvent를 사용하여 패널에 연결된 후 UI를 갱신
RegisterCallback<AttachToPanelEvent>(OnAttachToPanel);
}
/// <summary>
/// 텍스트와 크기를 지정하여 라벨을 생성합니다.
/// </summary>
/// <param name="text">표시할 텍스트</param>
/// <param name="size">텍스트 크기</param>
public UTKLabel(string text, LabelSize size = LabelSize.Body1) : this()
{
Text = text;
Size = size;
}
/// <summary>
/// Material Icon과 텍스트를 함께 표시하는 라벨을 생성합니다.
/// </summary>
/// <param name="text">표시할 텍스트</param>
/// <param name="materialIcon">Material Icon 유니코드 문자 (예: UTKMaterialIcons.Settings)</param>
/// <param name="iconPosition">아이콘 위치 (기본: 왼쪽)</param>
/// <param name="size">텍스트 크기</param>
public UTKLabel(string text, string materialIcon, IconPosition iconPosition = IconPosition.Left, LabelSize size = LabelSize.Body1) : this()
{
Text = text;
Size = size;
IconPlacement = iconPosition;
SetMaterialIcon(materialIcon);
}
/// <summary>
/// Image Icon과 텍스트를 함께 표시하는 라벨을 생성합니다.
/// </summary>
/// <param name="text">표시할 텍스트</param>
/// <param name="imageIconPath">Image Icon 리소스 경로 (예: UTKImageIcons.IconSetting22)</param>
/// <param name="isImageIcon">true로 설정해야 합니다 (Image Icon 생성자 구분용)</param>
/// <param name="iconPosition">아이콘 위치 (기본: 왼쪽)</param>
/// <param name="size">텍스트 크기</param>
public UTKLabel(string text, string imageIconPath, bool isImageIcon, IconPosition iconPosition = IconPosition.Left, LabelSize size = LabelSize.Body1) : this()
{
Text = text;
Size = size;
IconPlacement = iconPosition;
if (isImageIcon)
{
SetImageIcon(imageIconPath);
}
else
{
SetMaterialIcon(imageIconPath);
}
}
/// <summary>
/// Material Icon만 표시하는 라벨을 생성합니다.
/// </summary>
/// <param name="materialIcon">Material Icon 유니코드 문자 (예: UTKMaterialIcons.Home)</param>
/// <param name="iconSize">아이콘 크기 (0이면 기본 크기)</param>
public UTKLabel(string materialIcon, int iconSize = 0) : this()
{
IconSize = iconSize;
SetMaterialIcon(materialIcon);
}
/// <summary>
/// Image Icon만 표시하는 라벨을 생성합니다.
/// </summary>
/// <param name="imageIconPath">Image Icon 리소스 경로 (예: UTKImageIcons.IconSetting22)</param>
/// <param name="isImageIcon">true로 설정해야 합니다 (Image Icon 생성자 구분용)</param>
/// <param name="iconSize">아이콘 크기 (0이면 기본 크기)</param>
public UTKLabel(string imageIconPath, bool isImageIcon, int iconSize = 0) : this()
{
IconSize = iconSize;
if (isImageIcon)
{
SetImageIcon(imageIconPath);
}
else
{
SetMaterialIcon(imageIconPath);
}
}
#endregion
#region UI Creation
private void CreateUI()
{
AddToClassList("utk-label");
_label = new Label { name = "label" };
_label.AddToClassList("utk-label__text");
Add(_label);
UpdateSize();
UpdateVariant();
UpdateTextAlign();
}
private void SubscribeToThemeChanges()
{
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
RegisterCallback<DetachFromPanelEvent>(_ =>
{
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
});
}
private void OnThemeChanged(UTKTheme theme)
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
}
private void OnAttachToPanel(AttachToPanelEvent evt)
{
// UXML 속성이 설정된 후 한 번만 UI 갱신
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanel);
if (_label != null) _label.text = _text;
UpdateSize();
UpdateVariant();
UpdateTextAlign();
EnableInClassList("utk-label--bold", _isBold);
EnableInClassList("utk-label--italic", _isItalic);
// 아이콘 속성이 설정된 경우 적용
if (!string.IsNullOrEmpty(_materialIconName))
{
SetMaterialIconByName(_materialIconName);
}
else if (!string.IsNullOrEmpty(_imageIconName))
{
SetImageIconByName(_imageIconName);
}
}
#endregion
#region Update Methods
private void UpdateSize()
{
RemoveFromClassList("utk-label--h1");
RemoveFromClassList("utk-label--h2");
RemoveFromClassList("utk-label--h3");
RemoveFromClassList("utk-label--body1");
RemoveFromClassList("utk-label--body2");
RemoveFromClassList("utk-label--label1");
RemoveFromClassList("utk-label--label2");
RemoveFromClassList("utk-label--label3");
RemoveFromClassList("utk-label--caption");
var sizeClass = _size switch
{
LabelSize.H1 => "utk-label--h1",
LabelSize.H2 => "utk-label--h2",
LabelSize.H3 => "utk-label--h3",
LabelSize.Body2 => "utk-label--body2",
LabelSize.Label1 => "utk-label--label1",
LabelSize.Label2 => "utk-label--label2",
LabelSize.Label3 => "utk-label--label3",
LabelSize.Caption => "utk-label--caption",
_ => "utk-label--body1"
};
AddToClassList(sizeClass);
}
private void UpdateVariant()
{
RemoveFromClassList("utk-label--primary");
RemoveFromClassList("utk-label--secondary");
RemoveFromClassList("utk-label--disabled");
RemoveFromClassList("utk-label--success");
RemoveFromClassList("utk-label--warning");
RemoveFromClassList("utk-label--error");
RemoveFromClassList("utk-label--info");
var variantClass = _variant switch
{
LabelVariant.Secondary => "utk-label--secondary",
LabelVariant.Disabled => "utk-label--disabled",
LabelVariant.Success => "utk-label--success",
LabelVariant.Warning => "utk-label--warning",
LabelVariant.Error => "utk-label--error",
LabelVariant.Info => "utk-label--info",
_ => "utk-label--primary"
};
AddToClassList(variantClass);
}
private void UpdateTextAlign()
{
RemoveFromClassList("utk-label--left");
RemoveFromClassList("utk-label--center");
RemoveFromClassList("utk-label--right");
var alignClass = _textAlign switch
{
TextAlign.Center => "utk-label--center",
TextAlign.Right => "utk-label--right",
_ => "utk-label--left"
};
AddToClassList(alignClass);
}
private void UpdateIconPosition()
{
if (_iconLabel != null)
{
_iconLabel.SendToBack();
if (_iconPosition == IconPosition.Right)
{
_iconLabel.BringToFront();
}
}
if (_imageIcon != null)
{
_imageIcon.SendToBack();
if (_iconPosition == IconPosition.Right)
{
_imageIcon.BringToFront();
}
}
}
private void UpdateIconSize()
{
var size = GetEffectiveIconSize();
if (_iconLabel != null && _iconLabel.style.display == DisplayStyle.Flex)
{
_iconLabel.style.fontSize = size;
}
if (_imageIcon != null && _imageIcon.style.display == DisplayStyle.Flex)
{
_imageIcon.style.width = size;
_imageIcon.style.height = size;
}
}
private void UpdateGap()
{
// 아이콘과 텍스트 사이의 간격을 적용
if (_iconLabel != null)
{
_iconLabel.style.marginRight = _iconPosition == IconPosition.Left ? _gap : 0;
_iconLabel.style.marginLeft = _iconPosition == IconPosition.Right ? _gap : 0;
}
if (_imageIcon != null)
{
_imageIcon.style.marginRight = _iconPosition == IconPosition.Left ? _gap : 0;
_imageIcon.style.marginLeft = _iconPosition == IconPosition.Right ? _gap : 0;
}
}
private int GetEffectiveIconSize()
{
if (_iconSize > 0) return _iconSize;
return _size switch
{
LabelSize.H1 => 28,
LabelSize.H2 => 24,
LabelSize.H3 => 20,
LabelSize.Body1 => 16,
LabelSize.Body2 => 14,
LabelSize.Label1 => 14,
LabelSize.Label2 => 12,
LabelSize.Label3 => 12,
LabelSize.Caption => 12,
_ => 16
};
}
#endregion
#region Icon Methods
/// <summary>
/// Material Icon을 설정합니다.
/// </summary>
/// <param name="icon">Material Icon 유니코드 문자 (예: UTKMaterialIcons.Settings)</param>
/// <param name="fontSize">아이콘 폰트 크기 (null이면 텍스트 크기에 맞춤)</param>
public void SetMaterialIcon(string icon, int? fontSize = null)
{
ClearImageIcon();
EnsureIconLabel();
if (_iconLabel != null)
{
_iconLabel.text = icon;
_iconLabel.style.display = DisplayStyle.Flex;
UTKMaterialIcons.ApplyIconStyle(_iconLabel, fontSize ?? GetEffectiveIconSize());
}
EnableInClassList("utk-label--has-icon", true);
UpdateIconPosition();
UpdateGap();
}
/// <summary>
/// Material Icon을 비동기로 설정합니다.
/// </summary>
/// <param name="icon">Material Icon 유니코드 문자 (예: UTKMaterialIcons.Settings)</param>
/// <param name="ct">취소 토큰</param>
/// <param name="fontSize">아이콘 폰트 크기 (null이면 텍스트 크기에 맞춤)</param>
public async UniTask SetMaterialIconAsync(string icon, CancellationToken ct = default, int? fontSize = null)
{
ClearImageIcon();
EnsureIconLabel();
if (_iconLabel != null)
{
_iconLabel.text = icon;
_iconLabel.style.display = DisplayStyle.Flex;
await UTKMaterialIcons.ApplyIconStyleAsync(_iconLabel, ct, fontSize ?? GetEffectiveIconSize());
}
EnableInClassList("utk-label--has-icon", true);
UpdateIconPosition();
UpdateGap();
}
/// <summary>
/// 아이콘 이름으로 Material Icon을 설정합니다.
/// </summary>
/// <param name="iconName">아이콘 이름 (예: "settings", "home")</param>
/// <param name="fontSize">아이콘 폰트 크기 (null이면 텍스트 크기에 맞춤)</param>
public void SetMaterialIconByName(string iconName, int? fontSize = null)
{
var iconChar = UTKMaterialIcons.GetIcon(iconName);
if (!string.IsNullOrEmpty(iconChar))
{
SetMaterialIcon(iconChar, fontSize);
}
else
{
Debug.LogWarning($"[UTKLabel] Material icon '{iconName}'을(를) 찾을 수 없습니다.");
}
}
/// <summary>
/// Image Icon을 설정합니다.
/// </summary>
/// <param name="resourcePath">이미지 리소스 경로 (예: UTKImageIcons.IconSetting22)</param>
/// <param name="iconSize">아이콘 크기 (null이면 텍스트 크기에 맞춤)</param>
public void SetImageIcon(string resourcePath, int? iconSize = null)
{
var texture = UTKImageIcons.LoadTexture(resourcePath);
if (texture != null)
{
ApplyImageIcon(texture, iconSize);
}
else
{
Debug.LogWarning($"[UTKLabel] Image icon '{resourcePath}'을(를) 로드할 수 없습니다.");
}
}
/// <summary>
/// Image Icon을 비동기로 설정합니다.
/// </summary>
/// <param name="resourcePath">이미지 리소스 경로 (예: UTKImageIcons.IconSetting22)</param>
/// <param name="ct">취소 토큰</param>
/// <param name="iconSize">아이콘 크기 (null이면 텍스트 크기에 맞춤)</param>
public async UniTask SetImageIconAsync(string resourcePath, CancellationToken ct = default, int? iconSize = null)
{
var texture = await UTKImageIcons.LoadTextureAsync(resourcePath, ct);
if (texture != null)
{
ApplyImageIcon(texture, iconSize);
}
else
{
Debug.LogWarning($"[UTKLabel] Image icon '{resourcePath}'을(를) 로드할 수 없습니다.");
}
}
/// <summary>
/// 아이콘 이름으로 Image Icon을 설정합니다.
/// </summary>
/// <param name="iconName">아이콘 이름 (예: "icon_setting_22", "btn_close_16")</param>
/// <param name="iconSize">아이콘 크기 (null이면 텍스트 크기에 맞춤)</param>
public void SetImageIconByName(string iconName, int? iconSize = null)
{
var texture = UTKImageIcons.LoadTextureByName(iconName);
if (texture != null)
{
ApplyImageIcon(texture, iconSize);
}
else
{
Debug.LogWarning($"[UTKLabel] Image icon '{iconName}'을(를) 찾을 수 없습니다.");
}
}
/// <summary>
/// 모든 아이콘을 제거합니다.
/// </summary>
public void ClearIcon()
{
ClearMaterialIcon();
ClearImageIcon();
EnableInClassList("utk-label--has-icon", false);
}
private void EnsureIconLabel()
{
if (_iconLabel != null) return;
_iconLabel = new Label
{
name = "icon",
pickingMode = PickingMode.Ignore
};
_iconLabel.AddToClassList("utk-label__icon");
_iconLabel.style.display = DisplayStyle.None;
Insert(0, _iconLabel);
}
private void ApplyImageIcon(Texture2D texture, int? iconSize)
{
ClearMaterialIcon();
if (_imageIcon == null)
{
_imageIcon = new VisualElement
{
name = "image-icon",
pickingMode = PickingMode.Ignore
};
_imageIcon.AddToClassList("utk-label__image-icon");
Insert(0, _imageIcon);
}
var size = iconSize ?? GetEffectiveIconSize();
_imageIcon.style.width = size;
_imageIcon.style.height = size;
_imageIcon.style.backgroundImage = new StyleBackground(texture);
_imageIcon.style.display = DisplayStyle.Flex;
EnableInClassList("utk-label--has-icon", true);
UpdateIconPosition();
UpdateGap();
}
private void ClearMaterialIcon()
{
if (_iconLabel != null)
{
_iconLabel.text = "";
_iconLabel.style.display = DisplayStyle.None;
}
}
private void ClearImageIcon()
{
if (_imageIcon != null)
{
_imageIcon.style.backgroundImage = StyleKeyword.None;
_imageIcon.style.display = DisplayStyle.None;
}
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
}
#endregion
}
}