FormattedTextImpl.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  1. // Copyright (c) The Avalonia Project. All rights reserved.
  2. // Licensed under the MIT license. See licence.md file in the project root for full license information.
  3. using Avalonia.Media;
  4. using Avalonia.Platform;
  5. using SkiaSharp;
  6. using System;
  7. using System.Collections.Generic;
  8. using System.Linq;
  9. namespace Avalonia.Skia
  10. {
  11. public class FormattedTextImpl : IFormattedTextImpl
  12. {
  13. public FormattedTextImpl(
  14. string text,
  15. Typeface typeface,
  16. TextAlignment textAlignment,
  17. TextWrapping wrapping,
  18. Size constraint,
  19. IReadOnlyList<FormattedTextStyleSpan> spans)
  20. {
  21. Text = text ?? string.Empty;
  22. // Replace 0 characters with zero-width spaces (200B)
  23. Text = Text.Replace((char)0, (char)0x200B);
  24. var skiaTypeface = TypefaceCache.GetTypeface(
  25. typeface?.FontFamilyName ?? "monospace",
  26. typeface?.Style ?? FontStyle.Normal,
  27. typeface?.Weight ?? FontWeight.Normal);
  28. _paint = new SKPaint();
  29. //currently Skia does not measure properly with Utf8 !!!
  30. //Paint.TextEncoding = SKTextEncoding.Utf8;
  31. _paint.TextEncoding = SKTextEncoding.Utf16;
  32. _paint.IsStroke = false;
  33. _paint.IsAntialias = true;
  34. _paint.LcdRenderText = true;
  35. _paint.SubpixelText = true;
  36. _paint.Typeface = skiaTypeface;
  37. _paint.TextSize = (float)(typeface?.FontSize ?? 12);
  38. _paint.TextAlign = textAlignment.ToSKTextAlign();
  39. _paint.BlendMode = SKBlendMode.Src;
  40. _wrapping = wrapping;
  41. _constraint = constraint;
  42. if (spans != null)
  43. {
  44. foreach (var span in spans)
  45. {
  46. if (span.ForegroundBrush != null)
  47. {
  48. SetForegroundBrush(span.ForegroundBrush, span.StartIndex, span.Length);
  49. }
  50. }
  51. }
  52. Rebuild();
  53. }
  54. public Size Constraint => _constraint;
  55. public Size Size => _size;
  56. public IEnumerable<FormattedTextLine> GetLines()
  57. {
  58. return _lines;
  59. }
  60. public TextHitTestResult HitTestPoint(Point point)
  61. {
  62. float y = (float)point.Y;
  63. var line = _skiaLines.Find(l => l.Top <= y && (l.Top + l.Height) > y);
  64. if (!line.Equals(default(AvaloniaFormattedTextLine)))
  65. {
  66. var rects = GetRects();
  67. for (int c = line.Start; c < line.Start + line.TextLength; c++)
  68. {
  69. var rc = rects[c];
  70. if (rc.Contains(point))
  71. {
  72. return new TextHitTestResult
  73. {
  74. IsInside = !(line.TextLength > line.Length),
  75. TextPosition = c,
  76. IsTrailing = (point.X - rc.X) > rc.Width / 2
  77. };
  78. }
  79. }
  80. int offset = 0;
  81. if (point.X >= (rects[line.Start].X + line.Width) / 2 && line.Length > 0)
  82. {
  83. offset = line.TextLength > line.Length ?
  84. line.Length : (line.Length - 1);
  85. }
  86. return new TextHitTestResult
  87. {
  88. IsInside = false,
  89. TextPosition = line.Start + offset,
  90. IsTrailing = Text.Length == (line.Start + offset + 1)
  91. };
  92. }
  93. bool end = point.X > _size.Width || point.Y > _lines.Sum(l => l.Height);
  94. return new TextHitTestResult()
  95. {
  96. IsInside = false,
  97. IsTrailing = end,
  98. TextPosition = end ? Text.Length - 1 : 0
  99. };
  100. }
  101. public Rect HitTestTextPosition(int index)
  102. {
  103. var rects = GetRects();
  104. if (index < 0 || index >= rects.Count)
  105. {
  106. var r = rects.LastOrDefault();
  107. return new Rect(r.X + r.Width, r.Y, 0, _lineHeight);
  108. }
  109. if (rects.Count == 0)
  110. {
  111. return new Rect(0, 0, 1, _lineHeight);
  112. }
  113. if (index == rects.Count)
  114. {
  115. var lr = rects[rects.Count - 1];
  116. return new Rect(new Point(lr.X + lr.Width, lr.Y), rects[index - 1].Size);
  117. }
  118. return rects[index];
  119. }
  120. public IEnumerable<Rect> HitTestTextRange(int index, int length)
  121. {
  122. List<Rect> result = new List<Rect>();
  123. var rects = GetRects();
  124. int lastIndex = index + length - 1;
  125. foreach (var line in _skiaLines.Where(l =>
  126. (l.Start + l.Length) > index &&
  127. lastIndex >= l.Start))
  128. {
  129. int lineEndIndex = line.Start + (line.Length > 0 ? line.Length - 1 : 0);
  130. double left = rects[line.Start > index ? line.Start : index].X;
  131. double right = rects[lineEndIndex > lastIndex ? lastIndex : lineEndIndex].Right;
  132. result.Add(new Rect(left, line.Top, right - left, line.Height));
  133. }
  134. return result;
  135. }
  136. public override string ToString()
  137. {
  138. return Text;
  139. }
  140. internal void Draw(DrawingContextImpl context,
  141. SKCanvas canvas, SKPoint origin,
  142. DrawingContextImpl.PaintWrapper foreground)
  143. {
  144. /* TODO: This originated from Native code, it might be useful for debugging character positions as
  145. * we improve the FormattedText support. Will need to port this to C# obviously. Rmove when
  146. * not needed anymore.
  147. SkPaint dpaint;
  148. ctx->Canvas->save();
  149. ctx->Canvas->translate(origin.fX, origin.fY);
  150. for (int c = 0; c < Lines.size(); c++)
  151. {
  152. dpaint.setARGB(255, 0, 0, 0);
  153. SkRect rc;
  154. rc.fLeft = 0;
  155. rc.fTop = Lines[c].Top;
  156. rc.fRight = Lines[c].Width;
  157. rc.fBottom = rc.fTop + LineOffset;
  158. ctx->Canvas->drawRect(rc, dpaint);
  159. }
  160. for (int c = 0; c < Length; c++)
  161. {
  162. dpaint.setARGB(255, c % 10 * 125 / 10 + 125, (c * 7) % 10 * 250 / 10, (c * 13) % 10 * 250 / 10);
  163. dpaint.setStyle(SkPaint::kFill_Style);
  164. ctx->Canvas->drawRect(Rects[c], dpaint);
  165. }
  166. ctx->Canvas->restore();
  167. */
  168. SKPaint paint = _paint;
  169. IDisposable currd = null;
  170. var currentWrapper = foreground;
  171. try
  172. {
  173. SKPaint currFGPaint = ApplyWrapperTo(ref foreground, ref currd, paint);
  174. bool hasCusomFGBrushes = _foregroundBrushes.Any();
  175. for (int c = 0; c < _skiaLines.Count; c++)
  176. {
  177. AvaloniaFormattedTextLine line = _skiaLines[c];
  178. float x = TransformX(origin.X, 0, paint.TextAlign);
  179. if (!hasCusomFGBrushes)
  180. {
  181. var subString = Text.Substring(line.Start, line.Length);
  182. canvas.DrawText(subString, x, origin.Y + line.Top + _lineOffset, paint);
  183. }
  184. else
  185. {
  186. float currX = x;
  187. string subStr;
  188. int len;
  189. for (int i = line.Start; i < line.Start + line.Length;)
  190. {
  191. var fb = GetNextForegroundBrush(ref line, i, out len);
  192. if (fb != null)
  193. {
  194. //TODO: figure out how to get the brush size
  195. currentWrapper = context.CreatePaint(fb, new Size());
  196. }
  197. else
  198. {
  199. if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose();
  200. currentWrapper = foreground;
  201. }
  202. subStr = Text.Substring(i, len);
  203. if (currFGPaint != currentWrapper.Paint)
  204. {
  205. currFGPaint = ApplyWrapperTo(ref currentWrapper, ref currd, paint);
  206. }
  207. canvas.DrawText(subStr, currX, origin.Y + line.Top + _lineOffset, paint);
  208. i += len;
  209. currX += paint.MeasureText(subStr);
  210. }
  211. }
  212. }
  213. }
  214. finally
  215. {
  216. if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose();
  217. currd?.Dispose();
  218. }
  219. }
  220. private const float MAX_LINE_WIDTH = 10000;
  221. private readonly List<KeyValuePair<FBrushRange, IBrush>> _foregroundBrushes =
  222. new List<KeyValuePair<FBrushRange, IBrush>>();
  223. private readonly List<FormattedTextLine> _lines = new List<FormattedTextLine>();
  224. private readonly SKPaint _paint;
  225. private readonly List<Rect> _rects = new List<Rect>();
  226. public string Text { get; }
  227. private readonly TextWrapping _wrapping;
  228. private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity);
  229. private float _lineHeight = 0;
  230. private float _lineOffset = 0;
  231. private Size _size;
  232. private List<AvaloniaFormattedTextLine> _skiaLines;
  233. private static SKPaint ApplyWrapperTo(ref DrawingContextImpl.PaintWrapper wrapper,
  234. ref IDisposable curr, SKPaint paint)
  235. {
  236. curr?.Dispose();
  237. curr = wrapper.ApplyTo(paint);
  238. return wrapper.Paint;
  239. }
  240. private static bool IsBreakChar(char c)
  241. {
  242. //white space or zero space whitespace
  243. return char.IsWhiteSpace(c) || c == '\u200B';
  244. }
  245. private static int LineBreak(string textInput, int textIndex, int stop,
  246. SKPaint paint, float maxWidth,
  247. out int trailingCount)
  248. {
  249. int lengthBreak;
  250. if (maxWidth == -1)
  251. {
  252. lengthBreak = stop - textIndex;
  253. }
  254. else
  255. {
  256. float measuredWidth;
  257. string subText = textInput.Substring(textIndex, stop - textIndex);
  258. lengthBreak = (int)paint.BreakText(subText, maxWidth, out measuredWidth) / 2;
  259. }
  260. //Check for white space or line breakers before the lengthBreak
  261. int startIndex = textIndex;
  262. int index = textIndex;
  263. int word_start = textIndex;
  264. bool prevBreak = true;
  265. trailingCount = 0;
  266. while (index < stop)
  267. {
  268. int prevText = index;
  269. char currChar = textInput[index++];
  270. bool currBreak = IsBreakChar(currChar);
  271. if (!currBreak && prevBreak)
  272. {
  273. word_start = prevText;
  274. }
  275. prevBreak = currBreak;
  276. if (index > startIndex + lengthBreak)
  277. {
  278. if (currBreak)
  279. {
  280. // eat the rest of the whitespace
  281. while (index < stop && IsBreakChar(textInput[index]))
  282. {
  283. index++;
  284. }
  285. trailingCount = index - prevText;
  286. }
  287. else
  288. {
  289. // backup until a whitespace (or 1 char)
  290. if (word_start == startIndex)
  291. {
  292. if (prevText > startIndex)
  293. {
  294. index = prevText;
  295. }
  296. }
  297. else
  298. {
  299. index = word_start;
  300. }
  301. }
  302. break;
  303. }
  304. if ('\n' == currChar)
  305. {
  306. int ret = index - startIndex;
  307. int lineBreakSize = 1;
  308. if (index < stop)
  309. {
  310. currChar = textInput[index++];
  311. if ('\r' == currChar)
  312. {
  313. ret = index - startIndex;
  314. ++lineBreakSize;
  315. }
  316. }
  317. trailingCount = lineBreakSize;
  318. return ret;
  319. }
  320. if ('\r' == currChar)
  321. {
  322. int ret = index - startIndex;
  323. int lineBreakSize = 1;
  324. if (index < stop)
  325. {
  326. currChar = textInput[index++];
  327. if ('\n' == currChar)
  328. {
  329. ret = index - startIndex;
  330. ++lineBreakSize;
  331. }
  332. }
  333. trailingCount = lineBreakSize;
  334. return ret;
  335. }
  336. }
  337. return index - startIndex;
  338. }
  339. private void BuildRects()
  340. {
  341. // Build character rects
  342. var fm = _paint.FontMetrics;
  343. SKTextAlign align = _paint.TextAlign;
  344. for (int li = 0; li < _skiaLines.Count; li++)
  345. {
  346. var line = _skiaLines[li];
  347. float prevRight = TransformX(0, line.Width, align);
  348. double nextTop = line.Top + line.Height;
  349. if (li + 1 < _skiaLines.Count)
  350. {
  351. nextTop = _skiaLines[li + 1].Top;
  352. }
  353. for (int i = line.Start; i < line.Start + line.TextLength; i++)
  354. {
  355. float w = _paint.MeasureText(Text[i].ToString());
  356. _rects.Add(new Rect(
  357. prevRight,
  358. line.Top,
  359. w,
  360. nextTop - line.Top));
  361. prevRight += w;
  362. }
  363. }
  364. }
  365. private IBrush GetNextForegroundBrush(ref AvaloniaFormattedTextLine line, int index, out int length)
  366. {
  367. IBrush result = null;
  368. int len = length = line.Start + line.Length - index;
  369. if (_foregroundBrushes.Any())
  370. {
  371. var bi = _foregroundBrushes.FindIndex(b =>
  372. b.Key.StartIndex <= index &&
  373. b.Key.EndIndex > index
  374. );
  375. if (bi > -1)
  376. {
  377. var match = _foregroundBrushes[bi];
  378. len = match.Key.EndIndex - index + 1;
  379. result = match.Value;
  380. if (len > 0 && len < length)
  381. {
  382. length = len;
  383. }
  384. }
  385. int endIndex = index + length;
  386. int max = bi == -1 ? _foregroundBrushes.Count : bi;
  387. var next = _foregroundBrushes.Take(max)
  388. .Where(b => b.Key.StartIndex < endIndex &&
  389. b.Key.StartIndex > index)
  390. .OrderBy(b => b.Key.StartIndex)
  391. .FirstOrDefault();
  392. if (next.Value != null)
  393. {
  394. length = next.Key.StartIndex - index;
  395. }
  396. }
  397. return result;
  398. }
  399. private List<Rect> GetRects()
  400. {
  401. if (Text.Length > _rects.Count)
  402. {
  403. BuildRects();
  404. }
  405. return _rects;
  406. }
  407. private void Rebuild()
  408. {
  409. var length = Text.Length;
  410. _lines.Clear();
  411. _rects.Clear();
  412. _skiaLines = new List<AvaloniaFormattedTextLine>();
  413. int curOff = 0;
  414. float curY = 0;
  415. var metrics = _paint.FontMetrics;
  416. var mTop = metrics.Top; // The greatest distance above the baseline for any glyph (will be <= 0).
  417. var mBottom = metrics.Bottom; // The greatest distance below the baseline for any glyph (will be >= 0).
  418. var mLeading = metrics.Leading; // The recommended distance to add between lines of text (will be >= 0).
  419. var mDescent = metrics.Descent; //The recommended distance below the baseline. Will be >= 0.
  420. var mAscent = metrics.Ascent; //The recommended distance above the baseline. Will be <= 0.
  421. var lastLineDescent = mBottom - mDescent;
  422. // This seems like the best measure of full vertical extent
  423. // matches Direct2D line height
  424. _lineHeight = mDescent - mAscent;
  425. // Rendering is relative to baseline
  426. _lineOffset = (-metrics.Ascent);
  427. string subString;
  428. float widthConstraint = (_constraint.Width != double.PositiveInfinity)
  429. ? (float)_constraint.Width
  430. : -1;
  431. for (int c = 0; curOff < length; c++)
  432. {
  433. float lineWidth = -1;
  434. int measured;
  435. int trailingnumber = 0;
  436. subString = Text.Substring(curOff);
  437. float constraint = -1;
  438. if (_wrapping == TextWrapping.Wrap)
  439. {
  440. constraint = widthConstraint <= 0 ? MAX_LINE_WIDTH : widthConstraint;
  441. if (constraint > MAX_LINE_WIDTH)
  442. constraint = MAX_LINE_WIDTH;
  443. }
  444. measured = LineBreak(Text, curOff, length, _paint, constraint, out trailingnumber);
  445. AvaloniaFormattedTextLine line = new AvaloniaFormattedTextLine();
  446. line.TextLength = measured;
  447. subString = Text.Substring(line.Start, line.TextLength);
  448. lineWidth = _paint.MeasureText(subString);
  449. line.Start = curOff;
  450. line.Length = measured - trailingnumber;
  451. line.Width = lineWidth;
  452. line.Height = _lineHeight;
  453. line.Top = curY;
  454. _skiaLines.Add(line);
  455. curY += _lineHeight;
  456. curY += mLeading;
  457. curOff += measured;
  458. }
  459. // Now convert to Avalonia data formats
  460. _lines.Clear();
  461. float maxX = 0;
  462. for (var c = 0; c < _skiaLines.Count; c++)
  463. {
  464. var w = _skiaLines[c].Width;
  465. if (maxX < w)
  466. maxX = w;
  467. _lines.Add(new FormattedTextLine(_skiaLines[c].TextLength, _skiaLines[c].Height));
  468. }
  469. if (_skiaLines.Count == 0)
  470. {
  471. _lines.Add(new FormattedTextLine(0, _lineHeight));
  472. _size = new Size(0, _lineHeight);
  473. }
  474. else
  475. {
  476. var lastLine = _skiaLines[_skiaLines.Count - 1];
  477. _size = new Size(maxX, lastLine.Top + lastLine.Height);
  478. }
  479. }
  480. private float TransformX(float originX, float lineWidth, SKTextAlign align)
  481. {
  482. float x = 0;
  483. if (align == SKTextAlign.Left)
  484. {
  485. x = originX;
  486. }
  487. else
  488. {
  489. double width = Constraint.Width > 0 && !double.IsPositiveInfinity(Constraint.Width) ?
  490. Constraint.Width :
  491. _size.Width;
  492. switch (align)
  493. {
  494. case SKTextAlign.Center: x = originX + (float)(width - lineWidth) / 2; break;
  495. case SKTextAlign.Right: x = originX + (float)(width - lineWidth); break;
  496. }
  497. }
  498. return x;
  499. }
  500. private void SetForegroundBrush(IBrush brush, int startIndex, int length)
  501. {
  502. var key = new FBrushRange(startIndex, length);
  503. int index = _foregroundBrushes.FindIndex(v => v.Key.Equals(key));
  504. if (index > -1)
  505. {
  506. _foregroundBrushes.RemoveAt(index);
  507. }
  508. if (brush != null)
  509. {
  510. _foregroundBrushes.Insert(0, new KeyValuePair<FBrushRange, IBrush>(key, brush));
  511. }
  512. }
  513. private struct AvaloniaFormattedTextLine
  514. {
  515. public float Height;
  516. public int Length;
  517. public int Start;
  518. public int TextLength;
  519. public float Top;
  520. public float Width;
  521. };
  522. private struct FBrushRange
  523. {
  524. public FBrushRange(int startIndex, int length)
  525. {
  526. StartIndex = startIndex;
  527. Length = length;
  528. }
  529. public int EndIndex => StartIndex + Length - 1;
  530. public int Length { get; private set; }
  531. public int StartIndex { get; private set; }
  532. public bool Intersects(int index, int len) =>
  533. (index + len) > StartIndex &&
  534. (StartIndex + Length) > index;
  535. public override string ToString()
  536. {
  537. return $"{StartIndex}-{EndIndex}";
  538. }
  539. }
  540. }
  541. }