FormattedTextImpl.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. using Avalonia.Media;
  2. using Avalonia.Platform;
  3. using SkiaSharp;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.Runtime.InteropServices;
  7. using System.Text;
  8. namespace Avalonia.Skia
  9. {
  10. unsafe class FormattedTextImpl : IFormattedTextImpl
  11. {
  12. public SKPaint Paint { get; private set; }
  13. public FormattedTextImpl(string text)
  14. {
  15. _text = text;
  16. Paint = new SKPaint();
  17. //currently Skia does not measure properly with Utf8 !!!
  18. //Paint.TextEncoding = SKTextEncoding.Utf8;
  19. Paint.TextEncoding = SKTextEncoding.Utf16;
  20. Paint.IsStroke = false;
  21. Paint.IsAntialias = true;
  22. LineOffset = 0;
  23. // Replace 0 characters with zero-width spaces (200B)
  24. _text = _text.Replace((char)0, (char)0x200B);
  25. }
  26. public static FormattedTextImpl Create(string text, string fontFamilyName, double fontSize, FontStyle fontStyle,
  27. TextAlignment textAlignment, FontWeight fontWeight)
  28. {
  29. var typeface = TypefaceCache.GetTypeface(fontFamilyName, fontStyle, fontWeight);
  30. FormattedTextImpl instance = new FormattedTextImpl(text);
  31. instance.Paint.Typeface = typeface;
  32. instance.Paint.TextSize = (float)fontSize;
  33. instance.Paint.TextAlign = textAlignment.ToSKTextAlign();
  34. instance.Rebuild();
  35. return instance;
  36. }
  37. private readonly string _text;
  38. readonly List<FormattedTextLine> _lines = new List<FormattedTextLine>();
  39. readonly List<Rect> _rects = new List<Rect>();
  40. List<AvaloniaFormattedTextLine> _skiaLines;
  41. SKRect[] _skiaRects;
  42. Size _size;
  43. const float MAX_LINE_WIDTH = 10000;
  44. float LineOffset;
  45. float WidthConstraint = -1;
  46. struct AvaloniaFormattedTextLine
  47. {
  48. public float Top;
  49. public int Start;
  50. public int Length;
  51. public float Height;
  52. public float Width;
  53. };
  54. public IEnumerable<FormattedTextLine> GetLines()
  55. {
  56. return _lines;
  57. }
  58. public TextHitTestResult HitTestPoint(Point point)
  59. {
  60. for (int c = 0; c < _rects.Count; c++)
  61. {
  62. //TODO: Detect line first
  63. var rc = _rects[c];
  64. if (rc.Contains(point))
  65. {
  66. return new TextHitTestResult
  67. {
  68. IsInside = true,
  69. TextPosition = c,
  70. IsTrailing = (point.X - rc.X) > rc.Width / 2
  71. };
  72. }
  73. }
  74. bool end = point.X > _size.Width || point.Y > _size.Height;
  75. return new TextHitTestResult() { IsTrailing = end, TextPosition = end ? _text.Length - 1 : 0 };
  76. }
  77. public Rect HitTestTextPosition(int index)
  78. {
  79. if (index < 0 || index >= _rects.Count)
  80. return new Rect();
  81. return _rects[index];
  82. }
  83. public IEnumerable<Rect> HitTestTextRange(int index, int length)
  84. {
  85. for (var c = 0; c < length; c++)
  86. yield return _rects[c + index];
  87. }
  88. public Size Measure()
  89. {
  90. return _size;
  91. }
  92. public void SetForegroundBrush(IBrush brush, int startIndex, int length)
  93. {
  94. // TODO: we need an implementation here to properly support FormattedText
  95. }
  96. void Rebuild()
  97. {
  98. var length = _text.Length;
  99. _lines.Clear();
  100. _skiaRects = new SKRect[length];
  101. _skiaLines = new List<AvaloniaFormattedTextLine>();
  102. int curOff = 0;
  103. float curY = 0;
  104. var metrics = Paint.FontMetrics;
  105. var mTop = metrics.Top; // The greatest distance above the baseline for any glyph (will be <= 0).
  106. var mBottom = metrics.Bottom; // The greatest distance below the baseline for any glyph (will be >= 0).
  107. var mLeading = metrics.Leading; // The recommended distance to add between lines of text (will be >= 0).
  108. // This seems like the best measure of full vertical extent
  109. float lineHeight = mBottom - mTop;
  110. // Rendering is relative to baseline
  111. LineOffset = -metrics.Top;
  112. string subString;
  113. for (int c = 0; curOff < length; c++)
  114. {
  115. float lineWidth = -1;
  116. int measured;
  117. int extraSkip = 0;
  118. if (WidthConstraint <= 0)
  119. {
  120. measured = length;
  121. }
  122. else
  123. {
  124. float constraint = WidthConstraint;
  125. if (constraint > MAX_LINE_WIDTH)
  126. constraint = MAX_LINE_WIDTH;
  127. subString = _text.Substring(curOff);
  128. measured = (int)Paint.BreakText(subString, constraint, out lineWidth) / 2;
  129. if (measured == 0)
  130. {
  131. measured = 1;
  132. lineWidth = -1;
  133. }
  134. char nextChar = ' ';
  135. if (curOff + measured < length)
  136. nextChar = _text[curOff + measured];
  137. if (nextChar != ' ')
  138. {
  139. // Perform scan for the last space and end the line there
  140. for (int si = curOff + measured - 1; si > curOff; si--)
  141. {
  142. if (_text[si] == ' ')
  143. {
  144. measured = si - curOff;
  145. extraSkip = 1;
  146. break;
  147. }
  148. }
  149. }
  150. }
  151. AvaloniaFormattedTextLine line = new AvaloniaFormattedTextLine();
  152. line.Start = curOff;
  153. line.Length = measured;
  154. line.Width = lineWidth;
  155. line.Height = lineHeight;
  156. line.Top = curY;
  157. if (line.Width < 0)
  158. line.Width = _skiaRects[line.Start + line.Length - 1].Right;
  159. // Build character rects
  160. for (int i = line.Start; i < line.Start + line.Length; i++)
  161. {
  162. float prevRight = 0;
  163. if (i != line.Start)
  164. prevRight = _skiaRects[i - 1].Right;
  165. subString = _text.Substring(line.Start, i - line.Start + 1);
  166. float w = Paint.MeasureText(subString);
  167. SKRect rc;
  168. rc.Left = prevRight;
  169. rc.Right = w;
  170. rc.Top = line.Top;
  171. rc.Bottom = line.Top + line.Height;
  172. _skiaRects[i] = rc;
  173. }
  174. subString = _text.Substring(line.Start, line.Length);
  175. line.Width = Paint.MeasureText(subString);
  176. _skiaLines.Add(line);
  177. curY += lineHeight;
  178. // TODO: We may want to consider adding Leading to the vertical line spacing but for now
  179. // it appears to make no difference. Revisit as part of FormattedText improvements.
  180. //
  181. //curY += mLeading;
  182. curOff += measured + extraSkip;
  183. }
  184. // Now convert to Avalonia data formats
  185. _lines.Clear();
  186. _rects.Clear();
  187. float maxX = 0;
  188. for (var c = 0; c < _skiaLines.Count; c++)
  189. {
  190. var w = _skiaLines[c].Width;
  191. if (maxX < w)
  192. maxX = w;
  193. _lines.Add(new FormattedTextLine(_skiaLines[c].Length, _skiaLines[c].Height));
  194. }
  195. for (var c = 0; c < _text.Length; c++)
  196. {
  197. _rects.Add(_skiaRects[c].ToAvaloniaRect());
  198. }
  199. if (_skiaLines.Count == 0)
  200. {
  201. _size = new Size();
  202. }
  203. else
  204. {
  205. var lastLine = _skiaLines[_skiaLines.Count - 1];
  206. _size = new Size(maxX, lastLine.Top + lastLine.Height);
  207. }
  208. }
  209. internal void Draw(SKCanvas canvas, SKPoint origin, DrawingContextImpl.PaintWrapper foreground)
  210. {
  211. SKPaint paint = Paint;
  212. /* TODO: This originated from Native code, it might be useful for debugging character positions as
  213. * we improve the FormattedText support. Will need to port this to C# obviously. Rmove when
  214. * not needed anymore.
  215. SkPaint dpaint;
  216. ctx->Canvas->save();
  217. ctx->Canvas->translate(origin.fX, origin.fY);
  218. for (int c = 0; c < Lines.size(); c++)
  219. {
  220. dpaint.setARGB(255, 0, 0, 0);
  221. SkRect rc;
  222. rc.fLeft = 0;
  223. rc.fTop = Lines[c].Top;
  224. rc.fRight = Lines[c].Width;
  225. rc.fBottom = rc.fTop + LineOffset;
  226. ctx->Canvas->drawRect(rc, dpaint);
  227. }
  228. for (int c = 0; c < Length; c++)
  229. {
  230. dpaint.setARGB(255, c % 10 * 125 / 10 + 125, (c * 7) % 10 * 250 / 10, (c * 13) % 10 * 250 / 10);
  231. dpaint.setStyle(SkPaint::kFill_Style);
  232. ctx->Canvas->drawRect(Rects[c], dpaint);
  233. }
  234. ctx->Canvas->restore();
  235. */
  236. using (foreground.ApplyTo(paint))
  237. {
  238. for (int c = 0; c < _skiaLines.Count; c++)
  239. {
  240. AvaloniaFormattedTextLine line = _skiaLines[c];
  241. var subString = _text.Substring(line.Start, line.Length);
  242. float x = 0;
  243. //this is a quick fix so we have skia rendering
  244. //properly right and center align
  245. //TODO: find a better implementation including
  246. //hittesting and text selection working properly
  247. switch (Paint.TextAlign)
  248. {
  249. case SKTextAlign.Left: x = origin.X; break;
  250. case SKTextAlign.Center: x = origin.X + line.Width; break;
  251. case SKTextAlign.Right: x = origin.X + line.Width * 2; break;
  252. }
  253. canvas.DrawText(subString, x, origin.Y + line.Top + LineOffset, paint);
  254. }
  255. }
  256. }
  257. Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity);
  258. public Size Constraint
  259. {
  260. get { return _constraint; }
  261. set
  262. {
  263. if (_constraint == value)
  264. return;
  265. _constraint = value;
  266. WidthConstraint = (_constraint.Width != double.PositiveInfinity)
  267. ? (float)_constraint.Width
  268. : -1;
  269. Rebuild();
  270. }
  271. }
  272. public override string ToString()
  273. {
  274. return _text;
  275. }
  276. public void Dispose()
  277. {
  278. }
  279. }
  280. }