| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657 |
- // Copyright (c) The Avalonia Project. All rights reserved.
- // Licensed under the MIT license. See licence.md file in the project root for full license information.
- using Avalonia.Media;
- using Avalonia.Platform;
- using SkiaSharp;
- using System;
- using System.Collections.Generic;
- using System.Linq;
- namespace Avalonia.Skia
- {
- public class FormattedTextImpl : IFormattedTextImpl
- {
- public FormattedTextImpl(
- string text,
- Typeface typeface,
- TextAlignment textAlignment,
- TextWrapping wrapping,
- Size constraint,
- IReadOnlyList<FormattedTextStyleSpan> spans)
- {
- Text = text ?? string.Empty;
- // Replace 0 characters with zero-width spaces (200B)
- Text = Text.Replace((char)0, (char)0x200B);
- var skiaTypeface = TypefaceCache.GetTypeface(
- typeface?.FontFamilyName ?? "monospace",
- typeface?.Style ?? FontStyle.Normal,
- typeface?.Weight ?? FontWeight.Normal);
- _paint = new SKPaint();
- //currently Skia does not measure properly with Utf8 !!!
- //Paint.TextEncoding = SKTextEncoding.Utf8;
- _paint.TextEncoding = SKTextEncoding.Utf16;
- _paint.IsStroke = false;
- _paint.IsAntialias = true;
- _paint.LcdRenderText = true;
- _paint.SubpixelText = true;
- _paint.Typeface = skiaTypeface;
- _paint.TextSize = (float)(typeface?.FontSize ?? 12);
- _paint.TextAlign = textAlignment.ToSKTextAlign();
- _paint.BlendMode = SKBlendMode.Src;
- _wrapping = wrapping;
- _constraint = constraint;
- if (spans != null)
- {
- foreach (var span in spans)
- {
- if (span.ForegroundBrush != null)
- {
- SetForegroundBrush(span.ForegroundBrush, span.StartIndex, span.Length);
- }
- }
- }
- Rebuild();
- }
- public Size Constraint => _constraint;
- public Size Size => _size;
- public IEnumerable<FormattedTextLine> GetLines()
- {
- return _lines;
- }
- public TextHitTestResult HitTestPoint(Point point)
- {
- float y = (float)point.Y;
- var line = _skiaLines.Find(l => l.Top <= y && (l.Top + l.Height) > y);
- if (!line.Equals(default(AvaloniaFormattedTextLine)))
- {
- var rects = GetRects();
- for (int c = line.Start; c < line.Start + line.TextLength; c++)
- {
- var rc = rects[c];
- if (rc.Contains(point))
- {
- return new TextHitTestResult
- {
- IsInside = !(line.TextLength > line.Length),
- TextPosition = c,
- IsTrailing = (point.X - rc.X) > rc.Width / 2
- };
- }
- }
- int offset = 0;
- if (point.X >= (rects[line.Start].X + line.Width) / 2 && line.Length > 0)
- {
- offset = line.TextLength > line.Length ?
- line.Length : (line.Length - 1);
- }
- return new TextHitTestResult
- {
- IsInside = false,
- TextPosition = line.Start + offset,
- IsTrailing = Text.Length == (line.Start + offset + 1)
- };
- }
- bool end = point.X > _size.Width || point.Y > _lines.Sum(l => l.Height);
- return new TextHitTestResult()
- {
- IsInside = false,
- IsTrailing = end,
- TextPosition = end ? Text.Length - 1 : 0
- };
- }
- public Rect HitTestTextPosition(int index)
- {
- var rects = GetRects();
- if (index < 0 || index >= rects.Count)
- {
- var r = rects.LastOrDefault();
- return new Rect(r.X + r.Width, r.Y, 0, _lineHeight);
- }
- if (rects.Count == 0)
- {
- return new Rect(0, 0, 1, _lineHeight);
- }
- if (index == rects.Count)
- {
- var lr = rects[rects.Count - 1];
- return new Rect(new Point(lr.X + lr.Width, lr.Y), rects[index - 1].Size);
- }
- return rects[index];
- }
- public IEnumerable<Rect> HitTestTextRange(int index, int length)
- {
- List<Rect> result = new List<Rect>();
- var rects = GetRects();
- int lastIndex = index + length - 1;
- foreach (var line in _skiaLines.Where(l =>
- (l.Start + l.Length) > index &&
- lastIndex >= l.Start))
- {
- int lineEndIndex = line.Start + (line.Length > 0 ? line.Length - 1 : 0);
- double left = rects[line.Start > index ? line.Start : index].X;
- double right = rects[lineEndIndex > lastIndex ? lastIndex : lineEndIndex].Right;
- result.Add(new Rect(left, line.Top, right - left, line.Height));
- }
- return result;
- }
- public override string ToString()
- {
- return Text;
- }
- internal void Draw(DrawingContextImpl context,
- SKCanvas canvas, SKPoint origin,
- DrawingContextImpl.PaintWrapper foreground)
- {
- /* TODO: This originated from Native code, it might be useful for debugging character positions as
- * we improve the FormattedText support. Will need to port this to C# obviously. Rmove when
- * not needed anymore.
- SkPaint dpaint;
- ctx->Canvas->save();
- ctx->Canvas->translate(origin.fX, origin.fY);
- for (int c = 0; c < Lines.size(); c++)
- {
- dpaint.setARGB(255, 0, 0, 0);
- SkRect rc;
- rc.fLeft = 0;
- rc.fTop = Lines[c].Top;
- rc.fRight = Lines[c].Width;
- rc.fBottom = rc.fTop + LineOffset;
- ctx->Canvas->drawRect(rc, dpaint);
- }
- for (int c = 0; c < Length; c++)
- {
- dpaint.setARGB(255, c % 10 * 125 / 10 + 125, (c * 7) % 10 * 250 / 10, (c * 13) % 10 * 250 / 10);
- dpaint.setStyle(SkPaint::kFill_Style);
- ctx->Canvas->drawRect(Rects[c], dpaint);
- }
- ctx->Canvas->restore();
- */
- SKPaint paint = _paint;
- IDisposable currd = null;
- var currentWrapper = foreground;
- try
- {
- SKPaint currFGPaint = ApplyWrapperTo(ref foreground, ref currd, paint);
- bool hasCusomFGBrushes = _foregroundBrushes.Any();
- for (int c = 0; c < _skiaLines.Count; c++)
- {
- AvaloniaFormattedTextLine line = _skiaLines[c];
- float x = TransformX(origin.X, 0, paint.TextAlign);
- if (!hasCusomFGBrushes)
- {
- var subString = Text.Substring(line.Start, line.Length);
- canvas.DrawText(subString, x, origin.Y + line.Top + _lineOffset, paint);
- }
- else
- {
- float currX = x;
- string subStr;
- int len;
- for (int i = line.Start; i < line.Start + line.Length;)
- {
- var fb = GetNextForegroundBrush(ref line, i, out len);
- if (fb != null)
- {
- //TODO: figure out how to get the brush size
- currentWrapper = context.CreatePaint(fb, new Size());
- }
- else
- {
- if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose();
- currentWrapper = foreground;
- }
- subStr = Text.Substring(i, len);
- if (currFGPaint != currentWrapper.Paint)
- {
- currFGPaint = ApplyWrapperTo(ref currentWrapper, ref currd, paint);
- }
- canvas.DrawText(subStr, currX, origin.Y + line.Top + _lineOffset, paint);
- i += len;
- currX += paint.MeasureText(subStr);
- }
- }
- }
- }
- finally
- {
- if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose();
- currd?.Dispose();
- }
- }
- private const float MAX_LINE_WIDTH = 10000;
- private readonly List<KeyValuePair<FBrushRange, IBrush>> _foregroundBrushes =
- new List<KeyValuePair<FBrushRange, IBrush>>();
- private readonly List<FormattedTextLine> _lines = new List<FormattedTextLine>();
- private readonly SKPaint _paint;
- private readonly List<Rect> _rects = new List<Rect>();
- public string Text { get; }
- private readonly TextWrapping _wrapping;
- private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity);
- private float _lineHeight = 0;
- private float _lineOffset = 0;
- private Size _size;
- private List<AvaloniaFormattedTextLine> _skiaLines;
- private static SKPaint ApplyWrapperTo(ref DrawingContextImpl.PaintWrapper wrapper,
- ref IDisposable curr, SKPaint paint)
- {
- curr?.Dispose();
- curr = wrapper.ApplyTo(paint);
- return wrapper.Paint;
- }
- private static bool IsBreakChar(char c)
- {
- //white space or zero space whitespace
- return char.IsWhiteSpace(c) || c == '\u200B';
- }
- private static int LineBreak(string textInput, int textIndex, int stop,
- SKPaint paint, float maxWidth,
- out int trailingCount)
- {
- int lengthBreak;
- if (maxWidth == -1)
- {
- lengthBreak = stop - textIndex;
- }
- else
- {
- float measuredWidth;
- string subText = textInput.Substring(textIndex, stop - textIndex);
- lengthBreak = (int)paint.BreakText(subText, maxWidth, out measuredWidth) / 2;
- }
- //Check for white space or line breakers before the lengthBreak
- int startIndex = textIndex;
- int index = textIndex;
- int word_start = textIndex;
- bool prevBreak = true;
- trailingCount = 0;
- while (index < stop)
- {
- int prevText = index;
- char currChar = textInput[index++];
- bool currBreak = IsBreakChar(currChar);
- if (!currBreak && prevBreak)
- {
- word_start = prevText;
- }
- prevBreak = currBreak;
- if (index > startIndex + lengthBreak)
- {
- if (currBreak)
- {
- // eat the rest of the whitespace
- while (index < stop && IsBreakChar(textInput[index]))
- {
- index++;
- }
- trailingCount = index - prevText;
- }
- else
- {
- // backup until a whitespace (or 1 char)
- if (word_start == startIndex)
- {
- if (prevText > startIndex)
- {
- index = prevText;
- }
- }
- else
- {
- index = word_start;
- }
- }
- break;
- }
- if ('\n' == currChar)
- {
- int ret = index - startIndex;
- int lineBreakSize = 1;
- if (index < stop)
- {
- currChar = textInput[index++];
- if ('\r' == currChar)
- {
- ret = index - startIndex;
- ++lineBreakSize;
- }
- }
- trailingCount = lineBreakSize;
- return ret;
- }
- if ('\r' == currChar)
- {
- int ret = index - startIndex;
- int lineBreakSize = 1;
- if (index < stop)
- {
- currChar = textInput[index++];
- if ('\n' == currChar)
- {
- ret = index - startIndex;
- ++lineBreakSize;
- }
- }
- trailingCount = lineBreakSize;
- return ret;
- }
- }
- return index - startIndex;
- }
- private void BuildRects()
- {
- // Build character rects
- var fm = _paint.FontMetrics;
- SKTextAlign align = _paint.TextAlign;
- for (int li = 0; li < _skiaLines.Count; li++)
- {
- var line = _skiaLines[li];
- float prevRight = TransformX(0, line.Width, align);
- double nextTop = line.Top + line.Height;
- if (li + 1 < _skiaLines.Count)
- {
- nextTop = _skiaLines[li + 1].Top;
- }
- for (int i = line.Start; i < line.Start + line.TextLength; i++)
- {
- float w = _paint.MeasureText(Text[i].ToString());
- _rects.Add(new Rect(
- prevRight,
- line.Top,
- w,
- nextTop - line.Top));
- prevRight += w;
- }
- }
- }
- private IBrush GetNextForegroundBrush(ref AvaloniaFormattedTextLine line, int index, out int length)
- {
- IBrush result = null;
- int len = length = line.Start + line.Length - index;
- if (_foregroundBrushes.Any())
- {
- var bi = _foregroundBrushes.FindIndex(b =>
- b.Key.StartIndex <= index &&
- b.Key.EndIndex > index
- );
- if (bi > -1)
- {
- var match = _foregroundBrushes[bi];
- len = match.Key.EndIndex - index + 1;
- result = match.Value;
- if (len > 0 && len < length)
- {
- length = len;
- }
- }
- int endIndex = index + length;
- int max = bi == -1 ? _foregroundBrushes.Count : bi;
- var next = _foregroundBrushes.Take(max)
- .Where(b => b.Key.StartIndex < endIndex &&
- b.Key.StartIndex > index)
- .OrderBy(b => b.Key.StartIndex)
- .FirstOrDefault();
- if (next.Value != null)
- {
- length = next.Key.StartIndex - index;
- }
- }
- return result;
- }
- private List<Rect> GetRects()
- {
- if (Text.Length > _rects.Count)
- {
- BuildRects();
- }
- return _rects;
- }
- private void Rebuild()
- {
- var length = Text.Length;
- _lines.Clear();
- _rects.Clear();
- _skiaLines = new List<AvaloniaFormattedTextLine>();
- int curOff = 0;
- float curY = 0;
- var metrics = _paint.FontMetrics;
- var mTop = metrics.Top; // The greatest distance above the baseline for any glyph (will be <= 0).
- var mBottom = metrics.Bottom; // The greatest distance below the baseline for any glyph (will be >= 0).
- var mLeading = metrics.Leading; // The recommended distance to add between lines of text (will be >= 0).
- var mDescent = metrics.Descent; //The recommended distance below the baseline. Will be >= 0.
- var mAscent = metrics.Ascent; //The recommended distance above the baseline. Will be <= 0.
- var lastLineDescent = mBottom - mDescent;
- // This seems like the best measure of full vertical extent
- // matches Direct2D line height
- _lineHeight = mDescent - mAscent;
- // Rendering is relative to baseline
- _lineOffset = (-metrics.Ascent);
- string subString;
- float widthConstraint = (_constraint.Width != double.PositiveInfinity)
- ? (float)_constraint.Width
- : -1;
- for (int c = 0; curOff < length; c++)
- {
- float lineWidth = -1;
- int measured;
- int trailingnumber = 0;
- subString = Text.Substring(curOff);
- float constraint = -1;
- if (_wrapping == TextWrapping.Wrap)
- {
- constraint = widthConstraint <= 0 ? MAX_LINE_WIDTH : widthConstraint;
- if (constraint > MAX_LINE_WIDTH)
- constraint = MAX_LINE_WIDTH;
- }
- measured = LineBreak(Text, curOff, length, _paint, constraint, out trailingnumber);
- AvaloniaFormattedTextLine line = new AvaloniaFormattedTextLine();
- line.TextLength = measured;
- subString = Text.Substring(line.Start, line.TextLength);
- lineWidth = _paint.MeasureText(subString);
- line.Start = curOff;
- line.Length = measured - trailingnumber;
- line.Width = lineWidth;
- line.Height = _lineHeight;
- line.Top = curY;
- _skiaLines.Add(line);
- curY += _lineHeight;
- curY += mLeading;
- curOff += measured;
- }
- // Now convert to Avalonia data formats
- _lines.Clear();
- float maxX = 0;
- for (var c = 0; c < _skiaLines.Count; c++)
- {
- var w = _skiaLines[c].Width;
- if (maxX < w)
- maxX = w;
- _lines.Add(new FormattedTextLine(_skiaLines[c].TextLength, _skiaLines[c].Height));
- }
- if (_skiaLines.Count == 0)
- {
- _lines.Add(new FormattedTextLine(0, _lineHeight));
- _size = new Size(0, _lineHeight);
- }
- else
- {
- var lastLine = _skiaLines[_skiaLines.Count - 1];
- _size = new Size(maxX, lastLine.Top + lastLine.Height);
- }
- }
- private float TransformX(float originX, float lineWidth, SKTextAlign align)
- {
- float x = 0;
- if (align == SKTextAlign.Left)
- {
- x = originX;
- }
- else
- {
- double width = Constraint.Width > 0 && !double.IsPositiveInfinity(Constraint.Width) ?
- Constraint.Width :
- _size.Width;
- switch (align)
- {
- case SKTextAlign.Center: x = originX + (float)(width - lineWidth) / 2; break;
- case SKTextAlign.Right: x = originX + (float)(width - lineWidth); break;
- }
- }
- return x;
- }
- private void SetForegroundBrush(IBrush brush, int startIndex, int length)
- {
- var key = new FBrushRange(startIndex, length);
- int index = _foregroundBrushes.FindIndex(v => v.Key.Equals(key));
- if (index > -1)
- {
- _foregroundBrushes.RemoveAt(index);
- }
- if (brush != null)
- {
- _foregroundBrushes.Insert(0, new KeyValuePair<FBrushRange, IBrush>(key, brush));
- }
- }
- private struct AvaloniaFormattedTextLine
- {
- public float Height;
- public int Length;
- public int Start;
- public int TextLength;
- public float Top;
- public float Width;
- };
- private struct FBrushRange
- {
- public FBrushRange(int startIndex, int length)
- {
- StartIndex = startIndex;
- Length = length;
- }
- public int EndIndex => StartIndex + Length - 1;
- public int Length { get; private set; }
- public int StartIndex { get; private set; }
- public bool Intersects(int index, int len) =>
- (index + len) > StartIndex &&
- (StartIndex + Length) > index;
- public override string ToString()
- {
- return $"{StartIndex}-{EndIndex}";
- }
- }
- }
- }
|