TextLine.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. // RichTextKit
  2. // Copyright © 2019-2020 Topten Software. All Rights Reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License"); you may
  5. // not use this product except in compliance with the License. You may obtain
  6. // a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. // License for the specific language governing permissions and limitations
  14. // under the License.
  15. using SkiaSharp;
  16. using System;
  17. using System.Collections.Generic;
  18. using System.Linq;
  19. using System.Text;
  20. using System.Threading;
  21. using System.Threading.Tasks;
  22. using Topten.RichTextKit.Utils;
  23. namespace Topten.RichTextKit
  24. {
  25. /// <summary>
  26. /// Represents a laid out line of text.
  27. /// </summary>
  28. public class TextLine
  29. {
  30. /// <summary>
  31. /// Constructs a new TextLine.
  32. /// </summary>
  33. public TextLine()
  34. {
  35. }
  36. /// <summary>
  37. /// Gets the set of text runs comprising this line.
  38. /// </summary>
  39. /// <remarks>
  40. /// Font runs are order logically (ie: in code point index order)
  41. /// but may have unordered <see cref="FontRun.XCoord"/>'s when right to
  42. /// left text is in use.
  43. /// </remarks>
  44. public IReadOnlyList<FontRun> Runs => RunsInternal;
  45. /// <summary>
  46. /// Gets the text block that owns this line.
  47. /// </summary>
  48. public TextBlock TextBlock
  49. {
  50. get;
  51. internal set;
  52. }
  53. /// <summary>
  54. /// Gets the next line in this text block, or null if this is the last line.
  55. /// </summary>
  56. public TextLine NextLine
  57. {
  58. get
  59. {
  60. int index = (TextBlock.Lines as List<TextLine>).IndexOf(this);
  61. if (index < 0 || index + 1 >= TextBlock.Lines.Count)
  62. return null;
  63. return TextBlock.Lines[index + 1];
  64. }
  65. }
  66. /// <summary>
  67. /// Gets the previous line in this text block, or null if this is the first line.
  68. /// </summary>
  69. public TextLine PreviousLine
  70. {
  71. get
  72. {
  73. int index = (TextBlock.Lines as List<TextLine>).IndexOf(this);
  74. if (index <= 0)
  75. return null;
  76. return TextBlock.Lines[index - 1];
  77. }
  78. }
  79. /// <summary>
  80. /// Gets the y-coordinate of the top of this line, relative to the top of the text block.
  81. /// </summary>
  82. public float YCoord
  83. {
  84. get;
  85. internal set;
  86. }
  87. /// <summary>
  88. /// Gets the base line of this line (relative to <see cref="YCoord"/>)
  89. /// </summary>
  90. public float BaseLine
  91. {
  92. get;
  93. internal set;
  94. }
  95. /// <summary>
  96. /// Gets the maximum magnitude ascent of all font runs in this line.
  97. /// </summary>
  98. /// <remarks>
  99. /// The ascent is reported as a negative value from the base line.
  100. /// </remarks>
  101. public float MaxAscent
  102. {
  103. get;
  104. internal set;
  105. }
  106. /// <summary>
  107. /// Gets the maximum descent of all font runs in this line.
  108. /// </summary>
  109. /// <remarks>
  110. /// The descent is reported as a positive value from the base line.
  111. /// </remarks>
  112. public float MaxDescent
  113. {
  114. get;
  115. internal set;
  116. }
  117. /// <summary>
  118. /// Gets the text height of this line.
  119. /// </summary>
  120. /// <remarks>
  121. /// The text height of a line is the sum of the ascent and desent.
  122. /// </remarks>
  123. public float TextHeight => -MaxAscent + MaxDescent;
  124. /// <summary>
  125. /// Gets the height of this line
  126. /// </summary>
  127. /// <remarks>
  128. /// The height of a line is based on the font and <see cref="IStyle.LineHeight"/>
  129. /// value of all runs in this line.
  130. /// </remarks>
  131. public float Height
  132. {
  133. get;
  134. internal set;
  135. }
  136. /// <summary>
  137. /// The width of the content on this line, excluding trailing whitespace and overhang.
  138. /// </summary>
  139. public float Width
  140. {
  141. get;
  142. internal set;
  143. }
  144. /// <summary>
  145. /// Paint this line
  146. /// </summary>
  147. /// <param name="ctx">The paint context</param>
  148. internal void Paint(PaintTextContext ctx)
  149. {
  150. foreach (var r in Runs)
  151. {
  152. r.PaintBackground(ctx);
  153. }
  154. foreach (var r in Runs)
  155. {
  156. r.Paint(ctx);
  157. }
  158. }
  159. /// <summary>
  160. /// Code point index of start of this line
  161. /// </summary>
  162. public int Start
  163. {
  164. get
  165. {
  166. var pl = PreviousLine;
  167. return PreviousLine == null ? 0 : PreviousLine.End;
  168. }
  169. }
  170. /// <summary>
  171. /// The length of this line in codepoints
  172. /// </summary>
  173. public int Length => End - Start;
  174. /// <summary>
  175. /// The code point index of the first character after this line
  176. /// </summary>
  177. public int End
  178. {
  179. get
  180. {
  181. // Get the last run that's not an ellipsis
  182. var lastRun = this.Runs.LastOrDefault(x => x.RunKind != FontRunKind.Ellipsis);
  183. // If last run found, then it's the end of the run, other wise it's the start index
  184. return lastRun == null ? Start : lastRun.End;
  185. }
  186. }
  187. /// <summary>
  188. /// Hit test this line, working out the cluster the x position is over
  189. /// and closest to.
  190. /// </summary>
  191. /// <remarks>
  192. /// This method only populates the code point indicies in the returned result
  193. /// and the line indicies will be -1
  194. /// </remarks>
  195. /// <param name="x">The xcoord relative to the text block</param>
  196. public HitTestResult HitTest(float x)
  197. {
  198. var htr = new HitTestResult();
  199. htr.OverLine = -1;
  200. htr.ClosestLine = -1;
  201. HitTest(x, ref htr);
  202. return htr;
  203. }
  204. /// <summary>
  205. /// Hit test this line, working out the cluster the x position is over
  206. /// and closest to.
  207. /// </summary>
  208. /// <param name="x">The xcoord relative to the text block</param>
  209. /// <param name="htr">HitTestResult to be filled out</param>
  210. internal void HitTest(float x, ref HitTestResult htr)
  211. {
  212. // Working variables
  213. float closestXPosition = 0;
  214. int closestCodePointIndex = -1;
  215. if (Runs.Count > 0)
  216. {
  217. // If caret is beyond the end of the line...
  218. var lastRun = Runs[Runs.Count - 1];
  219. if ((lastRun.Direction == TextDirection.LTR && x >= lastRun.XCoord + lastRun.Width) ||
  220. (lastRun.Direction == TextDirection.RTL && x < lastRun.XCoord))
  221. {
  222. // Special handling for clicking after a soft line break ('\n') in which case
  223. // the caret should be positioned before the new line character, not after it
  224. // as this would cause the cursor to appear on the next line).
  225. if (lastRun.RunKind == FontRunKind.TrailingWhitespace || lastRun.RunKind == FontRunKind.Ellipsis)
  226. {
  227. if (lastRun.CodePoints.Length > 0 &&
  228. (lastRun.CodePoints[lastRun.CodePoints.Length - 1] == '\n') ||
  229. (lastRun.CodePoints[lastRun.CodePoints.Length - 1] == 0x2029)
  230. )
  231. {
  232. htr.ClosestCodePointIndex = lastRun.End - 1;
  233. return;
  234. }
  235. }
  236. }
  237. }
  238. // Check all runs
  239. foreach (var r in Runs)
  240. {
  241. // Ignore ellipsis runs
  242. if (r.RunKind == FontRunKind.Ellipsis)
  243. continue;
  244. if (x < r.XCoord)
  245. {
  246. // Before the run...
  247. updateClosest(r.XCoord, r.Direction == TextDirection.LTR ? r.Start : r.End, r.Direction);
  248. }
  249. else if (x >= r.XCoord + r.Width)
  250. {
  251. // After the run...
  252. updateClosest(r.XCoord + r.Width, r.Direction == TextDirection.RTL ? r.Start : r.End, r.Direction);
  253. }
  254. else
  255. {
  256. // Inside the run
  257. for (int i = 0; i < r.Clusters.Length;)
  258. {
  259. // Get the xcoord of this cluster
  260. var codePointIndex = r.Clusters[i];
  261. var xcoord1 = r.GetXCoordOfCodePointIndex(codePointIndex);
  262. // Find the code point of the next cluster
  263. var j = i;
  264. while (j < r.Clusters.Length && r.Clusters[j] == r.Clusters[i])
  265. j++;
  266. // Get the xcoord of other side of this cluster
  267. int codePointIndexOther;
  268. if (r.Direction == TextDirection.LTR)
  269. {
  270. if (j == r.Clusters.Length)
  271. {
  272. codePointIndexOther = r.End;
  273. }
  274. else
  275. {
  276. codePointIndexOther = r.Clusters[j];
  277. }
  278. }
  279. else
  280. {
  281. if (i > 0)
  282. {
  283. codePointIndexOther = r.Clusters[i - 1];
  284. }
  285. else
  286. {
  287. codePointIndexOther = r.End;
  288. }
  289. }
  290. // Gethte xcoord of the other side of the cluster
  291. var xcoord2 = r.GetXCoordOfCodePointIndex(codePointIndexOther);
  292. // Ensure order correct for easier in-range check
  293. if (xcoord1 > xcoord2)
  294. {
  295. var temp = xcoord1;
  296. xcoord1 = xcoord2;
  297. xcoord2 = temp;
  298. }
  299. // On the character?
  300. if (x >= xcoord1 && x < xcoord2)
  301. {
  302. // Store this as the cluster the point is over
  303. htr.OverCodePointIndex = codePointIndex;
  304. // Don't move to the rhs (or lhs) of a line break
  305. if (r.CodePoints[codePointIndex - r.Start] == '\n')
  306. {
  307. htr.ClosestCodePointIndex = codePointIndex;
  308. }
  309. else
  310. {
  311. // Work out if position is closer to the left or right side of the cluster
  312. if (x < (xcoord1 + xcoord2) / 2)
  313. {
  314. htr.ClosestCodePointIndex = r.Direction == TextDirection.LTR ? codePointIndex : codePointIndexOther;
  315. }
  316. else
  317. {
  318. htr.ClosestCodePointIndex = r.Direction == TextDirection.LTR ? codePointIndexOther : codePointIndex;
  319. }
  320. }
  321. if (htr.ClosestCodePointIndex == End)
  322. {
  323. htr.AltCaretPosition = true;
  324. }
  325. return;
  326. }
  327. // Move to the next cluster
  328. i = j;
  329. }
  330. }
  331. }
  332. // Store closest character
  333. htr.ClosestCodePointIndex = closestCodePointIndex;
  334. if (htr.ClosestCodePointIndex == End)
  335. {
  336. htr.AltCaretPosition = true;
  337. }
  338. // Helper for updating closest caret position
  339. void updateClosest(float xPosition, int codePointIndex, TextDirection dir)
  340. {
  341. if (closestCodePointIndex == -1 || Math.Abs(xPosition - x) < Math.Abs(closestXPosition - x))
  342. {
  343. closestXPosition = xPosition;
  344. closestCodePointIndex = codePointIndex;
  345. }
  346. }
  347. }
  348. internal void UpdateOverhang(float right, ref float leftOverhang, ref float rightOverhang)
  349. {
  350. foreach (var r in Runs)
  351. {
  352. r.UpdateOverhang(right, ref leftOverhang, ref rightOverhang);
  353. }
  354. }
  355. /// <summary>
  356. /// Internal List of runs
  357. /// </summary>
  358. internal List<FontRun> RunsInternal = new List<FontRun>();
  359. internal static ThreadLocal<ObjectPool<TextLine>> Pool = new ThreadLocal<ObjectPool<TextLine>>(() => new ObjectPool<TextLine>()
  360. {
  361. Cleaner = (r) =>
  362. {
  363. r.TextBlock = null;
  364. r.RunsInternal.Clear();
  365. }
  366. });
  367. }
  368. }