Skip to content

Instantly share code, notes, and snippets.

@dougvalenta
Last active February 20, 2018 10:05
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dougvalenta/98ab013af9f4615ae1e38962e99b4320 to your computer and use it in GitHub Desktop.
Save dougvalenta/98ab013af9f4615ae1e38962e99b4320 to your computer and use it in GitHub Desktop.
Hypertext for Unity UI
/*
* Copyright 2018 Doug Valenta
* Licensed under the MIT license
*/
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Text.RegularExpressions;
[AddComponentMenu("UI/Hypertext")]
public class Hypertext : Text, ICanvasRaycastFilter, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler
{
static readonly Regex ANCHOR_REGEX = new Regex(@"<a( +href=(?<href>\S*))? *>(?<content>.*?)</a>");
static readonly Color32 WHITE32 = new Color32(255, 255, 255, 255);
static readonly Color TRANSPARENT = new Color(0f, 0f, 0f, 0f);
public enum LinkUnderlineMode
{
Underline,
UnderlineOnHover,
NoUnderline
}
public Color LinkColor = Color.blue;
public Color LinkColorOnHover = Color.red;
public LinkUnderlineMode LinkUnderline;
public float LinkUnderlineVerticalOffset = -1f;
public float LinkUnderlineThickness = 1f;
protected class Link
{
public int Index;
public int Length;
public List<Rect> Rects = new List<Rect>();
public string Href;
}
[System.NonSerialized] protected string TextToRender = "";
protected List<Link> Links = new List<Link>();
[System.NonSerialized] protected Link LastRaycastedLink = null;
[System.NonSerialized] protected int LastRaycastedLinkIndex = -1;
[System.NonSerialized] protected bool Hover = false;
public override string text
{
get
{
return m_Text;
}
set
{
if (string.IsNullOrEmpty(value))
{
if (string.IsNullOrEmpty(m_Text))
return;
m_Text = "";
TextToRender = "";
Links.Clear();
SetVerticesDirty();
}
else if (m_Text != value)
{
m_Text = value;
PrepareTextToRender();
SetVerticesDirty();
SetLayoutDirty();
}
}
}
protected override void OnEnable()
{
base.OnEnable();
PrepareTextToRender();
}
public void UpdateMaterialProperties()
{
if (m_Material == null)
{
m_Material = new Material(Shader.Find("UI/Hypertext"));
}
material.SetColor("_LinkColor", LinkColor);
material.SetColor("_LinkColorOnHover", LinkColorOnHover);
material.SetColor("_LinkUnderlineColor", LinkUnderline == LinkUnderlineMode.Underline ? LinkColor : TRANSPARENT);
material.SetColor("_LinkUnderlineColorOnHover", LinkColorOnHover);
}
public void UpdateHoverIndex()
{
if (Hover)
{
material.SetFloat("_LinkHoverIndex", LastRaycastedLinkIndex);
}
else
{
material.SetFloat("_LinkHoverIndex", -1f);
}
}
public void PrepareTextToRender()
{
int index = 0;
TextToRender = "";
Links.Clear();
foreach (Match match in ANCHOR_REGEX.Matches(m_Text))
{
TextToRender += m_Text.Substring(index, match.Index - index);
string content = match.Groups["content"].Value;
Link link = new Link();
link.Index = TextToRender.Length;
link.Length = content.Length;
TextToRender += content;
link.Href = match.Groups["href"].Value;
Links.Add(link);
index = match.Index + match.Length;
}
TextToRender += m_Text.Substring(index);
}
public virtual bool IsRaycastLocationValid(Vector2 screenPoint, UnityEngine.Camera camera)
{
Vector2 localPoint;
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, camera, out localPoint))
{
for (int i = 0; i < Links.Count; i++)
{
Link link = Links[i];
foreach (Rect rect in link.Rects)
{
if (rect.Contains(localPoint))
{
if (Hover && i != LastRaycastedLinkIndex)
{
LastRaycastedLinkIndex = i;
LastRaycastedLink = link;
UpdateHoverIndex();
}
else
{
LastRaycastedLinkIndex = i;
LastRaycastedLink = link;
}
return true;
}
}
}
}
return false;
}
public virtual void OnPointerEnter(PointerEventData data)
{
// Change the cursor to a pointer
Hover = true;
UpdateHoverIndex();
}
public virtual void OnPointerExit(PointerEventData data)
{
// Change the cursor back to default
Hover = false;
UpdateHoverIndex();
}
public virtual void OnPointerClick(PointerEventData data)
{
if (LastRaycastedLink != null)
{
// Lot's of things you could do here
// - Call a UnityEvent
// - Add a ScriptableObject that maps actions to hrefs
Debug.Log(LastRaycastedLink.Href);
}
}
readonly UIVertex[] m_TempVerts = new UIVertex[4];
protected override void OnPopulateMesh(VertexHelper toFill)
{
if (font == null) return;
m_DisableFontTextureRebuiltCallback = true;
Vector2 extents = rectTransform.rect.size;
var settings = GetGenerationSettings(extents);
cachedTextGenerator.PopulateWithErrors(TextToRender, settings, gameObject);
IList<UIVertex> verts = cachedTextGenerator.verts;
IList<UILineInfo> lines = cachedTextGenerator.lines;
float unitsPerPixel = 1f / pixelsPerUnit;
Vector2 roundingOffset = new Vector2(verts[0].position.x, verts[0].position.y) * unitsPerPixel;
roundingOffset = PixelAdjustPoint(roundingOffset) - roundingOffset;
toFill.Clear();
bool rounding = roundingOffset != Vector2.zero;
Link currentLink = null;
int currentLinkIndex = -1;
int nextLinkCharacter = -1;
if (Links.Count > 0) nextLinkCharacter = Links[0].Index;
bool inLink = false;
int currentLine = 0;
int nextLineCharacter = -1;
if (lines.Count > 1) nextLineCharacter = lines[1].startCharIdx;
Rect currentRect = new Rect();
int visibleCharacters = cachedTextGenerator.characterCountVisible;
for (int c = 0; c < visibleCharacters; c++)
{
if (inLink && c == currentLink.Index + currentLink.Length)
{
inLink = false;
}
if (c == nextLineCharacter)
{
currentLine++;
if (lines.Count > currentLine + 1) nextLineCharacter = lines[currentLine + 1].startCharIdx;
else nextLineCharacter = -1;
}
if (c == nextLinkCharacter)
{
currentLinkIndex++;
currentLink = Links[currentLinkIndex];
if (Links.Count > currentLinkIndex + 1) nextLinkCharacter = Links[currentLinkIndex + 1].Index;
else nextLinkCharacter = -1;
inLink = true;
}
for (int v = 0; v < 4; v++)
{
UIVertex vertex = verts[c * 4 + v];
vertex.position *= unitsPerPixel;
if (rounding)
{
vertex.position.x += roundingOffset.x;
vertex.position.y += roundingOffset.y;
}
if (inLink)
{
vertex.uv1 = new Vector2(currentLinkIndex, 0f);
vertex.color = Color.white;
}
else
{
vertex.uv1 = Vector2.left;
}
m_TempVerts[v] = vertex;
}
if (inLink)
{
if (c == currentLink.Index || c == lines[currentLine].startCharIdx)
{
currentRect.x = m_TempVerts[3].position.x;
currentRect.y = m_TempVerts[3].position.y;
currentRect.height = lines[currentLine].height * unitsPerPixel;
}
if (c == currentLink.Index + currentLink.Length - 1 || c == nextLineCharacter - 1)
{
if (TextToRender[c] == ' ')
{
currentRect.xMax = m_TempVerts[3].position.x;
}
else
{
currentRect.xMax = m_TempVerts[2].position.x;
}
currentLink.Rects.Add(currentRect);
if (LinkUnderline != LinkUnderlineMode.NoUnderline)
{
UIVertex vertex0 = new UIVertex();
vertex0.position.x = currentRect.xMin;
vertex0.position.y = currentRect.yMin + LinkUnderlineVerticalOffset;// * unitsPerPixel;
vertex0.color = WHITE32;
vertex0.uv1 = new Vector2(currentLinkIndex, 1);
UIVertex vertex1 = new UIVertex();
vertex1.position.x = currentRect.xMin;
vertex1.position.y = currentRect.yMin + (LinkUnderlineVerticalOffset - LinkUnderlineThickness);// * unitsPerPixel;
vertex1.color = WHITE32;
vertex1.uv1 = new Vector2(currentLinkIndex, 1);
UIVertex vertex2 = new UIVertex();
vertex2.position.x = currentRect.xMax;
vertex2.position.y = currentRect.yMin + (LinkUnderlineVerticalOffset - LinkUnderlineThickness);// * unitsPerPixel;
vertex2.color = WHITE32;
vertex2.uv1 = new Vector2(currentLinkIndex, 1);
UIVertex vertex3 = new UIVertex();
vertex3.position.x = currentRect.xMax;
vertex3.position.y = currentRect.yMin + LinkUnderlineVerticalOffset;// * unitsPerPixel;
vertex3.color = WHITE32;
vertex3.uv1 = new Vector2(currentLinkIndex, 1);
toFill.AddUIVertexQuad(new UIVertex[] { vertex0, vertex1, vertex2, vertex3 });
}
}
}
toFill.AddUIVertexQuad(m_TempVerts);
}
m_DisableFontTextureRebuiltCallback = false;
}
public override float preferredWidth
{
get
{
var settings = GetGenerationSettings(Vector2.zero);
return cachedTextGeneratorForLayout.GetPreferredWidth(TextToRender, settings) / pixelsPerUnit;
}
}
public override float preferredHeight
{
get
{
var settings = GetGenerationSettings(new Vector2(GetPixelAdjustedRect().size.x, 0.0f));
return cachedTextGeneratorForLayout.GetPreferredHeight(TextToRender, settings) / pixelsPerUnit;
}
}
}
/*
* Copyright 2018 Doug Valenta
* Licensed under the MIT license
*/
// PUT THIS FILE IN AN EDITOR FOLDER!
using UnityEngine;
using UnityEditor;
using UnityEditor.UI;
[CustomEditor(typeof(Hypertext), true)]
[CanEditMultipleObjects]
public class HypertextEditor : GraphicEditor
{
[MenuItem("GameObject/UI/Hypertext")]
static void CreateHypertext(MenuCommand command)
{
GameObject gameObject = new GameObject("Hypertext", typeof(RectTransform));
GameObjectUtility.SetParentAndAlign(gameObject, command.context as GameObject);
gameObject.AddComponent(typeof(CanvasRenderer));
Hypertext hypertext = gameObject.AddComponent<Hypertext>();
Selection.activeGameObject = gameObject;
hypertext.canvas.additionalShaderChannels |= AdditionalCanvasShaderChannels.TexCoord1;
}
protected SerializedProperty m_Text;
protected SerializedProperty m_FontData;
protected SerializedProperty LinkColorProperty;
protected SerializedProperty LinkColorOnHoverProperty;
protected SerializedProperty LinkUnderlineProperty;
protected SerializedProperty LinkUnderlineVerticalOffsetProperty;
protected SerializedProperty LinkUnderlineThicknessProperty;
protected override void OnEnable()
{
base.OnEnable();
m_Text = serializedObject.FindProperty("m_Text");
m_FontData = serializedObject.FindProperty("m_FontData");
LinkColorProperty = serializedObject.FindProperty("LinkColor");
LinkColorOnHoverProperty = serializedObject.FindProperty("LinkColorOnHover");
LinkUnderlineProperty = serializedObject.FindProperty("LinkUnderline");
LinkUnderlineVerticalOffsetProperty = serializedObject.FindProperty("LinkUnderlineVerticalOffset");
LinkUnderlineThicknessProperty = serializedObject.FindProperty("LinkUnderlineThickness");
foreach (Object targetObject in targets)
{
((Hypertext)targetObject).PrepareTextToRender();
((Hypertext)targetObject).UpdateMaterialProperties();
((Hypertext)targetObject).SetVerticesDirty();
}
}
public override void OnInspectorGUI()
{
serializedObject.Update();
bool shouldPrepareTextToRender;
bool shouldUpdateMaterialProperties;
bool shouldSetVerticesDirty;
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_Text);
shouldPrepareTextToRender = EditorGUI.EndChangeCheck();
EditorGUILayout.PropertyField(m_FontData);
EditorGUILayout.LabelField("Links", EditorStyles.boldLabel);
EditorGUI.indentLevel++;
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(LinkColorProperty);
EditorGUILayout.PropertyField(LinkColorOnHoverProperty);
shouldUpdateMaterialProperties = EditorGUI.EndChangeCheck();
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(LinkUnderlineProperty);
EditorGUILayout.PropertyField(LinkUnderlineThicknessProperty);
EditorGUILayout.PropertyField(LinkUnderlineVerticalOffsetProperty);
shouldSetVerticesDirty = EditorGUI.EndChangeCheck();
shouldUpdateMaterialProperties = shouldUpdateMaterialProperties || shouldSetVerticesDirty;
EditorGUI.indentLevel--;
EditorGUILayout.PropertyField(m_Color);
RaycastControlsGUI();
serializedObject.ApplyModifiedProperties();
if (shouldPrepareTextToRender)
{
foreach (Object targetObject in targets)
{
((Hypertext)targetObject).PrepareTextToRender();
}
}
if (shouldUpdateMaterialProperties)
{
foreach (Object targetObject in targets)
{
((Hypertext)targetObject).UpdateMaterialProperties();
}
}
if (shouldSetVerticesDirty)
{
foreach (Object targetObject in targets)
{
((Hypertext)targetObject).SetVerticesDirty();
}
}
}
}
// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)
// Modified by Doug Valenta 2018
Shader "UI/Hypertext"
{
Properties
{
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)
[HideInInspector] _LinkColor ("Link Color", Color) = (0, 0, 1, 1)
[HideInInspector] _LinkColorOnHover ("Link Color On Hover", Color) = (1, 0, 0, 1)
[HideInInspector] _LinkUnderlineColor ("Link Underline Color", Color) = (0, 0, 1, 0)
[HideInInspector] _LinkUnderlineColorOnHover ("Link Underline Color On Hover", Color) = (1, 0, 0, 1)
[HideInInspector] _LinkHoverIndex ("Link Hover Index", Float) = -1
_StencilComp ("Stencil Comparison", Float) = 8
_Stencil ("Stencil ID", Float) = 0
_StencilOp ("Stencil Operation", Float) = 0
_StencilWriteMask ("Stencil Write Mask", Float) = 255
_StencilReadMask ("Stencil Read Mask", Float) = 255
_ColorMask ("Color Mask", Float) = 15
[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
}
SubShader
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
"CanUseSpriteAtlas"="True"
}
Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}
Cull Off
Lighting Off
ZWrite Off
ZTest [unity_GUIZTestMode]
Blend SrcAlpha OneMinusSrcAlpha
ColorMask [_ColorMask]
Pass
{
Name "Default"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0
#include "UnityCG.cginc"
#include "UnityUI.cginc"
#pragma multi_compile __ UNITY_UI_CLIP_RECT
#pragma multi_compile __ UNITY_UI_ALPHACLIP
struct appdata_t
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
float2 texcoord1 : TEXCOORD1;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float3 texcoord : TEXCOORD0;
float4 worldPosition : TEXCOORD1;
UNITY_VERTEX_OUTPUT_STEREO
};
fixed4 _Color;
fixed4 _TextureSampleAdd;
float4 _ClipRect;
fixed4 _LinkColor, _LinkColorOnHover;
fixed4 _LinkUnderlineColor, _LinkUnderlineColorOnHover;
half _LinkHoverIndex;
v2f vert(appdata_t v)
{
v2f OUT;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
OUT.worldPosition = v.vertex;
OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);
OUT.texcoord.xy = v.texcoord;
OUT.color = v.color * _Color;
half isLink = step(0, v.texcoord1.x);
half isHover = step(0, v.texcoord1.x - _LinkHoverIndex) * step(v.texcoord1.x - _LinkHoverIndex, 0) * step(0, _LinkHoverIndex);
isLink *= (1 - isHover);
half isUnderline = step(1, v.texcoord1.y);
half isLinkText = isLink * (1 - isUnderline);
half isLinkTextHover = isHover * (1 - isUnderline);
half isLinkUnderline = isLink * isUnderline;
half isLinkUnderlineHover = isHover * isUnderline;
OUT.color *= _LinkColor * isLinkText + _LinkColorOnHover * isLinkTextHover + _LinkUnderlineColor * isLinkUnderline + _LinkUnderlineColorOnHover * isLinkUnderlineHover + (1 - isLink - isHover);
OUT.texcoord.z = isLinkUnderline * _LinkUnderlineColor.a + isLinkUnderlineHover * _LinkUnderlineColorOnHover.a;
return OUT;
}
sampler2D _MainTex;
fixed4 frag(v2f IN) : SV_Target
{
half4 color = (tex2D(_MainTex, IN.texcoord.xy) + _TextureSampleAdd) * IN.color;
color.a = min(1, color.a + IN.texcoord.z);
#ifdef UNITY_UI_CLIP_RECT
color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
#endif
#ifdef UNITY_UI_ALPHACLIP
clip (color.a - 0.001);
#endif
return color;
}
ENDCG
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment