Restructured for new direction.
This commit is contained in:
400
Assets/Scripts/UI/Menus/Components/AnimatedSelectionButton.cs
Normal file
400
Assets/Scripts/UI/Menus/Components/AnimatedSelectionButton.cs
Normal file
@@ -0,0 +1,400 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using PrimeTween;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BriarQueen.UI.Menus.Components
|
||||
{
|
||||
[ExecuteAlways]
|
||||
[RequireComponent(typeof(Button))]
|
||||
public class AnimatedSelectionButton : MonoBehaviour,
|
||||
IPointerEnterHandler,
|
||||
IPointerExitHandler,
|
||||
IPointerClickHandler,
|
||||
ISelectHandler,
|
||||
IDeselectHandler
|
||||
{
|
||||
private const string DefaultRevealPropertyName = "_Reveal";
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] private Button _button;
|
||||
[SerializeField] private RectTransform _selectionBackground;
|
||||
[SerializeField] private TextMeshProUGUI _label;
|
||||
|
||||
[Header("Background Sizing")]
|
||||
[SerializeField] private Vector2 _backgroundPadding = new(24f, 6f);
|
||||
[SerializeField] private bool _matchBackgroundToLabelPosition = true;
|
||||
|
||||
[Header("Background Material")]
|
||||
[SerializeField] private bool _useShaderReveal = true;
|
||||
[SerializeField] private Material _selectionMaterial;
|
||||
[SerializeField] private Color _selectionColor = new(0.02f, 0.018f, 0.015f, 0.35f);
|
||||
[SerializeField] private string _revealPropertyName = "_Reveal";
|
||||
|
||||
[Header("Edit Preview")]
|
||||
[SerializeField] private bool _previewInEditMode;
|
||||
[SerializeField][Range(0f, 1f)] private float _previewReveal = 1f;
|
||||
|
||||
[Header("Tween Settings")]
|
||||
[SerializeField] private float _selectDuration = 0.18f;
|
||||
[SerializeField] private float _deselectDuration = 0.12f;
|
||||
[SerializeField] private Ease _ease = Ease.InOutSine;
|
||||
[SerializeField] private bool _useUnscaledTime = true;
|
||||
|
||||
private CancellationTokenSource _selectionCts;
|
||||
private Graphic _selectionBackgroundGraphic;
|
||||
private Material _sourceSelectionMaterial;
|
||||
private Material _runtimeSelectionMaterial;
|
||||
private Sequence _selectionSequence;
|
||||
private int _animationVersion;
|
||||
private int _colorPropertyId;
|
||||
private int _revealPropertyId;
|
||||
private float _backgroundProgress;
|
||||
|
||||
private bool _isSelected;
|
||||
|
||||
// Group listens to these to coordinate highlight state
|
||||
public event Action<AnimatedSelectionButton> SelectionRequested;
|
||||
public event Action<AnimatedSelectionButton> HoverEntered;
|
||||
public event Action<AnimatedSelectionButton> HoverExited;
|
||||
|
||||
public Button Button
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_button == null)
|
||||
_button = GetComponent<Button>();
|
||||
return _button;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSelected => _isSelected;
|
||||
|
||||
// ── Unity lifecycle ───────────────────────────────────────────
|
||||
|
||||
private void Reset() => ResolveReferences();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
ResolveReferences();
|
||||
EnsureRuntimeSelectionMaterial();
|
||||
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
ApplyEditPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyInstant(false);
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
ResolveReferences();
|
||||
|
||||
if (Application.isPlaying) return;
|
||||
|
||||
DestroyRuntimeSelectionMaterial();
|
||||
EnsureRuntimeSelectionMaterial();
|
||||
ApplyEditPreview();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Application.isPlaying || !_previewInEditMode) return;
|
||||
ApplyEditPreview();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
CancelSelectionTween();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
CancelSelectionTween();
|
||||
DestroyRuntimeSelectionMaterial();
|
||||
}
|
||||
|
||||
// ── Pointer / selection events ────────────────────────────────
|
||||
|
||||
public void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
if (Button != null && !Button.interactable) return;
|
||||
HoverEntered?.Invoke(this);
|
||||
}
|
||||
|
||||
public void OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
HoverExited?.Invoke(this);
|
||||
}
|
||||
|
||||
public void OnPointerClick(PointerEventData eventData)
|
||||
{
|
||||
if (Button != null && !Button.interactable) return;
|
||||
SelectionRequested?.Invoke(this);
|
||||
}
|
||||
|
||||
public void OnSelect(BaseEventData eventData)
|
||||
{
|
||||
if (Button != null && !Button.interactable) return;
|
||||
HoverEntered?.Invoke(this);
|
||||
SelectionRequested?.Invoke(this);
|
||||
}
|
||||
|
||||
public void OnDeselect(BaseEventData eventData)
|
||||
{
|
||||
HoverExited?.Invoke(this);
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Called by the group to set whether this button's selected state.</summary>
|
||||
public void SetSelectedState(bool selected)
|
||||
{
|
||||
_isSelected = selected;
|
||||
}
|
||||
|
||||
/// <summary>Called by the group to drive the highlight visual directly.</summary>
|
||||
public void SetHighlight(bool show, bool instant = false)
|
||||
{
|
||||
if (instant || !gameObject.activeInHierarchy)
|
||||
{
|
||||
CancelSelectionTween();
|
||||
RefreshBackgroundSize();
|
||||
ApplyBackgroundProgress(show ? 1f : 0f);
|
||||
if (_selectionBackground != null)
|
||||
_selectionBackground.gameObject.SetActive(show);
|
||||
return;
|
||||
}
|
||||
|
||||
AnimateSelection(show).Forget();
|
||||
}
|
||||
|
||||
/// <summary>Legacy — drives both state and highlight together for non-group usage.</summary>
|
||||
public void SetSelected(bool selected, bool instant = false)
|
||||
{
|
||||
_isSelected = selected;
|
||||
SetHighlight(selected, instant);
|
||||
}
|
||||
|
||||
public void ApplyInstant(bool selected)
|
||||
{
|
||||
_isSelected = selected;
|
||||
RefreshBackgroundSize();
|
||||
ApplyBackgroundProgress(selected ? 1f : 0f);
|
||||
|
||||
if (_selectionBackground != null)
|
||||
_selectionBackground.gameObject.SetActive(selected);
|
||||
}
|
||||
|
||||
public GameObject GetSelectableGameObject() => gameObject;
|
||||
|
||||
// ── Internal ──────────────────────────────────────────────────
|
||||
|
||||
private async UniTaskVoid AnimateSelection(bool show)
|
||||
{
|
||||
CancelSelectionTween();
|
||||
|
||||
if (_selectionBackground == null) return;
|
||||
|
||||
RefreshBackgroundSize();
|
||||
|
||||
var duration = show ? _selectDuration : _deselectDuration;
|
||||
if (duration <= 0f)
|
||||
{
|
||||
ApplyInstant(_isSelected);
|
||||
return;
|
||||
}
|
||||
|
||||
_selectionBackground.gameObject.SetActive(true);
|
||||
EnsureRuntimeSelectionMaterial();
|
||||
_selectionCts = new CancellationTokenSource();
|
||||
_animationVersion++;
|
||||
|
||||
var localVersion = _animationVersion;
|
||||
var target = show ? 1f : 0f;
|
||||
var start = _backgroundProgress;
|
||||
|
||||
_selectionSequence = Sequence.Create(useUnscaledTime: _useUnscaledTime)
|
||||
.Group(Tween.Custom(
|
||||
start,
|
||||
target,
|
||||
duration,
|
||||
ApplyBackgroundProgress,
|
||||
_ease,
|
||||
useUnscaledTime: _useUnscaledTime));
|
||||
|
||||
try
|
||||
{
|
||||
await _selectionSequence.ToUniTask(cancellationToken: _selectionCts.Token);
|
||||
ApplyBackgroundProgress(target);
|
||||
|
||||
if (!show)
|
||||
_selectionBackground.gameObject.SetActive(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (localVersion == _animationVersion)
|
||||
{
|
||||
_selectionSequence = default;
|
||||
|
||||
if (_selectionCts != null)
|
||||
{
|
||||
_selectionCts.Dispose();
|
||||
_selectionCts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyBackgroundProgress(float progress)
|
||||
{
|
||||
if (_selectionBackground == null) return;
|
||||
|
||||
EnsureRuntimeSelectionMaterial();
|
||||
|
||||
var scale = _selectionBackground.localScale;
|
||||
_backgroundProgress = Mathf.Clamp01(progress);
|
||||
scale.x = UsesShaderReveal() ? 1f : _backgroundProgress;
|
||||
_selectionBackground.localScale = scale;
|
||||
|
||||
if (UsesShaderReveal())
|
||||
ApplyRuntimeMaterialProperties();
|
||||
}
|
||||
|
||||
private void RefreshBackgroundSize()
|
||||
{
|
||||
if (_selectionBackground == null || _label == null) return;
|
||||
|
||||
_label.ForceMeshUpdate();
|
||||
|
||||
var preferredSize = _label.GetPreferredValues(_label.text, Mathf.Infinity, Mathf.Infinity);
|
||||
var backgroundSize = preferredSize + _backgroundPadding;
|
||||
|
||||
_selectionBackground.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, backgroundSize.x);
|
||||
_selectionBackground.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, backgroundSize.y);
|
||||
|
||||
if (!_matchBackgroundToLabelPosition) return;
|
||||
|
||||
var labelRect = _label.rectTransform;
|
||||
_selectionBackground.position = labelRect.TransformPoint(labelRect.rect.center);
|
||||
}
|
||||
|
||||
private void ResolveReferences()
|
||||
{
|
||||
if (_button == null)
|
||||
_button = GetComponent<Button>();
|
||||
|
||||
if (_label == null)
|
||||
_label = GetComponentInChildren<TextMeshProUGUI>(true);
|
||||
|
||||
if (_selectionBackground != null && _selectionBackgroundGraphic == null)
|
||||
_selectionBackgroundGraphic = _selectionBackground.GetComponent<Graphic>();
|
||||
|
||||
_revealPropertyId = Shader.PropertyToID(string.IsNullOrWhiteSpace(_revealPropertyName)
|
||||
? DefaultRevealPropertyName
|
||||
: _revealPropertyName);
|
||||
_colorPropertyId = Shader.PropertyToID("_Color");
|
||||
}
|
||||
|
||||
private void ApplyEditPreview()
|
||||
{
|
||||
if (_selectionBackground == null) return;
|
||||
|
||||
RefreshBackgroundSize();
|
||||
|
||||
if (!_previewInEditMode)
|
||||
{
|
||||
ApplyBackgroundProgress(0f);
|
||||
_selectionBackground.gameObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_selectionBackground.gameObject.SetActive(true);
|
||||
ApplyBackgroundProgress(_previewReveal);
|
||||
}
|
||||
|
||||
private void EnsureRuntimeSelectionMaterial()
|
||||
{
|
||||
if (!_useShaderReveal || _selectionBackground == null) return;
|
||||
|
||||
if (_selectionBackgroundGraphic == null)
|
||||
_selectionBackgroundGraphic = _selectionBackground.GetComponent<Graphic>();
|
||||
|
||||
if (_selectionBackgroundGraphic == null || _runtimeSelectionMaterial != null) return;
|
||||
|
||||
var sourceMaterial = _selectionMaterial != null
|
||||
? _selectionMaterial
|
||||
: _selectionBackgroundGraphic.material;
|
||||
|
||||
if (sourceMaterial == null || !sourceMaterial.HasProperty(_revealPropertyId)) return;
|
||||
|
||||
_sourceSelectionMaterial = sourceMaterial;
|
||||
_runtimeSelectionMaterial = new Material(sourceMaterial)
|
||||
{
|
||||
name = $"{sourceMaterial.name} (Runtime)",
|
||||
hideFlags = HideFlags.DontSave
|
||||
};
|
||||
ApplyRuntimeMaterialProperties();
|
||||
_selectionBackgroundGraphic.material = _runtimeSelectionMaterial;
|
||||
}
|
||||
|
||||
private void ApplyRuntimeMaterialProperties()
|
||||
{
|
||||
if (_runtimeSelectionMaterial == null) return;
|
||||
|
||||
if (_runtimeSelectionMaterial.HasProperty(_revealPropertyId))
|
||||
_runtimeSelectionMaterial.SetFloat(_revealPropertyId, _backgroundProgress);
|
||||
|
||||
if (_runtimeSelectionMaterial.HasProperty(_colorPropertyId))
|
||||
_runtimeSelectionMaterial.SetColor(_colorPropertyId, _selectionColor);
|
||||
}
|
||||
|
||||
private bool UsesShaderReveal()
|
||||
{
|
||||
return _useShaderReveal
|
||||
&& _runtimeSelectionMaterial != null
|
||||
&& _runtimeSelectionMaterial.HasProperty(_revealPropertyId);
|
||||
}
|
||||
|
||||
private void DestroyRuntimeSelectionMaterial()
|
||||
{
|
||||
if (_runtimeSelectionMaterial == null) return;
|
||||
|
||||
if (_selectionBackgroundGraphic != null &&
|
||||
_selectionBackgroundGraphic.material == _runtimeSelectionMaterial)
|
||||
_selectionBackgroundGraphic.material = _sourceSelectionMaterial;
|
||||
|
||||
if (Application.isPlaying)
|
||||
Destroy(_runtimeSelectionMaterial);
|
||||
else
|
||||
DestroyImmediate(_runtimeSelectionMaterial);
|
||||
|
||||
_runtimeSelectionMaterial = null;
|
||||
_sourceSelectionMaterial = null;
|
||||
}
|
||||
|
||||
private void CancelSelectionTween()
|
||||
{
|
||||
if (_selectionSequence.isAlive)
|
||||
_selectionSequence.Stop();
|
||||
|
||||
_selectionSequence = default;
|
||||
|
||||
if (_selectionCts == null) return;
|
||||
|
||||
try { _selectionCts.Cancel(); } catch { }
|
||||
_selectionCts.Dispose();
|
||||
_selectionCts = null;
|
||||
_animationVersion++;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user