SimpleTextFormatter.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  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. while (runIndex < textRuns.Count)
  163. {
  164. var currentRun = textRuns[runIndex];
  165. currentWidth += currentRun.GlyphRun.Bounds.Width;
  166. if (currentWidth > availableWidth)
  167. {
  168. var measuredLength = MeasureText(currentRun, paragraphWidth);
  169. if (measuredLength < text.End)
  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. var splitResult = SplitTextRuns(textRuns, measuredLength);
  192. var textLineMetrics =
  193. TextLineMetrics.Create(splitResult.First, paragraphWidth, paragraphProperties.TextAlignment);
  194. return new SimpleTextLine(text.Take(measuredLength), splitResult.First, textLineMetrics);
  195. }
  196. availableWidth -= currentRun.GlyphRun.Bounds.Width;
  197. runIndex++;
  198. }
  199. return new SimpleTextLine(text, textRuns,
  200. TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment));
  201. }
  202. /// <summary>
  203. /// Measures the number of characters that fits into available width.
  204. /// </summary>
  205. /// <param name="textRun">The text run.</param>
  206. /// <param name="availableWidth">The available width.</param>
  207. /// <returns></returns>
  208. private int MeasureText(ShapedTextRun textRun, double availableWidth)
  209. {
  210. if (textRun.GlyphRun.Bounds.Width < availableWidth)
  211. {
  212. return textRun.Text.Length;
  213. }
  214. var measuredWidth = 0.0;
  215. var index = 0;
  216. for (; index < textRun.GlyphRun.GlyphAdvances.Length; index++)
  217. {
  218. var advance = textRun.GlyphRun.GlyphAdvances[index];
  219. if (measuredWidth + advance > availableWidth)
  220. {
  221. break;
  222. }
  223. measuredWidth += advance;
  224. }
  225. var cluster = textRun.GlyphRun.GlyphClusters[index];
  226. var characterHit = textRun.GlyphRun.FindNearestCharacterHit(cluster, out _);
  227. return characterHit.FirstCharacterIndex - textRun.GlyphRun.Characters.Start +
  228. (textRun.GlyphRun.IsLeftToRight ? characterHit.TrailingLength : 0);
  229. }
  230. /// <summary>
  231. /// Creates an ellipsis.
  232. /// </summary>
  233. /// <param name="textStyle">The text style.</param>
  234. /// <returns></returns>
  235. private static ShapedTextRun CreateEllipsisRun(TextStyle textStyle)
  236. {
  237. var formatterImpl = AvaloniaLocator.Current.GetService<ITextShaperImpl>();
  238. var glyphRun = formatterImpl.ShapeText(s_ellipsis, textStyle.TextFormat);
  239. return new ShapedTextRun(glyphRun, textStyle);
  240. }
  241. private readonly struct SplitTextRunsResult
  242. {
  243. public SplitTextRunsResult(IReadOnlyList<ShapedTextRun> first, IReadOnlyList<ShapedTextRun> second)
  244. {
  245. First = first;
  246. Second = second;
  247. }
  248. /// <summary>
  249. /// Gets the first text runs.
  250. /// </summary>
  251. /// <value>
  252. /// The first text runs.
  253. /// </value>
  254. public IReadOnlyList<ShapedTextRun> First { get; }
  255. /// <summary>
  256. /// Gets the second text runs.
  257. /// </summary>
  258. /// <value>
  259. /// The second text runs.
  260. /// </value>
  261. public IReadOnlyList<ShapedTextRun> Second { get; }
  262. }
  263. /// <summary>
  264. /// Split a sequence of runs into two segments at specified length.
  265. /// </summary>
  266. /// <param name="textRuns">The text run's.</param>
  267. /// <param name="length">The length to split at.</param>
  268. /// <returns></returns>
  269. private static SplitTextRunsResult SplitTextRuns(IReadOnlyList<ShapedTextRun> textRuns, int length)
  270. {
  271. var currentLength = 0;
  272. for (var i = 0; i < textRuns.Count; i++)
  273. {
  274. var currentRun = textRuns[i];
  275. if (currentLength + currentRun.GlyphRun.Characters.Length < length)
  276. {
  277. currentLength += currentRun.GlyphRun.Characters.Length;
  278. continue;
  279. }
  280. var firstCount = currentRun.GlyphRun.Characters.Length > 1 ? i + 1 : i;
  281. var first = new ShapedTextRun[firstCount];
  282. if (firstCount > 1)
  283. {
  284. for (var j = 0; j < i; j++)
  285. {
  286. first[j] = textRuns[j];
  287. }
  288. }
  289. var secondCount = textRuns.Count - firstCount;
  290. if (currentLength + currentRun.GlyphRun.Characters.Length == length)
  291. {
  292. var second = new ShapedTextRun[secondCount];
  293. var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0;
  294. if (secondCount > 0)
  295. {
  296. for (var j = 0; j < secondCount; j++)
  297. {
  298. second[j] = textRuns[i + j + offset];
  299. }
  300. }
  301. first[i] = currentRun;
  302. return new SplitTextRunsResult(first, second);
  303. }
  304. else
  305. {
  306. secondCount++;
  307. var second = new ShapedTextRun[secondCount];
  308. if (secondCount > 0)
  309. {
  310. for (var j = 1; j < secondCount; j++)
  311. {
  312. second[j] = textRuns[i + j];
  313. }
  314. }
  315. var split = currentRun.Split(length - currentLength);
  316. first[i] = split.First;
  317. second[0] = split.Second;
  318. return new SplitTextRunsResult(first, second);
  319. }
  320. }
  321. return new SplitTextRunsResult(textRuns, null);
  322. }
  323. }
  324. }