Exemplo n.º 1
0
        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 cbi = _foregroundBrushes.FirstOrDefault(b => b.Key.Intersects(index, len));

                if (cbi.Value != null)
                {
                    var r = cbi.Key;

                    if (r.StartIndex > index)
                    {
                        len = r.StartIndex - index;
                    }
                    else
                    {
                        len    = r.EndIndex - index + 1;
                        result = cbi.Value;
                    }

                    if (len > 0 && len < length)
                    {
                        length = len;
                    }
                }
            }

            return(result);
        }
Exemplo n.º 2
0
        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;
                    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);
        }
Exemplo n.º 3
0
        internal void Draw(SKCanvas canvas, SKPoint origin)
        {
            SKPaint paint = Paint;

            /* 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();
             */

            for (int c = 0; c < _skiaLines.Count; c++)
            {
                AvaloniaFormattedTextLine line = _skiaLines[c];
                var subString = _text.Substring(line.Start, line.Length);
                canvas.DrawText(subString, origin.X, origin.Y + line.Top + LineOffset, paint);
            }
        }
Exemplo n.º 4
0
        public TextHitTestResult HitTestPoint(Point point)
        {
            float y = (float)point.Y;

            AvaloniaFormattedTextLine line = default;

            float nextTop = 0;

            foreach (var currentLine in _skiaLines)
            {
                if (currentLine.Top <= y)
                {
                    line    = currentLine;
                    nextTop = currentLine.Top + currentLine.Height;
                }
                else
                {
                    nextTop = currentLine.Top;
                    break;
                }
            }

            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) && line.Length > 0)
                {
                    offset = line.TextLength > line.Length ?
                             line.Length : (line.Length - 1);
                }

                if (y < nextTop)
                {
                    return(new TextHitTestResult
                    {
                        IsInside = false,
                        TextPosition = line.Start + offset,
                        IsTrailing = Text.Length == (line.Start + offset + 1)
                    });
                }
            }

            bool end = point.X > _bounds.Width || point.Y > _lines.Sum(l => l.Height);

            return(new TextHitTestResult()
            {
                IsInside = false,
                IsTrailing = end,
                TextPosition = end ? Text.Length - 1 : 0
            });
        }
Exemplo n.º 5
0
        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 = double.IsPositiveInfinity(_constraint.Width)
                                        ? -1
                                        : (float)_constraint.Width;

            while (curOff < length)
            {
                float lineWidth = -1;
                int   measured;
                int   trailingnumber = 0;

                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.Start      = curOff;
                line.TextLength = measured;
                subString       = Text.Substring(line.Start, line.TextLength);
                lineWidth       = _paint.MeasureText(subString);
                line.Length     = measured - trailingnumber;
                line.Width      = lineWidth;
                line.Height     = _lineHeight;
                line.Top        = curY;

                _skiaLines.Add(line);

                curY   += _lineHeight;
                curY   += mLeading;
                curOff += measured;

                //if this is the last line and there are trailing newline characters then
                //insert a additional line
                if (curOff >= length)
                {
                    var subStringMinusNewlines = subString.TrimEnd('\n', '\r');
                    var lengthDiff             = subString.Length - subStringMinusNewlines.Length;
                    if (lengthDiff > 0)
                    {
                        AvaloniaFormattedTextLine lastLine = new AvaloniaFormattedTextLine();
                        lastLine.TextLength = lengthDiff;
                        lastLine.Start      = curOff - lengthDiff;
                        var lastLineSubString = Text.Substring(line.Start, line.TextLength);
                        var lastLineWidth     = _paint.MeasureText(lastLineSubString);
                        lastLine.Length = 0;
                        lastLine.Width  = lastLineWidth;
                        lastLine.Height = _lineHeight;
                        lastLine.Top    = curY;
                        lastLine.IsEmptyTrailingLine = true;

                        _skiaLines.Add(lastLine);

                        curY += _lineHeight;
                        curY += mLeading;
                    }
                }
            }

            // 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));
                _bounds = new Rect(0, 0, 0, _lineHeight);
            }
            else
            {
                var lastLine = _skiaLines[_skiaLines.Count - 1];
                _bounds = new Rect(0, 0, maxX, lastLine.Top + lastLine.Height);

                if (double.IsPositiveInfinity(Constraint.Width))
                {
                    return;
                }

                switch (_paint.TextAlign)
                {
                case SKTextAlign.Center:
                    _bounds = new Rect(Constraint).CenterRect(_bounds);
                    break;

                case SKTextAlign.Right:
                    _bounds = new Rect(
                        Constraint.Width - _bounds.Width,
                        0,
                        _bounds.Width,
                        _bounds.Height);
                    break;
                }
            }
        }
