using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Amanotes.Core
{
///
/// An that scales a to
/// fit inside or fill a area.
///
///
/// - fitFill = 0 – uniform fit: entire sprite is visible (letterbox).
/// - fitFill = 1 – uniform fill: sprite covers the full area (crops edges).
/// - Values in between lerp the local scale between the two extremes.
///
///
/// Usage: add this component to a GameObject, assign it as a listener on a
/// (or let the Widget2D editor scan it automatically),
/// and set in the inspector.
///
[AddComponentMenu("Widget2D/Sprite Fit Listener")]
public class SpriteFitListener : MonoBehaviour, ISizeListener
{
[SerializeField] private SpriteRenderer _spriteRenderer;
[SerializeField]
[Range(0f, 1f)]
[Tooltip("0 = fit (letterbox), 1 = fill (crop). Lerps the uniform scale between the two extremes.")]
private float _fitFill = 0f;
///
/// Fit/fill ratio. 0 = fit, 1 = fill, values in between blend the two.
/// Setting this at runtime immediately re-applies the last received size.
///
public float fitFill
{
get => _fitFill;
set
{
_fitFill = Mathf.Clamp01(value);
if (_lastSize.sqrMagnitude > 0f) ApplyScale(_lastSize);
}
}
// Serialized so it survives domain reloads and prefab instantiation.
// Set by OnResize; used to re-apply when the sprite or fitFill changes.
[SerializeField] private Vector2 _lastSize;
// ─── ISizeListener ────────────────────────────────────────────────────
///
public void OnResize(Vector2 size)
{
_lastSize = size;
ApplyScale(size);
}
// ─── Internal ─────────────────────────────────────────────────────────
private void ApplyScale(Vector2 widgetSize)
{
if (_spriteRenderer == null || _spriteRenderer.sprite == null) return;
if (widgetSize.x <= 0f || widgetSize.y <= 0f) return;
var sprite = _spriteRenderer.sprite;
var rect = sprite.rect;
var ppu = sprite.pixelsPerUnit;
var orgSize = new Vector2(rect.width / ppu, rect.height / ppu);
var sx = widgetSize.x / orgSize.x;
var sy = widgetSize.y / orgSize.y;
var minS = Mathf.Min(sx, sy);
var maxS = Mathf.Max(sx, sy);
var s = Mathf.Lerp(minS, maxS, _fitFill);
_spriteRenderer.transform.localScale = Vector3.one * s;
}
///
/// Re-applies the fit/fill scale using the last size received from the Widget2D.
/// Call this after changing the sprite externally so the scale is recalculated
/// for the new sprite's aspect ratio.
///
public void Refresh()
{
if (_lastSize.sqrMagnitude > 0f) ApplyScale(_lastSize);
}
///
/// Swaps the target sprite and immediately refreshes the scale.
/// Convenience wrapper around assigning spriteRenderer.sprite + .
///
public void SetSprite(Sprite sprite)
{
if (_spriteRenderer == null) return;
_spriteRenderer.sprite = sprite;
Refresh();
}
// ─── Unity lifecycle ──────────────────────────────────────────────────
private void Awake()
{
if (_spriteRenderer == null)
_spriteRenderer = GetComponent();
}
#if UNITY_EDITOR
// Tracks the sprite seen last OnValidate so we can detect inspector changes.
[System.NonSerialized] private Sprite _lastKnownSprite;
private void OnValidate()
{
if (_spriteRenderer == null)
_spriteRenderer = GetComponent();
if (PrefabUtility.IsPartOfPrefabAsset(gameObject)) return;
EditorApplication.delayCall += () =>
{
if (this == null) return;
// Re-apply whenever fitFill is tweaked OR the sprite was swapped
// in the inspector (detected by reference change).
var currentSprite = _spriteRenderer != null ? _spriteRenderer.sprite : null;
bool spriteChanged = currentSprite != _lastKnownSprite;
_lastKnownSprite = currentSprite;
if (_lastSize.sqrMagnitude > 0f)
Refresh();
};
}
private void Reset()
{
_spriteRenderer = GetComponent();
}
#endif
}
}