2025-10-03 00:02:43 -04:00
/// Credit drHogan
/// Sourced from - http://forum.unity3d.com/threads/screenspace-camera-tooltip-controller-sweat-and-tears.293991/#post-1938929
/// updated simonDarksideJ - refactored code to be more performant.
/// updated lucasvinbr - mixed with BoundTooltip, should work with Screenspace Camera (non-rotated) and Overlay
/// *Note - only works for non-rotated Screenspace Camera and Screenspace Overlay canvases at present, needs updating to include rotated Screenspace Camera and Worldspace!
//ToolTip is written by Emiliano Pastorelli, H&R Tallinn (Estonia), http://www.hammerandravens.com
//Copyright (c) 2015 Emiliano Pastorelli, H&R - Hammer&Ravens, Tallinn, Estonia.
//All rights reserved.
//Redistribution and use in source and binary forms are permitted
//provided that the above copyright notice and this paragraph are
//duplicated in all such forms and that any documentation,
//advertising materials, and other materials related to such
//distribution and use acknowledge that the software was developed
//by H&R, Hammer&Ravens. The name of the
//H&R, Hammer&Ravens may not be used to endorse or promote products derived
//from this software without specific prior written permission.
//THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
//IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
2026-03-20 11:56:50 -04:00
using TMPro ;
2025-10-03 00:02:43 -04:00
namespace UnityEngine.UI.Extensions
{
[RequireComponent(typeof(RectTransform))]
[AddComponentMenu("UI/Extensions/Tooltip/Tooltip")]
public class ToolTip : MonoBehaviour
{
//text of the tooltip
#if UNITY_2022_1_OR_NEWER
2026-03-20 11:56:50 -04:00
private TMP_Text _text ;
2025-10-03 00:02:43 -04:00
#else
private Text _text ;
#endif
private RectTransform _rectTransform , canvasRectTransform ;
2026-03-20 11:56:50 -04:00
[ Tooltip (
"The canvas used by the tooltip as positioning and scaling reference. Should usually be the root Canvas of the hierarchy this component is in" ) ]
2025-10-03 00:02:43 -04:00
public Canvas canvas ;
[ Tooltip ( "Sets if tooltip triggers will run ForceUpdateCanvases and refresh the tooltip's layout group " +
2026-03-20 11:56:50 -04:00
"(if any) when hovered, in order to prevent momentousness misplacement sometimes caused by ContentSizeFitters" ) ]
public bool tooltipTriggersCanForceCanvasUpdate ;
2025-10-03 00:02:43 -04:00
/// <summary>
2026-03-20 11:56:50 -04:00
/// the tooltip's Layout Group, if any
2025-10-03 00:02:43 -04:00
/// </summary>
private LayoutGroup _layoutGroup ;
//if the tooltip is inside a UI element
private bool _inside ;
2026-03-20 11:56:50 -04:00
private float width , height ; //, canvasWidth, canvasHeight;
2025-10-03 00:02:43 -04:00
2026-03-20 11:56:50 -04:00
public float YShift , xShift ;
2025-10-03 00:02:43 -04:00
2026-03-20 11:56:50 -04:00
[HideInInspector] public RenderMode guiMode ;
2025-10-03 00:02:43 -04:00
private Camera _guiCamera ;
public Camera GuiCamera
{
get
{
2026-03-20 11:56:50 -04:00
if ( ! _guiCamera ) _guiCamera = Camera . main ;
2025-10-03 00:02:43 -04:00
return _guiCamera ;
}
}
private Vector3 screenLowerLeft , screenUpperRight , shiftingVector ;
/// <summary>
2026-03-20 11:56:50 -04:00
/// a screen-space point where the tooltip would be placed before applying X and Y shifts and border checks
2025-10-03 00:02:43 -04:00
/// </summary>
private Vector3 baseTooltipPos ;
private Vector3 newTTPos ;
private Vector3 adjustedNewTTPos ;
private Vector3 adjustedTTLocalPos ;
private Vector3 shifterForBorders ;
private float borderTest ;
// Standard Singleton Access
private static ToolTip instance ;
2026-03-20 11:56:50 -04:00
2025-10-03 00:02:43 -04:00
public static ToolTip Instance
{
get
{
if ( instance = = null )
{
#if UNITY_2023_1_OR_NEWER
instance = FindFirstObjectByType < ToolTip > ( ) ;
#else
instance = FindObjectOfType < ToolTip > ( ) ;
#endif
}
2026-03-20 11:56:50 -04:00
2025-10-03 00:02:43 -04:00
return instance ;
}
}
2026-03-20 11:56:50 -04:00
private void Reset ( )
{
2025-10-03 00:02:43 -04:00
canvas = GetComponentInParent < Canvas > ( ) ;
canvas = canvas . rootCanvas ;
}
// Use this for initialization
public void Awake ( )
{
instance = this ;
2026-03-20 11:56:50 -04:00
if ( ! canvas )
{
2025-10-03 00:02:43 -04:00
canvas = GetComponentInParent < Canvas > ( ) ;
canvas = canvas . rootCanvas ;
}
_guiCamera = canvas . worldCamera ;
guiMode = canvas . renderMode ;
_rectTransform = GetComponent < RectTransform > ( ) ;
canvasRectTransform = canvas . GetComponent < RectTransform > ( ) ;
_layoutGroup = GetComponentInChildren < LayoutGroup > ( ) ;
#if UNITY_2022_1_OR_NEWER
2026-03-20 11:56:50 -04:00
_text = GetComponentInChildren < TMP_Text > ( ) ;
2025-10-03 00:02:43 -04:00
#else
_text = GetComponentInChildren < Text > ( ) ;
#endif
_inside = false ;
2026-03-20 11:56:50 -04:00
gameObject . SetActive ( false ) ;
2025-10-03 00:02:43 -04:00
}
public void SetTooltip ( string ttext )
{
SetTooltip ( ttext , transform . position ) ;
}
//Call this function externally to set the text of the template and activate the tooltip
public void SetTooltip ( string ttext , Vector3 basePos , bool refreshCanvasesBeforeGetSize = false )
{
baseTooltipPos = basePos ;
//set the text
2026-03-20 11:56:50 -04:00
if ( _text )
2025-10-03 00:02:43 -04:00
_text . text = ttext ;
2026-03-20 11:56:50 -04:00
else
2025-10-03 00:02:43 -04:00
Debug . LogWarning ( "[ToolTip] Couldn't set tooltip text, tooltip has no child Text component" ) ;
ContextualTooltipUpdate ( refreshCanvasesBeforeGetSize ) ;
}
//call this function on mouse exit to deactivate the template
public void HideTooltip ( )
{
gameObject . SetActive ( false ) ;
_inside = false ;
}
// Update is called once per frame
2026-03-20 11:56:50 -04:00
private void Update ( )
2025-10-03 00:02:43 -04:00
{
2026-03-20 11:56:50 -04:00
if ( _inside ) ContextualTooltipUpdate ( ) ;
2025-10-03 00:02:43 -04:00
}
/// <summary>
2026-03-20 11:56:50 -04:00
/// forces rebuilding of Canvases in order to update the tooltip's content size fitting.
/// Can prevent the tooltip from being visibly misplaced for one frame when being resized.
/// Only runs if tooltipTriggersCanForceCanvasUpdate is true
2025-10-03 00:02:43 -04:00
/// </summary>
2026-03-20 11:56:50 -04:00
public void RefreshTooltipSize ( )
{
if ( tooltipTriggersCanForceCanvasUpdate )
{
2025-10-03 00:02:43 -04:00
Canvas . ForceUpdateCanvases ( ) ;
2026-03-20 11:56:50 -04:00
if ( _layoutGroup )
{
2025-10-03 00:02:43 -04:00
_layoutGroup . enabled = false ;
_layoutGroup . enabled = true ;
}
}
}
/// <summary>
2026-03-20 11:56:50 -04:00
/// Runs the appropriate tooltip placement method, according to the parent canvas's render mode
2025-10-03 00:02:43 -04:00
/// </summary>
/// <param name="refreshCanvasesBeforeGettingSize"></param>
2026-03-20 11:56:50 -04:00
public void ContextualTooltipUpdate ( bool refreshCanvasesBeforeGettingSize = false )
{
switch ( guiMode )
{
2025-10-03 00:02:43 -04:00
case RenderMode . ScreenSpaceCamera :
OnScreenSpaceCamera ( refreshCanvasesBeforeGettingSize ) ;
break ;
case RenderMode . ScreenSpaceOverlay :
OnScreenSpaceOverlay ( refreshCanvasesBeforeGettingSize ) ;
break ;
}
}
//main tooltip edge of screen guard and movement - camera
public void OnScreenSpaceCamera ( bool refreshCanvasesBeforeGettingSize = false )
{
shiftingVector . x = xShift ;
shiftingVector . y = YShift ;
baseTooltipPos . z = canvas . planeDistance ;
newTTPos = GuiCamera . ScreenToViewportPoint ( baseTooltipPos - shiftingVector ) ;
adjustedNewTTPos = GuiCamera . ViewportToWorldPoint ( newTTPos ) ;
gameObject . SetActive ( true ) ;
if ( refreshCanvasesBeforeGettingSize ) RefreshTooltipSize ( ) ;
//consider scaled dimensions when comparing against the edges
width = transform . lossyScale . x * _rectTransform . sizeDelta [ 0 ] ;
height = transform . lossyScale . y * _rectTransform . sizeDelta [ 1 ] ;
// check and solve problems for the tooltip that goes out of the screen on the horizontal axis
2026-03-20 11:56:50 -04:00
RectTransformUtility . ScreenPointToWorldPointInRectangle ( canvasRectTransform , Vector2 . zero , GuiCamera ,
out screenLowerLeft ) ;
RectTransformUtility . ScreenPointToWorldPointInRectangle ( canvasRectTransform ,
new Vector2 ( Screen . width , Screen . height ) , GuiCamera , out screenUpperRight ) ;
2025-10-03 00:02:43 -04:00
//check for right edge of screen
2026-03-20 11:56:50 -04:00
borderTest = adjustedNewTTPos . x + width / 2 ;
2025-10-03 00:02:43 -04:00
if ( borderTest > screenUpperRight . x )
{
shifterForBorders . x = borderTest - screenUpperRight . x ;
adjustedNewTTPos . x - = shifterForBorders . x ;
}
2026-03-20 11:56:50 -04:00
2025-10-03 00:02:43 -04:00
//check for left edge of screen
2026-03-20 11:56:50 -04:00
borderTest = adjustedNewTTPos . x - width / 2 ;
2025-10-03 00:02:43 -04:00
if ( borderTest < screenLowerLeft . x )
{
shifterForBorders . x = screenLowerLeft . x - borderTest ;
adjustedNewTTPos . x + = shifterForBorders . x ;
}
// check and solve problems for the tooltip that goes out of the screen on the vertical axis
//check for lower edge of the screen
2026-03-20 11:56:50 -04:00
borderTest = adjustedNewTTPos . y - height / 2 ;
if ( borderTest < screenLowerLeft . y )
{
2025-10-03 00:02:43 -04:00
shifterForBorders . y = screenLowerLeft . y - borderTest ;
adjustedNewTTPos . y + = shifterForBorders . y ;
}
//check for upper edge of the screen
2026-03-20 11:56:50 -04:00
borderTest = adjustedNewTTPos . y + height / 2 ;
2025-10-03 00:02:43 -04:00
if ( borderTest > screenUpperRight . y )
{
shifterForBorders . y = borderTest - screenUpperRight . y ;
adjustedNewTTPos . y - = shifterForBorders . y ;
}
//failed attempt to circumvent issues caused when rotating the camera
adjustedNewTTPos = transform . rotation * adjustedNewTTPos ;
transform . position = adjustedNewTTPos ;
adjustedTTLocalPos = transform . localPosition ;
adjustedTTLocalPos . z = 0 ;
transform . localPosition = adjustedTTLocalPos ;
_inside = true ;
}
//main tooltip edge of screen guard and movement - overlay
2026-03-20 11:56:50 -04:00
public void OnScreenSpaceOverlay ( bool refreshCanvasesBeforeGettingSize = false )
{
2025-10-03 00:02:43 -04:00
shiftingVector . x = xShift ;
shiftingVector . y = YShift ;
newTTPos = ( baseTooltipPos - shiftingVector ) / canvas . scaleFactor ;
adjustedNewTTPos = newTTPos ;
gameObject . SetActive ( true ) ;
if ( refreshCanvasesBeforeGettingSize ) RefreshTooltipSize ( ) ;
width = _rectTransform . sizeDelta [ 0 ] ;
height = _rectTransform . sizeDelta [ 1 ] ;
// check and solve problems for the tooltip that goes out of the screen on the horizontal axis
//screen's 0 = overlay canvas's 0 (always?)
screenLowerLeft = Vector3 . zero ;
screenUpperRight = canvasRectTransform . sizeDelta ;
//check for right edge of screen
2026-03-20 11:56:50 -04:00
borderTest = newTTPos . x + width / 2 ;
if ( borderTest > screenUpperRight . x )
{
2025-10-03 00:02:43 -04:00
shifterForBorders . x = borderTest - screenUpperRight . x ;
adjustedNewTTPos . x - = shifterForBorders . x ;
}
2026-03-20 11:56:50 -04:00
2025-10-03 00:02:43 -04:00
//check for left edge of screen
2026-03-20 11:56:50 -04:00
borderTest = adjustedNewTTPos . x - width / 2 ;
if ( borderTest < screenLowerLeft . x )
{
2025-10-03 00:02:43 -04:00
shifterForBorders . x = screenLowerLeft . x - borderTest ;
adjustedNewTTPos . x + = shifterForBorders . x ;
}
// check and solve problems for the tooltip that goes out of the screen on the vertical axis
//check for lower edge of the screen
2026-03-20 11:56:50 -04:00
borderTest = adjustedNewTTPos . y - height / 2 ;
if ( borderTest < screenLowerLeft . y )
{
2025-10-03 00:02:43 -04:00
shifterForBorders . y = screenLowerLeft . y - borderTest ;
adjustedNewTTPos . y + = shifterForBorders . y ;
}
//check for upper edge of the screen
2026-03-20 11:56:50 -04:00
borderTest = adjustedNewTTPos . y + height / 2 ;
if ( borderTest > screenUpperRight . y )
{
2025-10-03 00:02:43 -04:00
shifterForBorders . y = borderTest - screenUpperRight . y ;
adjustedNewTTPos . y - = shifterForBorders . y ;
}
//remove scale factor for the actual positioning of the TT
adjustedNewTTPos * = canvas . scaleFactor ;
transform . position = adjustedNewTTPos ;
_inside = true ;
}
}
}