Exemplo n.º 6
0
        internal void Draw(DrawingContextImpl context,
                           SKCanvas canvas,
                           SKPoint origin,
                           DrawingContextImpl.PaintWrapper foreground,
                           bool canUseLcdRendering)
        {
            /* 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();
             */
            using (var paint = _paint.Clone())
            {
                IDisposable currd          = null;
                var         currentWrapper = foreground;
                SKPaint     currentPaint   = null;
                try
                {
                    ApplyWrapperTo(ref currentPaint, foreground, ref currd, paint, canUseLcdRendering);
                    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;
                            float  measure;
                            int    len;
                            float  factor;
                            switch (paint.TextAlign)
                            {
                            case SKTextAlign.Left:
                                factor = 0;
                                break;

                            case SKTextAlign.Center:
                                factor = 0.5f;
                                break;

                            case SKTextAlign.Right:
                                factor = 1;
                                break;

                            default:
                                throw new ArgumentOutOfRangeException();
                            }

                            var textLine = Text.Substring(line.Start, line.Length);
                            currX -= textLine.Length == 0 ? 0 : paint.MeasureText(textLine) * factor;

                            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(new SKPaint {
                                        IsAntialias = true
                                    }, fb,
                                                                         new Size());
                                }
                                else
                                {
                                    if (!currentWrapper.Equals(foreground))
                                    {
                                        currentWrapper.Dispose();
                                    }
                                    currentWrapper = foreground;
                                }

                                subStr  = Text.Substring(i, len);
                                measure = paint.MeasureText(subStr);
                                currX  += measure * factor;

                                ApplyWrapperTo(ref currentPaint, currentWrapper, ref currd, paint, canUseLcdRendering);

                                canvas.DrawText(subStr, currX, origin.Y + line.Top + _lineOffset, paint);

                                i     += len;
                                currX += measure * (1 - factor);
                            }
                        }
                    }
                }
                finally
                {
                    if (!currentWrapper.Equals(foreground))
                    {
                        currentWrapper.Dispose();
                    }
                    currd?.Dispose();
                }
            }
        }
Exemplo n.º 7
0
        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 + lastLineDescent);
            }
            else
            {
                var lastLine = _skiaLines[_skiaLines.Count - 1];
                _size = new Size(maxX, lastLine.Top + lastLine.Height);
            }
        }
Exemplo n.º 8
0
        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;
        }
Exemplo n.º 9
0
        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.Top;

            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 + lastLineDescent);
            }
            else
            {
                var lastLine = _skiaLines[_skiaLines.Count - 1];
                _size = new Size(maxX, lastLine.Top + lastLine.Height + lastLineDescent);
            }
        }
Exemplo n.º 10
0
        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();
            }
        }
Exemplo n.º 11
0
        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 cbi = _foregroundBrushes.FirstOrDefault(b => b.Key.Intersects(index, len));

                if (cbi.Value != null)
                {
                    var r = cbi.Key;

                    if (r.StartIndex > index)
                    {
                        len = r.StartIndex - index;
                    }
                    else
                    {
                        len = r.EndIndex - index + 1;
                        result = cbi.Value;
                    }

                    if (len > 0 && len < length)
                    {
                        length = len;
                    }
                }
            }

            return result;
        }
