567 lines
18 KiB
C#
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
|
|
}
|
|
}
|