Last active
February 20, 2018 10:05
-
-
Save dougvalenta/98ab013af9f4615ae1e38962e99b4320 to your computer and use it in GitHub Desktop.
Hypertext for Unity UI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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(); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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