namespace UIWidgets
{
using System;
using System.Collections.Generic;
using UIWidgets.Attributes;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
///
/// Rotatable.
/// N - north (top).
/// S - south (bottom).
/// E - east (right).
/// W - west (left).
///
[AddComponentMenu("UI/New UI Widgets/Interactions/Rotatable")]
[DisallowMultipleComponent]
[RequireComponent(typeof(RectTransform))]
public class Rotatable : UIBehaviourConditional,
IBeginDragHandler, IEndDragHandler, IDragHandler,
IPointerEnterHandler, IPointerExitHandler,
ILateUpdatable
{
///
/// Rotation directions.
///
[Serializable]
public struct Directions : IEquatable
{
///
/// Allow rotate with top left corner.
///
public bool TopLeft;
///
/// Allow rotate with top right corner.
///
public bool TopRight;
///
/// Allow rotate with bottom left corner.
///
public bool BottomLeft;
///
/// Allow rotate with bottom right corner.
///
public bool BottomRight;
///
/// Initializes a new instance of the struct.
///
/// If set to true allow rotation from top left corner.
/// If set to true allow rotation from top right corner.
/// If set to true allow rotation from bottom left corner.
/// If set to true allow rotation from bottom right corner.
public Directions(bool topLeft = true, bool topRight = true, bool bottomLeft = true, bool bottomRight = true)
{
TopLeft = topLeft;
TopRight = topRight;
BottomLeft = bottomLeft;
BottomRight = bottomRight;
}
///
/// Gets a value indicating whether any direction is allowed.
///
/// true if active; otherwise, false.
public bool Active
{
get
{
return TopLeft || TopRight || BottomLeft || BottomRight;
}
}
///
/// North-West or South-East.
///
/// true if allowed direction is NWSE; otherwise, false.
public bool NWSE
{
get
{
return TopLeft || BottomRight;
}
}
///
/// North-East or South-West.
///
/// true if allowed direction is NESW; otherwise, false.
public bool NESW
{
get
{
return TopRight || BottomLeft;
}
}
///
/// Determines whether the specified object is equal to the current object.
///
/// The object to compare with the current object.
/// true if the specified object is equal to the current object; otherwise, false.
public override bool Equals(object obj)
{
if (obj is Directions)
{
return Equals((Directions)obj);
}
return false;
}
///
/// Determines whether the specified object is equal to the current object.
///
/// The object to compare with the current object.
/// true if the specified object is equal to the current object; otherwise, false.
public bool Equals(Directions other)
{
return TopLeft == other.TopLeft
&& TopRight == other.TopRight
&& BottomLeft == other.BottomLeft
&& BottomRight == other.BottomRight;
}
///
/// Hash function.
///
/// A hash code for the current object.
public override int GetHashCode()
{
var code = 0;
if (TopLeft)
{
code += 1;
}
if (TopRight)
{
code += 2;
}
if (BottomLeft)
{
code += 4;
}
if (BottomRight)
{
code += 8;
}
return code;
}
///
/// Compare specified colors.
///
/// First direction.
/// Second direction.
/// true if the directions are equal; otherwise, false.
public static bool operator ==(Directions directions1, Directions directions2)
{
return directions1.Equals(directions2);
}
///
/// Compare specified directions.
///
/// First direction.
/// Second direction.
/// true if the directions not equal; otherwise, false.
public static bool operator !=(Directions directions1, Directions directions2)
{
return !directions1.Equals(directions2);
}
}
#region Interactable
[SerializeField]
bool interactable = true;
///
/// Is widget interactable.
///
/// true if interactable; otherwise, false.
public bool Interactable
{
get
{
return interactable;
}
set
{
if (interactable != value)
{
interactable = value;
InteractableChanged();
}
}
}
///
/// If the canvas groups allow interaction.
///
protected bool GroupsAllowInteraction = true;
///
/// The CanvasGroup cache.
///
protected List CanvasGroupCache = new List();
///
/// Process the CanvasGroupChanged event.
///
protected override void OnCanvasGroupChanged()
{
var groupAllowInteraction = true;
var t = transform;
while (t != null)
{
t.GetComponents(CanvasGroupCache);
var shouldBreak = false;
foreach (var canvas_group in CanvasGroupCache)
{
if (!canvas_group.interactable)
{
groupAllowInteraction = false;
shouldBreak = true;
}
shouldBreak |= canvas_group.ignoreParentGroups;
}
if (shouldBreak)
{
break;
}
t = t.parent;
}
if (groupAllowInteraction != GroupsAllowInteraction)
{
GroupsAllowInteraction = groupAllowInteraction;
InteractableChanged();
}
}
///
/// Returns true if the GameObject and the Component are active.
///
/// true if the GameObject and the Component are active; otherwise false.
public override bool IsActive()
{
return base.IsActive() && GroupsAllowInteraction && Interactable;
}
///
/// Is instance interactable?
///
/// true if instance interactable; otherwise false
public bool IsInteractable()
{
return GroupsAllowInteraction && Interactable;
}
///
/// Process interactable change.
///
protected virtual void InteractableChanged()
{
if (!base.IsActive())
{
return;
}
OnInteractableChange(GroupsAllowInteraction && Interactable);
}
///
/// Process interactable change.
///
/// Current interactable state.
protected virtual void OnInteractableChange(bool interactableState)
{
if (!interactableState)
{
if (IsCursorOver)
{
IsCursorOver = false;
if (!processDrag)
{
ResetCursor();
}
}
}
}
#endregion
///
/// Rotate directions.
///
[SerializeField]
public Directions RotateDirections = new Directions(true, true, true, true);
///
/// The active region in points from left or right border where rotation allowed.
///
[SerializeField]
[Tooltip("Maximum padding from border where rotation allowed.")]
public float ActiveRegion = 5;
///
/// Limit rotation angle.
///
[SerializeField]
public bool LimitRotation = false;
[SerializeField]
[Tooltip("Allowed value is in range [-180..180]")]
[EditorConditionBool("LimitRotation")]
float angleMin = -180f;
///
/// The minimum angle.
/// Allowed value is in range [-180..180].
///
public float AngleMin
{
get
{
return angleMin;
}
set
{
angleMin = Mathf.Clamp(value, -180f, 180f);
}
}
[SerializeField]
[Tooltip("Allowed value is in range [-180..180]")]
[EditorConditionBool("LimitRotation")]
float angleMax = 180f;
///
/// The maximum angle.
/// Allowed value is in range [-180..180].
///
public float AngleMax
{
get
{
return angleMax;
}
set
{
angleMax = Mathf.Clamp(value, -180f, 180f);
}
}
[SerializeField]
[Tooltip("Allowed value is in range [0..180). Set 0 to disable.")]
float angleStep = 1f;
///
/// Angle step.
///
public float AngleStep
{
get
{
return angleStep;
}
set
{
angleStep = Mathf.Clamp(value, 0f, 180f);
}
}
///
/// The current camera. For Screen Space - Overlay let it empty.
///
[NonSerialized]
protected Camera CurrentCamera;
///
/// Cursors.
///
[SerializeField]
public Cursors Cursors;
///
/// The cursor NW texture.
///
[SerializeField]
[EditorConditionBlock("Cursors")]
[Obsolete("Replaced with Cursors and UICursor.Cursors.")]
public Texture2D CursorTopLeftTexture;
///
/// The cursor NW hot spot.
///
[SerializeField]
[EditorConditionBlock("Cursors")]
[Obsolete("Replaced with Cursors and UICursor.Cursors.")]
public Vector2 CursorTopLeftHotSpot = new Vector2(16, 16);
///
/// The cursor NE texture.
///
[SerializeField]
[EditorConditionBlock("Cursors")]
[Obsolete("Replaced with Cursors and UICursor.Cursors.")]
public Texture2D CursorTopRightTexture;
///
/// The cursor NE hot spot.
///
[SerializeField]
[EditorConditionBlock("Cursors")]
[Obsolete("Replaced with Cursors and UICursor.Cursors.")]
public Vector2 CursorTopRightHotSpot = new Vector2(16, 16);
///
/// The cursor SW texture.
///
[SerializeField]
[EditorConditionBlock("Cursors")]
[Obsolete("Replaced with Cursors and UICursor.Cursors.")]
public Texture2D CursorBottomLeftTexture;
///
/// The cursor SW hot spot.
///
[SerializeField]
[EditorConditionBlock("Cursors")]
[Obsolete("Replaced with Cursors and UICursor.Cursors.")]
public Vector2 CursorBottomLeftHotSpot = new Vector2(16, 16);
///
/// The cursor SE texture.
///
[SerializeField]
[EditorConditionBlock("Cursors")]
[Obsolete("Replaced with Cursors and UICursor.Cursors.")]
public Texture2D CursorBottomRightTexture;
///
/// The cursor SE hot spot.
///
[SerializeField]
[EditorConditionBlock("Cursors")]
[Obsolete("Replaced with Cursors and UICursor.Cursors.")]
public Vector2 CursorBottomRightHotSpot = new Vector2(16, 16);
///
/// The default cursor texture.
///
[SerializeField]
[EditorConditionBlock("Cursors")]
[Obsolete("Replaced with Cursors and UICursor.Cursors.")]
public Texture2D DefaultCursorTexture;
///
/// The default cursor hot spot.
///
[SerializeField]
[EditorConditionBlock("Cursors")]
[Obsolete("Replaced with Cursors and UICursor.Cursors.")]
public Vector2 DefaultCursorHotSpot;
///
/// Start rotation event.
///
[SerializeField]
public RotatableEvent OnStartRotate = new RotatableEvent();
///
/// During rotation event.
///
[SerializeField]
public RotatableEvent OnRotate = new RotatableEvent();
///
/// End rotation event.
///
[SerializeField]
public RotatableEvent OnEndRotate = new RotatableEvent();
RectTransform rectTransform;
///
/// Target.
///
public RectTransform RectTransform
{
get
{
if (rectTransform == null)
{
rectTransform = transform as RectTransform;
}
return rectTransform;
}
}
///
/// Is target is self?
///
protected bool IsTargetSelf;
RectTransform target;
///
/// Target.
///
public RectTransform Target
{
get
{
if (target == null)
{
IsTargetSelf = true;
target = transform as RectTransform;
}
return target;
}
set
{
if (value == null)
{
throw new ArgumentNullException("value");
}
IsTargetSelf = value.GetInstanceID() == transform.GetInstanceID();
target = value;
if (!IsTargetSelf)
{
var le = Utilities.GetOrAddComponent(this);
le.ignoreLayout = true;
RectTransform.SetParent(target.parent, false);
CopyRectTransformValues();
}
}
}
///
/// Current drag regions.
///
protected Resizable.Regions regions;
///
/// Drag regions.
///
protected Resizable.Regions dragRegions;
///
/// Allow to handle drag event.
///
protected bool processDrag;
///
/// Start this instance.
///
protected override void Start()
{
base.Start();
Init();
}
bool isInited;
///
/// Init this instance.
///
public virtual void Init()
{
if (isInited)
{
return;
}
isInited = true;
#pragma warning disable 0618
if (DefaultCursorTexture != null)
{
UICursor.ObsoleteWarning();
}
#pragma warning restore 0618
}
///
/// Can change cursor?
///
protected bool CanChangeCursor
{
get
{
return UICursor.CanSet(this) && CompatibilityInput.MousePresent && IsCursorOver;
}
}
///
/// The cursor changed.
///
protected bool cursorChanged;
///
/// Is cursor over.
///
protected bool IsCursorOver;
///
/// Start rotation angle.
///
protected float StartAngle;
///
/// Called by a BaseInputModule when an OnPointerEnter event occurs.
///
/// Event data.
public void OnPointerEnter(PointerEventData eventData)
{
if (!IsActive())
{
return;
}
CurrentCamera = eventData.pressEventCamera;
IsCursorOver = true;
}
///
/// Called by a BaseInputModule when an OnPointerExit event occurs.
///
/// Event data.
public void OnPointerExit(PointerEventData eventData)
{
IsCursorOver = false;
if (!processDrag)
{
ResetCursor();
}
}
///
/// Process application focus event.
///
/// Application has focus?
protected virtual void OnApplicationFocus(bool hasFocus)
{
if (!hasFocus)
{
IsCursorOver = false;
}
}
///
/// Process the enable event.
///
protected override void OnEnable()
{
base.OnEnable();
Updater.AddLateUpdate(this);
}
///
/// Process the disable event.
///
protected override void OnDisable()
{
base.OnDisable();
Updater.RemoveLateUpdate(this);
}
///
/// Late update.
///
public virtual void RunLateUpdate()
{
if (!IsActive())
{
return;
}
if (!CanChangeCursor)
{
return;
}
if (CompatibilityInput.IsMouseLeftButtonPressed)
{
return;
}
if (processDrag)
{
return;
}
Vector2 point;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(Target, CompatibilityInput.MousePosition, CurrentCamera, out point))
{
return;
}
var r = Target.rect;
if (r.Contains(point))
{
UpdateRegions(point);
}
else
{
regions.Reset();
}
UpdateCursor();
}
void UpdateRegions(Vector2 point)
{
regions.Top = CheckTop(point);
regions.Bottom = CheckBottom(point);
regions.Left = CheckLeft(point);
regions.Right = CheckRight(point);
}
///
/// Updates the cursor.
///
protected virtual void UpdateCursor()
{
if (regions.TopLeft && RotateDirections.TopLeft)
{
cursorChanged = true;
UICursor.Set(this, GetTopLeftCursor());
}
else if (regions.TopRight && RotateDirections.TopRight)
{
cursorChanged = true;
UICursor.Set(this, GetTopRightCursor());
}
else if (regions.BottomLeft && RotateDirections.BottomLeft)
{
cursorChanged = true;
UICursor.Set(this, GetBottomLeftCursor());
}
else if (regions.BottomRight && RotateDirections.BottomRight)
{
cursorChanged = true;
UICursor.Set(this, GetBottomRightCursor());
}
else if (cursorChanged)
{
ResetCursor();
}
}
///
/// Get top left cursor.
///
/// Cursor.
protected virtual Cursors.Cursor GetTopLeftCursor()
{
if (Cursors != null)
{
return Cursors.NorthWestRotateArrow;
}
if (UICursor.Cursors != null)
{
return UICursor.Cursors.NorthWestRotateArrow;
}
return default(Cursors.Cursor);
}
///
/// Get top right cursor.
///
/// Cursor.
protected virtual Cursors.Cursor GetTopRightCursor()
{
if (Cursors != null)
{
return Cursors.NorthEastRotateArrow;
}
if (UICursor.Cursors != null)
{
return UICursor.Cursors.NorthEastRotateArrow;
}
return default(Cursors.Cursor);
}
///
/// Get bottom left cursor.
///
/// Cursor.
protected virtual Cursors.Cursor GetBottomLeftCursor()
{
if (Cursors != null)
{
return Cursors.SouthWestRotateArrow;
}
if (UICursor.Cursors != null)
{
return UICursor.Cursors.SouthWestRotateArrow;
}
return default(Cursors.Cursor);
}
///
/// Get bottom right cursor.
///
/// Cursor.
protected virtual Cursors.Cursor GetBottomRightCursor()
{
if (Cursors != null)
{
return Cursors.SouthEastRotateArrow;
}
if (UICursor.Cursors != null)
{
return UICursor.Cursors.SouthEastRotateArrow;
}
return default(Cursors.Cursor);
}
///
/// Checks if point in the top region.
///
/// true, if point in the top region, false otherwise.
/// Point.
bool CheckTop(Vector2 point)
{
var rect = Target.rect;
rect.position = new Vector2(rect.position.x, rect.position.y + rect.height - ActiveRegion);
rect.height = ActiveRegion;
return rect.Contains(point);
}
///
/// Checks if point in the right region.
///
/// true, if right was checked, false otherwise.
/// Point.
bool CheckBottom(Vector2 point)
{
var rect = Target.rect;
rect.height = ActiveRegion;
return rect.Contains(point);
}
///
/// Checks if point in the left region.
///
/// true, if point in the left region, false otherwise.
/// Point.
bool CheckLeft(Vector2 point)
{
var rect = Target.rect;
rect.width = ActiveRegion;
return rect.Contains(point);
}
///
/// Checks if point in the right region.
///
/// true, if right was checked, false otherwise.
/// Point.
bool CheckRight(Vector2 point)
{
var rect = Target.rect;
rect.position = new Vector2(rect.position.x + rect.width - ActiveRegion, rect.position.y);
rect.width = ActiveRegion;
return rect.Contains(point);
}
///
/// Process the begin drag event.
///
/// Event data.
public virtual void OnBeginDrag(PointerEventData eventData)
{
if (!IsActive())
{
return;
}
Vector2 point;
processDrag = false;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(Target, eventData.pressPosition, eventData.pressEventCamera, out point))
{
return;
}
UpdateRegions(point);
processDrag = IsAllowedDrag();
dragRegions = regions;
UpdateCursor();
if (processDrag)
{
var angle = 0f;
if (regions.TopLeft)
{
angle = -135f;
}
else if (regions.TopRight)
{
angle = -45f;
}
else if (regions.BottomLeft && RotateDirections.BottomLeft)
{
angle = 135f;
}
else if (regions.BottomRight && RotateDirections.BottomRight)
{
angle = 45f;
}
InitRotate(angle);
OnStartRotate.Invoke(this);
if (!eventData.used)
{
eventData.Use();
}
}
}
///
/// Determines whether drag allowed with current active regions and specified directions.
///
/// true if this instance is allowed drag; otherwise, false.
protected virtual bool IsAllowedDrag()
{
return (regions.TopLeft && RotateDirections.TopLeft)
|| (regions.BottomRight && RotateDirections.BottomRight)
|| (regions.TopRight && RotateDirections.TopRight)
|| (regions.BottomLeft && RotateDirections.BottomLeft);
}
void ResetCursor()
{
cursorChanged = false;
UICursor.Reset(this);
}
///
/// Process the end drag event.
///
/// Event data.
public virtual void OnEndDrag(PointerEventData eventData)
{
ResetCursor();
if (processDrag)
{
processDrag = false;
OnEndRotate.Invoke(this);
}
}
///
/// Process the drag event.
///
/// Event data.
public virtual void OnDrag(PointerEventData eventData)
{
if (!processDrag)
{
return;
}
if (eventData.used)
{
return;
}
eventData.Use();
Rotate(eventData);
}
///
/// Init rotation.
///
/// Start angle for the rotation.
public virtual void InitRotate(float startAngle)
{
StartAngle = startAngle;
}
///
/// Rotate this instance using specified event data.
///
/// Event data.
public virtual void Rotate(PointerEventData eventData)
{
Vector2 point;
Target.localRotation = Quaternion.Euler(Vector3.zero);
RectTransformUtility.ScreenPointToLocalPointInRectangle(Target, eventData.position, eventData.pressEventCamera, out point);
var base_angle = DragPoint2Angle(point);
Rotate(base_angle);
OnRotate.Invoke(this);
}
///
/// Rotate.
///
/// Base angle.
public virtual void Rotate(float baseAngle)
{
var angle = ProcessAngle(baseAngle);
var rotation = Target.localRotation.eulerAngles;
rotation.z = angle;
Target.localRotation = Quaternion.Euler(rotation);
CopyRectTransformValues();
}
///
/// Process angle value.
///
/// Initial angle.
/// Angle with applied limits and step.
protected float ProcessAngle(float baseAngle)
{
var angle = (baseAngle + StartAngle) % 360f;
if (angle > 180f)
{
angle -= 360f;
}
if (angle <= -180f)
{
angle += 360f;
}
if (AngleStep > 0)
{
angle = Mathf.Round(angle / AngleStep) * AngleStep;
}
if (LimitRotation)
{
angle = ClampAngle(angle);
}
return angle;
}
///
/// Get shortest distance between angle.
///
/// First angle.
/// Second angle.
/// Shortest distance.
protected virtual float AngleDistance(float a, float b)
{
var d = Mathf.Abs(a - b);
if (d > 180f)
{
d = 360f - d;
}
return d;
}
///
/// Clamp angle.
///
/// Angle.
/// Angle limited by AngleMin and AngleMax.
protected float ClampAngle(float angle)
{
if ((angle >= AngleMin) && (angle <= AngleMax))
{
return angle;
}
var distance2min = AngleDistance(angle + 180f, AngleMin + 180f);
var distance2max = AngleDistance(angle + 180f, AngleMax + 180f);
return (distance2min < distance2max) ? AngleMin : AngleMax;
}
///
/// Convert drag point to angle.
///
/// Point.
/// Angle.
protected float DragPoint2Angle(Vector2 point)
{
var size = Target.rect.size;
var relative = new Vector2(
point.x + (size.x * (Target.pivot.x - 0.5f)),
point.y + (size.y * (Target.pivot.y - 0.5f)));
return Point2Angle(relative);
}
///
/// Convert point to the angle.
///
/// Point.
/// Angle in range [-180f, 180f].
public static float Point2Angle(Vector2 point)
{
var angle_rad = Mathf.Atan2(point.y, point.x);
var angle = angle_rad * Mathf.Rad2Deg;
angle %= 360f;
if (angle > 180f)
{
angle -= 360f;
}
return angle;
}
///
/// Copy RectTransform values.
///
protected void CopyRectTransformValues()
{
if (!IsTargetSelf)
{
UtilitiesRectTransform.CopyValues(Target, RectTransform);
}
}
#if UNITY_EDITOR
///
/// Reset this instance.
///
protected override void Reset()
{
CursorsDPISelector.Require(this);
base.Reset();
}
///
/// Validate this instance.
///
protected override void OnValidate()
{
base.OnValidate();
AngleMin = angleMin;
AngleMax = angleMax;
AngleStep = angleStep;
}
#endif
}
}