TextFormatterTests.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. using System;
  2. using System.Collections.Generic;
  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 TextFormatterTests
  12. {
  13. [Fact]
  14. public void Should_Format_TextRuns_With_Default_Style()
  15. {
  16. using (Start())
  17. {
  18. const string text = "0123456789";
  19. var defaultProperties =
  20. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black);
  21. var textSource = new SingleBufferTextSource(text, defaultProperties);
  22. var formatter = new TextFormatterImpl();
  23. var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  24. new GenericTextParagraphProperties(defaultProperties));
  25. Assert.Single(textLine.TextRuns);
  26. var textRun = textLine.TextRuns[0];
  27. Assert.Equal(defaultProperties.Typeface, textRun.Properties.Typeface);
  28. Assert.Equal(defaultProperties.ForegroundBrush, textRun.Properties.ForegroundBrush);
  29. Assert.Equal(text.Length, textRun.Text.Length);
  30. }
  31. }
  32. [Fact]
  33. public void Should_Format_TextRuns_With_Multiple_Buffers()
  34. {
  35. using (Start())
  36. {
  37. var defaultProperties =
  38. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black);
  39. var textSource = new MultiBufferTextSource(defaultProperties);
  40. var formatter = new TextFormatterImpl();
  41. var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  42. new GenericTextParagraphProperties(defaultProperties));
  43. Assert.Equal(5, textLine.TextRuns.Count);
  44. Assert.Equal(50, textLine.TextRange.Length);
  45. }
  46. }
  47. [Fact]
  48. public void Should_Format_TextRuns_With_TextRunStyles()
  49. {
  50. using (Start())
  51. {
  52. const string text = "0123456789";
  53. var defaultProperties =
  54. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black);
  55. var GenericTextRunPropertiesRuns = new[]
  56. {
  57. new ValueSpan<TextRunProperties>(0, 3, defaultProperties),
  58. new ValueSpan<TextRunProperties>(3, 3,
  59. new GenericTextRunProperties(Typeface.Default, 13, foregroundBrush: Brushes.Black)),
  60. new ValueSpan<TextRunProperties>(6, 3,
  61. new GenericTextRunProperties(Typeface.Default, 14, foregroundBrush: Brushes.Black)),
  62. new ValueSpan<TextRunProperties>(9, 1, defaultProperties)
  63. };
  64. var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, GenericTextRunPropertiesRuns);
  65. var formatter = new TextFormatterImpl();
  66. var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  67. new GenericTextParagraphProperties(defaultProperties));
  68. Assert.Equal(text.Length, textLine.TextRange.Length);
  69. for (var i = 0; i < GenericTextRunPropertiesRuns.Length; i++)
  70. {
  71. var GenericTextRunPropertiesRun = GenericTextRunPropertiesRuns[i];
  72. var textRun = textLine.TextRuns[i];
  73. Assert.Equal(GenericTextRunPropertiesRun.Length, textRun.Text.Length);
  74. }
  75. }
  76. }
  77. [Theory]
  78. [InlineData("0123", 1)]
  79. [InlineData("\r\n", 1)]
  80. [InlineData("👍b", 2)]
  81. [InlineData("a👍b", 3)]
  82. [InlineData("a👍子b", 4)]
  83. public void Should_Produce_Unique_Runs(string text, int numberOfRuns)
  84. {
  85. using (Start())
  86. {
  87. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  88. var textSource = new SingleBufferTextSource(text, defaultProperties);
  89. var formatter = new TextFormatterImpl();
  90. var textLine =
  91. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  92. new GenericTextParagraphProperties(defaultProperties));
  93. Assert.Equal(numberOfRuns, textLine.TextRuns.Count);
  94. }
  95. }
  96. [Fact]
  97. public void Should_Split_Run_On_Script()
  98. {
  99. using (Start())
  100. {
  101. const string text = "ABCDالدولي";
  102. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  103. var textSource = new SingleBufferTextSource(text, defaultProperties);
  104. var formatter = new TextFormatterImpl();
  105. var textLine =
  106. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  107. new GenericTextParagraphProperties(defaultProperties));
  108. Assert.Equal(4, textLine.TextRuns[0].Text.Length);
  109. }
  110. }
  111. [InlineData("𐐷𐐷𐐷𐐷𐐷", 10, 1)]
  112. [InlineData("01234 56789 01234 56789", 6, 4)]
  113. [Theory]
  114. public void Should_Wrap_With_Overflow(string text, int expectedCharactersPerLine, int expectedNumberOfLines)
  115. {
  116. using (Start())
  117. {
  118. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  119. var textSource = new SingleBufferTextSource(text, defaultProperties);
  120. var formatter = new TextFormatterImpl();
  121. var numberOfLines = 0;
  122. var currentPosition = 0;
  123. while (currentPosition < text.Length)
  124. {
  125. var textLine =
  126. formatter.FormatLine(textSource, currentPosition, 1,
  127. new GenericTextParagraphProperties(defaultProperties, textWrap : TextWrapping.WrapWithOverflow));
  128. if (text.Length - currentPosition > expectedCharactersPerLine)
  129. {
  130. Assert.Equal(expectedCharactersPerLine, textLine.TextRange.Length);
  131. }
  132. currentPosition += textLine.TextRange.Length;
  133. numberOfLines++;
  134. }
  135. Assert.Equal(expectedNumberOfLines, numberOfLines);
  136. }
  137. }
  138. [InlineData("Whether to turn off HTTPS. This option only applies if Individual, " +
  139. "IndividualB2C, SingleOrg, or MultiOrg aren't used for &#8209;&#8209;auth."
  140. , "Noto Sans", 40)]
  141. [InlineData("01234 56789 01234 56789", "Noto Mono", 7)]
  142. [Theory]
  143. public void Should_Wrap(string text, string familyName, int numberOfCharactersPerLine)
  144. {
  145. using (Start())
  146. {
  147. var lineBreaker = new LineBreakEnumerator(text.AsMemory());
  148. var expected = new List<int>();
  149. while (lineBreaker.MoveNext())
  150. {
  151. expected.Add(lineBreaker.Current.PositionWrap - 1);
  152. }
  153. var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#" +
  154. familyName);
  155. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  156. var textSource = new SingleBufferTextSource(text, defaultProperties);
  157. var formatter = new TextFormatterImpl();
  158. var glyph = typeface.GlyphTypeface.GetGlyph('a');
  159. var advance = typeface.GlyphTypeface.GetGlyphAdvance(glyph) *
  160. (12.0 / typeface.GlyphTypeface.DesignEmHeight);
  161. var paragraphWidth = advance * numberOfCharactersPerLine;
  162. var currentPosition = 0;
  163. while (currentPosition < text.Length)
  164. {
  165. var textLine =
  166. formatter.FormatLine(textSource, currentPosition, paragraphWidth,
  167. new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap));
  168. Assert.True(expected.Contains(textLine.TextRange.End));
  169. var index = expected.IndexOf(textLine.TextRange.End);
  170. for (var i = 0; i <= index; i++)
  171. {
  172. expected.RemoveAt(0);
  173. }
  174. currentPosition += textLine.TextRange.Length;
  175. }
  176. }
  177. }
  178. [Fact]
  179. public void Should_Produce_Fixed_Height_Lines()
  180. {
  181. using (Start())
  182. {
  183. const string text = "012345";
  184. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  185. var textSource = new SingleBufferTextSource(text, defaultProperties);
  186. var formatter = new TextFormatterImpl();
  187. var textLine =
  188. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  189. new GenericTextParagraphProperties(defaultProperties, lineHeight: 50));
  190. Assert.Equal(50, textLine.Height);
  191. }
  192. }
  193. [Fact]
  194. public void Should_Not_Produce_TextLine_Wider_Than_ParagraphWidth()
  195. {
  196. using (Start())
  197. {
  198. const string text =
  199. "Multiline TextBlock with TextWrapping.\r\rLorem ipsum dolor sit amet, consectetur adipiscing elit. " +
  200. "Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. " +
  201. "Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. " +
  202. "Vivamus pretium ornare est.";
  203. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  204. var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
  205. var textSource = new SingleBufferTextSource(text, defaultProperties);
  206. var formatter = new TextFormatterImpl();
  207. var textSourceIndex = 0;
  208. while (textSourceIndex < text.Length)
  209. {
  210. var textLine =
  211. formatter.FormatLine(textSource, textSourceIndex, 200, paragraphProperties);
  212. Assert.True(textLine.Width <= 200);
  213. textSourceIndex += textLine.TextRange.Length;
  214. }
  215. }
  216. }
  217. [Fact]
  218. public void Wrap_Should_Not_Produce_Empty_Lines()
  219. {
  220. using (Start())
  221. {
  222. const string text = "012345";
  223. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  224. var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.Wrap);
  225. var textSource = new SingleBufferTextSource(text, defaultProperties);
  226. var formatter = new TextFormatterImpl();
  227. var textSourceIndex = 0;
  228. while (textSourceIndex < text.Length)
  229. {
  230. var textLine =
  231. formatter.FormatLine(textSource, textSourceIndex, 3, paragraphProperties);
  232. Assert.NotEqual(0, textLine.TextRange.Length);
  233. textSourceIndex += textLine.TextRange.Length;
  234. }
  235. Assert.Equal(text.Length, textSourceIndex);
  236. }
  237. }
  238. [InlineData("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor",
  239. new []{ "Lorem ipsum ", "dolor sit amet, ", "consectetur ", "adipisicing elit, ", "sed do eiusmod "})]
  240. [Theory]
  241. public void Should_Produce_Wrapped_And_Trimmed_Lines(string text, string[] expectedLines)
  242. {
  243. using (Start())
  244. {
  245. var typeface = new Typeface("Verdana");
  246. var defaultProperties = new GenericTextRunProperties(typeface, 32, foregroundBrush: Brushes.Black);
  247. var styleSpans = new[]
  248. {
  249. new ValueSpan<TextRunProperties>(0, 5,
  250. new GenericTextRunProperties(typeface, 48)),
  251. new ValueSpan<TextRunProperties>(6, 11,
  252. new GenericTextRunProperties(new Typeface("Verdana", weight: FontWeight.Bold), 32)),
  253. new ValueSpan<TextRunProperties>(28, 28,
  254. new GenericTextRunProperties(new Typeface("Verdana", FontStyle.Italic),32))
  255. };
  256. var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, styleSpans);
  257. var formatter = new TextFormatterImpl();
  258. var currentPosition = 0;
  259. var currentHeight = 0d;
  260. var currentLineIndex = 0;
  261. while (currentPosition < text.Length && currentLineIndex < expectedLines.Length)
  262. {
  263. var textLine =
  264. formatter.FormatLine(textSource, currentPosition, 300,
  265. new GenericTextParagraphProperties(defaultProperties, textWrap: TextWrapping.WrapWithOverflow));
  266. currentPosition += textLine.TextRange.Length;
  267. if (textLine.Width > 300 || currentHeight + textLine.Height > 240)
  268. {
  269. textLine = textLine.Collapse(new TextTrailingWordEllipsis(300, defaultProperties));
  270. }
  271. currentHeight += textLine.Height;
  272. var currentText = text.Substring(textLine.TextRange.Start, textLine.TextRange.Length);
  273. Assert.Equal(expectedLines[currentLineIndex], currentText);
  274. currentLineIndex++;
  275. }
  276. Assert.Equal(expectedLines.Length,currentLineIndex);
  277. }
  278. }
  279. [InlineData(TextAlignment.Left)]
  280. [InlineData(TextAlignment.Center)]
  281. [InlineData(TextAlignment.Right)]
  282. [Theory]
  283. public void Should_Align_TextLine(TextAlignment textAlignment)
  284. {
  285. using (Start())
  286. {
  287. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  288. var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textAlignment);
  289. var textSource = new SingleBufferTextSource("0123456789", defaultProperties);
  290. var formatter = new TextFormatterImpl();
  291. var textLine =
  292. formatter.FormatLine(textSource, 0, 100, paragraphProperties);
  293. var expectedOffset = TextLine.GetParagraphOffsetX(textLine.Width, 100, textAlignment);
  294. Assert.Equal(expectedOffset, textLine.Start);
  295. }
  296. }
  297. [Fact]
  298. public void Should_FormatLine_With_Emergency_Breaks()
  299. {
  300. using (Start())
  301. {
  302. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  303. var paragraphProperties = new GenericTextParagraphProperties(defaultProperties);
  304. var textSource = new SingleBufferTextSource("0123456789_0123456789_0123456789_0123456789", defaultProperties);
  305. var formatter = new TextFormatterImpl();
  306. var textLine =
  307. formatter.FormatLine(textSource, 0, 33, paragraphProperties);
  308. Assert.NotNull(textLine.TextLineBreak.RemainingCharacters);
  309. }
  310. }
  311. public static IDisposable Start()
  312. {
  313. var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
  314. .With(renderInterface: new PlatformRenderInterface(null),
  315. textShaperImpl: new TextShaperImpl()));
  316. AvaloniaLocator.CurrentMutable
  317. .Bind<FontManager>().ToConstant(new FontManager(new CustomFontManagerImpl()));
  318. return disposable;
  319. }
  320. }
  321. }