#nullable enable
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
///
/// 커스텀 버튼 컴포넌트.
/// 텍스트와 아이콘을 동시에 표시하거나, 아이콘만 표시할 수 있습니다.
/// 배경 색상, 외곽선 굵기 등을 설정할 수 있습니다.
///
///
/// C# 코드에서 사용:
///
/// // 기본 버튼 생성
/// var btn = new UTKButton("확인");
/// btn.OnClicked += () => Debug.Log("클릭됨!");
///
/// // 텍스트와 Material Icon이 있는 버튼
/// var saveBtn = new UTKButton("저장", UTKMaterialIcons.Save, UTKButton.ButtonVariant.Primary);
///
/// // 텍스트와 아이콘 (크기 지정)
/// var largeIconBtn = new UTKButton("설정", UTKMaterialIcons.Settings, UTKButton.ButtonVariant.Primary, 28);
/// var smallImageBtn = new UTKButton("닫기", UTKImageIcons.BtnClose22, UTKButton.ButtonVariant.Danger, 20);
///
/// // Material Icon 설정
/// // 텍스트와 아이콘 (크기 지정)
/// var largeIconBtn = new UTKButton("설정", UTKMaterialIcons.Settings, UTKButton.ButtonVariant.Primary, 28);
/// var smallImageBtn = new UTKButton("닫기", UTKImageIcons.BtnClose22, UTKButton.ButtonVariant.Danger, 20);
///
/// // Text 버튼 스타일에서 아이콘 크기 지정
/// var textSmallIcon = new UTKButton("Small", UTKMaterialIcons.Settings, UTKButton.ButtonVariant.Text, 16);
/// var textMediumIcon = new UTKButton("Medium", UTKMaterialIcons.Settings, UTKButton.ButtonVariant.Text, 24);
/// var textLargeIcon = new UTKButton("Large", UTKMaterialIcons.Settings, UTKButton.ButtonVariant.Text, 32);
///
/// // Material Icon 설정
/// var imgBtn = new UTKButton("닫기");
/// imgBtn.SetImageIcon(UTKImageIcons.BtnClose22);
/// imgBtn.SetImageIconByName("icon_setting_22");
///
/// // 비동기 아이콘 설정
/// await btn.SetMaterialIconAsync(UTKMaterialIcons.Search);
/// await btn.SetImageIconAsync(UTKImageIcons.IconSetting22);
///
/// // 아이콘만 표시하는 버튼
/// var iconOnlyBtn = new UTKButton { IconOnly = true };
/// iconOnlyBtn.SetMaterialIcon(UTKMaterialIcons.Close);
///
/// // 버튼 스타일 변형
/// btn.Variant = UTKButton.ButtonVariant.Primary;
/// btn.Variant = UTKButton.ButtonVariant.Danger;
/// btn.Variant = UTKButton.ButtonVariant.Ghost;
///
/// // 버튼 크기
/// btn.Size = UTKButton.ButtonSize.Small;
/// btn.Size = UTKButton.ButtonSize.Large;
///
/// // 주의: 생성자에서 icon 파라미터로 아이콘을 전달하면 다음 순서로 타입이 감지됩니다.
/// // 1. UTKMaterialIcons에서 먼저 검사 (Material Symbols Outlined 아이콘)
/// // 2. UTKImageIcons에서 검사 (이미지 기반 아이콘)
/// // 3. 둘 다 아니면 텍스트로 처리됨
///
/// // 아이콘 제거
/// btn.ClearIcon();
///
/// UXML에서 사용:
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
/// UXML에서 로드 후 C#에서 아이콘 설정:
///
/// var root = GetComponent().rootVisualElement;
/// var btn = root.Q("my-button");
/// btn.SetMaterialIcon(UTKMaterialIcons.Settings);
///
///
[UxmlElement]
public partial class UTKButton : VisualElement, IDisposable
{
#region Constants
private const string USS_PATH = "UIToolkit/Button/UTKButton";
#endregion
#region Fields
private bool _disposed;
private Label? _iconLabel;
private Label? _textLabel;
private VisualElement? _imageIcon;
// UXML 직렬화용 backing field - 이름을 소스 생성기가 인식하지 못하게 변경
private string textValue = "";
private string iconValue = "";
private ButtonVariant variantValue = ButtonVariant.Normal;
private ButtonSize sizeValue = ButtonSize.Medium;
private Color? _backgroundColor;
private int borderWidthValue = -1;
private bool iconOnlyValue;
private bool isEnabledValue = true;
#endregion
#region Events
/// 버튼 클릭 이벤트
public event Action? OnClicked;
#endregion
#region Properties
/// 버튼 텍스트
[UxmlAttribute("text")]
public string Text
{
get => textValue;
set
{
textValue = value;
UpdateContent();
}
}
/// 아이콘 (유니코드 문자 또는 텍스트)
[UxmlAttribute("icon")]
public string Icon
{
get => iconValue;
set
{
iconValue = value;
UpdateContent();
}
}
/// 버튼 스타일 변형
[UxmlAttribute("variant")]
public ButtonVariant Variant
{
get => variantValue;
set
{
variantValue = value;
UpdateVariant();
}
}
/// 버튼 크기
[UxmlAttribute("size")]
public ButtonSize Size
{
get => sizeValue;
set
{
sizeValue = value;
UpdateSize();
}
}
/// 커스텀 배경 색상 (null이면 기본 스타일 사용)
public Color? BackgroundColor
{
get => _backgroundColor;
set
{
_backgroundColor = value;
UpdateCustomStyles();
}
}
/// 외곽선 굵기 (-1이면 기본값 사용)
[UxmlAttribute("border-width")]
public int BorderWidth
{
get => borderWidthValue;
set
{
borderWidthValue = value;
UpdateCustomStyles();
}
}
/// 아이콘만 표시 모드
[UxmlAttribute("icon-only")]
public bool IconOnly
{
get => iconOnlyValue;
set
{
iconOnlyValue = value;
UpdateContent();
}
}
/// 버튼 활성화 상태
[UxmlAttribute("is-enabled")]
public bool IsEnabled
{
get => isEnabledValue;
set
{
isEnabledValue = value;
SetEnabled(value);
EnableInClassList("utk-button--disabled", !value);
}
}
#endregion
#region Enums
public enum ButtonVariant
{
Normal,
Primary,
Secondary,
Ghost,
Danger,
OutlineNormal,
OutlinePrimary,
OutlineDanger,
/// 배경과 외곽선이 투명하고 텍스트/아이콘만 표시
Text
}
public enum ButtonSize
{
Small,
Medium,
Large
}
#endregion
#region Constructor
public UTKButton()
{
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);
}
///
/// 텍스트, 아이콘, 변형, 크기를 설정하여 버튼을 생성합니다.
///
/// 버튼에 표시할 텍스트
/// 아이콘 이름 또는 경로 (Material Icon 또는 Image Icon)
/// 버튼 스타일 변형
/// 아이콘 크기 (생략 시 기본값 사용)
///
/// 아이콘 타입 감지 순서:
/// 1. UTKMaterialIcons 검사 (Material Symbols Outlined 아이콘)
/// 2. UTKImageIcons 검사 (이미지 기반 아이콘)
/// 3. 둘 다 아니면 텍스트로 처리됨
///
public UTKButton(string text, string icon = "", ButtonVariant variant = ButtonVariant.Normal, int? iconSize = null) : this()
{
textValue = text;
iconValue = icon;
variantValue = variant;
UpdateContent();
UpdateVariant();
// 아이콘 타입 자동 감지 및 적용
if (!string.IsNullOrEmpty(icon))
{
// 1순위: UTKMaterialIcons에 해당하는지 확인
string iconChar = UTKMaterialIcons.GetIcon(icon);
if (iconChar != string.Empty)
{
SetMaterialIcon(iconChar, iconSize);
}
// 2순위: UTKImageIcons에 해당하는지 확인
else if (!string.IsNullOrEmpty(UTKImageIcons.GetPath(icon)))
{
SetImageIcon(icon, iconSize);
}
// 3순위: 둘 다 아니면 현재 로직 유지 (UpdateContent에서 이미 처리됨)
}
}
#endregion
#region UI Creation
private void CreateUI()
{
AddToClassList("utk-button");
focusable = true;
pickingMode = PickingMode.Position;
_iconLabel = new Label
{
name = "icon",
pickingMode = PickingMode.Ignore
};
_iconLabel.AddToClassList("utk-button__icon");
Add(_iconLabel);
_textLabel = new Label
{
name = "text",
pickingMode = PickingMode.Ignore
};
_textLabel.AddToClassList("utk-button__text");
Add(_textLabel);
UpdateContent();
UpdateVariant();
UpdateSize();
}
private void SetupEvents()
{
RegisterCallback(OnClick);
RegisterCallback(OnKeyDown);
}
private void SubscribeToThemeChanges()
{
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
RegisterCallback(_ =>
{
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);
UpdateContent();
UpdateVariant();
UpdateSize();
UpdateCustomStyles();
// IsEnabled 상태 적용
SetEnabled(isEnabledValue);
EnableInClassList("utk-button--disabled", !isEnabledValue);
}
private void OnClick(ClickEvent evt)
{
if (!isEnabledValue) return;
OnClicked?.Invoke();
evt.StopPropagation();
}
private void OnKeyDown(KeyDownEvent evt)
{
if (!isEnabledValue) return;
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.Space)
{
OnClicked?.Invoke();
evt.StopPropagation();
}
}
#endregion
#region Update Methods
private void UpdateContent()
{
bool hasIcon = !string.IsNullOrEmpty(iconValue);
bool hasText = !string.IsNullOrEmpty(textValue);
if (_iconLabel != null)
{
_iconLabel.text = iconValue;
_iconLabel.style.display = hasIcon ? DisplayStyle.Flex : DisplayStyle.None;
}
if (_textLabel != null)
{
_textLabel.text = textValue;
_textLabel.style.display = (iconOnlyValue || !hasText) ? DisplayStyle.None : DisplayStyle.Flex;
}
EnableInClassList("utk-button--icon-only", iconOnlyValue || (hasIcon && !hasText));
EnableInClassList("utk-button--has-icon", hasIcon && hasText && !iconOnlyValue);
}
private void UpdateVariant()
{
RemoveFromClassList("utk-button--normal");
RemoveFromClassList("utk-button--primary");
RemoveFromClassList("utk-button--secondary");
RemoveFromClassList("utk-button--ghost");
RemoveFromClassList("utk-button--danger");
RemoveFromClassList("utk-button--outline-normal");
RemoveFromClassList("utk-button--outline-primary");
RemoveFromClassList("utk-button--outline-danger");
RemoveFromClassList("utk-button--text");
var variantClass = variantValue switch
{
ButtonVariant.Primary => "utk-button--primary",
ButtonVariant.Secondary => "utk-button--secondary",
ButtonVariant.Ghost => "utk-button--ghost",
ButtonVariant.Danger => "utk-button--danger",
ButtonVariant.OutlineNormal => "utk-button--outline-normal",
ButtonVariant.OutlinePrimary => "utk-button--outline-primary",
ButtonVariant.OutlineDanger => "utk-button--outline-danger",
ButtonVariant.Text => "utk-button--text",
_ => "utk-button--normal"
};
AddToClassList(variantClass);
}
private void UpdateSize()
{
RemoveFromClassList("utk-button--small");
RemoveFromClassList("utk-button--medium");
RemoveFromClassList("utk-button--large");
var sizeClass = sizeValue switch
{
ButtonSize.Small => "utk-button--small",
ButtonSize.Large => "utk-button--large",
_ => "utk-button--medium"
};
AddToClassList(sizeClass);
}
private void UpdateCustomStyles()
{
if (_backgroundColor.HasValue)
{
style.backgroundColor = _backgroundColor.Value;
}
else
{
style.backgroundColor = StyleKeyword.Null;
}
if (borderWidthValue >= 0)
{
style.borderTopWidth = borderWidthValue;
style.borderBottomWidth = borderWidthValue;
style.borderLeftWidth = borderWidthValue;
style.borderRightWidth = borderWidthValue;
}
else
{
style.borderTopWidth = StyleKeyword.Null;
style.borderBottomWidth = StyleKeyword.Null;
style.borderLeftWidth = StyleKeyword.Null;
style.borderRightWidth = StyleKeyword.Null;
}
}
#endregion
#region Icon Methods
///
/// Material Icon을 설정합니다.
///
/// Material Icon 유니코드 문자 (예: UTKMaterialIcons.Settings)
/// 아이콘 폰트 크기 (null이면 버튼 크기에 맞춤)
public void SetMaterialIcon(string icon, int? fontSize = null)
{
ClearImageIcon();
Icon = icon;
if (_iconLabel != null)
{
UTKMaterialIcons.ApplyIconStyle(_iconLabel, fontSize ?? GetDefaultIconSize());
}
}
///
/// Material Icon을 비동기로 설정합니다.
///
/// Material Icon 유니코드 문자 (예: UTKMaterialIcons.Settings)
/// 취소 토큰
/// 아이콘 폰트 크기 (null이면 버튼 크기에 맞춤)
public async UniTask SetMaterialIconAsync(string icon, CancellationToken ct = default, int? fontSize = null)
{
ClearImageIcon();
Icon = icon;
if (_iconLabel != null)
{
await UTKMaterialIcons.ApplyIconStyleAsync(_iconLabel, ct, fontSize ?? GetDefaultIconSize());
}
}
///
/// 아이콘 이름으로 Material Icon을 설정합니다.
///
/// 아이콘 이름 (예: "settings", "home")
/// 아이콘 폰트 크기 (null이면 버튼 크기에 맞춤)
public void SetMaterialIconByName(string iconName, int? fontSize = null)
{
string iconChar = UTKMaterialIcons.GetIcon(iconName);
if (iconChar != string.Empty)
{
SetMaterialIcon(iconChar, fontSize);
}
else
{
Debug.LogWarning($"[UTKButton] 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($"[UTKButton] 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($"[UTKButton] 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($"[UTKButton] Image icon '{iconName}'을(를) 찾을 수 없습니다.");
}
}
///
/// 모든 아이콘을 제거합니다.
///
public void ClearIcon()
{
Icon = "";
ClearImageIcon();
if (_iconLabel != null)
{
_iconLabel.style.display = DisplayStyle.None;
}
}
private void ApplyImageIcon(Texture2D texture, int? iconSize)
{
// 기존 아이콘 Label 숨기기
Icon = "";
if (_iconLabel != null)
{
_iconLabel.style.display = DisplayStyle.None;
}
// 이미지 아이콘용 VisualElement 생성 또는 재사용
if (_imageIcon == null)
{
_imageIcon = new VisualElement
{
name = "image-icon",
pickingMode = PickingMode.Ignore
};
_imageIcon.AddToClassList("utk-button__image-icon");
Insert(0, _imageIcon);
}
var size = iconSize ?? GetDefaultIconSize();
_imageIcon.style.width = size;
_imageIcon.style.height = size;
_imageIcon.style.backgroundImage = new StyleBackground(texture);
_imageIcon.style.display = DisplayStyle.Flex;
EnableInClassList("utk-button--has-image-icon", true);
}
private void ClearImageIcon()
{
if (_imageIcon != null)
{
_imageIcon.style.display = DisplayStyle.None;
}
EnableInClassList("utk-button--has-image-icon", false);
}
private int GetDefaultIconSize()
{
return sizeValue switch
{
ButtonSize.Small => 16,
ButtonSize.Large => 24,
_ => 20 // Medium
};
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
OnClicked = null;
}
#endregion
}
}