TextFormatterImpl.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. using System.Collections.Generic;
  2. using Avalonia.Media.TextFormatting.Unicode;
  3. using Avalonia.Platform;
  4. using Avalonia.Utilities;
  5. namespace Avalonia.Media.TextFormatting
  6. {
  7. internal class TextFormatterImpl : TextFormatter
  8. {
  9. private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' });
  10. /// <inheritdoc cref="TextFormatter.FormatLine"/>
  11. public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
  12. TextParagraphProperties paragraphProperties, TextLineBreak previousLineBreak = null)
  13. {
  14. var textTrimming = paragraphProperties.TextTrimming;
  15. var textWrapping = paragraphProperties.TextWrapping;
  16. TextLine textLine = null;
  17. var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, previousLineBreak, out var nextLineBreak);
  18. var textRange = GetTextRange(textRuns);
  19. if (textTrimming != TextTrimming.None)
  20. {
  21. textLine = PerformTextTrimming(textRuns, textRange, paragraphWidth, paragraphProperties);
  22. }
  23. else
  24. {
  25. switch (textWrapping)
  26. {
  27. case TextWrapping.NoWrap:
  28. {
  29. var textLineMetrics =
  30. TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties);
  31. textLine = new TextLineImpl(textRuns, textLineMetrics, nextLineBreak);
  32. break;
  33. }
  34. case TextWrapping.WrapWithOverflow:
  35. case TextWrapping.Wrap:
  36. {
  37. textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties);
  38. break;
  39. }
  40. }
  41. }
  42. return textLine;
  43. }
  44. /// <summary>
  45. /// Fetches text runs.
  46. /// </summary>
  47. /// <param name="textSource">The text source.</param>
  48. /// <param name="firstTextSourceIndex">The first text source index.</param>
  49. /// <param name="previousLineBreak">Previous line break. Can be null.</param>
  50. /// <param name="nextLineBreak">Next line break. Can be null.</param>
  51. /// <returns>
  52. /// The formatted text runs.
  53. /// </returns>
  54. private static IReadOnlyList<ShapedTextCharacters> FetchTextRuns(ITextSource textSource,
  55. int firstTextSourceIndex, TextLineBreak previousLineBreak, out TextLineBreak nextLineBreak)
  56. {
  57. nextLineBreak = default;
  58. var currentLength = 0;
  59. var textRuns = new List<ShapedTextCharacters>();
  60. if (previousLineBreak != null)
  61. {
  62. foreach (var shapedCharacters in previousLineBreak.RemainingCharacters)
  63. {
  64. if (shapedCharacters == null)
  65. {
  66. continue;
  67. }
  68. textRuns.Add(shapedCharacters);
  69. if (TryGetLineBreak(shapedCharacters, out var runLineBreak))
  70. {
  71. var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap);
  72. nextLineBreak = new TextLineBreak(splitResult.Second);
  73. return splitResult.First;
  74. }
  75. currentLength += shapedCharacters.Text.Length;
  76. }
  77. }
  78. firstTextSourceIndex += currentLength;
  79. var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex);
  80. while (textRunEnumerator.MoveNext())
  81. {
  82. var textRun = textRunEnumerator.Current;
  83. switch (textRun)
  84. {
  85. case TextCharacters textCharacters:
  86. {
  87. var shapeableRuns = textCharacters.GetShapeableCharacters();
  88. foreach (var run in shapeableRuns)
  89. {
  90. var glyphRun = TextShaper.Current.ShapeText(run.Text, run.Properties.Typeface,
  91. run.Properties.FontRenderingEmSize, run.Properties.CultureInfo);
  92. var shapedCharacters = new ShapedTextCharacters(glyphRun, run.Properties);
  93. textRuns.Add(shapedCharacters);
  94. }
  95. break;
  96. }
  97. }
  98. if (TryGetLineBreak(textRun, out var runLineBreak))
  99. {
  100. var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap);
  101. nextLineBreak = new TextLineBreak(splitResult.Second);
  102. return splitResult.First;
  103. }
  104. currentLength += textRun.Text.Length;
  105. }
  106. return textRuns;
  107. }
  108. private static bool TryGetLineBreak(TextRun textRun, out LineBreak lineBreak)
  109. {
  110. lineBreak = default;
  111. if (textRun.Text.IsEmpty)
  112. {
  113. return false;
  114. }
  115. var lineBreakEnumerator = new LineBreakEnumerator(textRun.Text);
  116. while (lineBreakEnumerator.MoveNext())
  117. {
  118. if (!lineBreakEnumerator.Current.Required)
  119. {
  120. continue;
  121. }
  122. lineBreak = lineBreakEnumerator.Current;
  123. if (lineBreak.PositionWrap >= textRun.Text.Length)
  124. {
  125. return true;
  126. }
  127. //The line breaker isn't treating \n\r as a pair so we have to fix that here.
  128. if (textRun.Text[lineBreak.PositionMeasure] == '\n'
  129. && textRun.Text[lineBreak.PositionWrap] == '\r')
  130. {
  131. lineBreak = new LineBreak(lineBreak.PositionMeasure, lineBreak.PositionWrap + 1,
  132. lineBreak.Required);
  133. }
  134. return true;
  135. }
  136. return false;
  137. }
  138. /// <summary>
  139. /// Performs text trimming and returns a trimmed line.
  140. /// </summary>
  141. /// <param name="textRuns">The text runs to perform the trimming on.</param>
  142. /// <param name="textRange">The text range that is covered by the text runs.</param>
  143. /// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param>
  144. /// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties,
  145. /// such as TextWrapping, TextAlignment, or TextStyle.</param>
  146. /// <returns></returns>
  147. private static TextLine PerformTextTrimming(IReadOnlyList<ShapedTextCharacters> textRuns, TextRange textRange,
  148. double paragraphWidth, TextParagraphProperties paragraphProperties)
  149. {
  150. var textTrimming = paragraphProperties.TextTrimming;
  151. var availableWidth = paragraphWidth;
  152. var currentWidth = 0.0;
  153. var runIndex = 0;
  154. while (runIndex < textRuns.Count)
  155. {
  156. var currentRun = textRuns[runIndex];
  157. currentWidth += currentRun.GlyphRun.Bounds.Width;
  158. if (currentWidth > availableWidth)
  159. {
  160. var ellipsisRun = CreateEllipsisRun(currentRun.Properties);
  161. var measuredLength = MeasureText(currentRun, availableWidth - ellipsisRun.GlyphRun.Bounds.Width);
  162. if (textTrimming == TextTrimming.WordEllipsis)
  163. {
  164. if (measuredLength < textRange.End)
  165. {
  166. var currentBreakPosition = 0;
  167. var lineBreaker = new LineBreakEnumerator(currentRun.Text);
  168. while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
  169. {
  170. var nextBreakPosition = lineBreaker.Current.PositionWrap;
  171. if (nextBreakPosition == 0)
  172. {
  173. break;
  174. }
  175. if (nextBreakPosition > measuredLength)
  176. {
  177. break;
  178. }
  179. currentBreakPosition = nextBreakPosition;
  180. }
  181. measuredLength = currentBreakPosition;
  182. }
  183. }
  184. var splitResult = SplitTextRuns(textRuns, measuredLength);
  185. var trimmedRuns = new List<ShapedTextCharacters>(splitResult.First.Count + 1);
  186. trimmedRuns.AddRange(splitResult.First);
  187. trimmedRuns.Add(ellipsisRun);
  188. var textLineMetrics =
  189. TextLineMetrics.Create(trimmedRuns, textRange, paragraphWidth, paragraphProperties);
  190. return new TextLineImpl(trimmedRuns, textLineMetrics);
  191. }
  192. availableWidth -= currentRun.GlyphRun.Bounds.Width;
  193. runIndex++;
  194. }
  195. return new TextLineImpl(textRuns,
  196. TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties));
  197. }
  198. /// <summary>
  199. /// Performs text wrapping returns a list of text lines.
  200. /// </summary>
  201. /// <param name="textRuns">The text run's.</param>
  202. /// <param name="textRange">The text range that is covered by the text runs.</param>
  203. /// <param name="paragraphWidth">The paragraph width.</param>
  204. /// <param name="paragraphProperties">The text paragraph properties.</param>
  205. /// <returns>The wrapped text line.</returns>
  206. private static TextLine PerformTextWrapping(IReadOnlyList<ShapedTextCharacters> textRuns, TextRange textRange,
  207. double paragraphWidth, TextParagraphProperties paragraphProperties)
  208. {
  209. var availableWidth = paragraphWidth;
  210. var currentWidth = 0.0;
  211. var runIndex = 0;
  212. var length = 0;
  213. while (runIndex < textRuns.Count)
  214. {
  215. var currentRun = textRuns[runIndex];
  216. if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth)
  217. {
  218. var measuredLength = MeasureText(currentRun, paragraphWidth - currentWidth);
  219. if (measuredLength < currentRun.Text.Length)
  220. {
  221. if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
  222. {
  223. var lineBreaker = new LineBreakEnumerator(currentRun.Text.Skip(measuredLength));
  224. if (lineBreaker.MoveNext())
  225. {
  226. measuredLength += lineBreaker.Current.PositionWrap;
  227. }
  228. else
  229. {
  230. measuredLength = currentRun.Text.Length;
  231. }
  232. }
  233. else
  234. {
  235. var currentBreakPosition = -1;
  236. var lineBreaker = new LineBreakEnumerator(currentRun.Text);
  237. while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
  238. {
  239. var nextBreakPosition = lineBreaker.Current.PositionWrap;
  240. if (nextBreakPosition == 0)
  241. {
  242. break;
  243. }
  244. if (nextBreakPosition > measuredLength)
  245. {
  246. break;
  247. }
  248. currentBreakPosition = nextBreakPosition;
  249. }
  250. if (currentBreakPosition != -1)
  251. {
  252. measuredLength = currentBreakPosition;
  253. }
  254. }
  255. }
  256. length += measuredLength;
  257. var splitResult = SplitTextRuns(textRuns, length);
  258. var textLineMetrics = TextLineMetrics.Create(splitResult.First,
  259. new TextRange(textRange.Start, length), paragraphWidth, paragraphProperties);
  260. var lineBreak = splitResult.Second != null && splitResult.Second.Count > 0 ?
  261. new TextLineBreak(splitResult.Second) :
  262. null;
  263. return new TextLineImpl(splitResult.First, textLineMetrics, lineBreak);
  264. }
  265. currentWidth += currentRun.GlyphRun.Bounds.Width;
  266. length += currentRun.GlyphRun.Characters.Length;
  267. runIndex++;
  268. }
  269. return new TextLineImpl(textRuns,
  270. TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties));
  271. }
  272. /// <summary>
  273. /// Measures the number of characters that fits into available width.
  274. /// </summary>
  275. /// <param name="textCharacters">The text run.</param>
  276. /// <param name="availableWidth">The available width.</param>
  277. /// <returns></returns>
  278. private static int MeasureText(ShapedTextCharacters textCharacters, double availableWidth)
  279. {
  280. var glyphRun = textCharacters.GlyphRun;
  281. if (glyphRun.Bounds.Width < availableWidth)
  282. {
  283. return glyphRun.Characters.Length;
  284. }
  285. var glyphCount = 0;
  286. var currentWidth = 0.0;
  287. if (glyphRun.GlyphAdvances.IsEmpty)
  288. {
  289. var glyphTypeface = glyphRun.GlyphTypeface;
  290. for (var i = 0; i < glyphRun.GlyphClusters.Length; i++)
  291. {
  292. var glyph = glyphRun.GlyphIndices[i];
  293. var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale;
  294. if (currentWidth + advance > availableWidth)
  295. {
  296. break;
  297. }
  298. currentWidth += advance;
  299. glyphCount++;
  300. }
  301. }
  302. else
  303. {
  304. for (var i = 0; i < glyphRun.GlyphAdvances.Length; i++)
  305. {
  306. var advance = glyphRun.GlyphAdvances[i];
  307. if (currentWidth + advance > availableWidth)
  308. {
  309. break;
  310. }
  311. currentWidth += advance;
  312. glyphCount++;
  313. }
  314. }
  315. if (glyphCount == glyphRun.GlyphIndices.Length)
  316. {
  317. return glyphRun.Characters.Length;
  318. }
  319. if (glyphRun.GlyphClusters.IsEmpty)
  320. {
  321. return glyphCount;
  322. }
  323. var firstCluster = glyphRun.GlyphClusters[0];
  324. var lastCluster = glyphRun.GlyphClusters[glyphCount];
  325. return lastCluster - firstCluster;
  326. }
  327. /// <summary>
  328. /// Creates an ellipsis.
  329. /// </summary>
  330. /// <param name="properties">The text run properties.</param>
  331. /// <returns></returns>
  332. private static ShapedTextCharacters CreateEllipsisRun(TextRunProperties properties)
  333. {
  334. var formatterImpl = AvaloniaLocator.Current.GetService<ITextShaperImpl>();
  335. var glyphRun = formatterImpl.ShapeText(s_ellipsis, properties.Typeface, properties.FontRenderingEmSize,
  336. properties.CultureInfo);
  337. return new ShapedTextCharacters(glyphRun, properties);
  338. }
  339. /// <summary>
  340. /// Gets the text range that is covered by the text runs.
  341. /// </summary>
  342. /// <param name="textRuns">The text runs.</param>
  343. /// <returns>The text range that is covered by the text runs.</returns>
  344. private static TextRange GetTextRange(IReadOnlyList<TextRun> textRuns)
  345. {
  346. if (textRuns is null || textRuns.Count == 0)
  347. {
  348. return new TextRange();
  349. }
  350. var firstTextRun = textRuns[0];
  351. if (textRuns.Count == 1)
  352. {
  353. return new TextRange(firstTextRun.Text.Start, firstTextRun.Text.Length);
  354. }
  355. var start = firstTextRun.Text.Start;
  356. var end = textRuns[textRuns.Count - 1].Text.End + 1;
  357. return new TextRange(start, end - start);
  358. }
  359. /// <summary>
  360. /// Split a sequence of runs into two segments at specified length.
  361. /// </summary>
  362. /// <param name="textRuns">The text run's.</param>
  363. /// <param name="length">The length to split at.</param>
  364. /// <returns>The split text runs.</returns>
  365. private static SplitTextRunsResult SplitTextRuns(IReadOnlyList<ShapedTextCharacters> textRuns, int length)
  366. {
  367. var currentLength = 0;
  368. for (var i = 0; i < textRuns.Count; i++)
  369. {
  370. var currentRun = textRuns[i];
  371. if (currentLength + currentRun.GlyphRun.Characters.Length < length)
  372. {
  373. currentLength += currentRun.GlyphRun.Characters.Length;
  374. continue;
  375. }
  376. var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i;
  377. var first = new ShapedTextCharacters[firstCount];
  378. if (firstCount > 1)
  379. {
  380. for (var j = 0; j < i; j++)
  381. {
  382. first[j] = textRuns[j];
  383. }
  384. }
  385. var secondCount = textRuns.Count - firstCount;
  386. if (currentLength + currentRun.GlyphRun.Characters.Length == length)
  387. {
  388. var second = new ShapedTextCharacters[secondCount];
  389. var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0;
  390. if (secondCount > 0)
  391. {
  392. for (var j = 0; j < secondCount; j++)
  393. {
  394. second[j] = textRuns[i + j + offset];
  395. }
  396. }
  397. first[i] = currentRun;
  398. return new SplitTextRunsResult(first, second);
  399. }
  400. else
  401. {
  402. secondCount++;
  403. var second = new ShapedTextCharacters[secondCount];
  404. if (secondCount > 0)
  405. {
  406. for (var j = 1; j < secondCount; j++)
  407. {
  408. second[j] = textRuns[i + j];
  409. }
  410. }
  411. var split = currentRun.Split(length - currentLength);
  412. first[i] = split.First;
  413. second[0] = split.Second;
  414. return new SplitTextRunsResult(first, second);
  415. }
  416. }
  417. return new SplitTextRunsResult(textRuns, null);
  418. }
  419. private readonly struct SplitTextRunsResult
  420. {
  421. public SplitTextRunsResult(IReadOnlyList<ShapedTextCharacters> first, IReadOnlyList<ShapedTextCharacters> second)
  422. {
  423. First = first;
  424. Second = second;
  425. }
  426. /// <summary>
  427. /// Gets the first text runs.
  428. /// </summary>
  429. /// <value>
  430. /// The first text runs.
  431. /// </value>
  432. public IReadOnlyList<ShapedTextCharacters> First { get; }
  433. /// <summary>
  434. /// Gets the second text runs.
  435. /// </summary>
  436. /// <value>
  437. /// The second text runs.
  438. /// </value>
  439. public IReadOnlyList<ShapedTextCharacters> Second { get; }
  440. }
  441. private struct TextRunEnumerator
  442. {
  443. private readonly ITextSource _textSource;
  444. private int _pos;
  445. public TextRunEnumerator(ITextSource textSource, int firstTextSourceIndex)
  446. {
  447. _textSource = textSource;
  448. _pos = firstTextSourceIndex;
  449. Current = null;
  450. }
  451. // ReSharper disable once MemberHidesStaticFromOuterClass
  452. public TextRun Current { get; private set; }
  453. public bool MoveNext()
  454. {
  455. Current = _textSource.GetTextRun(_pos);
  456. if (Current is null)
  457. {
  458. return false;
  459. }
  460. if (Current.TextSourceLength == 0)
  461. {
  462. return false;
  463. }
  464. _pos += Current.TextSourceLength;
  465. return !(Current is TextEndOfLine);
  466. }
  467. }
  468. }
  469. }