TextLayoutTests.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. using System;
  2. using System.Linq;
  3. using Avalonia.Media;
  4. using Avalonia.Media.TextFormatting;
  5. using Avalonia.Media.TextFormatting.Unicode;
  6. using Avalonia.UnitTests;
  7. using Avalonia.Utilities;
  8. using Xunit;
  9. namespace Avalonia.Skia.UnitTests.Media.TextFormatting
  10. {
  11. public class TextLayoutTests
  12. {
  13. private static readonly string s_singleLineText = "0123456789";
  14. private static readonly string s_multiLineText = "012345678\r\r0123456789";
  15. [InlineData("01234\r01234\r", 3)]
  16. [InlineData("01234\r01234", 2)]
  17. [Theory]
  18. public void Should_Break_Lines(string text, int numberOfLines)
  19. {
  20. using (Start())
  21. {
  22. var layout = new TextLayout(
  23. text,
  24. Typeface.Default,
  25. 12.0f,
  26. Brushes.Black);
  27. Assert.Equal(numberOfLines, layout.TextLines.Count);
  28. }
  29. }
  30. [Fact]
  31. public void Should_Apply_TextStyleSpan_To_Text_In_Between()
  32. {
  33. using (Start())
  34. {
  35. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  36. var spans = new[]
  37. {
  38. new ValueSpan<TextRunProperties>(1, 2,
  39. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
  40. };
  41. var layout = new TextLayout(
  42. s_multiLineText,
  43. Typeface.Default,
  44. 12.0f,
  45. Brushes.Black.ToImmutable(),
  46. textStyleOverrides: spans);
  47. var textLine = layout.TextLines[0];
  48. Assert.Equal(3, textLine.TextRuns.Count);
  49. var textRun = textLine.TextRuns[1];
  50. Assert.Equal(2, textRun.Text.Length);
  51. var actual = textRun.Text.Buffer.Span.ToString();
  52. Assert.Equal("12", actual);
  53. Assert.Equal(foreground, textRun.Properties.ForegroundBrush);
  54. }
  55. }
  56. [Fact]
  57. public void Should_Not_Alter_Lines_After_TextStyleSpan_Was_Applied()
  58. {
  59. using (Start())
  60. {
  61. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  62. for (var i = 4; i < s_multiLineText.Length; i++)
  63. {
  64. var spans = new[]
  65. {
  66. new ValueSpan<TextRunProperties>(0, i,
  67. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
  68. };
  69. var expected = new TextLayout(
  70. s_multiLineText,
  71. Typeface.Default,
  72. 12.0f,
  73. Brushes.Black.ToImmutable(),
  74. textWrapping: TextWrapping.Wrap,
  75. maxWidth: 25);
  76. var actual = new TextLayout(
  77. s_multiLineText,
  78. Typeface.Default,
  79. 12.0f,
  80. Brushes.Black.ToImmutable(),
  81. textWrapping: TextWrapping.Wrap,
  82. maxWidth: 25,
  83. textStyleOverrides: spans);
  84. Assert.Equal(expected.TextLines.Count, actual.TextLines.Count);
  85. for (var j = 0; j < actual.TextLines.Count; j++)
  86. {
  87. Assert.Equal(expected.TextLines[j].TextRange.Length, actual.TextLines[j].TextRange.Length);
  88. Assert.Equal(expected.TextLines[j].TextRuns.Sum(x => x.Text.Length),
  89. actual.TextLines[j].TextRuns.Sum(x => x.Text.Length));
  90. }
  91. }
  92. }
  93. }
  94. [Fact]
  95. public void Should_Apply_TextStyleSpan_To_Text_At_Start()
  96. {
  97. using (Start())
  98. {
  99. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  100. var spans = new[]
  101. {
  102. new ValueSpan<TextRunProperties>(0, 2,
  103. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
  104. };
  105. var layout = new TextLayout(
  106. s_singleLineText,
  107. Typeface.Default,
  108. 12.0f,
  109. Brushes.Black.ToImmutable(),
  110. textStyleOverrides: spans);
  111. var textLine = layout.TextLines[0];
  112. Assert.Equal(2, textLine.TextRuns.Count);
  113. var textRun = textLine.TextRuns[0];
  114. Assert.Equal(2, textRun.Text.Length);
  115. var actual = s_singleLineText.Substring(textRun.Text.Start,
  116. textRun.Text.Length);
  117. Assert.Equal("01", actual);
  118. Assert.Equal(foreground, textRun.Properties.ForegroundBrush);
  119. }
  120. }
  121. [Fact]
  122. public void Should_Apply_TextStyleSpan_To_Text_At_End()
  123. {
  124. using (Start())
  125. {
  126. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  127. var spans = new[]
  128. {
  129. new ValueSpan<TextRunProperties>(8, 2,
  130. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)),
  131. };
  132. var layout = new TextLayout(
  133. s_singleLineText,
  134. Typeface.Default,
  135. 12.0f,
  136. Brushes.Black.ToImmutable(),
  137. textStyleOverrides: spans);
  138. var textLine = layout.TextLines[0];
  139. Assert.Equal(2, textLine.TextRuns.Count);
  140. var textRun = textLine.TextRuns[1];
  141. Assert.Equal(2, textRun.Text.Length);
  142. var actual = textRun.Text.Buffer.Span.ToString();
  143. Assert.Equal("89", actual);
  144. Assert.Equal(foreground, textRun.Properties.ForegroundBrush);
  145. }
  146. }
  147. [Fact]
  148. public void Should_Apply_TextStyleSpan_To_Single_Character()
  149. {
  150. using (Start())
  151. {
  152. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  153. var spans = new[]
  154. {
  155. new ValueSpan<TextRunProperties>(0, 1,
  156. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
  157. };
  158. var layout = new TextLayout(
  159. "0",
  160. Typeface.Default,
  161. 12.0f,
  162. Brushes.Black.ToImmutable(),
  163. textStyleOverrides: spans);
  164. var textLine = layout.TextLines[0];
  165. Assert.Equal(1, textLine.TextRuns.Count);
  166. var textRun = textLine.TextRuns[0];
  167. Assert.Equal(1, textRun.Text.Length);
  168. Assert.Equal(foreground, textRun.Properties.ForegroundBrush);
  169. }
  170. }
  171. [Fact]
  172. public void Should_Apply_TextSpan_To_Unicode_String_In_Between()
  173. {
  174. using (Start())
  175. {
  176. const string text = "😄😄😄😄";
  177. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  178. var spans = new[]
  179. {
  180. new ValueSpan<TextRunProperties>(2, 2,
  181. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
  182. };
  183. var layout = new TextLayout(
  184. text,
  185. Typeface.Default,
  186. 12.0f,
  187. Brushes.Black.ToImmutable(),
  188. textStyleOverrides: spans);
  189. var textLine = layout.TextLines[0];
  190. Assert.Equal(3, textLine.TextRuns.Count);
  191. var textRun = textLine.TextRuns[1];
  192. Assert.Equal(2, textRun.Text.Length);
  193. var actual = textRun.Text.Buffer.Span.ToString();
  194. Assert.Equal("😄", actual);
  195. Assert.Equal(foreground, textRun.Properties.ForegroundBrush);
  196. }
  197. }
  198. [Fact]
  199. public void TextLength_Should_Be_Equal_To_TextLine_Length_Sum()
  200. {
  201. using (Start())
  202. {
  203. var layout = new TextLayout(
  204. s_multiLineText,
  205. Typeface.Default,
  206. 12.0f,
  207. Brushes.Black.ToImmutable());
  208. Assert.Equal(s_multiLineText.Length, layout.TextLines.Sum(x => x.TextRange.Length));
  209. }
  210. }
  211. [Fact]
  212. public void TextLength_Should_Be_Equal_To_TextRun_TextLength_Sum()
  213. {
  214. using (Start())
  215. {
  216. var layout = new TextLayout(
  217. s_multiLineText,
  218. Typeface.Default,
  219. 12.0f,
  220. Brushes.Black.ToImmutable());
  221. Assert.Equal(
  222. s_multiLineText.Length,
  223. layout.TextLines.Select(textLine =>
  224. textLine.TextRuns.Sum(textRun => textRun.Text.Length))
  225. .Sum());
  226. }
  227. }
  228. [Fact]
  229. public void TextLength_Should_Be_Equal_To_TextRun_TextLength_Sum_After_Wrap_With_Style_Applied()
  230. {
  231. using (Start())
  232. {
  233. const string text =
  234. "Multiline TextBox with TextWrapping.\r\rLorem ipsum dolor sit amet, consectetur adipiscing elit. " +
  235. "Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. " +
  236. "Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.";
  237. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  238. var spans = new[]
  239. {
  240. new ValueSpan<TextRunProperties>(0, 24,
  241. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
  242. };
  243. var layout = new TextLayout(
  244. text,
  245. Typeface.Default,
  246. 12.0f,
  247. Brushes.Black.ToImmutable(),
  248. textWrapping: TextWrapping.Wrap,
  249. maxWidth: 180,
  250. textStyleOverrides: spans);
  251. Assert.Equal(
  252. text.Length,
  253. layout.TextLines.Select(textLine =>
  254. textLine.TextRuns.Sum(textRun => textRun.Text.Length))
  255. .Sum());
  256. }
  257. }
  258. [Fact]
  259. public void Should_Apply_TextStyleSpan_To_MultiLine()
  260. {
  261. using (Start())
  262. {
  263. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  264. var spans = new[]
  265. {
  266. new ValueSpan<TextRunProperties>(5, 20,
  267. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
  268. };
  269. var layout = new TextLayout(
  270. s_multiLineText,
  271. Typeface.Default,
  272. 12.0f,
  273. Brushes.Black.ToImmutable(),
  274. maxWidth: 200,
  275. maxHeight: 125,
  276. textStyleOverrides: spans);
  277. Assert.Equal(foreground, layout.TextLines[0].TextRuns[1].Properties.ForegroundBrush);
  278. Assert.Equal(foreground, layout.TextLines[1].TextRuns[0].Properties.ForegroundBrush);
  279. Assert.Equal(foreground, layout.TextLines[2].TextRuns[0].Properties.ForegroundBrush);
  280. }
  281. }
  282. [Fact]
  283. public void Should_Hit_Test_SurrogatePair()
  284. {
  285. using (Start())
  286. {
  287. const string text = "😄😄";
  288. var layout = new TextLayout(
  289. text,
  290. Typeface.Default,
  291. 12.0f,
  292. Brushes.Black.ToImmutable());
  293. var shapedRun = (ShapedTextCharacters)layout.TextLines[0].TextRuns[0];
  294. var glyphRun = shapedRun.GlyphRun;
  295. var width = glyphRun.Size.Width;
  296. var characterHit = glyphRun.GetCharacterHitFromDistance(width, out _);
  297. Assert.Equal(2, characterHit.FirstCharacterIndex);
  298. Assert.Equal(2, characterHit.TrailingLength);
  299. }
  300. }
  301. [Theory]
  302. [InlineData("☝🏿", new ushort[] { 0 })]
  303. [InlineData("☝🏿 ab", new ushort[] { 0, 3, 4, 5 })]
  304. [InlineData("ab ☝🏿", new ushort[] { 0, 1, 2, 3 })]
  305. public void Should_Create_Valid_Clusters_For_Text(string text, ushort[] clusters)
  306. {
  307. using (Start())
  308. {
  309. var layout = new TextLayout(
  310. text,
  311. Typeface.Default,
  312. 12.0f,
  313. Brushes.Black.ToImmutable());
  314. var textLine = layout.TextLines[0];
  315. var index = 0;
  316. foreach (var textRun in textLine.TextRuns)
  317. {
  318. var shapedRun = (ShapedTextCharacters)textRun;
  319. var glyphRun = shapedRun.GlyphRun;
  320. var glyphClusters = glyphRun.GlyphClusters;
  321. var expected = clusters.Skip(index).Take(glyphClusters.Length).ToArray();
  322. Assert.Equal(expected, glyphRun.GlyphClusters);
  323. index += glyphClusters.Length;
  324. }
  325. }
  326. }
  327. [Theory]
  328. [InlineData("abcde\r\n", 7)] // Carriage Return + Line Feed
  329. [InlineData("abcde\n\r", 7)] // This isn't valid but we somehow have to support it.
  330. [InlineData("abcde\u000A", 6)] // Line Feed
  331. [InlineData("abcde\u000B", 6)] // Vertical Tab
  332. [InlineData("abcde\u000C", 6)] // Form Feed
  333. [InlineData("abcde\u000D", 6)] // Carriage Return
  334. public void Should_Break_With_BreakChar(string text, int expectedLength)
  335. {
  336. using (Start())
  337. {
  338. var layout = new TextLayout(
  339. text,
  340. Typeface.Default,
  341. 12.0f,
  342. Brushes.Black.ToImmutable());
  343. Assert.Equal(2, layout.TextLines.Count);
  344. Assert.Equal(1, layout.TextLines[0].TextRuns.Count);
  345. Assert.Equal(expectedLength, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Length);
  346. Assert.Equal(5, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[5]);
  347. if (expectedLength == 7)
  348. {
  349. Assert.Equal(5, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[6]);
  350. }
  351. }
  352. }
  353. [Fact]
  354. public void Should_Have_One_Run_With_Common_Script()
  355. {
  356. using (Start())
  357. {
  358. var layout = new TextLayout(
  359. "abcde\r\n",
  360. Typeface.Default,
  361. 12.0f,
  362. Brushes.Black.ToImmutable());
  363. Assert.Equal(1, layout.TextLines[0].TextRuns.Count);
  364. }
  365. }
  366. [Fact]
  367. public void Should_Layout_Corrupted_Text()
  368. {
  369. using (Start())
  370. {
  371. var text = new string(new[] { '\uD802', '\uD802', '\uD802', '\uD802', '\uD802', '\uD802', '\uD802' });
  372. var layout = new TextLayout(
  373. text,
  374. Typeface.Default,
  375. 12,
  376. Brushes.Black.ToImmutable());
  377. var textLine = layout.TextLines[0];
  378. var textRun = (ShapedTextCharacters)textLine.TextRuns[0];
  379. Assert.Equal(7, textRun.Text.Length);
  380. var replacementGlyph = Typeface.Default.GlyphTypeface.GetGlyph(Codepoint.ReplacementCodepoint);
  381. foreach (var glyph in textRun.GlyphRun.GlyphIndices)
  382. {
  383. Assert.Equal(replacementGlyph, glyph);
  384. }
  385. }
  386. }
  387. [InlineData("0123456789\r0123456789")]
  388. [InlineData("0123456789")]
  389. [Theory]
  390. public void Should_Include_First_Line_When_Constraint_Is_Surpassed(string text)
  391. {
  392. using (Start())
  393. {
  394. var glyphTypeface = Typeface.Default.GlyphTypeface;
  395. var emHeight = glyphTypeface.DesignEmHeight;
  396. var lineHeight = (glyphTypeface.Descent - glyphTypeface.Ascent) * (12.0 / emHeight);
  397. var layout = new TextLayout(
  398. text,
  399. Typeface.Default,
  400. 12,
  401. Brushes.Black.ToImmutable(),
  402. maxHeight: lineHeight - lineHeight * 0.5);
  403. Assert.Equal(1, layout.TextLines.Count);
  404. Assert.Equal(lineHeight, layout.Size.Height);
  405. }
  406. }
  407. [InlineData("0123456789\r\n0123456789\r\n0123456789", 0, 3)]
  408. [InlineData("0123456789\r\n0123456789\r\n0123456789", 1, 1)]
  409. [InlineData("0123456789\r\n0123456789\r\n0123456789", 4, 3)]
  410. [Theory]
  411. public void Should_Not_Exceed_MaxLines(string text, int maxLines, int expectedLines)
  412. {
  413. using (Start())
  414. {
  415. var layout = new TextLayout(
  416. text,
  417. Typeface.Default,
  418. 12,
  419. Brushes.Black,
  420. maxWidth: 50,
  421. maxLines: maxLines);
  422. Assert.Equal(expectedLines, layout.TextLines.Count);
  423. }
  424. }
  425. [Fact]
  426. public void Should_Produce_Fixed_Height_Lines()
  427. {
  428. using (Start())
  429. {
  430. var layout = new TextLayout(
  431. s_multiLineText,
  432. Typeface.Default,
  433. 12,
  434. Brushes.Black,
  435. lineHeight: 50);
  436. foreach (var line in layout.TextLines)
  437. {
  438. Assert.Equal(50, line.LineMetrics.Size.Height);
  439. }
  440. }
  441. }
  442. private const string Text = "日本でTest一番読まれている英字新聞・ジャパンタイムズが発信する国内外ニュースと、様々なジャンルの特集記事。";
  443. [Fact(Skip = "Only used for profiling.")]
  444. public void Should_Wrap()
  445. {
  446. using (Start())
  447. {
  448. for (var i = 0; i < 2000; i++)
  449. {
  450. var layout = new TextLayout(
  451. Text,
  452. Typeface.Default,
  453. 12,
  454. Brushes.Black,
  455. textWrapping: TextWrapping.Wrap,
  456. maxWidth: 50);
  457. }
  458. }
  459. }
  460. [Fact]
  461. public void Should_Wrap_Min_OneCharacter_EveryLine()
  462. {
  463. using (Start())
  464. {
  465. var layout = new TextLayout(
  466. s_singleLineText,
  467. Typeface.Default,
  468. 12,
  469. Brushes.Black,
  470. textWrapping: TextWrapping.Wrap,
  471. maxWidth: 3);
  472. //every character should be new line as there not enough space for even one character
  473. Assert.Equal(s_singleLineText.Length, layout.TextLines.Count);
  474. }
  475. }
  476. private static IDisposable Start()
  477. {
  478. var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
  479. .With(renderInterface: new PlatformRenderInterface(null),
  480. textShaperImpl: new TextShaperImpl(),
  481. fontManagerImpl: new CustomFontManagerImpl()));
  482. return disposable;
  483. }
  484. }
  485. }