TextLineImpl.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. using System.Collections.Generic;
  2. using Avalonia.Media.TextFormatting.Unicode;
  3. using Avalonia.Platform;
  4. namespace Avalonia.Media.TextFormatting
  5. {
  6. internal class TextLineImpl : TextLine
  7. {
  8. private readonly List<ShapedTextCharacters> _textRuns;
  9. public TextLineImpl(List<ShapedTextCharacters> textRuns, TextLineMetrics lineMetrics,
  10. TextLineBreak lineBreak = null, bool hasCollapsed = false)
  11. {
  12. _textRuns = textRuns;
  13. LineMetrics = lineMetrics;
  14. TextLineBreak = lineBreak;
  15. HasCollapsed = hasCollapsed;
  16. }
  17. /// <inheritdoc/>
  18. public override TextRange TextRange => LineMetrics.TextRange;
  19. /// <inheritdoc/>
  20. public override IReadOnlyList<TextRun> TextRuns => _textRuns;
  21. /// <inheritdoc/>
  22. public override TextLineMetrics LineMetrics { get; }
  23. /// <inheritdoc/>
  24. public override TextLineBreak TextLineBreak { get; }
  25. /// <inheritdoc/>
  26. public override bool HasCollapsed { get; }
  27. /// <inheritdoc/>
  28. public override void Draw(DrawingContext drawingContext)
  29. {
  30. var currentX = 0.0;
  31. foreach (var textRun in _textRuns)
  32. {
  33. using (drawingContext.PushPostTransform(Matrix.CreateTranslation(currentX, 0)))
  34. {
  35. textRun.Draw(drawingContext);
  36. }
  37. currentX += textRun.Size.Width;
  38. }
  39. }
  40. /// <inheritdoc/>
  41. public override TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList)
  42. {
  43. if (collapsingPropertiesList == null || collapsingPropertiesList.Length == 0)
  44. {
  45. return this;
  46. }
  47. var collapsingProperties = collapsingPropertiesList[0];
  48. var runIndex = 0;
  49. var currentWidth = 0.0;
  50. var textRange = TextRange;
  51. var collapsedLength = 0;
  52. TextLineMetrics textLineMetrics;
  53. var shapedSymbol = CreateShapedSymbol(collapsingProperties.Symbol);
  54. var availableWidth = collapsingProperties.Width - shapedSymbol.Size.Width;
  55. while (runIndex < _textRuns.Count)
  56. {
  57. var currentRun = _textRuns[runIndex];
  58. currentWidth += currentRun.Size.Width;
  59. if (currentWidth > availableWidth)
  60. {
  61. var measuredLength = TextFormatterImpl.MeasureCharacters(currentRun, availableWidth, runIndex);
  62. var currentBreakPosition = 0;
  63. if (measuredLength < textRange.End)
  64. {
  65. var lineBreaker = new LineBreakEnumerator(currentRun.Text);
  66. while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
  67. {
  68. var nextBreakPosition = lineBreaker.Current.PositionWrap;
  69. if (nextBreakPosition == 0)
  70. {
  71. break;
  72. }
  73. if (nextBreakPosition > measuredLength)
  74. {
  75. break;
  76. }
  77. currentBreakPosition = nextBreakPosition;
  78. }
  79. }
  80. if (collapsingProperties.Style == TextCollapsingStyle.TrailingWord)
  81. {
  82. measuredLength = currentBreakPosition;
  83. }
  84. collapsedLength += measuredLength;
  85. var splitResult = TextFormatterImpl.SplitTextRuns(_textRuns, collapsedLength);
  86. var shapedTextCharacters = new List<ShapedTextCharacters>(splitResult.First.Count + 1);
  87. shapedTextCharacters.AddRange(splitResult.First);
  88. shapedTextCharacters.Add(shapedSymbol);
  89. textRange = new TextRange(textRange.Start, collapsedLength);
  90. var shapedWidth = GetShapedWidth(shapedTextCharacters);
  91. textLineMetrics = new TextLineMetrics(new Size(shapedWidth, LineMetrics.Size.Height),
  92. LineMetrics.TextBaseline, textRange, false);
  93. return new TextLineImpl(shapedTextCharacters, textLineMetrics, TextLineBreak, true);
  94. }
  95. availableWidth -= currentRun.Size.Width;
  96. collapsedLength += currentRun.GlyphRun.Characters.Length;
  97. runIndex++;
  98. }
  99. textLineMetrics =
  100. new TextLineMetrics(LineMetrics.Size.WithWidth(LineMetrics.Size.Width + shapedSymbol.Size.Width),
  101. LineMetrics.TextBaseline, TextRange, LineMetrics.HasOverflowed);
  102. return new TextLineImpl(new List<ShapedTextCharacters>(_textRuns) { shapedSymbol }, textLineMetrics, null,
  103. true);
  104. }
  105. /// <inheritdoc/>
  106. public override CharacterHit GetCharacterHitFromDistance(double distance)
  107. {
  108. if (distance < 0)
  109. {
  110. // hit happens before the line, return the first position
  111. return new CharacterHit(TextRange.Start);
  112. }
  113. // process hit that happens within the line
  114. var characterHit = new CharacterHit();
  115. foreach (var run in _textRuns)
  116. {
  117. characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _);
  118. if (distance <= run.Size.Width)
  119. {
  120. break;
  121. }
  122. distance -= run.Size.Width;
  123. }
  124. return characterHit;
  125. }
  126. /// <inheritdoc/>
  127. public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
  128. {
  129. return DistanceFromCodepointIndex(characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0));
  130. }
  131. /// <inheritdoc/>
  132. public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
  133. {
  134. if (TryFindNextCharacterHit(characterHit, out var nextCharacterHit))
  135. {
  136. return nextCharacterHit;
  137. }
  138. if (characterHit.FirstCharacterIndex + characterHit.TrailingLength <= TextRange.Start + TextRange.Length)
  139. {
  140. return characterHit; // Can't move, we're after the last character
  141. }
  142. var runIndex = GetRunIndexAtCodepointIndex(TextRange.End);
  143. var textRun = _textRuns[runIndex];
  144. characterHit = textRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
  145. return characterHit; // Can't move, we're after the last character
  146. }
  147. /// <inheritdoc/>
  148. public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
  149. {
  150. if (TryFindPreviousCharacterHit(characterHit, out var previousCharacterHit))
  151. {
  152. return previousCharacterHit;
  153. }
  154. if (characterHit.FirstCharacterIndex < TextRange.Start)
  155. {
  156. characterHit = new CharacterHit(TextRange.Start);
  157. }
  158. return characterHit; // Can't move, we're before the first character
  159. }
  160. /// <inheritdoc/>
  161. public override CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit)
  162. {
  163. // same operation as move-to-previous
  164. return GetPreviousCaretCharacterHit(characterHit);
  165. }
  166. /// <summary>
  167. /// Get distance from line start to the specified codepoint index.
  168. /// </summary>
  169. private double DistanceFromCodepointIndex(int codepointIndex)
  170. {
  171. var currentDistance = 0.0;
  172. foreach (var textRun in _textRuns)
  173. {
  174. if (codepointIndex > textRun.Text.End)
  175. {
  176. currentDistance += textRun.Size.Width;
  177. continue;
  178. }
  179. return currentDistance + textRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(codepointIndex));
  180. }
  181. return currentDistance;
  182. }
  183. /// <summary>
  184. /// Tries to find the next character hit.
  185. /// </summary>
  186. /// <param name="characterHit">The current character hit.</param>
  187. /// <param name="nextCharacterHit">The next character hit.</param>
  188. /// <returns></returns>
  189. private bool TryFindNextCharacterHit(CharacterHit characterHit, out CharacterHit nextCharacterHit)
  190. {
  191. nextCharacterHit = characterHit;
  192. var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
  193. if (codepointIndex > TextRange.End)
  194. {
  195. return false; // Cannot go forward anymore
  196. }
  197. var runIndex = GetRunIndexAtCodepointIndex(codepointIndex);
  198. while (runIndex < TextRuns.Count)
  199. {
  200. var run = _textRuns[runIndex];
  201. var foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
  202. var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength ==
  203. TextRange.Length;
  204. var characterIndex = codepointIndex - run.Text.Start;
  205. var codepoint = Codepoint.ReadAt(run.GlyphRun.Characters, characterIndex, out _);
  206. if (codepoint.IsBreakChar)
  207. {
  208. foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(codepointIndex - 1, out _);
  209. isAtEnd = true;
  210. }
  211. nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
  212. foundCharacterHit :
  213. new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength);
  214. if (isAtEnd || nextCharacterHit.FirstCharacterIndex > characterHit.FirstCharacterIndex)
  215. {
  216. return true;
  217. }
  218. runIndex++;
  219. }
  220. return false;
  221. }
  222. /// <summary>
  223. /// Tries to find the previous character hit.
  224. /// </summary>
  225. /// <param name="characterHit">The current character hit.</param>
  226. /// <param name="previousCharacterHit">The previous character hit.</param>
  227. /// <returns></returns>
  228. private bool TryFindPreviousCharacterHit(CharacterHit characterHit, out CharacterHit previousCharacterHit)
  229. {
  230. if (characterHit.FirstCharacterIndex == TextRange.Start)
  231. {
  232. previousCharacterHit = new CharacterHit(TextRange.Start);
  233. return true;
  234. }
  235. previousCharacterHit = characterHit;
  236. var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
  237. if (codepointIndex < TextRange.Start)
  238. {
  239. return false; // Cannot go backward anymore.
  240. }
  241. var runIndex = GetRunIndexAtCodepointIndex(codepointIndex);
  242. while (runIndex >= 0)
  243. {
  244. var run = _textRuns[runIndex];
  245. var foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
  246. previousCharacterHit = characterHit.TrailingLength != 0 ?
  247. foundCharacterHit :
  248. new CharacterHit(foundCharacterHit.FirstCharacterIndex);
  249. if (previousCharacterHit.FirstCharacterIndex < characterHit.FirstCharacterIndex)
  250. {
  251. return true;
  252. }
  253. runIndex--;
  254. }
  255. return false;
  256. }
  257. /// <summary>
  258. /// Gets the run index of the specified codepoint index.
  259. /// </summary>
  260. /// <param name="codepointIndex">The codepoint index.</param>
  261. /// <returns>The text run index.</returns>
  262. private int GetRunIndexAtCodepointIndex(int codepointIndex)
  263. {
  264. if (codepointIndex >= TextRange.End)
  265. {
  266. return _textRuns.Count - 1;
  267. }
  268. if (codepointIndex <= 0)
  269. {
  270. return 0;
  271. }
  272. var runIndex = 0;
  273. while (runIndex < _textRuns.Count)
  274. {
  275. var run = _textRuns[runIndex];
  276. if (run.Text.End > codepointIndex)
  277. {
  278. return runIndex;
  279. }
  280. runIndex++;
  281. }
  282. return runIndex;
  283. }
  284. /// <summary>
  285. /// Creates a shaped symbol.
  286. /// </summary>
  287. /// <param name="textRun">The symbol run to shape.</param>
  288. /// <returns>
  289. /// The shaped symbol.
  290. /// </returns>
  291. internal static ShapedTextCharacters CreateShapedSymbol(TextRun textRun)
  292. {
  293. var formatterImpl = AvaloniaLocator.Current.GetService<ITextShaperImpl>();
  294. var glyphRun = formatterImpl.ShapeText(textRun.Text, textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize,
  295. textRun.Properties.CultureInfo);
  296. return new ShapedTextCharacters(glyphRun, textRun.Properties);
  297. }
  298. /// <summary>
  299. /// Gets the shaped width of specified shaped text characters.
  300. /// </summary>
  301. /// <param name="shapedTextCharacters">The shaped text characters.</param>
  302. /// <returns>
  303. /// The shaped width.
  304. /// </returns>
  305. private static double GetShapedWidth(IReadOnlyList<ShapedTextCharacters> shapedTextCharacters)
  306. {
  307. var shapedWidth = 0.0;
  308. for (var i = 0; i < shapedTextCharacters.Count; i++)
  309. {
  310. shapedWidth += shapedTextCharacters[i].Size.Width;
  311. }
  312. return shapedWidth;
  313. }
  314. }
  315. }