TextFormatterTests.cs 46 KB

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