using Nova;
using UnityEngine;
namespace NovaSamples.UIControls
{
///
/// A simple example of a Tooltip componenent, which can activate a
/// when a designated is hovered using Nova's API.
///
public class Tooltip : MonoBehaviour
{
private static readonly Vector2[] OffsetCorners = new Vector2[] { new Vector2(1, -1), new Vector2(-1, -1), new Vector2(-1, 1), new Vector2(1, 1) };
[Tooltip("The UIBlock to dispay and move while the tooltip is triggered.")]
public UIBlock TooltipVisual = null;
[Tooltip("The Interactable or Scroller whose bounds will define the area where the tooltip should be active when hovered.")]
public GestureRecognizer TriggerRegion = null;
[Header("Repositioning")]
[Tooltip("If false, the tooltip will only hide/show but will not move.")]
public bool FollowCursor = true;
[Tooltip("The size of the cursor in pixels. Used to position the tooltip such that the cursor doesn't obscure it.")]
public Rect CursorRect = new Rect(Vector2.zero, Vector2.one * 32);
[Tooltip("If true, this tooltip will always try to position its tooltip visual within the bounds defined by the Trigger Region's UI Block.")]
public bool KeepWithinRootBounds = true;
[Tooltip("The camera used to route input. If left null, will try to fall back to first the ScreenSpace Target Camera and then the Main camera.")]
public Camera InputCamera = null;
public void OnEnable()
{
if (TooltipVisual != null)
{
// Make sure to start disabled
TooltipVisual.gameObject.SetActive(false);
}
if (TriggerRegion == null)
{
// Can't subscribe to gesture events
return;
}
// Subscribe to desired gestures
TriggerRegion.UIBlock.AddGestureHandler(ShowTooltip);
TriggerRegion.UIBlock.AddGestureHandler(MoveTooltip);
TriggerRegion.UIBlock.AddGestureHandler(HideTooltip);
}
public void OnDisable()
{
if (TriggerRegion == null)
{
// Can't unsubscribe from gesture events
return;
}
// Unsubscribe from gestures
TriggerRegion.UIBlock.RemoveGestureHandler(ShowTooltip);
TriggerRegion.UIBlock.RemoveGestureHandler(MoveTooltip);
TriggerRegion.UIBlock.RemoveGestureHandler(HideTooltip);
}
///
/// Show the on hovered.
///
/// The hover event data, which we can use to position the tooltip.
private void ShowTooltip(Gesture.OnHover evt)
{
if (TooltipVisual == null)
{
// Nothing to show
return;
}
TooltipVisual.gameObject.SetActive(true);
TooltipVisual.CalculateLayout();
UpdateTooltipPosition(evt.PointerWorldPosition);
}
///
/// Update the tooltip position on as the pointer moves.
///
/// The move event data, which we can use to position the tooltip.
private void MoveTooltip(Gesture.OnMove evt)
{
if (TooltipVisual == null)
{
return;
}
UpdateTooltipPosition(evt.PointerWorldPosition);
}
///
/// Move the tooltip visual relative to the cursor.
///
/// The cursor position in world space.
private void UpdateTooltipPosition(Vector3 pointerWorldPosition)
{
if (!FollowCursor)
{
// We don't want to move the tooltip.
return;
}
Transform parent = TooltipVisual.transform.parent;
Matrix4x4 parentLocalToWorldMatrix = parent != null ? parent.localToWorldMatrix : Matrix4x4.identity;
Matrix4x4 parentWorldToLocal = parent != null ? parent.worldToLocalMatrix : Matrix4x4.identity;
// Get the cursor position relative to the tooltip
Vector2 pointerPositionParentSpace = parentWorldToLocal.MultiplyPoint(pointerWorldPosition);
// The tooltip visual size in local space
Vector2 visualSize = TooltipVisual.CalculatedSize.XY.Value * TooltipVisual.transform.localScale.x;
// The extents of the tooltip visual in local space
Vector2 extents = 0.5f * visualSize;
// The default offset direction, which is to the bottom right of the cursor
Vector2 offsetCorner = OffsetCorners[0];
// Get the amount we need to offset the tooltip in
// local space based on the pixel size of the cursor
Vector2 cursorOffset = GetApproximateWorldCursorOffset(pointerWorldPosition, offsetCorner);
// The position to move the tooltip visual, relative to where it is now.
Vector3 newLocalPosition = pointerPositionParentSpace + offsetCorner * (extents + cursorOffset);
// If desired, try to flip the tooltip visual offset such that it doesn't
// render outside the bounds of its root UIBlock
if (KeepWithinRootBounds)
{
// Get the root UIBlock to bounds check against
UIBlock root = TooltipVisual.Root;
// The bounds of the root in root space
Vector2 rootSize = root.CalculatedSize.XY.Value;
Rect rootBounds = new Rect(-0.5f * rootSize, rootSize);
// Try all four corners around the cursor.
// Finishes on the top left of the cursor if they all fail.
for (int i = 1; i < OffsetCorners.Length; ++i)
{
// The point that's going to be the furthest from the cursor (in local space)
Vector3 furthestPointLocalSpace = newLocalPosition + (Vector3)(offsetCorner * extents);
// Convert the furthest point into world space
Vector3 furthestPointWorldSpace = parentLocalToWorldMatrix.MultiplyPoint(furthestPointLocalSpace);
// Convert the furthest point into root space
Vector3 furthestPointRootSpace = root.transform.InverseTransformPoint(furthestPointWorldSpace);
if (rootBounds.Contains(furthestPointRootSpace))
{
// In bounds, so we don't need to keep checkout corners.
break;
}
// Move on to the next corner
offsetCorner = OffsetCorners[i];
cursorOffset = GetApproximateWorldCursorOffset(pointerWorldPosition, offsetCorner);
newLocalPosition = pointerPositionParentSpace + (offsetCorner * (extents + cursorOffset));
}
}
// Move the tooltip to its new local position
TooltipVisual.TrySetLocalPosition(newLocalPosition);
}
///
/// Hide the on unhovered.
///
/// The unhover event data.
private void HideTooltip(Gesture.OnUnhover evt)
{
if (TooltipVisual == null)
{
// Nothing to hide
return;
}
TooltipVisual.gameObject.SetActive(false);
}
///
/// Calculates an offset to adjust the tooltip position such that the cursor doesn't obscure it.
///
///
/// This value will be incorrect if the camera used for the calculation
/// is not the camera used to raycast the cursor for the initial gesture
/// (or in the XR case where the cursor raycast comes from a controller).
///
private Vector2 GetApproximateWorldCursorOffset(Vector3 cursorWorldPosition, Vector2 offsetDirection)
{
// flip y because yMax in rect space is down
Vector2 cursorOffset = new Vector2(offsetDirection.x < 0 ? CursorRect.xMin : CursorRect.xMax,
offsetDirection.y > 0 ? CursorRect.yMin : CursorRect.yMax);
UIBlock root = TooltipVisual.Root;
Camera camera = InputCamera;
if (camera == null)
{
// If the Input camera is unassigned, try to fallback to the screen space camera
if (root.TryGetComponent(out ScreenSpace screenSpace) && screenSpace.TargetCamera != null)
{
camera = screenSpace.TargetCamera;
}
else
{
camera = Camera.main;
}
}
if (camera == null)
{
// Main camera not found.
return Vector2.zero;
}
// Convert cursor position to screen space
Vector2 minScreenPosition = camera.WorldToScreenPoint(cursorWorldPosition);
// Get the offset position of the cursor rect in screen space
Vector2 maxScreenPosition = minScreenPosition + cursorOffset;
// Convert the screen position into a ray
Ray maxRay = camera.ScreenPointToRay(maxScreenPosition);
Plane rootPlane = new Plane(root.transform.forward, root.transform.position);
// Raycast against the UI Block root plane
if (!rootPlane.Raycast(maxRay, out float distanceToRoot))
{
return Vector2.zero;
}
// The position in the world of the max point on the cursor rect
Vector3 cursorMaxPoint = maxRay.GetPoint(distanceToRoot);
// The size of the cursor in world space
Vector2 cursorSize = cursorWorldPosition - cursorMaxPoint;
float parentScale = TooltipVisual.transform.parent != null ? TooltipVisual.transform.parent.lossyScale.x : 1;
// Absolute value of the cursor size, in root local space
return Vector2.Max(cursorSize, -cursorSize) / parentScale;
}
}
}