// Copyright (c) 2015 - 2023 Doozy Entertainment. All Rights Reserved. // This code can only be used under the standard Unity Asset Store End User License Agreement // A Copy of the EULA APPENDIX 1 is available at http://unity3d.com/company/legal/as_terms using System; using System.Collections.Generic; using System.Linq; using Doozy.Runtime.Common.Attributes; using Doozy.Runtime.Common.Extensions; using Doozy.Runtime.Common.Utils; using Doozy.Runtime.Global; using Doozy.Runtime.Reactor.Reactions; using Doozy.Runtime.Signals; using Doozy.Runtime.UIManager.Components; using Doozy.Runtime.UIManager.Containers.Internal; using Doozy.Runtime.UIManager.Input; using Doozy.Runtime.UIManager.ScriptableObjects; using Doozy.Runtime.UIManager.Triggers; using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.EventSystems; using UnityEngine.UI; #if INPUT_SYSTEM_PACKAGE using UnityEngine.InputSystem.UI; #endif // ReSharper disable MemberCanBePrivate.Global // ReSharper disable UnusedMember.Global // ReSharper disable PartialTypeWithSinglePart namespace Doozy.Runtime.UIManager.Containers { /// /// Specialized container that behaves like a tooltip, /// with parenting, tracking and positioning options. /// [AddComponentMenu("Doozy/UI/Containers/UITooltip")] [DisallowMultipleComponent] public partial class UITooltip : UIContainerComponent { #if UNITY_EDITOR [UnityEditor.MenuItem("GameObject/Doozy/UI/Containers/UITooltip", false, 8)] private static void CreateComponent(UnityEditor.MenuCommand menuCommand) { GameObjectUtils.AddToScene("UITooltip", false, true); } #endif /// Describes where a tooltip can be instantiated under public enum Parenting { /// The parent of the tooltip will be the TooltipCanvas TooltipsCanvas = 0, /// The parent of the tooltip will be the RectTransform of UITooltipTrigger that triggered the tooltip TooltipTrigger = 1, /// The parent of the tooltip will be the RectTransform of the GameObject that has the given UITagId UITag = 2 } /// Describes the how the tooltip behaves when it is shown public enum Tracking { /// The tooltip will be shown at a predefined position and it will stay there until it is hidden Disabled = 0, /// The tooltip will follow the pointer until it is hidden FollowPointer = 1, /// The tooltip will follow the GameObject of the trigger until it is hidden FollowTrigger = 2, /// The tooltip will follow the GameObject of the given UITag until it is hidden FollowTarget = 3 } /// Describes where the tooltip should be positioned relative to the tracked target public enum Positioning { TopLeft = 0, TopCenter = 1, TopRight = 2, MiddleLeft = 3, MiddleCenter = 4, MiddleRight = 5, BottomLeft = 6, BottomCenter = 7, BottomRight = 8 } /// Maximum sorting order value for tooltips public const int k_MaxSortingOrder = 32767; /// Default tooltip name public const string k_DefaultTooltipName = "None"; /// Default category used by the UITagId to identify the default Tooltip Canvas public const string k_DefaultTooltipCanvasUITagCategory = "UITooltip"; /// Default name used by the UITagId to identify the default Tooltip Canvas public const string k_DefaultTooltipCanvasUITagName = "Canvas"; /// Get all the visible tooltips (returns all tooltips that are either in the isVisible or isShowing state) public static IEnumerable visibleTooltips => database.Where(item => item.isVisible || item.isShowing); private LayoutElement m_LayoutElement; /// Reference to the LayoutElement attached to this GameObject public LayoutElement layoutElement => m_LayoutElement ? m_LayoutElement : m_LayoutElement = GetComponent() ?? gameObject.AddComponent(); /// Internal static reference to the default canvas used to display tooltips [ClearOnReload] private static Canvas s_tooltipsCanvas; /// /// Static reference to the default canvas used to display tooltips (tooltips get parented to this canvas). /// You can override the default canvas by referencing another canvas (at runtime) to be used as the default canvas. /// public static Canvas tooltipsCanvas { get { if (s_tooltipsCanvas != null) return s_tooltipsCanvas; //look for UITag UITag uiTag = UITag.GetTags(k_DefaultTooltipCanvasUITagCategory, k_DefaultTooltipCanvasUITagName).FirstOrDefault(); if (uiTag != null) { s_tooltipsCanvas = uiTag.GetComponent(); if (s_tooltipsCanvas != null) return s_tooltipsCanvas; } //create Tooltip Canvas s_tooltipsCanvas = new GameObject("Tooltips Canvas").AddComponent(); s_tooltipsCanvas.renderMode = RenderMode.ScreenSpaceOverlay; s_tooltipsCanvas.overrideSorting = true; s_tooltipsCanvas.sortingOrder = k_MaxSortingOrder; //add the default tag uiTag = s_tooltipsCanvas.gameObject.AddComponent(); uiTag.Id.Category = k_DefaultTooltipCanvasUITagCategory; uiTag.Id.Name = k_DefaultTooltipCanvasUITagName; return s_tooltipsCanvas; } set => s_tooltipsCanvas = value; } #region Tooltip rootCanvas rootCanvasRectTransform private Canvas m_TooltipRootCanvas; /// Internal reference to the root canvas of this tooltip internal Canvas tooltipRootCanvas { get => m_TooltipRootCanvas; set { tooltipRootCanvasRectTransform = null; m_TooltipRootCanvas = value; if (value == null) return; tooltipRootCanvasRectTransform = value.GetComponent(); } } /// Internal reference to the RectTransform of the root canvas of this tooltip internal RectTransform tooltipRootCanvasRectTransform { get; private set; } #endregion #region Target RectTransform rootCanvas rootCanvasRectTransform private RectTransform m_TargetRectTransform; /// Internal reference to the current target RectTransform internal RectTransform targetRectTransform { get => m_TargetRectTransform; set { targetRootCanvas = null; targetRootCanvasRectTransform = null; m_TargetRectTransform = value; if (value == null) return; targetRootCanvas = value.GetComponentInParent().rootCanvas; targetRootCanvasRectTransform = targetRootCanvas.GetComponent(); } } /// Internal reference to the current target root canvas internal Canvas targetRootCanvas { get; private set; } /// Internal reference to the current target root canvas RectTransform internal RectTransform targetRootCanvasRectTransform { get; private set; } #endregion /// List of all the labels this UITooltip has public List Labels = new List(); /// TRUE if the tooltip has at least one TextMeshProUGUI label reference public bool hasLabels => Labels.RemoveNulls().Count > 0; /// List of all the images this UITooltip has public List Images = new List(); /// TRUE if the tooltip has at least one Image reference public bool hasImages => Images.RemoveNulls().Count > 0; /// List of all the buttons this UITooltip has public List Buttons = new List(); /// TRUE if the tooltip has at least one UIButton reference public bool hasButtons => Buttons.RemoveNulls().Count > 0; /// Tooltip rectTransform.rect public Rect rect => rectTransform.rect; /// Tooltip rectTransform.rect.width public float width => rect.width; /// Tooltip rectTransform.rect.height public float height => rect.height; /// Tooltip rectTransform.pivot.x public float pivotX => rectTransform.pivot.x; /// Tooltip rectTransform.pivot.y public float pivotY => rectTransform.pivot.y; /// Set where a tooltip should be instantiated under public Parenting ParentMode = Parenting.TooltipsCanvas; /// Set how the tooltip behaves when it is shown public Tracking TrackingMode = Tracking.Disabled; /// Set where the tooltip should be positioned relative to the tracked target public Positioning PositionMode = Positioning.MiddleCenter; /// /// Id used to identify the designated parent where the tooltip should be parented under, /// after is has been instantiated /// public UITagId ParentTag; /// /// Id used to identify the follow target when the tracking mode is set to FollowTarget /// public UITagId FollowTag; /// /// Set the offset applied to the tooltip position, /// after all the positioning has been applied /// public Vector3 PositionOffset = Vector3.zero; /// /// Set a maximum width for the tooltip. /// If the value is 0, the tooltip will be automatically sized to fit the content") /// public float MaximumWidth; /// /// Keep the tooltip in screen at all times, while it is shown /// public bool KeepInScreen = true; /// /// Enable override sorting and set the sorting order to the maximum value, /// for the Canvas component attached to this tooltip /// public bool OverrideSorting = true; /// /// Hide (close) the tooltip when the user clicks on any of the UIButton references. /// At runtime, a 'hide tooltip' event will be added to all the referenced UIButtons (if any). /// public bool HideOnAnyButton = true; /// Set the next 'Back' button event to hide (close) this tooltip public bool HideOnBackButton = true; #if INPUT_SYSTEM_PACKAGE private static InputSystemUIInputModule s_inputSystemUIInputModule; public static InputSystemUIInputModule inputModule { get { if (s_inputSystemUIInputModule != null) return s_inputSystemUIInputModule; if (EventSystem.current == null) return null; s_inputSystemUIInputModule = EventSystem.current.GetComponent(); return s_inputSystemUIInputModule; } } #endif /// Pointer's current position public static Vector2 pointerPosition { get { #pragma warning disable CS0162 #if LEGACY_INPUT_MANAGER { return UnityEngine.Input.mousePosition; } #elif INPUT_SYSTEM_PACKAGE { return inputModule.point.action.ReadValue(); } #endif // ReSharper disable once HeuristicUnreachableCode return Vector2.zero; #pragma warning restore CS0162 } } /// Internal flag to track if show has been called private bool showHasBeenCalled { get; set; } /// Internal flag to track if hide has been called private bool hideHasBeenCalled { get; set; } /// Internal flag to check it the tooltip has a move animator for the show animation private bool showHasMovement { get; set; } /// Internal flag to check it the tooltip has a move animator for the hide animation private bool hideHasMovement { get; set; } /// Internal reference to the tooltip's show move reaction (if any) private UIMoveReaction showMoveReaction { get; set; } /// Internal reference to the tooltip's hide move reaction (if any) private UIMoveReaction hideMoveReaction { get; set; } /// /// Internal flag used to keep track if the hide event was added to the buttons. /// This is needed to avoid adding the event multiple times to the same button. /// private bool addedHideEventToButtons { get; set; } /// Internal flag used to keep track if this tooltip added a PointerClickTrigger to it private bool addedHideOnClick { get; set; } #region Parent Info public RectTransform parentRectTransform { get; internal set; } #endregion #region Trigger private UITooltipTrigger m_Trigger; public UITooltipTrigger trigger { get => m_Trigger; set { triggerRectTransform = null; m_Trigger = value; if (value == null) return; triggerRectTransform = value.GetComponent(); } } /// RectTransform of the trigger to track when the tooltip is shown and tracking is set to FollowTrigger public RectTransform triggerRectTransform { get; private set; } /// TRUE if the tooltip has a trigger reference public bool hasTrigger => trigger != null; /// TRUE if the trigger transform has a RectTransform public bool hasTriggerRectTransform => triggerRectTransform != null; #endregion #region Follow Target private GameObject m_FollowTarget; /// GameObject to track when the tooltip is shown and tracking is set to FollowTarget public GameObject followTarget { get => m_FollowTarget; set { followTargetRectTransform = null; m_FollowTarget = value; if (value == null) return; followTargetRectTransform = value.GetComponent(); } } /// RectTransform of the follow target to track when the tooltip is shown and tracking is set to FollowTarget public RectTransform followTargetRectTransform { get; private set; } /// TRUE if the tooltip has a follow target reference public bool hasFollowTarget => followTarget != null; /// TRUE if the follow target transform has a RectTransform public bool hasFollowTargetRectTransform => followTargetRectTransform != null; #endregion /// 'Back' button signal receiver used to trigger the hiding of this UITooltip if HideOnBackButton is TRUE public SignalReceiver backButtonReceiver { get; set; } protected override void Awake() { base.Awake(); addedHideEventToButtons = false; //initialize the 'Back' button signal receiver backButtonReceiver = new SignalReceiver().SetOnSignalCallback(signal => { if (!HideOnBackButton) return; if (isHidden || isHiding) return; Hide(); }); } protected override void OnEnable() { base.OnEnable(); //connect to the 'Back' button secondary signal stream //(this stream ignores the disabled state for the 'Back' button) BackButton.streamIgnoreDisabled.ConnectReceiver(backButtonReceiver); } protected override void OnDisable() { base.OnDisable(); //disconnect from the 'Back' button secondary signal stream //(this stream ignores the disabled state for the 'Back' button) BackButton.streamIgnoreDisabled.DisconnectReceiver(backButtonReceiver); } private void LateUpdate() { CheckIfShowOrHideHaveMoveReactions(); if (isShowing && showHasMovement || isHiding && hideHasMovement) { ApplyPositioning(); return; } ApplyTracking(); ApplyPositioning(); ApplyKeepInScreen(); SetCustomStartPosition(rectTransform.anchoredPosition3D, false); } /// Validate the tooltip's settings public virtual void Validate() { Labels.RemoveNulls(); Images.RemoveNulls(); Buttons.RemoveNulls(); } public override void Show() { UpdateTarget(); ApplyMaximumWidth(); ApplyHideOnAnyButton(); base.Show(); Coroutiner.ExecuteLater ( () => { if (this == null) return; this.ApplyOverrideSorting(); }, 3 //number of frames to wait ); } public override void InstantShow(bool triggerCallbacks) { UpdateTarget(); ApplyMaximumWidth(); ApplyHideOnAnyButton(); base.InstantShow(triggerCallbacks); this.ApplyOverrideSorting(); } /// /// Update the reference to the root canvas and RectTransform of the root canvas of this tooltip's current target, if any. /// public void UpdateTarget() { updateTarget = false; targetRectTransform = null; switch (TrackingMode) { case Tracking.Disabled: case Tracking.FollowPointer: return; case Tracking.FollowTrigger: targetRectTransform = triggerRectTransform; break; case Tracking.FollowTarget: targetRectTransform = followTargetRectTransform; break; default: throw new ArgumentOutOfRangeException(); } } #region Public Methods /// /// Get a parent reference for the tooltip according to the tooltip's current ParentMode setting. /// This is not the same as the tooltip's transform.parent, which is the parent of the tooltip's GameObject. /// public RectTransform GetParent() { RectTransform parent; switch (ParentMode) { case Parenting.TooltipsCanvas: parent = tooltipsCanvas.GetComponent(); break; case Parenting.TooltipTrigger: if (trigger == null) { Debug.Log ( "[Tooltip] Parenting mode set to 'Tooltip Trigger' but no tooltip trigger is set." + "Used the TooltipCanvas as parent instead." ); parent = tooltipsCanvas.GetComponent(); break; } parent = trigger.GetComponent(); if (parent == null) { Debug.Log ( "[Tooltip] Parenting mode set to 'Tooltip Trigger' but the tooltip trigger has no RectTransform component." + "Used the TooltipCanvas as parent instead." ); parent = tooltipsCanvas.GetComponent(); } break; case Parenting.UITag: if (ParentTag == null) { Debug.Log ( "[Tooltip] Parenting mode set to 'UITag' but no UITag is set." + "Used the TooltipCanvas as parent instead." ); parent = tooltipsCanvas.GetComponent(); break; } var uiTag = UITag.GetFirstTag(ParentTag.Category, ParentTag.Name); if (uiTag == null) { Debug.Log ( "[Tooltip] Parenting mode set to 'UITag' but the UITag is not found." + "Used the TooltipCanvas as parent instead." ); parent = tooltipsCanvas.GetComponent(); break; } parent = uiTag.GetComponent(); if (parent == null) { Debug.Log ( "[Tooltip] Parenting mode set to 'UITag' but the UITag has no RectTransform component." + "Used the TooltipCanvas as parent instead." ); parent = tooltipsCanvas.GetComponent(); } break; default: throw new ArgumentOutOfRangeException(); } return parent; } #endregion #region Private Methods /// Set all the referenced buttons to hide the tooltip is HideOnAnyButton is enabled private void ApplyHideOnAnyButton() { if (addedHideEventToButtons) return; if (!hasButtons) return; if (!HideOnAnyButton) return; foreach (UIButton button in Buttons) { button.onClickBehaviour.Event.AddListener(Hide); button.onSubmitBehaviour.Event.AddListener(Hide); } addedHideEventToButtons = true; //only add the event once } /// Check is the tooltip is moved by a show/hide animation private void CheckIfShowOrHideHaveMoveReactions() { if (isShowing && !showHasBeenCalled) { showHasBeenCalled = true; showHasMovement = showReactions.Any(r => r.GetType() == typeof(UIMoveReaction) && ((UIMoveReaction)r).rectTransform == rectTransform && ((UIMoveReaction)r).enabled); if (showHasMovement) showMoveReaction = showReactions.First(r => r.GetType() == typeof(UIMoveReaction) && ((UIMoveReaction)r).rectTransform == rectTransform) as UIMoveReaction; return; } if (isHiding && !hideHasBeenCalled) { hideHasBeenCalled = true; hideHasMovement = hideReactions.Any(r => r.GetType() == typeof(UIMoveReaction) && ((UIMoveReaction)r).rectTransform == rectTransform && ((UIMoveReaction)r).enabled); if (hideHasMovement) hideMoveReaction = hideReactions.First(r => r.GetType() == typeof(UIMoveReaction) && ((UIMoveReaction)r).rectTransform == rectTransform) as UIMoveReaction; } } /// Update the tooltip position according to the tracking mode private void ApplyTracking() { Vector3 targetPosition; switch (TrackingMode) { case Tracking.Disabled: return; case Tracking.FollowPointer: targetPosition = pointerPosition; break; case Tracking.FollowTrigger: if (!hasTrigger) { TrackingMode = Tracking.Disabled; return; } targetPosition = trigger.transform.position; break; case Tracking.FollowTarget: if (!hasFollowTarget) { this.SetFollowTargetFromUITag(FollowTag.Category, FollowTag.Name); if (!hasFollowTarget) { TrackingMode = Tracking.Disabled; return; } UpdateTarget(); } targetPosition = followTarget.transform.position; break; default: throw new ArgumentOutOfRangeException(); } transform.position = targetPosition; } /// Update the tooltip position according to the positioning mode private void ApplyPositioning() { if (transform.parent == null) return; //cannot calculate the position if the tooltip doesn't have a parent Vector3 calculatedTargetPosition; switch (TrackingMode) { case Tracking.Disabled: calculatedTargetPosition = CalculatePositioningWhenTrackingIsDisabled(); break; case Tracking.FollowPointer: calculatedTargetPosition = CalculatePositioningWhenTrackingIsFollowPointer(); break; case Tracking.FollowTrigger: case Tracking.FollowTarget: calculatedTargetPosition = CalculatePositioningWhenTrackingIsEnabled(); break; default: throw new ArgumentOutOfRangeException(); } // Debug.Log($"[{TrackingMode}] calculatedTargetPosition: {calculatedTargetPosition}"); //apply the position offset (set by the developer) calculatedTargetPosition += PositionOffset; //check for NaN values calculatedTargetPosition.x = float.IsNaN(calculatedTargetPosition.x) ? 0 : calculatedTargetPosition.x; calculatedTargetPosition.y = float.IsNaN(calculatedTargetPosition.y) ? 0 : calculatedTargetPosition.y; calculatedTargetPosition.z = float.IsNaN(calculatedTargetPosition.z) ? 0 : calculatedTargetPosition.z; //check for infinite values calculatedTargetPosition.x = float.IsInfinity(calculatedTargetPosition.x) ? 0 : calculatedTargetPosition.x; calculatedTargetPosition.y = float.IsInfinity(calculatedTargetPosition.y) ? 0 : calculatedTargetPosition.y; calculatedTargetPosition.z = float.IsInfinity(calculatedTargetPosition.z) ? 0 : calculatedTargetPosition.z; //if the tooltip is showing and the show animation has a move reaction -> update the reaction's To value to the calculated position if (isShowing & showHasMovement) { showMoveReaction.SetTo(calculatedTargetPosition); return; } //if the tooltip is hiding and the hide animation has a move reaction -> update the reaction's From value to the calculated position if (isHiding & hideHasMovement) { hideMoveReaction.SetFrom(calculatedTargetPosition); return; } //apply the calculated anchored position rectTransform.anchoredPosition3D = calculatedTargetPosition; //update the tooltip anchored position with the calculated position } /// Update the tooltip position to keep it in screen, if KeepInScreen is enabled private void ApplyKeepInScreen() { if (!KeepInScreen) return; var cCorners = new Vector3[4]; tooltipRootCanvasRectTransform.GetWorldCorners(cCorners); Vector3 cBottomLeft = cCorners[0]; Vector3 cTopRight = cCorners[2]; Vector3 cSize = cTopRight - cBottomLeft; rectTransform.GetWorldCorners(cCorners); Vector3 tBottomLEft = cCorners[0]; Vector3 tTopRight = cCorners[2]; Vector3 tSize = tTopRight - tBottomLEft; Vector3 tPosition = rectTransform.position; Vector3 deltaBottomLeft = tPosition - tBottomLEft; Vector3 deltaTopRight = tTopRight - tPosition; tPosition.x = tSize.x < cSize.x ? Mathf.Clamp(tPosition.x, cBottomLeft.x + deltaBottomLeft.x, cTopRight.x - deltaTopRight.x) : Mathf.Clamp(tPosition.x, cTopRight.x - deltaTopRight.x, cBottomLeft.x + deltaBottomLeft.x); tPosition.y = tSize.y < cSize.y ? Mathf.Clamp(tPosition.y, cBottomLeft.y + deltaBottomLeft.y, cTopRight.y - deltaTopRight.y) : Mathf.Clamp(tPosition.y, cTopRight.y - deltaTopRight.y, cBottomLeft.y + deltaBottomLeft.y); rectTransform.position = tPosition; } /// Apply a maximum width constraint to the tooltip if one is set private void ApplyMaximumWidth() { if (MaximumWidth <= 0) return; //no maximum width set layoutElement.preferredWidth = -1; //reset the preferred width to -1 so it can be recalculated layoutElement.enabled = false; //disable the layout element so it doesn't influence the preferred width rectTransform.ForceUpdateRectTransforms(); //force the rect transform to update foreach (TextMeshProUGUI label in Labels) label.ForceMeshUpdate(); //force mesh updates on all labels float maxLabelWidth = Labels.Max(label => label.preferredWidth); //get the maximum width from all labels if (maxLabelWidth < MaximumWidth) return; //if the maximum width is smaller than the maximum width set by the user, no need to do anything layoutElement.enabled = true; //enable the layout element layoutElement.preferredWidth = MaximumWidth; //set the preferred width to the maximum width constraint rectTransform.ForceUpdateRectTransforms(); //force an update of the rect transform } /// Internal method used if tracking is disabled to calculate the position according to the positioning mode inside its parent /// The anchoredPosition3D to apply to the tooltip private Vector3 CalculatePositioningWhenTrackingIsDisabled() { if (transform.parent == null) return rectTransform.anchoredPosition3D; //if the tooltip is not parented, just return the initial anchored position float z = rectTransform.anchoredPosition3D.z; //save the z value of the anchored position Rect parentRect = parentRectTransform.rect; //get the parent rect float parentWidth = parentRect.width; //get the parent width float parentHeight = parentRect.height; //get the parent height return PositionMode switch { Positioning.TopLeft => new Vector3(-parentWidth / 2f + width * pivotX, parentHeight / 2f - height * pivotY, z), Positioning.TopCenter => new Vector3(0f, parentHeight / 2f - height * pivotY, z), Positioning.TopRight => new Vector3(parentWidth / 2f - width * (1 - pivotX), parentHeight / 2f - height * pivotY, z), Positioning.MiddleLeft => new Vector3(-parentWidth / 2f + width * pivotX, 0f, z), Positioning.MiddleCenter => new Vector3(0f, 0f, z), Positioning.MiddleRight => new Vector3(parentWidth / 2f - width * (1 - pivotX), 0f, z), Positioning.BottomLeft => new Vector3(-parentWidth / 2f + width * pivotX, -parentHeight / 2f + height * (1 - pivotY), z), Positioning.BottomCenter => new Vector3(0f, -parentHeight / 2f + height * (1 - pivotY), z), Positioning.BottomRight => new Vector3(parentWidth / 2f - width * (1 - pivotX), -parentHeight / 2f + height * (1 - pivotY), z), _ => throw new ArgumentOutOfRangeException() }; } /// Internal method used to calculate the position of the tooltip according to the positioning mode and the current pointer position /// The anchoredPosition3D to apply to the tooltip private Vector3 CalculatePositioningWhenTrackingIsFollowPointer() { Vector2 point; const float z = 0; switch (tooltipRootCanvas.renderMode) { case RenderMode.ScreenSpaceOverlay: point = parentRectTransform.InverseTransformPoint(pointerPosition); break; case RenderMode.ScreenSpaceCamera: case RenderMode.WorldSpace: RectTransformUtility.ScreenPointToLocalPointInRectangle(parentRectTransform, pointerPosition, tooltipRootCanvas.worldCamera, out point); break; default: throw new ArgumentOutOfRangeException(); } point -= parentRectTransform.rect.center; //adjust the point according to the positioning mode and the tooltip size and pivot point = PositionMode switch { Positioning.TopLeft => new Vector3(point.x - width * pivotX, point.y + height * pivotY, z), Positioning.TopCenter => new Vector3(point.x, point.y + height * pivotY, z), Positioning.TopRight => new Vector3(point.x + width * (1 - pivotX), point.y + height * pivotY, z), Positioning.MiddleLeft => new Vector3(point.x - width * pivotX, point.y, z), Positioning.MiddleCenter => new Vector3(point.x, point.y, z), Positioning.MiddleRight => new Vector3(point.x + width * (1 - pivotX), point.y, z), Positioning.BottomLeft => new Vector3(point.x - width * pivotX, point.y - height * (1 - pivotY), z), Positioning.BottomCenter => new Vector3(point.x, point.y - height * (1 - pivotY), z), Positioning.BottomRight => new Vector3(point.x + width * (1 - pivotX), point.y - height * (1 - pivotY), z), _ => throw new ArgumentOutOfRangeException() }; return point; } internal bool updateTarget { get; set; } /// Internal method used to calculate the position of the tooltip according to the positioning mode inside its parent /// The anchoredPosition3D to apply to the tooltip private Vector3 CalculatePositioningWhenTrackingIsEnabled() { if (updateTarget) UpdateTarget(); Vector3 point = parentRectTransform.InverseTransformPoint(targetRectTransform.position); //calculate the target offset according to the positioning mode Vector3 targetOffset = GetPositionOffset(targetRectTransform, PositionMode); //fix the target offset value if the rectTransform has a scale applied Vector3 localScale = targetRectTransform.localScale; targetOffset.x *= localScale.x; targetOffset.y *= localScale.y; //calculate the scale difference between the tooltip root canvas and the target root canvas Vector3 targetCanvasScale = targetRectTransform.lossyScale; Vector3 tooltipCanvasScale = rectTransform.lossyScale; var scaleDiff = new Vector3(targetCanvasScale.x / tooltipCanvasScale.x, targetCanvasScale.y / tooltipCanvasScale.y, targetCanvasScale.z / tooltipCanvasScale.z); //apply the scale difference to the target offset for better positioning targetOffset.x *= scaleDiff.x; targetOffset.y *= scaleDiff.y; targetOffset.z *= scaleDiff.z; //apply the calculated target offset point += targetOffset; //calculate the tooltip offset according to the positioning mode Vector3 tooltipOffset = GetPositionOffset(rectTransform, PositionMode); //apply the calculated tooltip offset` point += tooltipOffset; //return the calculated anchored position return point; } private static Vector3 GetPositionOffset(RectTransform rectTransform, Positioning positionMode) { Rect rect = rectTransform.rect; return positionMode switch { Positioning.TopLeft => new Vector3(rect.xMin, rect.yMax, 0), Positioning.TopCenter => new Vector3(rect.center.x, rect.yMax, 0), Positioning.TopRight => new Vector3(rect.xMax, rect.yMax, 0), Positioning.MiddleLeft => new Vector3(rect.xMin, rect.center.y, 0), Positioning.MiddleCenter => new Vector3(rect.center.x, rect.center.y, 0), Positioning.MiddleRight => new Vector3(rect.xMax, rect.center.y, 0), Positioning.BottomLeft => new Vector3(rect.xMin, rect.yMin, 0), Positioning.BottomCenter => new Vector3(rect.center.x, rect.yMin, 0), Positioning.BottomRight => new Vector3(rect.xMax, rect.yMin, 0), _ => throw new ArgumentOutOfRangeException() }; } #endregion #region Static Methods /// /// Instantiate a new tooltip from the prefab that is registered in the database. /// If a prefab with the given tooltip name is not found, null will be returned. /// /// The name of the tooltip prefab in the database /// The tooltip instance. Null if the prefab is not found. public static UITooltip Get(string tooltipName) { if (string.IsNullOrEmpty(tooltipName)) return null; GameObject prefab = UITooltipDatabase.instance.GetPrefab(tooltipName); if (prefab == null) { Debug.LogWarning($"UITooltip.Get({tooltipName}) - prefab not found in the database"); return null; } UITooltip tooltip = Instantiate(prefab) .GetComponent() .Reset(); //destroy the tooltip when it is hidden tooltip.OnHiddenCallback.Event.AddListener(() => { //sanity check to make sure the tooltip is not already destroyed if (tooltip == null) return; Destroy(tooltip.gameObject); tooltip = null; }); return tooltip; } /// Calls ShowTooltip on all UITooltipTriggers in the scene (that are active and enabled) public static void ShowAllTooltips() { foreach (UITooltipTrigger tooltipTrigger in UITooltipTrigger.database) { if (!tooltipTrigger.isActiveAndEnabled) return; tooltipTrigger.ShowTooltip(); } } /// Calls Hide on all UITooltips in the scene (that are active and enabled) public void HideAllTooltips() { foreach (UITooltip tooltip in database) { if (!tooltip.isActiveAndEnabled) return; tooltip.Hide(); } } #endregion } public static class UITooltipExtensions { /// Reset the tooltip to its initial state /// Target tooltip /// The tooltip instance public static T Reset(this T tooltip) where T : UITooltip { tooltip.updateTarget = true; tooltip.tooltipRootCanvas = null; tooltip.targetRectTransform = null; tooltip.parentRectTransform = null; tooltip.trigger = null; tooltip.followTarget = null; return tooltip; } /// Reparent the tooltip to a new parent /// Target tooltip /// The new parent /// The tooltip instance public static T SetParent(this T tooltip, RectTransform parent) where T : UITooltip { tooltip.tooltipRootCanvas = null; tooltip.parentRectTransform = parent; if (parent == null) return tooltip; tooltip.rectTransform.SetParent(parent, true); tooltip.rectTransform.CenterPivot(); tooltip.rectTransform.localScale = Vector3.one; tooltip.rectTransform.anchoredPosition3D = tooltip.CustomStartPosition; tooltip.tooltipRootCanvas = parent.GetComponentInParent().rootCanvas; return tooltip; } /// Set the follow target for when the tooltip is visible and tracking is enabled to follow a target /// Target tooltip /// Target trigger /// The tooltip instance public static T SetTrigger(this T tooltip, UITooltipTrigger target) where T : UITooltip { tooltip.trigger = target; return tooltip; } /// Set the follow target for when the tooltip is visible and tracking is enabled to follow a target /// Target tooltip /// Target game object /// The tooltip instance public static T SetFollowTarget(this T tooltip, GameObject target) where T : UITooltip { tooltip.followTarget = target; return tooltip; } /// Set the follow target for when the tooltip is visible and tracking is enabled to follow a target /// Target tooltip /// Category of the tag /// Name of the tag /// The tooltip instance public static T SetFollowTargetFromUITag(this T tooltip, string category, string name) where T : UITooltip { tooltip.followTarget = null; UITag tag = UITag.GetTags(category, name).FirstOrDefault(); if (tag != null) tooltip.followTarget = tag.gameObject; return tooltip; } /// Update the keep in screen setting /// Target tooltip /// TRUE to keep the tooltip in screen at all times, while it is visible /// The tooltip instance public static T SetKeepInScreen(this T tooltip, bool keepInScreen) where T : UITooltip { tooltip.KeepInScreen = keepInScreen; return tooltip; } /// Update the override sorting order setting /// Target tooltip /// New override sorting order value /// TRUE to apply the new value, FALSE to only update the setting /// The tooltip instance public static T SetOverrideSorting(this T target, bool overrideSortingOrder, bool apply = false) where T : UITooltip { target.OverrideSorting = overrideSortingOrder; if (apply) target.ApplyOverrideSorting(); return target; } /// Apply the override sorting order setting (if enabled) /// Target tooltip /// The tooltip instance public static T ApplyOverrideSorting(this T target) where T : UITooltip { if (!target.OverrideSorting) return target; target.canvas.overrideSorting = true; target.canvas.sortingOrder = UITooltip.k_MaxSortingOrder; if (!target.canvas.gameObject.activeInHierarchy) Debug.Log($"Cannot apply override sorting order to tooltip {target.name} because it is not active in the scene"); if (!target.canvas.enabled) Debug.Log($"Cannot apply override sorting order to tooltip {target.name} because its canvas is not enabled"); return target; } /// /// Set the text values for all the text mesh pro labels this tooltip has references to. /// Each string value will be set to the TextMeshProUI label with the same index in the Labels list. /// /// Target tooltip /// Text values to set /// The tooltip instance public static T SetTexts(this T tooltip, params string[] texts) where T : UITooltip { int textsCount = texts.Length; if (textsCount == 0) return tooltip; for (int i = 0; i < tooltip.Labels.Count; i++) { TextMeshProUGUI label = tooltip.Labels[i]; if (label == null) continue; label.SetText(i < textsCount ? texts[i] : string.Empty); label.ForceMeshUpdate(); } return tooltip; } /// /// Set the sprite references for all the Images this tooltip has references to. /// Each Sprite will be referenced to the Image with the same index in the Images list. /// /// Target tooltip /// Sprite references to set /// The tooltip instance public static T SetSprites(this T tooltip, params Sprite[] sprites) where T : UITooltip { int spritesCount = sprites.Length; if (spritesCount == 0) return tooltip; for (int i = 0; i < tooltip.Images.Count; i++) { Image image = tooltip.Images[i]; if (image == null) continue; image.sprite = i < spritesCount ? sprites[i] : null; } return tooltip; } /// /// Set the UnityEvents to invoke for all the UIButtons this tooltip has references to /// Each UnityEvent will be assigned to the UIButton with the same index in the Buttons list. /// The UnityEvent will be invoked when the UIButton's either on click or submit behaviour is invoked. /// /// Target tooltip /// UnityEvents to invoke /// The tooltip instance public static T SetEvents(this T tooltip, params UnityEvent[] events) where T : UITooltip { int eventsCount = events.Length; if (eventsCount == 0) return tooltip; bool hasGraphicRaycaster = tooltip.GetComponentInChildren(); for (int i = 0; i < tooltip.Buttons.Count; i++) { UIButton button = tooltip.Buttons[i]; //get the button if (button == null) continue; //if the button is null, continue if (!hasGraphicRaycaster) //if the tooltip doesn't have a graphic raycaster, check if the button has one if (!button.GetComponent()) //if the button doesn't have a graphic raycaster button.gameObject.AddComponent(); //add a graphic raycaster to the button if (eventsCount <= i) continue; //if the event count is less than the index, continue UnityEvent evt = events[i]; //get the event if (evt == null) continue; //if the event is null, continue button.onClickBehaviour.Event.AddListener(evt.Invoke); //add the event to the button on click behaviour button.onSubmitBehaviour.Event.AddListener(evt.Invoke); //add the event to the button on submit behaviour } return tooltip; } /// /// Set the UnityActions to invoke for all the UIButtons this tooltip has references to. /// Each UnityAction will be assigned to the UIButton with the same index in the Buttons list. /// The UnityAction will be invoked when the UIButton's either on click or submit behaviour is invoked. /// /// Target tooltip /// UnityActions to invoke /// The tooltip instance public static T SetEvents(this T tooltip, params UnityAction[] actions) where T : UITooltip { int actionsCount = actions.Length; if (actionsCount == 0) return tooltip; bool hasGraphicRaycaster = tooltip.GetComponentInChildren(); for (int i = 0; i < tooltip.Buttons.Count; i++) { UIButton button = tooltip.Buttons[i]; //get the button if (button == null) continue; //if the button is null, continue if (!hasGraphicRaycaster) //if the tooltip doesn't have a graphic raycaster, check if the button has one if (!button.GetComponent()) //if the button doesn't have a graphic raycaster button.gameObject.AddComponent(); //add a graphic raycaster to the button if (actionsCount <= i) continue; //if the event count is less than the index, continue UnityAction action = actions[i]; //get the action if (action == null) continue; //if the action is null, continue button.onClickBehaviour.Event.AddListener(action.Invoke); //add the action to the button on click behaviour button.onSubmitBehaviour.Event.AddListener(action.Invoke); //add the action to the button on submit behaviour } return tooltip; } } }