TextFormatterTests.cs 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086
  1. #nullable enable
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using Avalonia.Media;
  6. using Avalonia.Media.TextFormatting;
  7. using Avalonia.Media.TextFormatting.Unicode;
  8. using Avalonia.UnitTests;
  9. using Avalonia.Utilities;
  10. using Xunit;
  11. namespace Avalonia.Skia.UnitTests.Media.TextFormatting
  12. {
  13. public class TextFormatterTests
  14. {
  15. [Fact]
  16. public void Should_Format_TextRuns_With_Default_Style()
  17. {
  18. using (Start())
  19. {
  20. const string text = "0123456789";
  21. var defaultProperties =
  22. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black);
  23. var textSource = new SingleBufferTextSource(text, defaultProperties);
  24. var formatter = new TextFormatterImpl();
  25. var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  26. new GenericTextParagraphProperties(defaultProperties));
  27. Assert.NotNull(textLine);
  28. Assert.Single(textLine.TextRuns);
  29. var textRun = textLine.TextRuns[0];
  30. Assert.NotNull(textRun.Properties);
  31. Assert.Equal(defaultProperties.Typeface, textRun.Properties.Typeface);
  32. Assert.Equal(defaultProperties.ForegroundBrush, textRun.Properties.ForegroundBrush);
  33. Assert.Equal(text.Length, textRun.Length);
  34. }
  35. }
  36. [Fact]
  37. public void Should_Format_TextRuns_With_Multiple_Buffers()
  38. {
  39. using (Start())
  40. {
  41. var defaultProperties =
  42. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black);
  43. var textSource = new MultiBufferTextSource(defaultProperties);
  44. var formatter = new TextFormatterImpl();
  45. var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  46. new GenericTextParagraphProperties(defaultProperties));
  47. Assert.NotNull(textLine);
  48. Assert.Equal(5, textLine.TextRuns.Count);
  49. Assert.Equal(50, textLine.Length);
  50. }
  51. }
  52. private class TextSourceWithDummyRuns : ITextSource
  53. {
  54. private readonly TextRunProperties _properties;
  55. private readonly List<ValueSpan<TextRun>> _textRuns;
  56. public TextSourceWithDummyRuns(TextRunProperties properties)
  57. {
  58. _properties = properties;
  59. _textRuns = new List<ValueSpan<TextRun>>
  60. {
  61. new ValueSpan<TextRun>(0, 5, new TextCharacters("Hello", _properties)),
  62. new ValueSpan<TextRun>(5, 1, new DummyRun()),
  63. new ValueSpan<TextRun>(6, 1, new DummyRun()),
  64. new ValueSpan<TextRun>(7, 6, new TextCharacters(" World", _properties))
  65. };
  66. }
  67. public TextRun GetTextRun(int textSourceIndex)
  68. {
  69. foreach (var run in _textRuns)
  70. {
  71. if (textSourceIndex < run.Start + run.Length)
  72. {
  73. return run.Value;
  74. }
  75. }
  76. return new TextEndOfParagraph();
  77. }
  78. private class DummyRun : TextRun
  79. {
  80. public DummyRun()
  81. {
  82. Length = DefaultTextSourceLength;
  83. }
  84. public override int Length { get; }
  85. }
  86. }
  87. [Fact]
  88. public void Should_Format_TextLine_With_Non_Text_TextRuns()
  89. {
  90. using (Start())
  91. {
  92. var defaultProperties =
  93. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black);
  94. var textSource = new TextSourceWithDummyRuns(defaultProperties);
  95. var formatter = new TextFormatterImpl();
  96. var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  97. new GenericTextParagraphProperties(defaultProperties));
  98. Assert.NotNull(textLine);
  99. Assert.Equal(5, textLine.TextRuns.Count);
  100. Assert.Equal(14, textLine.Length);
  101. }
  102. }
  103. [Fact]
  104. public void Should_Format_TextRuns_With_TextRunStyles()
  105. {
  106. using (Start())
  107. {
  108. const string text = "0123456789";
  109. var defaultProperties =
  110. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black);
  111. var GenericTextRunPropertiesRuns = new[]
  112. {
  113. new ValueSpan<TextRunProperties>(0, 3, defaultProperties),
  114. new ValueSpan<TextRunProperties>(3, 3,
  115. new GenericTextRunProperties(Typeface.Default, 13, foregroundBrush: Brushes.Black)),
  116. new ValueSpan<TextRunProperties>(6, 3,
  117. new GenericTextRunProperties(Typeface.Default, 14, foregroundBrush: Brushes.Black)),
  118. new ValueSpan<TextRunProperties>(9, 1, defaultProperties)
  119. };
  120. var textSource = new FormattedTextSource(text, defaultProperties, GenericTextRunPropertiesRuns);
  121. var formatter = new TextFormatterImpl();
  122. var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  123. new GenericTextParagraphProperties(defaultProperties));
  124. Assert.NotNull(textLine);
  125. Assert.Equal(text.Length, textLine.Length);
  126. for (var i = 0; i < GenericTextRunPropertiesRuns.Length; i++)
  127. {
  128. var GenericTextRunPropertiesRun = GenericTextRunPropertiesRuns[i];
  129. var textRun = textLine.TextRuns[i];
  130. Assert.Equal(GenericTextRunPropertiesRun.Length, textRun.Length);
  131. }
  132. }
  133. }
  134. [Theory]
  135. [InlineData("0123", 1)]
  136. [InlineData("\r\n", 1)]
  137. [InlineData("👍b", 2)]
  138. [InlineData("a👍b", 3)]
  139. [InlineData("a👍子b", 4)]
  140. public void Should_Produce_Unique_Runs(string text, int numberOfRuns)
  141. {
  142. using (Start())
  143. {
  144. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  145. var textSource = new SingleBufferTextSource(text, defaultProperties);
  146. var formatter = new TextFormatterImpl();
  147. var textLine =
  148. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  149. new GenericTextParagraphProperties(defaultProperties));
  150. Assert.NotNull(textLine);
  151. Assert.Equal(numberOfRuns, textLine.TextRuns.Count);
  152. }
  153. }
  154. [Fact]
  155. public void Should_Produce_A_Single_Fallback_Run()
  156. {
  157. using (Start())
  158. {
  159. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  160. const string text = "👍 👍 👍 👍";
  161. var textSource = new SingleBufferTextSource(text, defaultProperties);
  162. var formatter = new TextFormatterImpl();
  163. var textLine =
  164. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  165. new GenericTextParagraphProperties(defaultProperties));
  166. Assert.NotNull(textLine);
  167. Assert.Equal(1, textLine.TextRuns.Count);
  168. }
  169. }
  170. [Fact]
  171. public void Should_Split_Run_On_Script()
  172. {
  173. using (Start())
  174. {
  175. const string text = "ABCDالدولي";
  176. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  177. var textSource = new SingleBufferTextSource(text, defaultProperties);
  178. var formatter = new TextFormatterImpl();
  179. var textLine =
  180. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  181. new GenericTextParagraphProperties(defaultProperties));
  182. Assert.NotNull(textLine);
  183. var firstRun = textLine.TextRuns[0];
  184. Assert.Equal(4, firstRun.Length);
  185. }
  186. }
  187. [InlineData("𐐷𐐷𐐷𐐷𐐷", 10, 1)]
  188. [InlineData("01234 56789 01234 56789", 6, 4)]
  189. [Theory]
  190. public void Should_Wrap_With_Overflow(string text, int expectedCharactersPerLine, int expectedNumberOfLines)
  191. {
  192. using (Start())
  193. {
  194. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  195. var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.WrapWithOverflow);
  196. var textSource = new SingleBufferTextSource("ABCDEFHFFHFJHKHFK", defaultProperties, true);
  197. var formatter = new TextFormatterImpl();
  198. formatter.FormatLine(textSource, 0, 33, paragraphProperties);
  199. textSource = new SingleBufferTextSource(text, defaultProperties);
  200. var numberOfLines = 0;
  201. var currentPosition = 0;
  202. while (currentPosition < text.Length)
  203. {
  204. var textLine =
  205. formatter.FormatLine(textSource, currentPosition, 1, paragraphProperties);
  206. Assert.NotNull(textLine);
  207. if (text.Length - currentPosition > expectedCharactersPerLine)
  208. {
  209. Assert.Equal(expectedCharactersPerLine, textLine.Length);
  210. }
  211. currentPosition += textLine.Length;
  212. numberOfLines++;
  213. }
  214. Assert.Equal(expectedNumberOfLines, numberOfLines);
  215. }
  216. }
  217. [InlineData("Whether to turn off HTTPS. This option only applies if Individual, " +
  218. "IndividualB2C, SingleOrg, or MultiOrg aren't used for &#8209;&#8209;auth."
  219. , "Noto Sans", 40)]
  220. [InlineData("01234 56789 01234 56789", "Noto Mono", 7)]
  221. [Theory]
  222. public void Should_Wrap(string text, string familyName, int numberOfCharactersPerLine)
  223. {
  224. using (Start())
  225. {
  226. var lineBreaker = new LineBreakEnumerator(text);
  227. var expected = new List<int>();
  228. while (lineBreaker.MoveNext(out var lineBreak))
  229. {
  230. expected.Add(lineBreak.PositionWrap - 1);
  231. }
  232. var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#" +
  233. familyName);
  234. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  235. var textSource = new SingleBufferTextSource(text, defaultProperties);
  236. var formatter = new TextFormatterImpl();
  237. var glyph = typeface.GlyphTypeface.GetGlyph('a');
  238. var advance = typeface.GlyphTypeface.GetGlyphAdvance(glyph) *
  239. (12.0 / typeface.GlyphTypeface.Metrics.DesignEmHeight);
  240. var paragraphWidth = advance * numberOfCharactersPerLine;
  241. var currentPosition = 0;
  242. while (currentPosition < text.Length)
  243. {
  244. var textLine =
  245. formatter.FormatLine(textSource, currentPosition, paragraphWidth,
  246. new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap));
  247. Assert.NotNull(textLine);
  248. var end = textLine.FirstTextSourceIndex + textLine.Length - 1;
  249. Assert.True(expected.Contains(end));
  250. var index = expected.IndexOf(end);
  251. for (var i = 0; i <= index; i++)
  252. {
  253. expected.RemoveAt(0);
  254. }
  255. currentPosition += textLine.Length;
  256. }
  257. }
  258. }
  259. [Fact]
  260. public void Should_Produce_Fixed_Height_Lines()
  261. {
  262. using (Start())
  263. {
  264. const string text = "012345";
  265. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  266. var textSource = new SingleBufferTextSource(text, defaultProperties);
  267. var formatter = new TextFormatterImpl();
  268. var textLine =
  269. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  270. new GenericTextParagraphProperties(defaultProperties, lineHeight: 50));
  271. Assert.NotNull(textLine);
  272. Assert.Equal(50, textLine.Height);
  273. }
  274. }
  275. [Fact]
  276. public void Should_Not_Produce_TextLine_Wider_Than_ParagraphWidth()
  277. {
  278. using (Start())
  279. {
  280. const string text =
  281. "Multiline TextBlock with TextWrapping.\r\rLorem ipsum dolor sit amet, consectetur adipiscing elit. " +
  282. "Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. " +
  283. "Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. " +
  284. "Vivamus pretium ornare est.";
  285. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  286. var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
  287. var textSource = new SingleBufferTextSource(text, defaultProperties);
  288. var formatter = new TextFormatterImpl();
  289. var textSourceIndex = 0;
  290. while (textSourceIndex < text.Length)
  291. {
  292. var textLine =
  293. formatter.FormatLine(textSource, textSourceIndex, 200, paragraphProperties);
  294. Assert.NotNull(textLine);
  295. Assert.True(textLine.Width <= 200);
  296. textSourceIndex += textLine.Length;
  297. }
  298. }
  299. }
  300. [Fact]
  301. public void Wrap_Should_Not_Produce_Empty_Lines()
  302. {
  303. using (Start())
  304. {
  305. const string text = "012345";
  306. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  307. var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
  308. var textSource = new SingleBufferTextSource(text, defaultProperties);
  309. var formatter = new TextFormatterImpl();
  310. var textSourceIndex = 0;
  311. while (textSourceIndex < text.Length)
  312. {
  313. var textLine =
  314. formatter.FormatLine(textSource, textSourceIndex, 3, paragraphProperties);
  315. Assert.NotNull(textLine);
  316. Assert.NotEqual(0, textLine.Length);
  317. textSourceIndex += textLine.Length;
  318. }
  319. Assert.Equal(text.Length, textSourceIndex);
  320. }
  321. }
  322. [InlineData("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor",
  323. new[] { "Lorem ipsum ", "dolor sit amet, ", "consectetur ", "adipisicing ", "elit, sed do ", "eiusmod tempor" })]
  324. [Theory]
  325. public void Should_Produce_Wrapped_And_Trimmed_Lines(string text, string[] expectedLines)
  326. {
  327. using (Start())
  328. {
  329. var typeface = Typeface.Default;
  330. var defaultProperties = new GenericTextRunProperties(typeface, 32, foregroundBrush: Brushes.Black);
  331. var styleSpans = new[]
  332. {
  333. new ValueSpan<TextRunProperties>(0, 5,
  334. new GenericTextRunProperties(typeface, 48)),
  335. new ValueSpan<TextRunProperties>(6, 11,
  336. new GenericTextRunProperties(new Typeface(FontFamily.Default, weight: FontWeight.Bold), 32)),
  337. new ValueSpan<TextRunProperties>(28, 28,
  338. new GenericTextRunProperties(new Typeface(FontFamily.Default, FontStyle.Italic),32))
  339. };
  340. var textSource = new FormattedTextSource(text, defaultProperties, styleSpans);
  341. var formatter = new TextFormatterImpl();
  342. var currentPosition = 0;
  343. var currentHeight = 0d;
  344. var currentLineIndex = 0;
  345. while (currentPosition < text.Length && currentLineIndex < expectedLines.Length)
  346. {
  347. var textLine =
  348. formatter.FormatLine(textSource, currentPosition, 300,
  349. new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.WrapWithOverflow));
  350. Assert.NotNull(textLine);
  351. currentPosition += textLine.Length;
  352. if (textLine.Width > 300 || currentHeight + textLine.Height > 240)
  353. {
  354. textLine = textLine.Collapse(new TextTrailingWordEllipsis(TextTrimming.DefaultEllipsisChar, 300, defaultProperties, FlowDirection.LeftToRight));
  355. }
  356. currentHeight += textLine.Height;
  357. var currentText = text.Substring(textLine.FirstTextSourceIndex, textLine.Length);
  358. Assert.Equal(expectedLines[currentLineIndex], currentText);
  359. currentLineIndex++;
  360. }
  361. Assert.Equal(expectedLines.Length, currentLineIndex);
  362. }
  363. }
  364. [InlineData("0123456789", TextAlignment.Left, FlowDirection.LeftToRight)]
  365. [InlineData("0123456789", TextAlignment.Center, FlowDirection.LeftToRight)]
  366. [InlineData("0123456789", TextAlignment.Right, FlowDirection.LeftToRight)]
  367. [InlineData("0123456789", TextAlignment.Left, FlowDirection.RightToLeft)]
  368. [InlineData("0123456789", TextAlignment.Center, FlowDirection.RightToLeft)]
  369. [InlineData("0123456789", TextAlignment.Right, FlowDirection.RightToLeft)]
  370. [InlineData("שנבגק", TextAlignment.Left, FlowDirection.RightToLeft)]
  371. [InlineData("שנבגק", TextAlignment.Center, FlowDirection.RightToLeft)]
  372. [InlineData("שנבגק", TextAlignment.Right, FlowDirection.RightToLeft)]
  373. [Theory]
  374. public void Should_Align_TextLine(string text, TextAlignment textAlignment, FlowDirection flowDirection)
  375. {
  376. using (Start())
  377. {
  378. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  379. var paragraphProperties = new GenericTextParagraphProperties(flowDirection, textAlignment, true, true,
  380. defaultProperties, TextWrapping.NoWrap, 0, 0, 0);
  381. var textSource = new SingleBufferTextSource(text, defaultProperties);
  382. var formatter = new TextFormatterImpl();
  383. var textLine =
  384. formatter.FormatLine(textSource, 0, 100, paragraphProperties);
  385. Assert.NotNull(textLine);
  386. var expectedOffset = 0d;
  387. switch (textAlignment)
  388. {
  389. case TextAlignment.Center:
  390. expectedOffset = 50 - textLine.Width / 2;
  391. break;
  392. case TextAlignment.Right:
  393. expectedOffset = 100 - textLine.WidthIncludingTrailingWhitespace;
  394. break;
  395. }
  396. Assert.Equal(expectedOffset, textLine.Start);
  397. }
  398. }
  399. [Fact]
  400. public void Should_Wrap_Syriac()
  401. {
  402. using (Start())
  403. {
  404. const string text =
  405. "܀ ܁ ܂ ܃ ܄ ܅ ܆ ܇ ܈ ܉ ܊ ܋ ܌ ܍ ܏ ܐ ܑ ܒ ܓ ܔ ܕ ܖ ܗ ܘ ܙ ܚ ܛ ܜ ܝ ܞ ܟ ܠ ܡ ܢ ܣ ܤ ܥ ܦ ܧ ܨ ܩ ܪ ܫ ܬ ܰ ܱ ܲ ܳ ܴ ܵ ܶ ܷ ܸ ܹ ܺ ܻ ܼ ܽ ܾ ܿ ݀ ݁ ݂ ݃ ݄ ݅ ݆ ݇ ݈ ݉ ݊";
  406. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  407. var paragraphProperties =
  408. new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
  409. var textSource = new SingleBufferTextSource(text, defaultProperties);
  410. var formatter = new TextFormatterImpl();
  411. var textPosition = 87;
  412. TextLineBreak? lastBreak = null;
  413. while (textPosition < text.Length)
  414. {
  415. var textLine =
  416. formatter.FormatLine(textSource, textPosition, 50, paragraphProperties, lastBreak);
  417. Assert.NotNull(textLine);
  418. Assert.Equal(textLine.Length, textLine.TextRuns.Sum(x => x.Length));
  419. textPosition += textLine.Length;
  420. lastBreak = textLine.TextLineBreak;
  421. }
  422. }
  423. }
  424. [Fact]
  425. public void Should_FormatLine_With_Emergency_Breaks()
  426. {
  427. using (Start())
  428. {
  429. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  430. var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
  431. var textSource = new SingleBufferTextSource("0123456789_0123456789_0123456789_0123456789", defaultProperties);
  432. var formatter = new TextFormatterImpl();
  433. var textLine =
  434. formatter.FormatLine(textSource, 0, 33, paragraphProperties);
  435. Assert.NotNull(textLine);
  436. var remainingRunsLineBreak = Assert.IsType<WrappingTextLineBreak>(textLine.TextLineBreak);
  437. var remainingRuns = remainingRunsLineBreak.AcquireRemainingRuns();
  438. Assert.NotNull(remainingRuns);
  439. Assert.NotEmpty(remainingRuns);
  440. }
  441. }
  442. [InlineData("פעילות הבינאום, W3C!")]
  443. [InlineData("abcABC")]
  444. [InlineData("זה כיף סתם לשמוע איך תנצח קרפד עץ טוב בגן")]
  445. [InlineData("טטטט abcDEF טטטט")]
  446. [Theory]
  447. public void Should_Not_Alter_TextRuns_After_TextStyles_Were_Applied(string text)
  448. {
  449. using (Start())
  450. {
  451. var formatter = new TextFormatterImpl();
  452. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  453. var paragraphProperties =
  454. new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.NoWrap);
  455. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  456. var expectedTextLine = formatter.FormatLine(new SingleBufferTextSource(text, defaultProperties),
  457. 0, double.PositiveInfinity, paragraphProperties);
  458. Assert.NotNull(expectedTextLine);
  459. var expectedRuns = expectedTextLine.TextRuns.Cast<ShapedTextRun>().ToList();
  460. var expectedGlyphs = expectedRuns
  461. .SelectMany(run => run.GlyphRun.GlyphInfos, (_, glyph) => glyph.GlyphIndex)
  462. .ToList();
  463. for (var i = 0; i < text.Length; i++)
  464. {
  465. for (var j = 1; i + j < text.Length; j++)
  466. {
  467. var spans = new[]
  468. {
  469. new ValueSpan<TextRunProperties>(i, j,
  470. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
  471. };
  472. var textSource = new FormattedTextSource(text, defaultProperties, spans);
  473. var textLine =
  474. formatter.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties);
  475. Assert.NotNull(textLine);
  476. var shapedRuns = textLine.TextRuns.Cast<ShapedTextRun>().ToList();
  477. var actualGlyphs = shapedRuns
  478. .SelectMany(x => x.GlyphRun.GlyphInfos, (_, glyph) => glyph.GlyphIndex)
  479. .ToList();
  480. Assert.Equal(expectedGlyphs, actualGlyphs);
  481. }
  482. }
  483. }
  484. }
  485. [Fact]
  486. public void Should_FormatLine_With_DrawableRuns()
  487. {
  488. var defaultRunProperties = new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black);
  489. var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties);
  490. var textSource = new CustomTextSource("Hello World ->");
  491. using (Start())
  492. {
  493. var textLine =
  494. TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties);
  495. Assert.NotNull(textLine);
  496. Assert.Equal(3, textLine.TextRuns.Count);
  497. Assert.True(textLine.TextRuns[1] is RectangleRun);
  498. }
  499. }
  500. [Fact]
  501. public void Should_Format_With_EndOfLineRun()
  502. {
  503. using (Start())
  504. {
  505. var defaultRunProperties = new GenericTextRunProperties(Typeface.Default);
  506. var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties);
  507. var textSource = new EndOfLineTextSource();
  508. var textLine =
  509. TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties);
  510. Assert.NotNull(textLine);
  511. Assert.NotNull(textLine.TextLineBreak);
  512. Assert.Equal(TextRun.DefaultTextSourceLength, textLine.Length);
  513. }
  514. }
  515. [Fact]
  516. public void Should_Return_Null_For_Empty_TextSource()
  517. {
  518. using (Start())
  519. {
  520. var defaultRunProperties = new GenericTextRunProperties(Typeface.Default);
  521. var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties);
  522. var textSource = new EmptyTextSource();
  523. var textLine = TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties);
  524. Assert.Null(textLine);
  525. }
  526. }
  527. [Fact]
  528. public void Should_Retain_TextEndOfParagraph_With_TextWrapping()
  529. {
  530. using (Start())
  531. {
  532. var defaultRunProperties = new GenericTextRunProperties(Typeface.Default);
  533. var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrap: TextWrapping.Wrap);
  534. var text = "Hello World";
  535. var textSource = new SimpleTextSource(text, defaultRunProperties);
  536. var pos = 0;
  537. TextLineBreak? previousLineBreak = null;
  538. TextLine? textLine = null;
  539. while (pos < text.Length)
  540. {
  541. textLine = TextFormatter.Current.FormatLine(textSource, pos, 30, paragraphProperties, previousLineBreak);
  542. Assert.NotNull(textLine);
  543. pos += textLine.Length;
  544. previousLineBreak = textLine.TextLineBreak;
  545. }
  546. Assert.NotNull(textLine);
  547. Assert.NotNull(textLine.TextLineBreak);
  548. Assert.NotNull(textLine.TextLineBreak.TextEndOfLine);
  549. }
  550. }
  551. [Fact]
  552. public void Should_HitTestStringWithInvisibleRuns()
  553. {
  554. var defaultRunProperties = new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black);
  555. var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties);
  556. //var textSource = new ListTextSource(
  557. using (Start())
  558. {
  559. var hello = new TextCharacters("Hello",
  560. new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black));
  561. var world = new TextCharacters("world",
  562. new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Red));
  563. var source = new ListTextSource(new InvisibleRun(1), hello, new InvisibleRun(1), world);
  564. var textLine =
  565. TextFormatter.Current.FormatLine(source, 0, double.PositiveInfinity, paragraphProperties);
  566. Assert.NotNull(textLine);
  567. void VerifyHit(int offset)
  568. {
  569. var glyphCenter = textLine.GetTextBounds(offset, 1)[0].Rectangle.Center;
  570. var hit = textLine.GetCharacterHitFromDistance(glyphCenter.X);
  571. Assert.Equal(offset, hit.FirstCharacterIndex);
  572. }
  573. VerifyHit(3);
  574. VerifyHit(8);
  575. }
  576. }
  577. [Fact]
  578. public void GetTextBounds_For_TextLine_With_ZeroWidthSpaces_Does_Not_Freeze()
  579. {
  580. var defaultRunProperties = new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black);
  581. var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties);
  582. using (Start())
  583. {
  584. var text = new TextCharacters("\u200B\u200B",
  585. new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black));
  586. var source = new ListTextSource(text, new InvisibleRun(1), new TextEndOfParagraph());
  587. var textLine =
  588. TextFormatter.Current.FormatLine(source, 0, double.PositiveInfinity, paragraphProperties);
  589. Assert.NotNull(textLine);
  590. var bounds = textLine.GetTextBounds(0, 3);
  591. Assert.Equal(1, bounds.Count);
  592. var runBounds = bounds[0].TextRunBounds;
  593. Assert.Equal(2, runBounds.Count);
  594. }
  595. }
  596. [Theory]
  597. [InlineData(TextWrapping.NoWrap),InlineData(TextWrapping.Wrap),InlineData(TextWrapping.WrapWithOverflow)]
  598. public void Line_Formatting_For_Oversized_Embedded_Runs_Does_Not_Produce_Empty_Lines(TextWrapping wrapping)
  599. {
  600. var defaultRunProperties = new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black);
  601. var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties,
  602. textWrap: wrapping);
  603. using (Start())
  604. {
  605. var source = new ListTextSource(new RectangleRun(new Rect(0, 0, 200, 10), Brushes.Aqua));
  606. var textLine = TextFormatter.Current.FormatLine(source, 0, 100, paragraphProperties);
  607. Assert.NotNull(textLine);
  608. Assert.Equal(200d, textLine.WidthIncludingTrailingWhitespace);
  609. }
  610. }
  611. [Theory]
  612. [InlineData(TextWrapping.NoWrap),InlineData(TextWrapping.Wrap),InlineData(TextWrapping.WrapWithOverflow)]
  613. public void Line_Formatting_For_Oversized_Embedded_Runs_Inside_Normal_Text_Does_Not_Produce_Empty_Lines(
  614. TextWrapping wrapping)
  615. {
  616. var defaultRunProperties = new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black);
  617. var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties,
  618. textWrap: wrapping);
  619. using (Start())
  620. {
  621. var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#DejaVu Sans"));
  622. var text1 = new TextCharacters("Hello",
  623. new GenericTextRunProperties(typeface, foregroundBrush: Brushes.Black));
  624. var text2 = new TextCharacters("world",
  625. new GenericTextRunProperties(typeface, foregroundBrush: Brushes.Black));
  626. var source = new ListTextSource(
  627. text1,
  628. new RectangleRun(new Rect(0, 0, 200, 10), Brushes.Aqua),
  629. new InvisibleRun(1),
  630. new TextEndOfLine(),
  631. text2,
  632. new TextEndOfParagraph(1));
  633. var lines = new List<TextLine>();
  634. var dcp = 0;
  635. for (var c = 0;; c++)
  636. {
  637. Assert.True(c < 1000, "Infinite loop");
  638. var textLine = TextFormatter.Current.FormatLine(source, dcp, 30, paragraphProperties);
  639. Assert.NotNull(textLine);
  640. lines.Add(textLine);
  641. dcp += textLine.Length;
  642. if (textLine.TextLineBreak is {} eol && eol.TextEndOfLine is TextEndOfParagraph)
  643. break;
  644. }
  645. Assert.NotEmpty(lines);
  646. }
  647. }
  648. class IncrementalTabProperties : TextParagraphProperties
  649. {
  650. public IncrementalTabProperties(TextRunProperties defaultTextRunProperties)
  651. {
  652. DefaultTextRunProperties = defaultTextRunProperties;
  653. }
  654. public override FlowDirection FlowDirection => default;
  655. public override TextAlignment TextAlignment => default;
  656. public override double LineHeight => default;
  657. public override bool FirstLineInParagraph => default;
  658. public override TextRunProperties DefaultTextRunProperties { get; }
  659. public override TextWrapping TextWrapping => default;
  660. public override double Indent => default;
  661. public override double DefaultIncrementalTab => 64;
  662. }
  663. [Fact]
  664. public void Line_With_IncrementalTab_Should_Return_Correct_Backspace_Position()
  665. {
  666. using (Start())
  667. {
  668. var typeface = new Typeface(FontFamily.Parse("resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#DejaVu Sans"));
  669. var defaultRunProperties = new GenericTextRunProperties(typeface, foregroundBrush: Brushes.Black);
  670. var paragraphProperties = new IncrementalTabProperties(defaultRunProperties);
  671. var text = new TextCharacters("ff",
  672. new GenericTextRunProperties(typeface, foregroundBrush: Brushes.Black));
  673. var source = new ListTextSource(text);
  674. var textLine = TextFormatter.Current.FormatLine(source, 0, double.PositiveInfinity, paragraphProperties);
  675. Assert.NotNull(textLine);
  676. var backspaceHit = textLine.GetBackspaceCaretCharacterHit(new CharacterHit(2));
  677. Assert.Equal(1, backspaceHit.FirstCharacterIndex);
  678. Assert.Equal(0, backspaceHit.TrailingLength);
  679. }
  680. }
  681. [Fact]
  682. public void Should_Wrap_Chinese()
  683. {
  684. using (Start())
  685. {
  686. var defaultRunProperties = new GenericTextRunProperties(Typeface.Default);
  687. var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrap: TextWrapping.Wrap);
  688. var text = "一二三四 TEXT 一二三四五六七八九十零";
  689. var textLine = TextFormatter.Current.FormatLine(new SimpleTextSource(text, defaultRunProperties), 0, 120, paragraphProperties);
  690. Assert.NotNull(textLine);
  691. Assert.Equal(3, textLine.TextRuns.Count);
  692. }
  693. }
  694. protected readonly record struct SimpleTextSource : ITextSource
  695. {
  696. private readonly string _text;
  697. private readonly TextRunProperties _defaultProperties;
  698. public SimpleTextSource(string text, TextRunProperties defaultProperties)
  699. {
  700. _text = text;
  701. _defaultProperties = defaultProperties;
  702. }
  703. public TextRun? GetTextRun(int textSourceIndex)
  704. {
  705. if (textSourceIndex > _text.Length)
  706. {
  707. return new TextEndOfParagraph();
  708. }
  709. var runText = _text.AsMemory(textSourceIndex);
  710. if (runText.IsEmpty)
  711. {
  712. return new TextEndOfParagraph();
  713. }
  714. return new TextCharacters(runText, _defaultProperties);
  715. }
  716. }
  717. private class EmptyTextSource : ITextSource
  718. {
  719. public TextRun? GetTextRun(int textSourceIndex)
  720. {
  721. return null;
  722. }
  723. }
  724. private class EndOfLineTextSource : ITextSource
  725. {
  726. public TextRun GetTextRun(int textSourceIndex)
  727. {
  728. return new TextEndOfLine();
  729. }
  730. }
  731. private class CustomTextSource : ITextSource
  732. {
  733. private readonly string _text;
  734. public CustomTextSource(string text)
  735. {
  736. _text = text;
  737. }
  738. public TextRun? GetTextRun(int textSourceIndex)
  739. {
  740. if (textSourceIndex >= _text.Length + TextRun.DefaultTextSourceLength + _text.Length)
  741. {
  742. return null;
  743. }
  744. if (textSourceIndex == _text.Length)
  745. {
  746. return new RectangleRun(new Rect(0, 0, 50, 50), Brushes.Green);
  747. }
  748. return new TextCharacters(_text, new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black));
  749. }
  750. }
  751. private class ListTextSource : ITextSource
  752. {
  753. private readonly List<TextRun> _runs;
  754. public ListTextSource(params TextRun[] runs) : this((IEnumerable<TextRun>)runs)
  755. {
  756. }
  757. public ListTextSource(IEnumerable<TextRun> runs)
  758. {
  759. _runs = runs.ToList();
  760. }
  761. public TextRun? GetTextRun(int textSourceIndex)
  762. {
  763. var off = 0;
  764. for (var c = 0; c < _runs.Count; c++)
  765. {
  766. var run = _runs[c];
  767. if (textSourceIndex >= off && textSourceIndex - off < run.Length)
  768. {
  769. if (run.Length == 1)
  770. return run;
  771. var chars = ((TextCharacters)run);
  772. return new TextCharacters(chars.Text.Slice(textSourceIndex - off), chars.Properties);
  773. }
  774. off += run.Length;
  775. }
  776. return null;
  777. }
  778. }
  779. private class RectangleRun : DrawableTextRun
  780. {
  781. private readonly Rect _rect;
  782. private readonly IBrush _fill;
  783. public RectangleRun(Rect rect, IBrush fill)
  784. {
  785. _rect = rect;
  786. _fill = fill;
  787. }
  788. public override Size Size => _rect.Size;
  789. public override double Baseline => 0;
  790. public override void Draw(DrawingContext drawingContext, Point origin)
  791. {
  792. using (drawingContext.PushTransform(Matrix.CreateTranslation(new Vector(origin.X, 0))))
  793. {
  794. drawingContext.FillRectangle(_fill, _rect);
  795. }
  796. }
  797. }
  798. private class InvisibleRun : TextRun
  799. {
  800. public InvisibleRun(int length)
  801. {
  802. Length = length;
  803. }
  804. public override int Length { get; }
  805. }
  806. public static IDisposable Start()
  807. {
  808. var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
  809. .With(renderInterface: new PlatformRenderInterface(),
  810. textShaperImpl: new TextShaperImpl()));
  811. AvaloniaLocator.CurrentMutable
  812. .Bind<FontManager>().ToConstant(new FontManager(new CustomFontManagerImpl()));
  813. return disposable;
  814. }
  815. }
  816. }