364 lines
10 KiB
C#
364 lines
10 KiB
C#
using UnityEngine;
|
|
using UnityEngine.EventSystems;
|
|
using UnityEngine.UI;
|
|
#if UNITY_EDITOR
|
|
using UnityEditor;
|
|
#endif
|
|
|
|
#if ENABLE_INPUT_SYSTEM
|
|
using UnityEngine.InputSystem;
|
|
#endif
|
|
|
|
namespace BriarQueen.UI.Menus
|
|
{
|
|
[RequireComponent(typeof(RectTransform))]
|
|
public class VerticalScrollbar : MonoBehaviour, IDragHandler, IPointerDownHandler
|
|
{
|
|
private static readonly Vector3[] Corners = new Vector3[4];
|
|
|
|
[Header("Hierarchy")]
|
|
[SerializeField]
|
|
private RectTransform _viewport;
|
|
|
|
[SerializeField]
|
|
private RectTransform _content;
|
|
|
|
[SerializeField]
|
|
private RectTransform _trackRect;
|
|
|
|
[SerializeField]
|
|
private RectTransform _handleRect;
|
|
|
|
[Header("Scroll Settings")]
|
|
[SerializeField]
|
|
private float _wheelPixels = 80f;
|
|
|
|
[SerializeField]
|
|
private float _padSpeed = 900f;
|
|
|
|
[SerializeField]
|
|
private float _inputSystemWheelScale = 0.05f;
|
|
|
|
[Header("Handle")]
|
|
[SerializeField]
|
|
private bool _useCustomHandleSizing;
|
|
|
|
[SerializeField]
|
|
private float _minHandleHeight = 24f;
|
|
|
|
[Header("Alignment")]
|
|
[SerializeField]
|
|
private bool _centerContentWhenNotScrollable = true;
|
|
|
|
[SerializeField]
|
|
private float _topInset = 6f;
|
|
|
|
[SerializeField]
|
|
private float _bottomInset = 6f;
|
|
|
|
[Header("Track")]
|
|
[SerializeField]
|
|
private bool _hideTrackWhenNotScrollable = true;
|
|
|
|
#if UNITY_EDITOR
|
|
[Header("Editor Debug")]
|
|
[SerializeField]
|
|
[Range(0f, 1f)]
|
|
private float _debugNormalized;
|
|
#endif
|
|
|
|
private bool _isScrollable;
|
|
private float _scrollRange;
|
|
private Camera _uiCamera;
|
|
|
|
public float Normalized { get; private set; }
|
|
|
|
private void Awake()
|
|
{
|
|
var canvas = GetComponentInParent<Canvas>();
|
|
if (canvas != null && canvas.renderMode == RenderMode.ScreenSpaceCamera)
|
|
_uiCamera = canvas.worldCamera;
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
Rebuild();
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
HandleMouseWheel();
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
private void OnValidate()
|
|
{
|
|
if (_viewport == null || _content == null || _trackRect == null || _handleRect == null)
|
|
return;
|
|
|
|
if (Application.isPlaying)
|
|
return;
|
|
|
|
Canvas.ForceUpdateCanvases();
|
|
LayoutRebuilder.ForceRebuildLayoutImmediate(_content);
|
|
|
|
if (!TryGetContentBounds(out var top, out var bottom))
|
|
return;
|
|
|
|
var contentHeight = top - bottom;
|
|
var viewportHeight = _viewport.rect.height - _topInset - _bottomInset;
|
|
|
|
_isScrollable = contentHeight > viewportHeight;
|
|
_scrollRange = Mathf.Max(0f, contentHeight - viewportHeight);
|
|
Normalized = Mathf.Clamp01(_debugNormalized);
|
|
|
|
if (_centerContentWhenNotScrollable && !_isScrollable)
|
|
{
|
|
CenterContent(top, bottom);
|
|
Normalized = 0f;
|
|
}
|
|
else
|
|
{
|
|
var offset = Mathf.Lerp(0f, _scrollRange, Normalized);
|
|
SetContentY(offset);
|
|
|
|
if (Normalized <= 0.0001f)
|
|
AlignFirstChildToTop();
|
|
}
|
|
|
|
UpdateTrackVisibility();
|
|
UpdateHandle();
|
|
}
|
|
#endif
|
|
|
|
public void OnDrag(PointerEventData eventData)
|
|
{
|
|
DragHandle(eventData);
|
|
}
|
|
|
|
public void OnPointerDown(PointerEventData eventData)
|
|
{
|
|
DragHandle(eventData);
|
|
}
|
|
|
|
public void Rebuild()
|
|
{
|
|
if (_viewport == null || _content == null)
|
|
return;
|
|
|
|
Canvas.ForceUpdateCanvases();
|
|
LayoutRebuilder.ForceRebuildLayoutImmediate(_content);
|
|
|
|
if (!TryGetContentBounds(out var top, out var bottom))
|
|
return;
|
|
|
|
var contentHeight = top - bottom;
|
|
var viewportHeight = _viewport.rect.height - _topInset - _bottomInset;
|
|
|
|
_isScrollable = contentHeight > viewportHeight;
|
|
_scrollRange = Mathf.Max(0f, contentHeight - viewportHeight);
|
|
|
|
if (_centerContentWhenNotScrollable && !_isScrollable)
|
|
{
|
|
CenterContent(top, bottom);
|
|
Normalized = 0f;
|
|
}
|
|
else
|
|
{
|
|
SetNormalized(Normalized);
|
|
}
|
|
|
|
UpdateTrackVisibility();
|
|
UpdateHandle();
|
|
}
|
|
|
|
public void SetNormalized(float normalized)
|
|
{
|
|
Normalized = Mathf.Clamp01(normalized);
|
|
|
|
if (!_isScrollable)
|
|
return;
|
|
|
|
var offset = Mathf.Lerp(0f, _scrollRange, Normalized);
|
|
SetContentY(offset);
|
|
|
|
if (Normalized <= 0.0001f)
|
|
AlignFirstChildToTop();
|
|
|
|
UpdateHandle();
|
|
}
|
|
|
|
private void CenterContent(float top, float bottom)
|
|
{
|
|
var contentCenter = (top + bottom) * 0.5f;
|
|
var viewportCenter = (_viewport.rect.yMin + _viewport.rect.yMax) * 0.5f;
|
|
|
|
var delta = viewportCenter - contentCenter;
|
|
|
|
var position = _content.anchoredPosition;
|
|
position.y += delta;
|
|
_content.anchoredPosition = position;
|
|
}
|
|
|
|
private void AlignFirstChildToTop()
|
|
{
|
|
RectTransform first = null;
|
|
|
|
for (var i = 0; i < _content.childCount; i++)
|
|
{
|
|
var child = _content.GetChild(i) as RectTransform;
|
|
if (child != null && child.gameObject.activeSelf)
|
|
{
|
|
first = child;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (first == null)
|
|
return;
|
|
|
|
first.GetWorldCorners(Corners);
|
|
|
|
var childTop = _viewport.InverseTransformPoint(Corners[1]).y;
|
|
var targetTop = _viewport.rect.yMax - _topInset;
|
|
|
|
var delta = targetTop - childTop;
|
|
|
|
var position = _content.anchoredPosition;
|
|
position.y += delta;
|
|
_content.anchoredPosition = position;
|
|
}
|
|
|
|
private void ScrollByPixels(float pixels)
|
|
{
|
|
if (!_isScrollable)
|
|
return;
|
|
|
|
var current = Normalized * _scrollRange;
|
|
var next = Mathf.Clamp(current + pixels, 0f, _scrollRange);
|
|
|
|
Normalized = _scrollRange > 0f ? next / _scrollRange : 0f;
|
|
SetNormalized(Normalized);
|
|
}
|
|
|
|
private void DragHandle(PointerEventData eventData)
|
|
{
|
|
if (!_isScrollable || _trackRect == null || _handleRect == null)
|
|
return;
|
|
|
|
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(_trackRect, eventData.position, _uiCamera,
|
|
out var localPoint))
|
|
return;
|
|
|
|
var halfHandleHeight = _handleRect.rect.height * 0.5f;
|
|
|
|
var min = _trackRect.rect.yMin + halfHandleHeight;
|
|
var max = _trackRect.rect.yMax - halfHandleHeight;
|
|
|
|
var y = Mathf.Clamp(localPoint.y, min, max);
|
|
var normalized = 1f - Mathf.InverseLerp(min, max, y);
|
|
|
|
SetNormalized(normalized);
|
|
}
|
|
|
|
private void HandleMouseWheel()
|
|
{
|
|
var wheel = ReadMouseWheelDelta();
|
|
if (Mathf.Abs(wheel) > 0.01f)
|
|
ScrollByPixels(-wheel * _wheelPixels);
|
|
}
|
|
|
|
private float ReadMouseWheelDelta()
|
|
{
|
|
#if ENABLE_INPUT_SYSTEM
|
|
if (Mouse.current != null)
|
|
return Mouse.current.scroll.ReadValue().y * _inputSystemWheelScale;
|
|
#elif ENABLE_LEGACY_INPUT_MANAGER
|
|
return Input.mouseScrollDelta.y;
|
|
#endif
|
|
return 0f;
|
|
}
|
|
|
|
private void SetContentY(float y)
|
|
{
|
|
var position = _content.anchoredPosition;
|
|
position.y = y;
|
|
_content.anchoredPosition = position;
|
|
|
|
#if UNITY_EDITOR
|
|
if (!Application.isPlaying)
|
|
{
|
|
Canvas.ForceUpdateCanvases();
|
|
SceneView.RepaintAll();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
private void UpdateHandle()
|
|
{
|
|
if (_trackRect == null || _handleRect == null)
|
|
return;
|
|
|
|
if (!_isScrollable)
|
|
{
|
|
_handleRect.anchoredPosition = Vector2.zero;
|
|
return;
|
|
}
|
|
|
|
if (_useCustomHandleSizing)
|
|
{
|
|
var ratio = Mathf.Clamp01(_viewport.rect.height / (_scrollRange + _viewport.rect.height));
|
|
var height = Mathf.Max(_trackRect.rect.height * ratio, _minHandleHeight);
|
|
_handleRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, height);
|
|
}
|
|
|
|
var half = _handleRect.rect.height * 0.5f;
|
|
|
|
var min = _trackRect.rect.yMin + half;
|
|
var max = _trackRect.rect.yMax - half;
|
|
|
|
var y = Mathf.Lerp(max, min, Normalized);
|
|
|
|
var position = _handleRect.anchoredPosition;
|
|
position.y = y;
|
|
_handleRect.anchoredPosition = position;
|
|
}
|
|
|
|
private void UpdateTrackVisibility()
|
|
{
|
|
if (_trackRect == null)
|
|
return;
|
|
|
|
if (_hideTrackWhenNotScrollable)
|
|
_trackRect.gameObject.SetActive(_isScrollable);
|
|
}
|
|
|
|
private bool TryGetContentBounds(out float top, out float bottom)
|
|
{
|
|
top = float.MinValue;
|
|
bottom = float.MaxValue;
|
|
|
|
var found = false;
|
|
|
|
for (var i = 0; i < _content.childCount; i++)
|
|
{
|
|
var child = _content.GetChild(i) as RectTransform;
|
|
if (child == null || !child.gameObject.activeSelf)
|
|
continue;
|
|
|
|
child.GetWorldCorners(Corners);
|
|
|
|
for (var c = 0; c < 4; c++)
|
|
{
|
|
var local = _viewport.InverseTransformPoint(Corners[c]);
|
|
top = Mathf.Max(top, local.y);
|
|
bottom = Mathf.Min(bottom, local.y);
|
|
}
|
|
|
|
found = true;
|
|
}
|
|
|
|
return found;
|
|
}
|
|
}
|
|
} |