Exemplo n.º 12
0
        void Rebuild()
        {
            var length = _text.Length;

            _lines.Clear();

            _skiaRects = new SKRect[length];
            _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).

            // This seems like the best measure of full vertical extent
            float lineHeight = mBottom - mTop;

            // Rendering is relative to baseline
            LineOffset = -metrics.Top;

            string subString;

            byte[]   bytes;
            GCHandle pinnedArray;
            IntPtr   pointer;

            for (int c = 0; curOff < length; c++)
            {
                float lineWidth = -1;
                int   measured;
                int   extraSkip = 0;
                if (WidthConstraint <= 0)
                {
                    measured = length;
                }
                else
                {
                    float constraint = WidthConstraint;
                    if (constraint > MAX_LINE_WIDTH)
                    {
                        constraint = MAX_LINE_WIDTH;
                    }

                    subString = _text.Substring(curOff);

                    // TODO: This method is not linking into SkiaSharp so we must use the RAW buffer version for now
                    //measured = (int)Paint.BreakText(subString, constraint, out lineWidth) / 2;
                    bytes       = Encoding.UTF8.GetBytes(subString);
                    pinnedArray = GCHandle.Alloc(bytes, GCHandleType.Pinned);
                    pointer     = pinnedArray.AddrOfPinnedObject();
                    // for some reason I have to pass nBytes * 2. I assume under the hood it expects Unicode/WChar??
                    measured = (int)Paint.BreakText(pointer, (IntPtr)(bytes.Length * 2), constraint, out lineWidth) / 2;
                    pinnedArray.Free();

                    if (measured == 0)
                    {
                        measured  = 1;
                        lineWidth = -1;
                    }

                    char nextChar = ' ';
                    if (curOff + measured < length)
                    {
                        nextChar = _text[curOff + measured];
                    }

                    if (nextChar != ' ')
                    {
                        // Perform scan for the last space and end the line there
                        for (int si = curOff + measured - 1; si > curOff; si--)
                        {
                            if (_text[si] == ' ')
                            {
                                measured  = si - curOff;
                                extraSkip = 1;
                                break;
                            }
                        }
                    }
                }

                AvaloniaFormattedTextLine line = new AvaloniaFormattedTextLine();
                line.Start  = curOff;
                line.Length = measured;
                line.Width  = lineWidth;
                line.Height = lineHeight;
                line.Top    = curY;

                if (line.Width < 0)
                {
                    line.Width = _skiaRects[line.Start + line.Length - 1].Right;
                }

                // Build character rects
                for (int i = line.Start; i < line.Start + line.Length; i++)
                {
                    float prevRight = 0;
                    if (i != line.Start)
                    {
                        prevRight = _skiaRects[i - 1].Right;
                    }

                    subString = _text.Substring(line.Start, i - line.Start + 1);
                    float w = Paint.MeasureText(subString);

                    SKRect rc;
                    rc.Left       = prevRight;
                    rc.Right      = w;
                    rc.Top        = line.Top;
                    rc.Bottom     = line.Top + line.Height;
                    _skiaRects[i] = rc;
                }

                subString  = _text.Substring(line.Start, line.Length);
                line.Width = Paint.MeasureText(subString);

                _skiaLines.Add(line);

                curY += lineHeight;

                // TODO: We may want to consider adding Leading to the vertical line spacing but for now
                // it appears to make no difference. Revisit as part of FormattedText improvements.
                //
                //curY += mLeading;

                curOff += measured + extraSkip;
            }

            // Now convert to Avalonia data formats
            _lines.Clear();
            _rects.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].Length, _skiaLines[c].Height));
            }

            for (var c = 0; c < _text.Length; c++)
            {
                _rects.Add(_skiaRects[c].ToAvaloniaRect());
            }

            if (_skiaLines.Count == 0)
            {
                _size = new Size();
            }
            else
            {
                var lastLine = _skiaLines[_skiaLines.Count - 1];
                _size = new Size(maxX, lastLine.Top + lastLine.Height);
            }
        }