FormattedTextImpl.cs 22 KB

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