TextLayoutTests.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. using Avalonia.Media;
  2. using System;
  3. using System.Globalization;
  4. using System.Linq;
  5. using System.Runtime.InteropServices;
  6. using System.Threading.Tasks;
  7. using Avalonia.Controls;
  8. using Avalonia.Media.TextFormatting;
  9. using Avalonia.Utilities;
  10. using Xunit;
  11. #if AVALONIA_SKIA
  12. namespace Avalonia.Skia.RenderTests
  13. #else
  14. namespace Avalonia.Direct2D1.RenderTests.Media
  15. #endif
  16. {
  17. public class TextLayoutTests : TestBase
  18. {
  19. private const double FontSize = 12;
  20. private const double MediumFontSize = 18;
  21. private const double BigFontSize = 32;
  22. private const double FontSizeHeight = 14.0625;//real value 13.59375
  23. private const string stringword = "word";
  24. private const string stringmiddle = "The quick brown fox jumps over the lazy dog";
  25. private const string stringmiddle2lines = "The quick brown fox\njumps over the lazy dog";
  26. private const string stringmiddle3lines = "01234567\n\n0123456789";
  27. private const string stringmiddlenewlines = "012345678\r 1234567\r\n 12345678\n0123456789";
  28. private const string stringlong =
  29. "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis " +
  30. "aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero" +
  31. " at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus " +
  32. "pretium ornare est.";
  33. public TextLayoutTests()
  34. : base(@"Media\TextFormatting\TextLayout")
  35. {
  36. }
  37. private static TextLayout Create(string text,
  38. double fontSize,
  39. FontStyle fontStyle,
  40. TextAlignment textAlignment,
  41. FontWeight fontWeight,
  42. TextWrapping wrapping,
  43. double widthConstraint)
  44. {
  45. var typeface = new Typeface(TestFontFamily, fontStyle, fontWeight);
  46. var formattedText = new TextLayout(text, typeface, fontSize, null, textAlignment, wrapping,
  47. maxWidth: widthConstraint == -1 ? double.PositiveInfinity : widthConstraint);
  48. return formattedText;
  49. }
  50. private static TextLayout Create(string text, double fontSize)
  51. {
  52. return Create(text, fontSize,
  53. FontStyle.Normal, TextAlignment.Left,
  54. FontWeight.Normal, TextWrapping.NoWrap,
  55. -1);
  56. }
  57. private static TextLayout Create(string text, double fontSize, TextAlignment alignment, double widthConstraint)
  58. {
  59. return Create(text, fontSize,
  60. FontStyle.Normal, alignment,
  61. FontWeight.Normal, TextWrapping.NoWrap,
  62. widthConstraint);
  63. }
  64. private static TextLayout Create(string text, double fontSize, TextWrapping wrap, double widthConstraint)
  65. {
  66. return Create(text, fontSize,
  67. FontStyle.Normal, TextAlignment.Left,
  68. FontWeight.Normal, wrap,
  69. widthConstraint);
  70. }
  71. [Theory]
  72. [InlineData("", FontSize, 0, FontSizeHeight)]
  73. [InlineData("x", FontSize, 7.20, FontSizeHeight)]
  74. [InlineData(stringword, FontSize, 28.80, FontSizeHeight)]
  75. [InlineData(stringmiddle, FontSize, 309.65, FontSizeHeight)]
  76. [InlineData(stringmiddle, MediumFontSize, 464.48, 21.09375)]
  77. [InlineData(stringmiddle, BigFontSize, 825.73, 37.5)]
  78. [InlineData(stringmiddle2lines, FontSize, 165.63, 2 * FontSizeHeight)]
  79. [InlineData(stringmiddle2lines, MediumFontSize, 248.44, 2 * 21.09375)]
  80. [InlineData(stringmiddle2lines, BigFontSize, 441.67, 2 * 37.5)]
  81. [InlineData(stringlong, FontSize, 2160.35, FontSizeHeight)]
  82. [InlineData(stringmiddlenewlines, FontSize, 72.01, 4 * FontSizeHeight)]
  83. public void Should_Measure_String_Correctly(string input, double fontSize, double expWidth, double expHeight)
  84. {
  85. var fmt = Create(input, fontSize);
  86. Assert.Equal(expWidth, fmt.Bounds.Width, 2);
  87. Assert.Equal(expHeight, fmt.Bounds.Height, 2);
  88. }
  89. [Theory]
  90. [InlineData("", 1, -1, TextWrapping.NoWrap)]
  91. [InlineData("x", 1, -1, TextWrapping.NoWrap)]
  92. [InlineData(stringword, 1, -1, TextWrapping.NoWrap)]
  93. [InlineData(stringmiddle, 1, -1, TextWrapping.NoWrap)]
  94. [InlineData(stringmiddle, 3, 150, TextWrapping.Wrap)]
  95. [InlineData(stringmiddle2lines, 2, -1, TextWrapping.NoWrap)]
  96. [InlineData(stringmiddle2lines, 3, 150, TextWrapping.Wrap)]
  97. [InlineData(stringlong, 1, -1, TextWrapping.NoWrap)]
  98. [InlineData(stringlong, 18, 150, TextWrapping.Wrap)]
  99. [InlineData(stringmiddlenewlines, 4, -1, TextWrapping.NoWrap)]
  100. [InlineData(stringmiddlenewlines, 4, 150, TextWrapping.Wrap)]
  101. public void Should_Break_Lines_String_Correctly(string input,
  102. int linesCount,
  103. double widthConstraint,
  104. TextWrapping wrap)
  105. {
  106. var fmt = Create(input, FontSize, wrap, widthConstraint);
  107. var constrained = fmt;
  108. var lines = constrained.TextLines.ToArray();
  109. Assert.Equal(linesCount, lines.Count());
  110. }
  111. [Theory]
  112. [InlineData("x", 0, 0, true, false, 0)]
  113. [InlineData(stringword, -1, -1, false, false, 0)]
  114. [InlineData(stringword, 25, 13, true, false, 3)]
  115. [InlineData(stringword, 28.70, 13.5, true, true, 4)]
  116. [InlineData(stringword, 30, 13, false, true, 4)]
  117. [InlineData(stringword + "\r\n", 30, 13, false, false, 4)]
  118. [InlineData(stringword + "\r\nnext", 30, 13, false, false, 4)]
  119. [InlineData(stringword, 300, 13, false, true, 4)]
  120. [InlineData(stringword + "\r\n", 300, 13, false, false, 4)]
  121. [InlineData(stringword + "\r\nnext", 300, 13, false, false, 4)]
  122. [InlineData(stringword, 300, 300, false, true, 4)]
  123. //TODO: Direct2D implementation return textposition 6
  124. //but the text is 6 length, can't find the logic for me it should be 5
  125. //[InlineData(stringword + "\r\n", 300, 300, false, false, 6)]
  126. [InlineData(stringword + "\r\nnext", 300, 300, false, true, 10)]
  127. [InlineData(stringword + "\r\nnext", 300, 25, false, true, 10)]
  128. [InlineData(stringword, 28, 15, false, true, 4)]
  129. [InlineData(stringword, 30, 15, false, true, 4)]
  130. [InlineData(stringmiddle3lines, 30, 15, false, false, 9)]
  131. [InlineData(stringmiddle3lines, 500, 13, false, false, 8)]
  132. [InlineData(stringmiddle3lines, 30, 25, false, false, 9)]
  133. [InlineData(stringmiddle3lines, -1, 30, false, false, 10)]
  134. public void Should_HitTestPoint_Correctly(string input,
  135. double x, double y,
  136. bool isInside, bool isTrailing, int pos)
  137. {
  138. var fmt = Create(input, FontSize);
  139. var htRes = fmt.HitTestPoint(new Point(x, y));
  140. Assert.Equal(pos, htRes.TextPosition);
  141. Assert.Equal(isInside, htRes.IsInside);
  142. Assert.Equal(isTrailing, htRes.IsTrailing);
  143. }
  144. [Theory]
  145. [InlineData("", 0, 0, 0, 0, FontSizeHeight)]
  146. [InlineData("x", 0, 0, 0, 7.20, FontSizeHeight)]
  147. [InlineData("x", -1, 7.20, 0, 0, FontSizeHeight)]
  148. [InlineData(stringword, 3, 21.60, 0, 7.20, FontSizeHeight)]
  149. [InlineData(stringword, 4, 21.60 + 7.20, 0, 0, FontSizeHeight)]
  150. [InlineData(stringmiddlenewlines, 10, 0, FontSizeHeight, 7.20, FontSizeHeight)]
  151. [InlineData(stringmiddlenewlines, 15, 36.01, FontSizeHeight, 7.20, FontSizeHeight)]
  152. [InlineData(stringmiddlenewlines, 20, 0, 2 * FontSizeHeight, 7.20, FontSizeHeight)]
  153. [InlineData(stringmiddlenewlines, -1, 72.01, 3 * FontSizeHeight, 0, FontSizeHeight)]
  154. public void Should_HitTestPosition_Correctly(string input,
  155. int index, double x, double y, double width, double height)
  156. {
  157. var fmt = Create(input, FontSize);
  158. var r = fmt.HitTestTextPosition(index);
  159. Assert.Equal(x, r.X, 2);
  160. Assert.Equal(y, r.Y, 2);
  161. Assert.Equal(width, r.Width, 2);
  162. Assert.Equal(height, r.Height, 2);
  163. }
  164. [Theory]
  165. [InlineData("x", 0, 200, 200 - 7.20, 0, 7.20, FontSizeHeight)]
  166. [InlineData(stringword, 0, 200, 171.20, 0, 7.20, FontSizeHeight)]
  167. [InlineData(stringword, 3, 200, 200 - 7.20, 0, 7.20, FontSizeHeight)]
  168. public void Should_HitTestPosition_RightAlign_Correctly(
  169. string input, int index, double widthConstraint,
  170. double x, double y, double width, double height)
  171. {
  172. //parse expected
  173. var fmt = Create(input, FontSize, TextAlignment.Right, widthConstraint);
  174. var constrained = fmt;
  175. var r = constrained.HitTestTextPosition(index);
  176. Assert.Equal(x, r.X, 2);
  177. Assert.Equal(y, r.Y, 2);
  178. Assert.Equal(width, r.Width, 2);
  179. Assert.Equal(height, r.Height, 2);
  180. }
  181. [Theory]
  182. [InlineData("x", 0, 200, 100 - 7.20 / 2, 0, 7.20, FontSizeHeight)]
  183. [InlineData(stringword, 0, 200, 85.6, 0, 7.20, FontSizeHeight)]
  184. [InlineData(stringword, 3, 200, 100 + 7.20, 0, 7.20, FontSizeHeight)]
  185. public void Should_HitTestPosition_CenterAlign_Correctly(
  186. string input, int index, double widthConstraint,
  187. double x, double y, double width, double height)
  188. {
  189. //parse expected
  190. var fmt = Create(input, FontSize, TextAlignment.Center, widthConstraint);
  191. var constrained = fmt;
  192. var r = constrained.HitTestTextPosition(index);
  193. Assert.Equal(x, r.X, 2);
  194. Assert.Equal(y, r.Y, 2);
  195. Assert.Equal(width, r.Width, 2);
  196. Assert.Equal(height, r.Height, 2);
  197. }
  198. [Theory]
  199. [InlineData("x", 0, 1, "0,0,7.20,14.0625")]
  200. [InlineData(stringword, 0, 4, "0,0,28.80,14.0625")]
  201. [InlineData(stringmiddlenewlines, 10, 10, "0,14.0625,57.61,14.0625")]
  202. [InlineData(stringmiddlenewlines, 10, 20, "0,14.0625,57.61,14.0625;0,28.125,64.81,14.0625")]
  203. [InlineData(stringmiddlenewlines, 10, 15, "0,14.0625,57.61,14.0625;0,28.125,36.01,14.0625")]
  204. [InlineData(stringmiddlenewlines, 15, 15, "36.01,14.0625,21.60,14.0625;0,28.125,64.81,14.0625")]
  205. public void Should_HitTestRange_Correctly(string input,
  206. int index, int length,
  207. string expectedRects)
  208. {
  209. //parse expected result
  210. var rects = expectedRects.Split(';').Select(s =>
  211. {
  212. double[] v = s.Split(',')
  213. .Select(sd => double.Parse(sd, CultureInfo.InvariantCulture)).ToArray();
  214. return new Rect(v[0], v[1], v[2], v[3]);
  215. }).ToArray();
  216. var fmt = Create(input, FontSize);
  217. var htRes = fmt.HitTestTextRange(index, length).ToArray();
  218. Assert.Equal(rects.Length, htRes.Length);
  219. for (int i = 0; i < rects.Length; i++)
  220. {
  221. var exr = rects[i];
  222. var r = htRes[i];
  223. Assert.Equal(exr.X, r.X, 2);
  224. Assert.Equal(exr.Y, r.Y, 2);
  225. Assert.Equal(exr.Width, r.Width, 2);
  226. Assert.Equal(exr.Height, r.Height, 2);
  227. }
  228. }
  229. [Fact]
  230. public async Task TextLayout_Basic()
  231. {
  232. // Skip test on OSX: text rendering is subtly different.
  233. if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
  234. return;
  235. var t = new TextLayout(
  236. "Avalonia!",
  237. new Typeface(TestFontFamily),
  238. 24,
  239. Brushes.Black);
  240. var target = new Border
  241. {
  242. Width = 200,
  243. Height = 200,
  244. Background = Brushes.White,
  245. Child = new DrawnControl(c =>
  246. {
  247. var textRect = t.Bounds;
  248. var bounds = new Rect(0, 0, 200, 200);
  249. var rect = bounds.CenterRect(textRect);
  250. c.DrawRectangle(Brushes.Yellow, null, rect);
  251. t.Draw(c, rect.Position);
  252. }),
  253. };
  254. await RenderToFile(target);
  255. CompareImages();
  256. }
  257. [Fact]
  258. public async Task TextLayout_Rotated()
  259. {
  260. // Skip test on OSX: text rendering is subtly different.
  261. if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
  262. return;
  263. var t = new TextLayout(
  264. "Avalonia!",
  265. new Typeface(TestFontFamily),
  266. 24,
  267. Brushes.Black);
  268. var target = new Border
  269. {
  270. Width = 200,
  271. Height = 200,
  272. Background = Brushes.White,
  273. Child = new DrawnControl(c =>
  274. {
  275. var textRect = t.Bounds;
  276. var bounds = new Rect(0, 0, 200, 200);
  277. var rect = bounds.CenterRect(textRect);
  278. var rotate = Matrix.CreateTranslation(-100, -100) *
  279. Matrix.CreateRotation(MathUtilities.Deg2Rad(90)) *
  280. Matrix.CreateTranslation(100, 100);
  281. using var transform = c.PushTransform(rotate);
  282. c.DrawRectangle(Brushes.Yellow, null, rect);
  283. t.Draw(c, rect.Position);
  284. }),
  285. };
  286. await RenderToFile(target);
  287. CompareImages();
  288. }
  289. private class DrawnControl : Control
  290. {
  291. private readonly Action<DrawingContext> _render;
  292. public DrawnControl(Action<DrawingContext> render) => _render = render;
  293. public override void Render(DrawingContext context) => _render(context);
  294. }
  295. }
  296. }