#nullable enable using System; using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.UIElements; namespace UVC.UIToolkit { /// /// 텍스트 라벨 컴포넌트. /// 다양한 스타일과 크기의 텍스트를 표시합니다. /// Material Icon 또는 Image Icon을 텍스트와 함께 또는 단독으로 표시할 수 있습니다. /// /// /// C# 코드에서 사용: /// /// // 기본 라벨 /// 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); /// /// UXML에서 사용: /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// [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 /// 텍스트 [UxmlAttribute] public string Text { get => _text; set { _text = value; if (_label != null) _label.text = value; } } /// 텍스트 크기 [UxmlAttribute] public LabelSize Size { get => _size; set { _size = value; UpdateSize(); } } /// 텍스트 변형 [UxmlAttribute] public LabelVariant Variant { get => _variant; set { _variant = value; UpdateVariant(); } } /// 굵은 글꼴 [UxmlAttribute] public bool IsBold { get => _isBold; set { _isBold = value; EnableInClassList("utk-label--bold", value); } } /// 기울임 글꼴 [UxmlAttribute] public bool IsItalic { get => _isItalic; set { _isItalic = value; EnableInClassList("utk-label--italic", value); } } /// 텍스트 정렬 [UxmlAttribute] public TextAlign TextAlignment { get => _textAlign; set { _textAlign = value; UpdateTextAlign(); } } /// 텍스트 선택 가능 여부 [UxmlAttribute] public bool IsSelectable { get => _isSelectable; set { _isSelectable = value; if (_label != null) { _label.selection.isSelectable = value; } } } /// 아이콘 위치 (텍스트 기준 왼쪽/오른쪽) [UxmlAttribute] public IconPosition IconPlacement { get => _iconPosition; set { _iconPosition = value; UpdateIconPosition(); } } /// 아이콘 크기 (0이면 텍스트 크기에 맞춤) [UxmlAttribute] public int IconSize { get => _iconSize; set { _iconSize = value; UpdateIconSize(); } } /// 아이콘과 텍스트 사이 간격 (px) [UxmlAttribute] public int Gap { get => _gap; set { _gap = value; UpdateGap(); } } /// Material Icon 이름 (예: "settings", "home") [UxmlAttribute] public string MaterialIcon { get => _materialIconName; set { _materialIconName = value; if (!string.IsNullOrEmpty(value)) { SetMaterialIconByName(value); } else { ClearIcon(); } } } /// Image Icon 이름 (예: "icon_setting_22", "btn_close_16") [UxmlAttribute] 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(USS_PATH); if (uss != null) { styleSheets.Add(uss); } CreateUI(); SubscribeToThemeChanges(); // UXML에서 로드될 때 속성이 설정된 후 UI 갱신 RegisterCallback(_ => { if (_label != null) _label.text = _text; UpdateSize(); UpdateVariant(); UpdateTextAlign(); }); } /// /// 텍스트와 크기를 지정하여 라벨을 생성합니다. /// /// 표시할 텍스트 /// 텍스트 크기 public UTKLabel(string text, LabelSize size = LabelSize.Body1) : this() { Text = text; Size = size; } /// /// Material Icon과 텍스트를 함께 표시하는 라벨을 생성합니다. /// /// 표시할 텍스트 /// Material Icon 유니코드 문자 (예: UTKMaterialIcons.Settings) /// 아이콘 위치 (기본: 왼쪽) /// 텍스트 크기 public UTKLabel(string text, string materialIcon, IconPosition iconPosition = IconPosition.Left, LabelSize size = LabelSize.Body1) : this() { Text = text; Size = size; IconPlacement = iconPosition; SetMaterialIcon(materialIcon); } /// /// Image Icon과 텍스트를 함께 표시하는 라벨을 생성합니다. /// /// 표시할 텍스트 /// Image Icon 리소스 경로 (예: UTKImageIcons.IconSetting22) /// true로 설정해야 합니다 (Image Icon 생성자 구분용) /// 아이콘 위치 (기본: 왼쪽) /// 텍스트 크기 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); } } /// /// Material Icon만 표시하는 라벨을 생성합니다. /// /// Material Icon 유니코드 문자 (예: UTKMaterialIcons.Home) /// 아이콘 크기 (0이면 기본 크기) public UTKLabel(string materialIcon, int iconSize = 0) : this() { IconSize = iconSize; SetMaterialIcon(materialIcon); } /// /// Image Icon만 표시하는 라벨을 생성합니다. /// /// Image Icon 리소스 경로 (예: UTKImageIcons.IconSetting22) /// true로 설정해야 합니다 (Image Icon 생성자 구분용) /// 아이콘 크기 (0이면 기본 크기) 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(_ => { UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged; }); } private void OnThemeChanged(UTKTheme theme) { UTKThemeManager.Instance.ApplyThemeToElement(this); } #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 /// /// Material Icon을 설정합니다. /// /// Material Icon 유니코드 문자 (예: UTKMaterialIcons.Settings) /// 아이콘 폰트 크기 (null이면 텍스트 크기에 맞춤) 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(); } /// /// Material Icon을 비동기로 설정합니다. /// /// Material Icon 유니코드 문자 (예: UTKMaterialIcons.Settings) /// 취소 토큰 /// 아이콘 폰트 크기 (null이면 텍스트 크기에 맞춤) 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(); } /// /// 아이콘 이름으로 Material Icon을 설정합니다. /// /// 아이콘 이름 (예: "settings", "home") /// 아이콘 폰트 크기 (null이면 텍스트 크기에 맞춤) 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}'을(를) 찾을 수 없습니다."); } } /// /// Image Icon을 설정합니다. /// /// 이미지 리소스 경로 (예: UTKImageIcons.IconSetting22) /// 아이콘 크기 (null이면 텍스트 크기에 맞춤) 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}'을(를) 로드할 수 없습니다."); } } /// /// Image Icon을 비동기로 설정합니다. /// /// 이미지 리소스 경로 (예: UTKImageIcons.IconSetting22) /// 취소 토큰 /// 아이콘 크기 (null이면 텍스트 크기에 맞춤) 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}'을(를) 로드할 수 없습니다."); } } /// /// 아이콘 이름으로 Image Icon을 설정합니다. /// /// 아이콘 이름 (예: "icon_setting_22", "btn_close_16") /// 아이콘 크기 (null이면 텍스트 크기에 맞춤) 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}'을(를) 찾을 수 없습니다."); } } /// /// 모든 아이콘을 제거합니다. /// 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 } }