Files
EnglewoodLAB/Assets/Scripts/UVC/UIToolkit/Button/UTKImageToggleButton.cs

567 lines
18 KiB
C#

#nullable enable
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 아이콘 토글 버튼 컴포넌트.
/// IsOn 상태에 따라 OnIcon/OffIcon 이미지가 전환됩니다.
/// Material Icons(1순위)와 Image Icons(2순위)를 자동 감지하여 적용합니다.
/// </summary>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // Material Icon 사용 (자동 감지)
/// var btn = new UTKImageToggleButton();
/// btn.OnIcon = UTKMaterialIcons.Visibility;
/// btn.OffIcon = UTKMaterialIcons.VisibilityOff;
/// btn.OnValueChanged += (isOn) => Debug.Log($"상태: {isOn}");
///
/// // Image Icon 사용 (자동 감지)
/// var btn2 = new UTKImageToggleButton(UTKImageIcons.IconEye, UTKImageIcons.IconSetting22);
///
/// // IsEnabled vs IsInteractive
/// btn.IsEnabled = false; // 시각적 비활성화 + 모든 변경 불가
/// btn.IsInteractive = false; // 사용자 입력만 차단, 시각적 활성화 유지
///
/// // 프로그래밍 방식으로 상태 변경 (notify 여부 선택)
/// btn.SetOn(true, notify: false);
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- Material Icon 자동 감지 -->
/// <utk:UTKImageToggleButton on-icon="visibility" off-icon="visibility_off" is-on="false" />
///
/// <!-- 비활성화 -->
/// <utk:UTKImageToggleButton on-icon="visibility" off-icon="visibility_off" is-enabled="false" />
///
/// <!-- 아이콘 크기 지정 -->
/// <utk:UTKImageToggleButton on-icon="visibility" off-icon="visibility_off" icon-size="20" />
/// </ui:UXML>
/// </code>
/// </example>
[UxmlElement]
public partial class UTKImageToggleButton : VisualElement, IDisposable
{
#region Constants
private const string UXML_PATH = "UIToolkit/Button/UTKImageToggleButton";
private const string USS_PATH = "UIToolkit/Button/UTKImageToggleButtonUss";
private const int DEFAULT_ICON_SIZE = 24;
#endregion
#region Fields
private bool _disposed;
private bool _isOn;
private bool _isEnabled = true;
private bool _isInteractive = true;
private string _onIcon = "";
private string _offIcon = "";
private int _iconSize = DEFAULT_ICON_SIZE;
private Vector2Int _size = Vector2Int.zero; // zero = 미설정 (USS 기본값 사용)
// 아이콘 표시용 요소 (캐싱)
private Label? _materialIconLabel;
private VisualElement? _imageIconElement;
// 현재 표시 중인 아이콘 타입
private enum IconDisplayType { None, Material, Image }
private IconDisplayType _currentDisplayType = IconDisplayType.None;
#endregion
#region Events
/// <summary>상태 변경 이벤트</summary>
public event Action<bool>? OnValueChanged;
#endregion
#region Properties
/// <summary>
/// On 상태일 때 표시할 아이콘.
/// Material Icon 이름(예: "visibility") 또는 Image Icon 경로(예: UTKImageIcons.IconEye)를 지정합니다.
/// Material Icons를 1순위로 자동 감지합니다.
/// </summary>
[UxmlAttribute("on-icon")]
public string OnIcon
{
get => _onIcon;
set
{
_onIcon = value;
UpdateIconDisplay();
}
}
/// <summary>
/// Off 상태일 때 표시할 아이콘.
/// Material Icon 이름(예: "visibility_off") 또는 Image Icon 경로를 지정합니다.
/// </summary>
[UxmlAttribute("off-icon")]
public string OffIcon
{
get => _offIcon;
set
{
_offIcon = value;
UpdateIconDisplay();
}
}
/// <summary>
/// 버튼 크기 (픽셀). Vector2Int.zero이면 USS 기본값(var(--size-icon-btn))을 사용합니다.
/// </summary>
[UxmlAttribute("size")]
public Vector2Int Size
{
get => _size;
set
{
_size = value;
if (_size == Vector2Int.zero)
{
style.width = StyleKeyword.Null;
style.height = StyleKeyword.Null;
}
else
{
style.width = _size.x;
style.height = _size.y;
}
}
}
/// <summary>아이콘 크기 (픽셀)</summary>
[UxmlAttribute("icon-size")]
public int IconSize
{
get => _iconSize;
set
{
_iconSize = value;
ApplyIconSize();
}
}
/// <summary>토글 상태</summary>
[UxmlAttribute("is-on")]
public bool IsOn
{
get => _isOn;
set => SetOn(value, notify: true);
}
/// <summary>활성화 상태. false이면 시각적 비활성화 + 모든 변경 불가.</summary>
[UxmlAttribute("is-enabled")]
public bool IsEnabled
{
get => _isEnabled;
set
{
_isEnabled = value;
SetEnabled(value);
EnableInClassList("utk-image-toggle-btn--disabled", !value);
}
}
/// <summary>
/// 상호작용 가능 여부. false이면 마우스/키보드 입력을 무시하지만
/// 프로그래밍 방식으로는 값 변경 가능. 시각적으로는 활성화 상태 유지.
/// </summary>
[UxmlAttribute("is-interactive")]
public bool IsInteractive
{
get => _isInteractive;
set
{
_isInteractive = value;
focusable = value;
EnableInClassList("utk-image-toggle-btn--non-interactive", !value);
}
}
#endregion
#region Constructor
public UTKImageToggleButton()
{
UTKThemeManager.Instance.ApplyThemeToElement(this);
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
{
styleSheets.Add(uss);
}
CreateUI();
SetupEvents();
SubscribeToThemeChanges();
}
/// <summary>
/// OnIcon/OffIcon을 지정하여 생성합니다.
/// Material Icon 이름 또는 Image Icon 경로를 사용할 수 있습니다.
/// </summary>
/// <param name="onIcon">On 상태 아이콘</param>
/// <param name="offIcon">Off 상태 아이콘</param>
/// <param name="iconSize">아이콘 크기 (픽셀)</param>
public UTKImageToggleButton(string onIcon, string offIcon, int iconSize = DEFAULT_ICON_SIZE) : this()
{
_onIcon = onIcon;
_offIcon = offIcon;
_iconSize = iconSize;
UpdateIconDisplay();
}
#endregion
#region Setup
private void CreateUI()
{
AddToClassList("utk-image-toggle-btn");
focusable = true;
pickingMode = PickingMode.Position;
var asset = Resources.Load<VisualTreeAsset>(UXML_PATH);
if (asset != null)
{
var root = asset.Instantiate();
_materialIconLabel = root.Q<Label>("material-icon");
_imageIconElement = root.Q<VisualElement>("image-icon");
Add(root);
}
else
{
CreateUIFallback();
}
UpdateIconDisplay();
}
private void CreateUIFallback()
{
_materialIconLabel = new Label
{
name = "material-icon",
pickingMode = PickingMode.Ignore
};
_materialIconLabel.AddToClassList("utk-image-toggle-btn__material-icon");
Add(_materialIconLabel);
_imageIconElement = new VisualElement
{
name = "image-icon",
pickingMode = PickingMode.Ignore
};
_imageIconElement.AddToClassList("utk-image-toggle-btn__image-icon");
Add(_imageIconElement);
}
private void SetupEvents()
{
RegisterCallback<ClickEvent>(OnClick);
RegisterCallback<KeyDownEvent>(OnKeyDown, TrickleDown.TrickleDown);
RegisterCallback<MouseDownEvent>(OnMouseDown, 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 OnClick(ClickEvent evt)
{
if (!_isEnabled || !_isInteractive) return;
SetOn(!_isOn, notify: true);
evt.StopPropagation();
}
private void OnMouseDown(MouseDownEvent evt)
{
if (!_isInteractive)
{
evt.StopImmediatePropagation();
evt.StopPropagation();
}
}
private void OnKeyDown(KeyDownEvent evt)
{
if (!_isInteractive) return;
if (!_isEnabled) return;
if (evt.keyCode == KeyCode.Space || evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)
{
SetOn(!_isOn, notify: true);
evt.StopPropagation();
}
}
#endregion
#region Methods
/// <summary>
/// 상태를 설정합니다.
/// </summary>
/// <param name="newValue">새 상태 값</param>
/// <param name="notify">true이면 OnValueChanged 이벤트 발생</param>
public void SetOn(bool newValue, bool notify)
{
if (_isOn == newValue) return;
_isOn = newValue;
EnableInClassList("utk-image-toggle-btn--on", _isOn);
UpdateIconDisplay();
if (notify)
{
OnValueChanged?.Invoke(_isOn);
}
}
/// <summary>
/// On 상태 아이콘을 Material Icon으로 설정합니다.
/// </summary>
/// <param name="icon">Material Icon 유니코드 문자 (예: UTKMaterialIcons.Visibility)</param>
public void SetOnMaterialIcon(string icon)
{
_onIcon = icon;
UpdateIconDisplay();
}
/// <summary>
/// Off 상태 아이콘을 Material Icon으로 설정합니다.
/// </summary>
/// <param name="icon">Material Icon 유니코드 문자 (예: UTKMaterialIcons.VisibilityOff)</param>
public void SetOffMaterialIcon(string icon)
{
_offIcon = icon;
UpdateIconDisplay();
}
/// <summary>
/// On 상태 아이콘을 Image Icon으로 설정합니다.
/// </summary>
/// <param name="resourcePath">이미지 리소스 경로 (예: UTKImageIcons.IconEye)</param>
public void SetOnImageIcon(string resourcePath)
{
_onIcon = resourcePath;
UpdateIconDisplay();
}
/// <summary>
/// Off 상태 아이콘을 Image Icon으로 설정합니다.
/// </summary>
/// <param name="resourcePath">이미지 리소스 경로</param>
public void SetOffImageIcon(string resourcePath)
{
_offIcon = resourcePath;
UpdateIconDisplay();
}
/// <summary>
/// On 상태 아이콘을 비동기로 설정합니다. (Material Icon)
/// </summary>
public async UniTask SetOnIconAsync(string icon, CancellationToken ct = default)
{
_onIcon = icon;
if (_isOn)
{
await ApplyMaterialIconAsync(icon, ct);
}
}
/// <summary>
/// Off 상태 아이콘을 비동기로 설정합니다. (Material Icon)
/// </summary>
public async UniTask SetOffIconAsync(string icon, CancellationToken ct = default)
{
_offIcon = icon;
if (!_isOn)
{
await ApplyMaterialIconAsync(icon, ct);
}
}
#endregion
#region Icon Display
/// <summary>현재 IsOn 상태에 맞는 아이콘을 표시합니다.</summary>
private void UpdateIconDisplay()
{
string icon = _isOn ? _onIcon : _offIcon;
if (string.IsNullOrEmpty(icon))
{
HideAllIcons();
return;
}
// 1순위: Material Icon 감지
string materialChar = UTKMaterialIcons.GetIcon(icon);
if (materialChar != string.Empty)
{
ApplyMaterialIcon(materialChar);
return;
}
// 2순위: Image Icon — 이름으로 조회 (예: "icon_eye_22x16")
string imagePath = UTKImageIcons.GetPath(icon);
if (!string.IsNullOrEmpty(imagePath))
{
ApplyImageIcon(imagePath);
return;
}
// 3순위: Image Icon — 전체 경로 직접 전달 (예: UTKImageIcons.IconEye22x16 상수)
var directTexture = UTKImageIcons.LoadTexture(icon);
if (directTexture != null)
{
ApplyImageIcon(directTexture);
return;
}
// 4순위: 직접 유니코드 문자(Material)인 경우 (예: UTKMaterialIcons.Visibility 상수값 직접 전달)
if (icon.Length == 1)
{
ApplyMaterialIcon(icon);
return;
}
HideAllIcons();
}
private void ApplyMaterialIcon(string iconChar)
{
if (_materialIconLabel == null) return;
_materialIconLabel.text = iconChar;
UTKMaterialIcons.ApplyIconStyle(_materialIconLabel, _iconSize);
_materialIconLabel.style.display = DisplayStyle.Flex;
if (_imageIconElement != null)
{
_imageIconElement.style.display = DisplayStyle.None;
}
_currentDisplayType = IconDisplayType.Material;
}
private async UniTask ApplyMaterialIconAsync(string iconChar, CancellationToken ct)
{
if (_materialIconLabel == null) return;
_materialIconLabel.text = iconChar;
await UTKMaterialIcons.ApplyIconStyleAsync(_materialIconLabel, ct, _iconSize);
_materialIconLabel.style.display = DisplayStyle.Flex;
if (_imageIconElement != null)
{
_imageIconElement.style.display = DisplayStyle.None;
}
_currentDisplayType = IconDisplayType.Material;
}
private void ApplyImageIcon(string resourcePath)
{
if (_imageIconElement == null) return;
var texture = UTKImageIcons.LoadTexture(resourcePath);
if (texture == null)
{
Debug.LogWarning($"[UTKImageToggleButton] Image icon '{resourcePath}'을(를) 로드할 수 없습니다.");
HideAllIcons();
return;
}
ApplyImageIcon(texture);
}
private void ApplyImageIcon(Texture2D texture)
{
if (_imageIconElement == null) return;
_imageIconElement.style.backgroundImage = new StyleBackground(texture);
_imageIconElement.style.width = _iconSize;
_imageIconElement.style.height = _iconSize;
_imageIconElement.style.display = DisplayStyle.Flex;
if (_materialIconLabel != null)
{
_materialIconLabel.style.display = DisplayStyle.None;
}
_currentDisplayType = IconDisplayType.Image;
}
private void ApplyIconSize()
{
switch (_currentDisplayType)
{
case IconDisplayType.Material:
if (_materialIconLabel != null)
{
UTKMaterialIcons.ApplyIconStyle(_materialIconLabel, _iconSize);
}
break;
case IconDisplayType.Image:
if (_imageIconElement != null)
{
_imageIconElement.style.width = _iconSize;
_imageIconElement.style.height = _iconSize;
}
break;
}
}
private void HideAllIcons()
{
if (_materialIconLabel != null)
{
_materialIconLabel.style.display = DisplayStyle.None;
}
if (_imageIconElement != null)
{
_imageIconElement.style.display = DisplayStyle.None;
}
_currentDisplayType = IconDisplayType.None;
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
UnregisterCallback<ClickEvent>(OnClick);
UnregisterCallback<KeyDownEvent>(OnKeyDown, TrickleDown.TrickleDown);
UnregisterCallback<MouseDownEvent>(OnMouseDown, TrickleDown.TrickleDown);
OnValueChanged = null;
}
#endregion
}
}