SimpleTextFormatter.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. using System;
  2. using System.Collections.Generic;
  3. using Avalonia.Media.TextFormatting.Unicode;
  4. using Avalonia.Platform;
  5. using Avalonia.Utility;
  6. namespace Avalonia.Media.TextFormatting
  7. {
  8. internal class SimpleTextFormatter : TextFormatter
  9. {
  10. private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' });
  11. /// <summary>
  12. /// Formats a text line.
  13. /// </summary>
  14. /// <param name="textSource">The text source.</param>
  15. /// <param name="firstTextSourceIndex">The first character index to start the text line from.</param>
  16. /// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param>
  17. /// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties,
  18. /// such as TextWrapping, TextAlignment, or TextStyle.</param>
  19. /// <returns>The formatted line.</returns>
  20. public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
  21. TextParagraphProperties paragraphProperties)
  22. {
  23. var textTrimming = paragraphProperties.TextTrimming;
  24. var textWrapping = paragraphProperties.TextWrapping;
  25. TextLine textLine;
  26. var textRuns = FormatTextRuns(textSource, firstTextSourceIndex, out var textPointer);
  27. if (textTrimming != TextTrimming.None)
  28. {
  29. textLine = PerformTextTrimming(textPointer, textRuns, paragraphWidth, paragraphProperties);
  30. }
  31. else
  32. {
  33. if (textWrapping == TextWrapping.Wrap)
  34. {
  35. textLine = PerformTextWrapping(textPointer, textRuns, paragraphWidth, paragraphProperties);
  36. }
  37. else
  38. {
  39. var textLineMetrics =
  40. TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment);
  41. textLine = new SimpleTextLine(textPointer, textRuns, textLineMetrics);
  42. }
  43. }
  44. return textLine;
  45. }
  46. /// <summary>
  47. /// Formats text runs with optional text style overrides.
  48. /// </summary>
  49. /// <param name="textSource">The text source.</param>
  50. /// <param name="firstTextSourceIndex">The first text source index.</param>
  51. /// <param name="textPointer">The text pointer that covers the formatted text runs.</param>
  52. /// <returns>
  53. /// The formatted text runs.
  54. /// </returns>
  55. private List<ShapedTextRun> FormatTextRuns(ITextSource textSource, int firstTextSourceIndex, out TextPointer textPointer)
  56. {
  57. var start = firstTextSourceIndex;
  58. var textRuns = new List<ShapedTextRun>();
  59. while (true)
  60. {
  61. var textRun = textSource.GetTextRun(firstTextSourceIndex);
  62. if (textRun.Text.IsEmpty)
  63. {
  64. break;
  65. }
  66. if (textRun is TextEndOfLine)
  67. {
  68. break;
  69. }
  70. if (!(textRun is TextCharacters))
  71. {
  72. throw new NotSupportedException("Run type not supported by the formatter.");
  73. }
  74. var runText = textRun.Text;
  75. while (!runText.IsEmpty)
  76. {
  77. var shapableTextStyleRun = CreateShapableTextStyleRun(runText, textRun.Style);
  78. var shapedRun = new ShapedTextRun(runText.Take(shapableTextStyleRun.TextPointer.Length),
  79. shapableTextStyleRun.Style);
  80. textRuns.Add(shapedRun);
  81. runText = runText.Skip(shapedRun.Text.Length);
  82. }
  83. firstTextSourceIndex += textRun.Text.Length;
  84. }
  85. textPointer = new TextPointer(start, firstTextSourceIndex - start);
  86. return textRuns;
  87. }
  88. /// <summary>
  89. /// Performs text trimming and returns a trimmed line.
  90. /// </summary>
  91. /// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param>
  92. /// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties,
  93. /// such as TextWrapping, TextAlignment, or TextStyle.</param>
  94. /// <param name="textRuns">The text runs to perform the trimming on.</param>
  95. /// <param name="text">The text that was used to construct the text runs.</param>
  96. /// <returns></returns>
  97. private TextLine PerformTextTrimming(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns,
  98. double paragraphWidth, TextParagraphProperties paragraphProperties)
  99. {
  100. var textTrimming = paragraphProperties.TextTrimming;
  101. var availableWidth = paragraphWidth;
  102. var currentWidth = 0.0;
  103. var runIndex = 0;
  104. while (runIndex < textRuns.Count)
  105. {
  106. var currentRun = textRuns[runIndex];
  107. currentWidth += currentRun.GlyphRun.Bounds.Width;
  108. if (currentWidth > availableWidth)
  109. {
  110. var ellipsisRun = CreateEllipsisRun(currentRun.Style);
  111. var measuredLength = MeasureText(currentRun, availableWidth - ellipsisRun.GlyphRun.Bounds.Width);
  112. if (textTrimming == TextTrimming.WordEllipsis)
  113. {
  114. if (measuredLength < text.End)
  115. {
  116. var currentBreakPosition = 0;
  117. var lineBreaker = new LineBreakEnumerator(currentRun.Text);
  118. while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
  119. {
  120. var nextBreakPosition = lineBreaker.Current.PositionWrap;
  121. if (nextBreakPosition == 0)
  122. {
  123. break;
  124. }
  125. if (nextBreakPosition > measuredLength)
  126. {
  127. break;
  128. }
  129. currentBreakPosition = nextBreakPosition;
  130. }
  131. measuredLength = currentBreakPosition;
  132. }
  133. }
  134. var splitResult = SplitTextRuns(textRuns, measuredLength);
  135. var trimmedRuns = new List<ShapedTextRun>(splitResult.First.Count + 1);
  136. trimmedRuns.AddRange(splitResult.First);
  137. trimmedRuns.Add(ellipsisRun);
  138. var textLineMetrics =
  139. TextLineMetrics.Create(trimmedRuns, paragraphWidth, paragraphProperties.TextAlignment);
  140. return new SimpleTextLine(text.Take(measuredLength), trimmedRuns, textLineMetrics);
  141. }
  142. availableWidth -= currentRun.GlyphRun.Bounds.Width;
  143. runIndex++;
  144. }
  145. return new SimpleTextLine(text, textRuns,
  146. TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment));
  147. }
  148. /// <summary>
  149. /// Performs text wrapping returns a list of text lines.
  150. /// </summary>
  151. /// <param name="paragraphProperties">The text paragraph properties.</param>
  152. /// <param name="textRuns">The text run'S.</param>
  153. /// <param name="text">The text to analyze for break opportunities.</param>
  154. /// <param name="paragraphWidth"></param>
  155. /// <returns></returns>
  156. private TextLine PerformTextWrapping(TextPointer text, IReadOnlyList<ShapedTextRun> textRuns,
  157. double paragraphWidth, TextParagraphProperties paragraphProperties)
  158. {
  159. var availableWidth = paragraphWidth;
  160. var currentWidth = 0.0;
  161. var runIndex = 0;
  162. var length = 0;
  163. while (runIndex < textRuns.Count)
  164. {
  165. var currentRun = textRuns[runIndex];
  166. if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth)
  167. {
  168. var measuredLength = MeasureText(currentRun, paragraphWidth - currentWidth);
  169. if (measuredLength < currentRun.Text.Length)
  170. {
  171. var currentBreakPosition = -1;
  172. var lineBreaker = new LineBreakEnumerator(currentRun.Text);
  173. while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
  174. {
  175. var nextBreakPosition = lineBreaker.Current.PositionWrap;
  176. if (nextBreakPosition == 0)
  177. {
  178. break;
  179. }
  180. if (nextBreakPosition > measuredLength)
  181. {
  182. break;
  183. }
  184. currentBreakPosition = nextBreakPosition;
  185. }
  186. if (currentBreakPosition != -1)
  187. {
  188. measuredLength = currentBreakPosition;
  189. }
  190. }
  191. length += measuredLength;
  192. var splitResult = SplitTextRuns(textRuns, length);
  193. var textLineMetrics =
  194. TextLineMetrics.Create(splitResult.First, paragraphWidth, paragraphProperties.TextAlignment);
  195. return new SimpleTextLine(text.Take(length), splitResult.First, textLineMetrics);
  196. }
  197. currentWidth += currentRun.GlyphRun.Bounds.Width;
  198. length += currentRun.GlyphRun.Characters.Length;
  199. runIndex++;
  200. }
  201. return new SimpleTextLine(text, textRuns,
  202. TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment));
  203. }
  204. /// <summary>
  205. /// Measures the number of characters that fits into available width.
  206. /// </summary>
  207. /// <param name="textRun">The text run.</param>
  208. /// <param name="availableWidth">The available width.</param>
  209. /// <returns></returns>
  210. private int MeasureText(ShapedTextRun textRun, double availableWidth)
  211. {
  212. if (textRun.GlyphRun.Bounds.Width < availableWidth)
  213. {
  214. return textRun.Text.Length;
  215. }
  216. var measuredWidth = 0.0;
  217. var index = 0;
  218. for (; index < textRun.GlyphRun.GlyphAdvances.Length; index++)
  219. {
  220. var advance = textRun.GlyphRun.GlyphAdvances[index];
  221. if (measuredWidth + advance > availableWidth)
  222. {
  223. index--;
  224. break;
  225. }
  226. measuredWidth += advance;
  227. }
  228. if(index < 0)
  229. {
  230. return 0;
  231. }
  232. var cluster = textRun.GlyphRun.GlyphClusters[index];
  233. var characterHit = textRun.GlyphRun.FindNearestCharacterHit(cluster, out _);
  234. return characterHit.FirstCharacterIndex - textRun.GlyphRun.Characters.Start +
  235. (textRun.GlyphRun.IsLeftToRight ? characterHit.TrailingLength : 0);
  236. }
  237. /// <summary>
  238. /// Creates an ellipsis.
  239. /// </summary>
  240. /// <param name="textStyle">The text style.</param>
  241. /// <returns></returns>
  242. private static ShapedTextRun CreateEllipsisRun(TextStyle textStyle)
  243. {
  244. var formatterImpl = AvaloniaLocator.Current.GetService<ITextShaperImpl>();
  245. var glyphRun = formatterImpl.ShapeText(s_ellipsis, textStyle.TextFormat);
  246. return new ShapedTextRun(glyphRun, textStyle);
  247. }
  248. private readonly struct SplitTextRunsResult
  249. {
  250. public SplitTextRunsResult(IReadOnlyList<ShapedTextRun> first, IReadOnlyList<ShapedTextRun> second)
  251. {
  252. First = first;
  253. Second = second;
  254. }
  255. /// <summary>
  256. /// Gets the first text runs.
  257. /// </summary>
  258. /// <value>
  259. /// The first text runs.
  260. /// </value>
  261. public IReadOnlyList<ShapedTextRun> First { get; }
  262. /// <summary>
  263. /// Gets the second text runs.
  264. /// </summary>
  265. /// <value>
  266. /// The second text runs.
  267. /// </value>
  268. public IReadOnlyList<ShapedTextRun> Second { get; }
  269. }
  270. /// <summary>
  271. /// Split a sequence of runs into two segments at specified length.
  272. /// </summary>
  273. /// <param name="textRuns">The text run's.</param>
  274. /// <param name="length">The length to split at.</param>
  275. /// <returns></returns>
  276. private static SplitTextRunsResult SplitTextRuns(IReadOnlyList<ShapedTextRun> textRuns, int length)
  277. {
  278. var currentLength = 0;
  279. for (var i = 0; i < textRuns.Count; i++)
  280. {
  281. var currentRun = textRuns[i];
  282. if (currentLength + currentRun.GlyphRun.Characters.Length < length)
  283. {
  284. currentLength += currentRun.GlyphRun.Characters.Length;
  285. continue;
  286. }
  287. var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i;
  288. var first = new ShapedTextRun[firstCount];
  289. if (firstCount > 1)
  290. {
  291. for (var j = 0; j < i; j++)
  292. {
  293. first[j] = textRuns[j];
  294. }
  295. }
  296. var secondCount = textRuns.Count - firstCount;
  297. if (currentLength + currentRun.GlyphRun.Characters.Length == length)
  298. {
  299. var second = new ShapedTextRun[secondCount];
  300. var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0;
  301. if (secondCount > 0)
  302. {
  303. for (var j = 0; j < secondCount; j++)
  304. {
  305. second[j] = textRuns[i + j + offset];
  306. }
  307. }
  308. first[i] = currentRun;
  309. return new SplitTextRunsResult(first, second);
  310. }
  311. else
  312. {
  313. secondCount++;
  314. var second = new ShapedTextRun[secondCount];
  315. if (secondCount > 0)
  316. {
  317. for (var j = 1; j < secondCount; j++)
  318. {
  319. second[j] = textRuns[i + j];
  320. }
  321. }
  322. var split = currentRun.Split(length - currentLength);
  323. first[i] = split.First;
  324. second[0] = split.Second;
  325. return new SplitTextRunsResult(first, second);
  326. }
  327. }
  328. return new SplitTextRunsResult(textRuns, null);
  329. }
  330. }
  331. }