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 } }