#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 } }