HypertextBase.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. /*
  2. * uGUI-Hypertext (https://github.com/setchi/uGUI-Hypertext)
  3. * Copyright (c) 2019 setchi
  4. * Licensed under MIT (https://github.com/setchi/uGUI-Hypertext/blob/master/LICENSE)
  5. */
  6. using System;
  7. using System.Collections.Generic;
  8. using System.Linq;
  9. using UnityEngine;
  10. using UnityEngine.EventSystems;
  11. using UnityEngine.UI;
  12. namespace Hypertext
  13. {
  14. public abstract class HypertextBase : Text, IPointerClickHandler
  15. {
  16. class Span
  17. {
  18. public readonly int StartIndex;
  19. public readonly int Length;
  20. public readonly Color Color;
  21. public readonly Action<string> Callback;
  22. public List<Rect> BoundingBoxes;
  23. public Span(int startIndex, int length, Color color, Action<string> callback)
  24. {
  25. StartIndex = startIndex;
  26. Length = length;
  27. Color = color;
  28. Callback = callback;
  29. BoundingBoxes = new List<Rect>();
  30. }
  31. };
  32. readonly List<Span> spans = new List<Span>();
  33. // TODO: 頂点が生成されない空白文字をすべて洗い出す
  34. readonly char[] invisibleChars =
  35. {
  36. Space,
  37. Tab,
  38. LineFeed
  39. };
  40. static readonly ObjectPool<List<UIVertex>> verticesPool = new ObjectPool<List<UIVertex>>(null, l => l.Clear());
  41. const int CharVerts = 6;
  42. const char
  43. Tab = '\t',
  44. LineFeed = '\n',
  45. Space = ' ',
  46. LesserThan = '<',
  47. GreaterThan = '>';
  48. int[] visibleCharIndexMap;
  49. Canvas rootCanvas;
  50. Canvas RootCanvas => rootCanvas ?? (rootCanvas = GetComponentInParent<Canvas>());
  51. /// <summary>
  52. /// 指定した部分文字列にクリックイベントリスナを登録します
  53. /// </summary>
  54. /// <param name="startIndex">部分文字列の開始文字位置</param>
  55. /// <param name="length">部分文字列の長さ</param>
  56. /// <param name="color">部分文字列につける色</param>
  57. /// <param name="onClick">部分文字列がクリックされたときのコールバック</param>
  58. protected void OnClick(int startIndex, int length, Color color, Action<string> onClick)
  59. {
  60. if (onClick == null)
  61. {
  62. throw new ArgumentNullException(nameof(onClick));
  63. }
  64. if (startIndex < 0 || startIndex > text.Length - 1)
  65. {
  66. throw new ArgumentOutOfRangeException(nameof(startIndex));
  67. }
  68. if (length < 1 || startIndex + length > text.Length)
  69. {
  70. throw new ArgumentOutOfRangeException(nameof(length));
  71. }
  72. spans.Add(new Span(startIndex, length, color, onClick));
  73. }
  74. /// <summary>
  75. /// イベントリスナを削除します
  76. /// </summary>
  77. public virtual void RemoveListeners()
  78. {
  79. spans.Clear();
  80. }
  81. /// <summary>
  82. /// イベントリスナを追加します
  83. /// テキストの変更などでイベントリスナの再登録が必要なときにも呼び出されます
  84. /// <see cref="HypertextBase.OnClick"/> を使ってクリックイベントリスナを登録してください
  85. /// </summary>
  86. protected abstract void AddListeners();
  87. readonly UIVertex[] tempVerts = new UIVertex[4];
  88. protected override void OnPopulateMesh(VertexHelper toFill)
  89. {
  90. if (font == null)
  91. {
  92. return;
  93. }
  94. m_DisableFontTextureRebuiltCallback = true;
  95. var extents = rectTransform.rect.size;
  96. var settings = GetGenerationSettings(extents);
  97. settings.generateOutOfBounds = true;
  98. cachedTextGenerator.PopulateWithErrors(text, settings, gameObject);
  99. var verts = cachedTextGenerator.verts;
  100. var unitsPerPixel = 1 / pixelsPerUnit;
  101. int vertCount = verts.Count;
  102. if (vertCount <= 0)
  103. {
  104. toFill.Clear();
  105. return;
  106. }
  107. var roundingOffset = new Vector2(verts[0].position.x, verts[0].position.y) * unitsPerPixel;
  108. roundingOffset = PixelAdjustPoint(roundingOffset) - roundingOffset;
  109. toFill.Clear();
  110. if (roundingOffset != Vector2.zero)
  111. {
  112. for (int i = 0; i < vertCount; ++i)
  113. {
  114. int tempVertsIndex = i & 3;
  115. tempVerts[tempVertsIndex] = verts[i];
  116. tempVerts[tempVertsIndex].position *= unitsPerPixel;
  117. tempVerts[tempVertsIndex].position.x += roundingOffset.x;
  118. tempVerts[tempVertsIndex].position.y += roundingOffset.y;
  119. if (tempVertsIndex == 3)
  120. {
  121. toFill.AddUIVertexQuad(tempVerts);
  122. }
  123. }
  124. }
  125. else
  126. {
  127. for (int i = 0; i < vertCount; ++i)
  128. {
  129. int tempVertsIndex = i & 3;
  130. tempVerts[tempVertsIndex] = verts[i];
  131. tempVerts[tempVertsIndex].position *= unitsPerPixel;
  132. if (tempVertsIndex == 3)
  133. {
  134. toFill.AddUIVertexQuad(tempVerts);
  135. }
  136. }
  137. }
  138. var vertices = verticesPool.Get();
  139. toFill.GetUIVertexStream(vertices);
  140. GenerateVisibleCharIndexMap(vertices.Count < text.Length * CharVerts);
  141. spans.Clear();
  142. AddListeners();
  143. GenerateHrefBoundingBoxes(ref vertices);
  144. toFill.Clear();
  145. toFill.AddUIVertexTriangleStream(vertices);
  146. verticesPool.Release(vertices);
  147. m_DisableFontTextureRebuiltCallback = false;
  148. }
  149. void GenerateHrefBoundingBoxes(ref List<UIVertex> vertices)
  150. {
  151. var verticesCount = vertices.Count;
  152. for (var i = 0; i < spans.Count; i++)
  153. {
  154. var span = spans[i];
  155. var startIndex = visibleCharIndexMap[span.StartIndex];
  156. var endIndex = visibleCharIndexMap[span.StartIndex + span.Length - 1];
  157. for (var textIndex = startIndex; textIndex <= endIndex; textIndex++)
  158. {
  159. var vertexStartIndex = textIndex * CharVerts;
  160. if (vertexStartIndex + CharVerts > verticesCount)
  161. {
  162. break;
  163. }
  164. var min = Vector2.one * float.MaxValue;
  165. var max = Vector2.one * float.MinValue;
  166. for (var vertexIndex = 0; vertexIndex < CharVerts; vertexIndex++)
  167. {
  168. var vertex = vertices[vertexStartIndex + vertexIndex];
  169. vertex.color = span.Color;
  170. vertices[vertexStartIndex + vertexIndex] = vertex;
  171. var pos = vertices[vertexStartIndex + vertexIndex].position;
  172. if (pos.y < min.y)
  173. {
  174. min.y = pos.y;
  175. }
  176. if (pos.x < min.x)
  177. {
  178. min.x = pos.x;
  179. }
  180. if (pos.y > max.y)
  181. {
  182. max.y = pos.y;
  183. }
  184. if (pos.x > max.x)
  185. {
  186. max.x = pos.x;
  187. }
  188. }
  189. span.BoundingBoxes.Add(new Rect {min = min, max = max});
  190. }
  191. // 文字ごとのバウンディングボックスを行ごとのバウンディングボックスにまとめる
  192. span.BoundingBoxes = CalculateLineBoundingBoxes(span.BoundingBoxes);
  193. }
  194. }
  195. static List<Rect> CalculateLineBoundingBoxes(List<Rect> charBoundingBoxes)
  196. {
  197. var lineBoundingBoxes = new List<Rect>();
  198. var lineStartIndex = 0;
  199. for (var i = 1; i < charBoundingBoxes.Count; i++)
  200. {
  201. if (charBoundingBoxes[i].xMin >= charBoundingBoxes[i - 1].xMin)
  202. {
  203. continue;
  204. }
  205. lineBoundingBoxes.Add(CalculateAABB(charBoundingBoxes.GetRange(lineStartIndex, i - lineStartIndex)));
  206. lineStartIndex = i;
  207. }
  208. if (lineStartIndex < charBoundingBoxes.Count)
  209. {
  210. lineBoundingBoxes.Add(CalculateAABB(charBoundingBoxes.GetRange(lineStartIndex, charBoundingBoxes.Count - lineStartIndex)));
  211. }
  212. return lineBoundingBoxes;
  213. }
  214. static Rect CalculateAABB(IReadOnlyList<Rect> rects)
  215. {
  216. var min = Vector2.one * float.MaxValue;
  217. var max = Vector2.one * float.MinValue;
  218. for (var i = 0; i < rects.Count; i++)
  219. {
  220. if (rects[i].xMin < min.x)
  221. {
  222. min.x = rects[i].xMin;
  223. }
  224. if (rects[i].yMin < min.y)
  225. {
  226. min.y = rects[i].yMin;
  227. }
  228. if (rects[i].xMax > max.x)
  229. {
  230. max.x = rects[i].xMax;
  231. }
  232. if (rects[i].yMax > max.y)
  233. {
  234. max.y = rects[i].yMax;
  235. }
  236. }
  237. return new Rect {min = min, max = max};
  238. }
  239. void GenerateVisibleCharIndexMap(bool verticesReduced)
  240. {
  241. if (visibleCharIndexMap == null || visibleCharIndexMap.Length < text.Length)
  242. {
  243. Array.Resize(ref visibleCharIndexMap, text.Length);
  244. }
  245. if (!verticesReduced)
  246. {
  247. for (var i = 0; i < visibleCharIndexMap.Length; i++)
  248. {
  249. visibleCharIndexMap[i] = i;
  250. }
  251. return;
  252. }
  253. var offset = 0;
  254. var inTag = false;
  255. for (var i = 0; i < text.Length; i++)
  256. {
  257. var character = text[i];
  258. if (inTag)
  259. {
  260. offset--;
  261. if (character == GreaterThan)
  262. {
  263. inTag = false;
  264. }
  265. }
  266. else if (supportRichText && character == LesserThan)
  267. {
  268. offset--;
  269. inTag = true;
  270. }
  271. else if (invisibleChars.Contains(character))
  272. {
  273. offset--;
  274. }
  275. visibleCharIndexMap[i] = Mathf.Max(0, i + offset);
  276. }
  277. }
  278. Vector3 CalculateLocalPosition(Vector3 position, Camera pressEventCamera)
  279. {
  280. if (!RootCanvas)
  281. {
  282. return Vector3.zero;
  283. }
  284. if (RootCanvas.renderMode == RenderMode.ScreenSpaceOverlay)
  285. {
  286. return transform.InverseTransformPoint(position);
  287. }
  288. RectTransformUtility.ScreenPointToLocalPointInRectangle(
  289. rectTransform,
  290. position,
  291. pressEventCamera,
  292. out var localPosition
  293. );
  294. return localPosition;
  295. }
  296. void IPointerClickHandler.OnPointerClick(PointerEventData eventData)
  297. {
  298. var localPosition = CalculateLocalPosition(eventData.position, eventData.pressEventCamera);
  299. for (var s = 0; s < spans.Count; s++)
  300. {
  301. for (var b = 0; b < spans[s].BoundingBoxes.Count; b++)
  302. {
  303. if (spans[s].BoundingBoxes[b].Contains(localPosition))
  304. {
  305. spans[s].Callback(text.Substring(spans[s].StartIndex, spans[s].Length));
  306. break;
  307. }
  308. }
  309. }
  310. }
  311. }
  312. }