You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
699 lines
16 KiB
699 lines
16 KiB
using UnityEngine; |
|
using UnityEngine.UI; |
|
using UnityEngine.EventSystems; |
|
using UnityEngine.Events; |
|
using System.Collections.Generic; |
|
using System; |
|
|
|
namespace UIWidgets |
|
{ |
|
/// <summary> |
|
/// Resizable event. |
|
/// </summary> |
|
[Serializable] |
|
public class ResizableEvent : UnityEvent<Resizable> |
|
{ |
|
|
|
} |
|
|
|
/// <summary> |
|
/// Resizable. |
|
/// N - north (top). |
|
/// S - south (bottom). |
|
/// E - east (right). |
|
/// W - west (left). |
|
/// </summary> |
|
[AddComponentMenu("UI/UIWidgets/Resizable")] |
|
public class Resizable : MonoBehaviour, |
|
IInitializePotentialDragHandler, IBeginDragHandler, IEndDragHandler, IDragHandler, |
|
IPointerEnterHandler, IPointerExitHandler |
|
{ |
|
/// <summary> |
|
/// Resize directions. |
|
/// </summary> |
|
[Serializable] |
|
public struct Directions { |
|
/// <summary> |
|
/// Allow resize from top. |
|
/// </summary> |
|
public bool Top; |
|
|
|
/// <summary> |
|
/// Allow resize from bottom. |
|
/// </summary> |
|
public bool Bottom; |
|
|
|
/// <summary> |
|
/// Allow resize from left. |
|
/// </summary> |
|
public bool Left; |
|
|
|
/// <summary> |
|
/// Allow resize from right. |
|
/// </summary> |
|
public bool Right; |
|
|
|
/// <summary> |
|
/// Initializes a new instance of the <see cref="UIWidgets.Resizable+Directions"/> struct. |
|
/// </summary> |
|
/// <param name="top">If set to <c>true</c> allow resize from top.</param> |
|
/// <param name="bottom">If set to <c>true</c> allow resize from bottom.</param> |
|
/// <param name="left">If set to <c>true</c> allow resize from left.</param> |
|
/// <param name="right">If set to <c>true</c> allow resize from right.</param> |
|
public Directions(bool top, bool bottom, bool left, bool right) |
|
{ |
|
Top = top; |
|
Bottom = bottom; |
|
Left = left; |
|
Right = right; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Active resize region. |
|
/// </summary> |
|
public struct Regions { |
|
/// <summary> |
|
/// The top. |
|
/// </summary> |
|
public bool Top; |
|
|
|
/// <summary> |
|
/// The bottom. |
|
/// </summary> |
|
public bool Bottom; |
|
|
|
/// <summary> |
|
/// The left. |
|
/// </summary> |
|
public bool Left; |
|
|
|
/// <summary> |
|
/// The right. |
|
/// </summary> |
|
public bool Right; |
|
|
|
/// <summary> |
|
/// NWSE |
|
/// </summary> |
|
/// <value><c>true</c> if cursor mode is NWSE; otherwise, <c>false</c>.</value> |
|
public bool NWSE { |
|
get { |
|
return (Top && Left) || (Bottom && Right); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// NESW. |
|
/// </summary> |
|
/// <value><c>true</c> if cursor mode is NESW; otherwise, <c>false</c>.</value> |
|
public bool NESW { |
|
get { |
|
return (Top && Right) || (Bottom && Left); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// NS |
|
/// </summary> |
|
/// <value><c>true</c> if cursor mode is NS; otherwise, <c>false</c>.</value> |
|
public bool NS { |
|
get { |
|
return (Top && !Right) || (Bottom && !Left); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// EW. |
|
/// </summary> |
|
/// <value><c>true</c> if cursor mode is EW; otherwise, <c>false</c>.</value> |
|
public bool EW { |
|
get { |
|
return (!Top && Right) || (!Bottom && Left); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Is any region active. |
|
/// </summary> |
|
/// <value><c>true</c> if any region active; otherwise, <c>false</c>.</value> |
|
public bool Active { |
|
get { |
|
return Top || Bottom || Left || Right; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Reset this instance. |
|
/// </summary> |
|
public void Reset() |
|
{ |
|
Top = false; |
|
Bottom = false; |
|
Left = false; |
|
Right = false; |
|
} |
|
|
|
/// <summary> |
|
/// Returns a string that represents the current object. |
|
/// </summary> |
|
/// <returns>A string that represents the current object.</returns> |
|
public override string ToString() |
|
{ |
|
return String.Format("Top: {0}; Bottom: {1}; Left: {2}; Right: {3}", Top, Bottom, Left, Right); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Is need to update RectTransform on Resize. |
|
/// </summary> |
|
[SerializeField] |
|
public bool UpdateRectTransform = true; |
|
|
|
/// <summary> |
|
/// Is need to update LayoutElement on Resize. |
|
/// </summary> |
|
[SerializeField] |
|
public bool UpdateLayoutElement = true; |
|
|
|
/// <summary> |
|
/// The active region in points from left or right border where resize allowed. |
|
/// </summary> |
|
[SerializeField] |
|
[Tooltip("Maximum padding from border where resize active.")] |
|
public float ActiveRegion = 5; |
|
|
|
/// <summary> |
|
/// The minimum size. |
|
/// </summary> |
|
[SerializeField] |
|
public Vector2 MinSize; |
|
|
|
/// <summary> |
|
/// The maximum size. |
|
/// </summary> |
|
[SerializeField] |
|
[Tooltip("Set 0 to unlimit.")] |
|
public Vector2 MaxSize; |
|
|
|
/// <summary> |
|
/// The keep aspect ratio. |
|
/// Aspect ratio applied after MinSize and MaxSize, so if RectTransform aspect ratio not equal MinSize and MaxSize aspect ratio then real size may be outside limit with one of axis. |
|
/// </summary> |
|
[SerializeField] |
|
public bool KeepAspectRatio; |
|
|
|
/// <summary> |
|
/// Resize directions. |
|
/// </summary> |
|
[SerializeField] |
|
public Directions ResizeDirections = new Directions(true, true, true, true); |
|
|
|
/// <summary> |
|
/// The current camera. For Screen Space - Overlay let it empty. |
|
/// </summary> |
|
[SerializeField] |
|
public Camera CurrentCamera; |
|
|
|
/// <summary> |
|
/// The cursor EW texture. |
|
/// </summary> |
|
[SerializeField] |
|
public Texture2D CursorEWTexture; |
|
|
|
/// <summary> |
|
/// The cursor EW hot spot. |
|
/// </summary> |
|
[SerializeField] |
|
public Vector2 CursorEWHotSpot = new Vector2(16, 16); |
|
|
|
/// <summary> |
|
/// The cursor NS texture. |
|
/// </summary> |
|
[SerializeField] |
|
public Texture2D CursorNSTexture; |
|
|
|
/// <summary> |
|
/// The cursor NS hot spot. |
|
/// </summary> |
|
[SerializeField] |
|
public Vector2 CursorNSHotSpot = new Vector2(16, 16); |
|
|
|
/// <summary> |
|
/// The cursor NESW texture. |
|
/// </summary> |
|
[SerializeField] |
|
public Texture2D CursorNESWTexture; |
|
|
|
/// <summary> |
|
/// The cursor NESW hot spot. |
|
/// </summary> |
|
[SerializeField] |
|
public Vector2 CursorNESWHotSpot = new Vector2(16, 16); |
|
|
|
/// <summary> |
|
/// The cursor NWSE texture. |
|
/// </summary> |
|
[SerializeField] |
|
public Texture2D CursorNWSETexture; |
|
|
|
/// <summary> |
|
/// The cursor NWSE hot spot. |
|
/// </summary> |
|
[SerializeField] |
|
public Vector2 CursorNWSEHotSpot = new Vector2(16, 16); |
|
|
|
/// <summary> |
|
/// The default cursor texture. |
|
/// </summary> |
|
[SerializeField] |
|
public Texture2D DefaultCursorTexture; |
|
|
|
/// <summary> |
|
/// The default cursor hot spot. |
|
/// </summary> |
|
[SerializeField] |
|
public Vector2 DefaultCursorHotSpot; |
|
|
|
/// <summary> |
|
/// OnStartResize event. |
|
/// </summary> |
|
public ResizableEvent OnStartResize = new ResizableEvent(); |
|
|
|
/// <summary> |
|
/// OnEndResize event. |
|
/// </summary> |
|
public ResizableEvent OnEndResize = new ResizableEvent(); |
|
|
|
RectTransform rectTransform; |
|
|
|
/// <summary> |
|
/// Gets the RectTransform. |
|
/// </summary> |
|
/// <value>RectTransform.</value> |
|
public RectTransform RectTransform { |
|
get { |
|
if (rectTransform==null) |
|
{ |
|
rectTransform = transform as RectTransform; |
|
} |
|
return rectTransform; |
|
} |
|
} |
|
|
|
LayoutElement layoutElement; |
|
|
|
/// <summary> |
|
/// Gets the LayoutElement. |
|
/// </summary> |
|
/// <value>LayoutElement.</value> |
|
public LayoutElement LayoutElement { |
|
get { |
|
if (layoutElement==null) |
|
{ |
|
layoutElement = GetComponent<LayoutElement>(); |
|
if (layoutElement==null) |
|
{ |
|
layoutElement = gameObject.AddComponent<LayoutElement>(); |
|
} |
|
} |
|
return layoutElement; |
|
} |
|
} |
|
|
|
Regions regions; |
|
Regions dragRegions; |
|
|
|
Canvas canvas; |
|
RectTransform canvasRect; |
|
|
|
bool processDrag; |
|
|
|
void Start() |
|
{ |
|
var layout = GetComponent<HorizontalOrVerticalLayoutGroup>(); |
|
if (layout) |
|
{ |
|
Utilites.UpdateLayout(layout); |
|
} |
|
|
|
Init(); |
|
} |
|
|
|
/// <summary> |
|
/// Raises the initialize potential drag event. |
|
/// </summary> |
|
/// <param name="eventData">Event data.</param> |
|
public void OnInitializePotentialDrag(PointerEventData eventData) |
|
{ |
|
Init(); |
|
} |
|
|
|
/// <summary> |
|
/// Init this instance. |
|
/// </summary> |
|
public void Init() |
|
{ |
|
canvasRect = Utilites.FindTopmostCanvas(transform) as RectTransform; |
|
canvas = canvasRect.GetComponent<Canvas>(); |
|
} |
|
|
|
protected static bool globalCursorSetted; |
|
protected bool cursorChanged; |
|
protected bool IsCursorOver; |
|
|
|
/// <summary> |
|
/// Called by a BaseInputModule when an OnPointerEnter event occurs. |
|
/// </summary> |
|
/// <param name="eventData">Event data.</param> |
|
public void OnPointerEnter(PointerEventData eventData) |
|
{ |
|
IsCursorOver = true; |
|
} |
|
|
|
/// <summary> |
|
/// Called by a BaseInputModule when an OnPointerExit event occurs. |
|
/// </summary> |
|
/// <param name="eventData">Event data.</param> |
|
public void OnPointerExit(PointerEventData eventData) |
|
{ |
|
IsCursorOver = false; |
|
|
|
globalCursorSetted = false; |
|
cursorChanged = false; |
|
Cursor.SetCursor(DefaultCursorTexture, DefaultCursorHotSpot, Utilites.GetCursorMode()); |
|
} |
|
|
|
void LateUpdate() |
|
{ |
|
if (globalCursorSetted && !cursorChanged) |
|
{ |
|
return ; |
|
} |
|
globalCursorSetted = false; |
|
if (!IsCursorOver) |
|
{ |
|
return ; |
|
} |
|
|
|
if (processDrag) |
|
{ |
|
return ; |
|
} |
|
if (!Input.mousePresent) |
|
{ |
|
return ; |
|
} |
|
|
|
Vector2 point; |
|
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(RectTransform, Input.mousePosition, CurrentCamera, out point)) |
|
{ |
|
return ; |
|
} |
|
var r = RectTransform.rect; |
|
if (!r.Contains(point)) |
|
{ |
|
regions.Reset(); |
|
UpdateCursor(); |
|
return ; |
|
} |
|
|
|
UpdateRegions(point); |
|
|
|
UpdateCursor(); |
|
} |
|
|
|
void UpdateRegions(Vector2 point) |
|
{ |
|
regions.Top = ResizeDirections.Top && CheckTop(point); |
|
regions.Bottom = ResizeDirections.Bottom && CheckBottom(point); |
|
regions.Left = ResizeDirections.Left && CheckLeft(point); |
|
regions.Right = ResizeDirections.Right && CheckRight(point); |
|
} |
|
|
|
void UpdateCursor() |
|
{ |
|
if (regions.NWSE) |
|
{ |
|
globalCursorSetted = true; |
|
cursorChanged = true; |
|
Cursor.SetCursor(CursorNWSETexture, CursorNWSEHotSpot, Utilites.GetCursorMode()); |
|
} |
|
else if (regions.NESW) |
|
{ |
|
globalCursorSetted = true; |
|
cursorChanged = true; |
|
Cursor.SetCursor(CursorNESWTexture, CursorNESWHotSpot, Utilites.GetCursorMode()); |
|
} |
|
else if (regions.NS) |
|
{ |
|
globalCursorSetted = true; |
|
cursorChanged = true; |
|
Cursor.SetCursor(CursorNSTexture, CursorNSHotSpot, Utilites.GetCursorMode()); |
|
} |
|
else if (regions.EW) |
|
{ |
|
globalCursorSetted = true; |
|
cursorChanged = true; |
|
Cursor.SetCursor(CursorEWTexture, CursorEWHotSpot, Utilites.GetCursorMode()); |
|
} |
|
else if (cursorChanged) |
|
{ |
|
globalCursorSetted = false; |
|
cursorChanged = false; |
|
Cursor.SetCursor(DefaultCursorTexture, DefaultCursorHotSpot, Utilites.GetCursorMode()); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Checks if point in the top region. |
|
/// </summary> |
|
/// <returns><c>true</c>, if point in the top region, <c>false</c> otherwise.</returns> |
|
/// <param name="point">Point.</param> |
|
bool CheckTop(Vector2 point) |
|
{ |
|
var rect = RectTransform.rect; |
|
|
|
rect.position = new Vector2(rect.position.x, rect.position.y + rect.height - ActiveRegion); |
|
rect.height = ActiveRegion; |
|
|
|
return rect.Contains(point); |
|
} |
|
|
|
/// <summary> |
|
/// Checks if point in the right region. |
|
/// </summary> |
|
/// <returns><c>true</c>, if right was checked, <c>false</c> otherwise.</returns> |
|
/// <param name="point">Point.</param> |
|
bool CheckBottom(Vector2 point) |
|
{ |
|
var rect = RectTransform.rect; |
|
rect.height = ActiveRegion; |
|
|
|
return rect.Contains(point); |
|
} |
|
|
|
/// <summary> |
|
/// Checks if point in the left region. |
|
/// </summary> |
|
/// <returns><c>true</c>, if point in the left region, <c>false</c> otherwise.</returns> |
|
/// <param name="point">Point.</param> |
|
bool CheckLeft(Vector2 point) |
|
{ |
|
var rect = RectTransform.rect; |
|
rect.width = ActiveRegion; |
|
|
|
return rect.Contains(point); |
|
} |
|
|
|
/// <summary> |
|
/// Checks if point in the right region. |
|
/// </summary> |
|
/// <returns><c>true</c>, if right was checked, <c>false</c> otherwise.</returns> |
|
/// <param name="point">Point.</param> |
|
bool CheckRight(Vector2 point) |
|
{ |
|
var rect = RectTransform.rect; |
|
|
|
rect.position = new Vector2(rect.position.x + rect.width - ActiveRegion, rect.position.y); |
|
rect.width = ActiveRegion; |
|
|
|
return rect.Contains(point); |
|
} |
|
|
|
/// <summary> |
|
/// Raises the begin drag event. |
|
/// </summary> |
|
/// <param name="eventData">Event data.</param> |
|
public void OnBeginDrag(PointerEventData eventData) |
|
{ |
|
Vector2 point; |
|
processDrag = false; |
|
|
|
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(RectTransform, eventData.pressPosition, eventData.pressEventCamera, out point)) |
|
{ |
|
return ; |
|
} |
|
|
|
UpdateRegions(point); |
|
|
|
processDrag = regions.Active; |
|
|
|
dragRegions = regions; |
|
|
|
UpdateCursor(); |
|
|
|
LayoutElement.preferredHeight = RectTransform.rect.height; |
|
LayoutElement.preferredWidth = RectTransform.rect.width; |
|
|
|
OnStartResize.Invoke(this); |
|
} |
|
|
|
void ResetCursor() |
|
{ |
|
globalCursorSetted = false; |
|
cursorChanged = false; |
|
|
|
Cursor.SetCursor(DefaultCursorTexture, DefaultCursorHotSpot, Utilites.GetCursorMode()); |
|
} |
|
|
|
/// <summary> |
|
/// Raises the end drag event. |
|
/// </summary> |
|
/// <param name="eventData">Event data.</param> |
|
public void OnEndDrag(PointerEventData eventData) |
|
{ |
|
ResetCursor(); |
|
|
|
processDrag = false; |
|
|
|
OnEndResize.Invoke(this); |
|
} |
|
|
|
/// <summary> |
|
/// Raises the drag event. |
|
/// </summary> |
|
/// <param name="eventData">Event data.</param> |
|
public void OnDrag(PointerEventData eventData) |
|
{ |
|
if (!processDrag) |
|
{ |
|
return ; |
|
} |
|
if (canvas==null) |
|
{ |
|
throw new MissingComponentException(gameObject.name + " not in Canvas hierarchy."); |
|
} |
|
|
|
Vector2 p1; |
|
RectTransformUtility.ScreenPointToLocalPointInRectangle(RectTransform, eventData.position, CurrentCamera, out p1); |
|
Vector2 p2; |
|
RectTransformUtility.ScreenPointToLocalPointInRectangle(RectTransform, eventData.position - eventData.delta, CurrentCamera, out p2); |
|
var delta = p1 - p2; |
|
|
|
if (UpdateRectTransform) |
|
{ |
|
PerformUpdateRectTransform(delta); |
|
} |
|
if (UpdateLayoutElement) |
|
{ |
|
PerformUpdateLayoutElement(delta); |
|
} |
|
} |
|
|
|
void PerformUpdateRectTransform(Vector2 delta) |
|
{ |
|
var pivot = RectTransform.pivot; |
|
var size = RectTransform.rect.size; |
|
var prev_size = size; |
|
var sign = new Vector2(1, 1); |
|
if (dragRegions.Left || dragRegions.Right) |
|
{ |
|
sign.x = dragRegions.Right ? +1 : -1; |
|
size.x = Mathf.Max(MinSize.x, size.x + (sign.x * delta.x)); |
|
if (MaxSize.x!=0f) |
|
{ |
|
size.x = Mathf.Min(MaxSize.x, size.x); |
|
} |
|
} |
|
if (dragRegions.Top || dragRegions.Bottom) |
|
{ |
|
sign.y = dragRegions.Top ? +1 : -1; |
|
size.y = Mathf.Max(MinSize.y, size.y + (sign.y * delta.y)); |
|
if (MaxSize.y!=0f) |
|
{ |
|
size.y = Mathf.Min(MaxSize.y, size.y); |
|
} |
|
} |
|
|
|
if (KeepAspectRatio) |
|
{ |
|
size = FixAspectRatio(size, prev_size); |
|
} |
|
|
|
var anchorSign = new Vector2(dragRegions.Right ? pivot.x : pivot.x - 1, dragRegions.Top ? pivot.y : pivot.y - 1); |
|
var anchorDelta = size - prev_size; |
|
anchorDelta = new Vector2(anchorDelta.x * anchorSign.x, anchorDelta.y * anchorSign.y); |
|
|
|
RectTransform.anchoredPosition += anchorDelta; |
|
RectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, size.x); |
|
RectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, size.y); |
|
} |
|
|
|
/// <summary> |
|
/// Fixs the aspect ratio. |
|
/// </summary> |
|
/// <returns>The aspect ratio.</returns> |
|
/// <param name="newSize">New size.</param> |
|
/// <param name="baseSize">Base size.</param> |
|
protected Vector2 FixAspectRatio(Vector2 newSize, Vector2 baseSize) |
|
{ |
|
var result = newSize; |
|
|
|
var aspectRatio = baseSize.x / baseSize.y; |
|
var sizeDelta = new Vector2(Mathf.Abs(newSize.x - baseSize.x), Mathf.Abs(newSize.y - baseSize.y)); |
|
if (sizeDelta.x >= sizeDelta.y) |
|
{ |
|
result.y = result.x / aspectRatio; |
|
} |
|
else |
|
{ |
|
result.x = result.y * aspectRatio; |
|
} |
|
|
|
return result; |
|
} |
|
|
|
void PerformUpdateLayoutElement(Vector2 delta) |
|
{ |
|
var size = new Vector2(LayoutElement.preferredWidth, LayoutElement.preferredHeight); |
|
var prev_size = size; |
|
if (dragRegions.Left || dragRegions.Right) |
|
{ |
|
var sign = (dragRegions.Right) ? +1 : -1; |
|
var width = Mathf.Max(MinSize.x, LayoutElement.preferredWidth + (sign * delta.x)); |
|
if (MaxSize.x!=0f) |
|
{ |
|
size.x = Mathf.Min(MaxSize.x, width); |
|
} |
|
} |
|
if (dragRegions.Top || dragRegions.Bottom) |
|
{ |
|
var sign = (dragRegions.Top) ? +1 : -1; |
|
var height = Mathf.Max(MinSize.y, LayoutElement.preferredHeight + (sign * delta.y)); |
|
if (MaxSize.y!=0f) |
|
{ |
|
size.y = Mathf.Min(MaxSize.y, height); |
|
} |
|
} |
|
|
|
if (KeepAspectRatio) |
|
{ |
|
size = FixAspectRatio(size, prev_size); |
|
} |
|
|
|
LayoutElement.preferredWidth = size.x; |
|
LayoutElement.preferredHeight = size.y; |
|
} |
|
} |
|
} |