TextFormatterTests.cs 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using Avalonia.Media;
  5. using Avalonia.Media.TextFormatting;
  6. using Avalonia.Media.TextFormatting.Unicode;
  7. using Avalonia.UnitTests;
  8. using Avalonia.Utilities;
  9. using Xunit;
  10. namespace Avalonia.Skia.UnitTests.Media.TextFormatting
  11. {
  12. public class TextFormatterTests
  13. {
  14. [Fact]
  15. public void Should_Format_TextRuns_With_Default_Style()
  16. {
  17. using (Start())
  18. {
  19. const string text = "0123456789";
  20. var defaultProperties =
  21. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black);
  22. var textSource = new SingleBufferTextSource(text, defaultProperties);
  23. var formatter = new TextFormatterImpl();
  24. var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  25. new GenericTextParagraphProperties(defaultProperties));
  26. Assert.Single(textLine.TextRuns);
  27. var textRun = textLine.TextRuns[0];
  28. Assert.Equal(defaultProperties.Typeface, textRun.Properties.Typeface);
  29. Assert.Equal(defaultProperties.ForegroundBrush, textRun.Properties.ForegroundBrush);
  30. Assert.Equal(text.Length, textRun.Length);
  31. }
  32. }
  33. [Fact]
  34. public void Should_Format_TextRuns_With_Multiple_Buffers()
  35. {
  36. using (Start())
  37. {
  38. var defaultProperties =
  39. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black);
  40. var textSource = new MultiBufferTextSource(defaultProperties);
  41. var formatter = new TextFormatterImpl();
  42. var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  43. new GenericTextParagraphProperties(defaultProperties));
  44. Assert.Equal(5, textLine.TextRuns.Count);
  45. Assert.Equal(50, textLine.Length);
  46. }
  47. }
  48. private class TextSourceWithDummyRuns : ITextSource
  49. {
  50. private readonly TextRunProperties _properties;
  51. private readonly List<ValueSpan<TextRun>> _textRuns;
  52. public TextSourceWithDummyRuns(TextRunProperties properties)
  53. {
  54. _properties = properties;
  55. _textRuns = new List<ValueSpan<TextRun>>
  56. {
  57. new ValueSpan<TextRun>(0, 5, new TextCharacters("Hello", _properties)),
  58. new ValueSpan<TextRun>(5, 1, new DummyRun()),
  59. new ValueSpan<TextRun>(6, 1, new DummyRun()),
  60. new ValueSpan<TextRun>(7, 6, new TextCharacters(" World", _properties))
  61. };
  62. }
  63. public TextRun GetTextRun(int textSourceIndex)
  64. {
  65. foreach (var run in _textRuns)
  66. {
  67. if (textSourceIndex < run.Start + run.Length)
  68. {
  69. return run.Value;
  70. }
  71. }
  72. return new TextEndOfParagraph();
  73. }
  74. private class DummyRun : TextRun
  75. {
  76. public DummyRun()
  77. {
  78. Length = DefaultTextSourceLength;
  79. }
  80. public override int Length { get; }
  81. }
  82. }
  83. [Fact]
  84. public void Should_Format_TextLine_With_Non_Text_TextRuns()
  85. {
  86. using (Start())
  87. {
  88. var defaultProperties =
  89. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black);
  90. var textSource = new TextSourceWithDummyRuns(defaultProperties);
  91. var formatter = new TextFormatterImpl();
  92. var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  93. new GenericTextParagraphProperties(defaultProperties));
  94. Assert.Equal(5, textLine.TextRuns.Count);
  95. Assert.Equal(14, textLine.Length);
  96. }
  97. }
  98. [Fact]
  99. public void Should_Format_TextRuns_With_TextRunStyles()
  100. {
  101. using (Start())
  102. {
  103. const string text = "0123456789";
  104. var defaultProperties =
  105. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black);
  106. var GenericTextRunPropertiesRuns = new[]
  107. {
  108. new ValueSpan<TextRunProperties>(0, 3, defaultProperties),
  109. new ValueSpan<TextRunProperties>(3, 3,
  110. new GenericTextRunProperties(Typeface.Default, 13, foregroundBrush: Brushes.Black)),
  111. new ValueSpan<TextRunProperties>(6, 3,
  112. new GenericTextRunProperties(Typeface.Default, 14, foregroundBrush: Brushes.Black)),
  113. new ValueSpan<TextRunProperties>(9, 1, defaultProperties)
  114. };
  115. var textSource = new FormattedTextSource(text, defaultProperties, GenericTextRunPropertiesRuns);
  116. var formatter = new TextFormatterImpl();
  117. var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  118. new GenericTextParagraphProperties(defaultProperties));
  119. Assert.Equal(text.Length, textLine.Length);
  120. for (var i = 0; i < GenericTextRunPropertiesRuns.Length; i++)
  121. {
  122. var GenericTextRunPropertiesRun = GenericTextRunPropertiesRuns[i];
  123. var textRun = textLine.TextRuns[i];
  124. Assert.Equal(GenericTextRunPropertiesRun.Length, textRun.Length);
  125. }
  126. }
  127. }
  128. [Theory]
  129. [InlineData("0123", 1)]
  130. [InlineData("\r\n", 1)]
  131. [InlineData("👍b", 2)]
  132. [InlineData("a👍b", 3)]
  133. [InlineData("a👍子b", 4)]
  134. public void Should_Produce_Unique_Runs(string text, int numberOfRuns)
  135. {
  136. using (Start())
  137. {
  138. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  139. var textSource = new SingleBufferTextSource(text, defaultProperties);
  140. var formatter = new TextFormatterImpl();
  141. var textLine =
  142. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  143. new GenericTextParagraphProperties(defaultProperties));
  144. Assert.Equal(numberOfRuns, textLine.TextRuns.Count);
  145. }
  146. }
  147. [Fact]
  148. public void Should_Produce_A_Single_Fallback_Run()
  149. {
  150. using (Start())
  151. {
  152. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  153. const string text = "👍 👍 👍 👍";
  154. var textSource = new SingleBufferTextSource(text, defaultProperties);
  155. var formatter = new TextFormatterImpl();
  156. var textLine =
  157. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  158. new GenericTextParagraphProperties(defaultProperties));
  159. Assert.Equal(1, textLine.TextRuns.Count);
  160. }
  161. }
  162. [Fact]
  163. public void Should_Split_Run_On_Script()
  164. {
  165. using (Start())
  166. {
  167. const string text = "ABCDالدولي";
  168. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  169. var textSource = new SingleBufferTextSource(text, defaultProperties);
  170. var formatter = new TextFormatterImpl();
  171. var textLine =
  172. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  173. new GenericTextParagraphProperties(defaultProperties));
  174. var firstRun = textLine.TextRuns[0];
  175. Assert.Equal(4, firstRun.Length);
  176. }
  177. }
  178. [InlineData("𐐷𐐷𐐷𐐷𐐷", 10, 1)]
  179. [InlineData("01234 56789 01234 56789", 6, 4)]
  180. [Theory]
  181. public void Should_Wrap_With_Overflow(string text, int expectedCharactersPerLine, int expectedNumberOfLines)
  182. {
  183. using (Start())
  184. {
  185. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  186. var textSource = new SingleBufferTextSource(text, defaultProperties);
  187. var formatter = new TextFormatterImpl();
  188. var numberOfLines = 0;
  189. var currentPosition = 0;
  190. while (currentPosition < text.Length)
  191. {
  192. var textLine =
  193. formatter.FormatLine(textSource, currentPosition, 1,
  194. new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.WrapWithOverflow));
  195. if (text.Length - currentPosition > expectedCharactersPerLine)
  196. {
  197. Assert.Equal(expectedCharactersPerLine, textLine.Length);
  198. }
  199. currentPosition += textLine.Length;
  200. numberOfLines++;
  201. }
  202. Assert.Equal(expectedNumberOfLines, numberOfLines);
  203. }
  204. }
  205. [InlineData("Whether to turn off HTTPS. This option only applies if Individual, " +
  206. "IndividualB2C, SingleOrg, or MultiOrg aren't used for &#8209;&#8209;auth."
  207. , "Noto Sans", 40)]
  208. [InlineData("01234 56789 01234 56789", "Noto Mono", 7)]
  209. [Theory]
  210. public void Should_Wrap(string text, string familyName, int numberOfCharactersPerLine)
  211. {
  212. using (Start())
  213. {
  214. var lineBreaker = new LineBreakEnumerator(text);
  215. var expected = new List<int>();
  216. while (lineBreaker.MoveNext(out var lineBreak))
  217. {
  218. expected.Add(lineBreak.PositionWrap - 1);
  219. }
  220. var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#" +
  221. familyName);
  222. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  223. var textSource = new SingleBufferTextSource(text, defaultProperties);
  224. var formatter = new TextFormatterImpl();
  225. var glyph = typeface.GlyphTypeface.GetGlyph('a');
  226. var advance = typeface.GlyphTypeface.GetGlyphAdvance(glyph) *
  227. (12.0 / typeface.GlyphTypeface.Metrics.DesignEmHeight);
  228. var paragraphWidth = advance * numberOfCharactersPerLine;
  229. var currentPosition = 0;
  230. while (currentPosition < text.Length)
  231. {
  232. var textLine =
  233. formatter.FormatLine(textSource, currentPosition, paragraphWidth,
  234. new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap));
  235. var end = textLine.FirstTextSourceIndex + textLine.Length - 1;
  236. Assert.True(expected.Contains(end));
  237. var index = expected.IndexOf(end);
  238. for (var i = 0; i <= index; i++)
  239. {
  240. expected.RemoveAt(0);
  241. }
  242. currentPosition += textLine.Length;
  243. }
  244. }
  245. }
  246. [Fact]
  247. public void Should_Produce_Fixed_Height_Lines()
  248. {
  249. using (Start())
  250. {
  251. const string text = "012345";
  252. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  253. var textSource = new SingleBufferTextSource(text, defaultProperties);
  254. var formatter = new TextFormatterImpl();
  255. var textLine =
  256. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  257. new GenericTextParagraphProperties(defaultProperties, lineHeight: 50));
  258. Assert.Equal(50, textLine.Height);
  259. }
  260. }
  261. [Fact]
  262. public void Should_Not_Produce_TextLine_Wider_Than_ParagraphWidth()
  263. {
  264. using (Start())
  265. {
  266. const string text =
  267. "Multiline TextBlock with TextWrapping.\r\rLorem ipsum dolor sit amet, consectetur adipiscing elit. " +
  268. "Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. " +
  269. "Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. " +
  270. "Vivamus pretium ornare est.";
  271. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  272. var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
  273. var textSource = new SingleBufferTextSource(text, defaultProperties);
  274. var formatter = new TextFormatterImpl();
  275. var textSourceIndex = 0;
  276. while (textSourceIndex < text.Length)
  277. {
  278. var textLine =
  279. formatter.FormatLine(textSource, textSourceIndex, 200, paragraphProperties);
  280. Assert.True(textLine.Width <= 200);
  281. textSourceIndex += textLine.Length;
  282. }
  283. }
  284. }
  285. [Fact]
  286. public void Wrap_Should_Not_Produce_Empty_Lines()
  287. {
  288. using (Start())
  289. {
  290. const string text = "012345";
  291. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  292. var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
  293. var textSource = new SingleBufferTextSource(text, defaultProperties);
  294. var formatter = new TextFormatterImpl();
  295. var textSourceIndex = 0;
  296. while (textSourceIndex < text.Length)
  297. {
  298. var textLine =
  299. formatter.FormatLine(textSource, textSourceIndex, 3, paragraphProperties);
  300. Assert.NotEqual(0, textLine.Length);
  301. textSourceIndex += textLine.Length;
  302. }
  303. Assert.Equal(text.Length, textSourceIndex);
  304. }
  305. }
  306. [InlineData("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor",
  307. new[] { "Lorem ipsum ", "dolor sit amet, ", "consectetur ", "adipisicing elit, ", "sed do eiusmod " })]
  308. [Theory]
  309. public void Should_Produce_Wrapped_And_Trimmed_Lines(string text, string[] expectedLines)
  310. {
  311. using (Start())
  312. {
  313. var typeface = new Typeface("Verdana");
  314. var defaultProperties = new GenericTextRunProperties(typeface, 32, foregroundBrush: Brushes.Black);
  315. var styleSpans = new[]
  316. {
  317. new ValueSpan<TextRunProperties>(0, 5,
  318. new GenericTextRunProperties(typeface, 48)),
  319. new ValueSpan<TextRunProperties>(6, 11,
  320. new GenericTextRunProperties(new Typeface("Verdana", weight: FontWeight.Bold), 32)),
  321. new ValueSpan<TextRunProperties>(28, 28,
  322. new GenericTextRunProperties(new Typeface("Verdana", FontStyle.Italic),32))
  323. };
  324. var textSource = new FormattedTextSource(text, defaultProperties, styleSpans);
  325. var formatter = new TextFormatterImpl();
  326. var currentPosition = 0;
  327. var currentHeight = 0d;
  328. var currentLineIndex = 0;
  329. while (currentPosition < text.Length && currentLineIndex < expectedLines.Length)
  330. {
  331. var textLine =
  332. formatter.FormatLine(textSource, currentPosition, 300,
  333. new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.WrapWithOverflow));
  334. currentPosition += textLine.Length;
  335. if (textLine.Width > 300 || currentHeight + textLine.Height > 240)
  336. {
  337. textLine = textLine.Collapse(new TextTrailingWordEllipsis(TextTrimming.DefaultEllipsisChar, 300, defaultProperties));
  338. }
  339. currentHeight += textLine.Height;
  340. var currentText = text.Substring(textLine.FirstTextSourceIndex, textLine.Length);
  341. Assert.Equal(expectedLines[currentLineIndex], currentText);
  342. currentLineIndex++;
  343. }
  344. Assert.Equal(expectedLines.Length, currentLineIndex);
  345. }
  346. }
  347. [InlineData("0123456789", TextAlignment.Left, FlowDirection.LeftToRight)]
  348. [InlineData("0123456789", TextAlignment.Center, FlowDirection.LeftToRight)]
  349. [InlineData("0123456789", TextAlignment.Right, FlowDirection.LeftToRight)]
  350. [InlineData("0123456789", TextAlignment.Left, FlowDirection.RightToLeft)]
  351. [InlineData("0123456789", TextAlignment.Center, FlowDirection.RightToLeft)]
  352. [InlineData("0123456789", TextAlignment.Right, FlowDirection.RightToLeft)]
  353. [InlineData("שנבגק", TextAlignment.Left, FlowDirection.RightToLeft)]
  354. [InlineData("שנבגק", TextAlignment.Center, FlowDirection.RightToLeft)]
  355. [InlineData("שנבגק", TextAlignment.Right, FlowDirection.RightToLeft)]
  356. [Theory]
  357. public void Should_Align_TextLine(string text, TextAlignment textAlignment, FlowDirection flowDirection)
  358. {
  359. using (Start())
  360. {
  361. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  362. var paragraphProperties = new GenericTextParagraphProperties(flowDirection, textAlignment, true, true,
  363. defaultProperties, TextWrapping.NoWrap, 0, 0, 0);
  364. var textSource = new SingleBufferTextSource(text, defaultProperties);
  365. var formatter = new TextFormatterImpl();
  366. var textLine =
  367. formatter.FormatLine(textSource, 0, 100, paragraphProperties);
  368. var expectedOffset = 0d;
  369. switch (textAlignment)
  370. {
  371. case TextAlignment.Center:
  372. expectedOffset = 50 - textLine.Width / 2;
  373. break;
  374. case TextAlignment.Right:
  375. expectedOffset = 100 - textLine.WidthIncludingTrailingWhitespace;
  376. break;
  377. }
  378. Assert.Equal(expectedOffset, textLine.Start);
  379. }
  380. }
  381. [Fact]
  382. public void Should_Wrap_Syriac()
  383. {
  384. using (Start())
  385. {
  386. const string text =
  387. "܀ ܁ ܂ ܃ ܄ ܅ ܆ ܇ ܈ ܉ ܊ ܋ ܌ ܍ ܏ ܐ ܑ ܒ ܓ ܔ ܕ ܖ ܗ ܘ ܙ ܚ ܛ ܜ ܝ ܞ ܟ ܠ ܡ ܢ ܣ ܤ ܥ ܦ ܧ ܨ ܩ ܪ ܫ ܬ ܰ ܱ ܲ ܳ ܴ ܵ ܶ ܷ ܸ ܹ ܺ ܻ ܼ ܽ ܾ ܿ ݀ ݁ ݂ ݃ ݄ ݅ ݆ ݇ ݈ ݉ ݊";
  388. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  389. var paragraphProperties =
  390. new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
  391. var textSource = new SingleBufferTextSource(text, defaultProperties);
  392. var formatter = new TextFormatterImpl();
  393. var textPosition = 87;
  394. TextLineBreak lastBreak = null;
  395. while (textPosition < text.Length)
  396. {
  397. var textLine =
  398. formatter.FormatLine(textSource, textPosition, 50, paragraphProperties, lastBreak);
  399. Assert.Equal(textLine.Length, textLine.TextRuns.Sum(x => x.Length));
  400. textPosition += textLine.Length;
  401. lastBreak = textLine.TextLineBreak;
  402. }
  403. }
  404. }
  405. [Fact]
  406. public void Should_FormatLine_With_Emergency_Breaks()
  407. {
  408. using (Start())
  409. {
  410. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  411. var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
  412. var textSource = new SingleBufferTextSource("0123456789_0123456789_0123456789_0123456789", defaultProperties);
  413. var formatter = new TextFormatterImpl();
  414. var textLine =
  415. formatter.FormatLine(textSource, 0, 33, paragraphProperties);
  416. var remainingRunsLineBreak = Assert.IsType<WrappingTextLineBreak>(textLine.TextLineBreak);
  417. var remainingRuns = remainingRunsLineBreak.AcquireRemainingRuns();
  418. Assert.NotNull(remainingRuns);
  419. Assert.NotEmpty(remainingRuns);
  420. }
  421. }
  422. [InlineData("פעילות הבינאום, W3C!")]
  423. [InlineData("abcABC")]
  424. [InlineData("זה כיף סתם לשמוע איך תנצח קרפד עץ טוב בגן")]
  425. [InlineData("טטטט abcDEF טטטט")]
  426. [Theory]
  427. public void Should_Not_Alter_TextRuns_After_TextStyles_Were_Applied(string text)
  428. {
  429. using (Start())
  430. {
  431. var formatter = new TextFormatterImpl();
  432. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  433. var paragraphProperties =
  434. new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.NoWrap);
  435. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  436. var expectedTextLine = formatter.FormatLine(new SingleBufferTextSource(text, defaultProperties),
  437. 0, double.PositiveInfinity, paragraphProperties);
  438. var expectedRuns = expectedTextLine.TextRuns.Cast<ShapedTextRun>().ToList();
  439. var expectedGlyphs = expectedRuns
  440. .SelectMany(run => run.GlyphRun.GlyphInfos, (_, glyph) => glyph.GlyphIndex)
  441. .ToList();
  442. for (var i = 0; i < text.Length; i++)
  443. {
  444. for (var j = 1; i + j < text.Length; j++)
  445. {
  446. var spans = new[]
  447. {
  448. new ValueSpan<TextRunProperties>(i, j,
  449. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
  450. };
  451. var textSource = new FormattedTextSource(text, defaultProperties, spans);
  452. var textLine =
  453. formatter.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties);
  454. var shapedRuns = textLine.TextRuns.Cast<ShapedTextRun>().ToList();
  455. var actualGlyphs = shapedRuns
  456. .SelectMany(x => x.GlyphRun.GlyphInfos, (_, glyph) => glyph.GlyphIndex)
  457. .ToList();
  458. Assert.Equal(expectedGlyphs, actualGlyphs);
  459. }
  460. }
  461. }
  462. }
  463. [Fact]
  464. public void Should_FormatLine_With_DrawableRuns()
  465. {
  466. var defaultRunProperties = new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black);
  467. var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties);
  468. var textSource = new CustomTextSource("Hello World ->");
  469. using (Start())
  470. {
  471. var textLine =
  472. TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties);
  473. Assert.Equal(3, textLine.TextRuns.Count);
  474. Assert.True(textLine.TextRuns[1] is RectangleRun);
  475. }
  476. }
  477. [Fact]
  478. public void Should_Format_With_EndOfLineRun()
  479. {
  480. using (Start())
  481. {
  482. var defaultRunProperties = new GenericTextRunProperties(Typeface.Default);
  483. var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties);
  484. var textSource = new EndOfLineTextSource();
  485. var textLine =
  486. TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties);
  487. Assert.NotNull(textLine.TextLineBreak);
  488. Assert.Equal(TextRun.DefaultTextSourceLength, textLine.Length);
  489. }
  490. }
  491. private class EndOfLineTextSource : ITextSource
  492. {
  493. public TextRun GetTextRun(int textSourceIndex)
  494. {
  495. return new TextEndOfLine();
  496. }
  497. }
  498. private class CustomTextSource : ITextSource
  499. {
  500. private readonly string _text;
  501. public CustomTextSource(string text)
  502. {
  503. _text = text;
  504. }
  505. public TextRun GetTextRun(int textSourceIndex)
  506. {
  507. if (textSourceIndex >= _text.Length + TextRun.DefaultTextSourceLength + _text.Length)
  508. {
  509. return null;
  510. }
  511. if (textSourceIndex == _text.Length)
  512. {
  513. return new RectangleRun(new Rect(0, 0, 50, 50), Brushes.Green);
  514. }
  515. return new TextCharacters(_text, new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black));
  516. }
  517. }
  518. private class RectangleRun : DrawableTextRun
  519. {
  520. private readonly Rect _rect;
  521. private readonly IBrush _fill;
  522. public RectangleRun(Rect rect, IBrush fill)
  523. {
  524. _rect = rect;
  525. _fill = fill;
  526. }
  527. public override Size Size => _rect.Size;
  528. public override double Baseline => 0;
  529. public override void Draw(DrawingContext drawingContext, Point origin)
  530. {
  531. using (drawingContext.PushPreTransform(Matrix.CreateTranslation(new Vector(origin.X, 0))))
  532. {
  533. drawingContext.FillRectangle(_fill, _rect);
  534. }
  535. }
  536. }
  537. public static IDisposable Start()
  538. {
  539. var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
  540. .With(renderInterface: new PlatformRenderInterface(),
  541. textShaperImpl: new TextShaperImpl()));
  542. AvaloniaLocator.CurrentMutable
  543. .Bind<FontManager>().ToConstant(new FontManager(new CustomFontManagerImpl()));
  544. return disposable;
  545. }
  546. }
  547. }