Restructured for new direction.

This commit is contained in:
2026-05-12 12:01:09 +01:00
parent 0439b6c1d2
commit c203f836b1
1134 changed files with 125569 additions and 213519 deletions

View 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++;
}
}
}