Browse Source

implemented SetForegroundBrush in skia FormattedTextImpl and refactored

donandren 9 years ago
parent
commit
39db3f0d0f

+ 1 - 1
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -306,7 +306,7 @@ namespace Avalonia.Skia
             using (var paint = CreatePaint(foreground, text.Measure()))
             {
                 var textImpl = text.PlatformImpl as FormattedTextImpl;
-                textImpl.Draw(this.Canvas, origin.ToSKPoint(), paint);
+                textImpl.Draw(Canvas, origin.ToSKPoint(), paint, CreatePaint);
             }
         }
 

+ 328 - 228
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@@ -1,20 +1,27 @@
+// 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;
-using System.Runtime.InteropServices;
-using System.Text;
 
 namespace Avalonia.Skia
 {
-    unsafe class FormattedTextImpl : IFormattedTextImpl
+    public class FormattedTextImpl : IFormattedTextImpl
     {
-        public FormattedTextImpl(string text, TextWrapping wrapping = TextWrapping.NoWrap)
+        public FormattedTextImpl(string text, string fontFamilyName, double fontSize, FontStyle fontStyle,
+                    TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping)
         {
             _text = text ?? string.Empty;
-            _wrapping = wrapping;
+
+            // Replace 0 characters with zero-width spaces (200B)
+            _text = _text.Replace((char)0, (char)0x200B);
+
+            var typeface = TypefaceCache.GetTypeface(fontFamilyName, fontStyle, fontWeight);
+
             _paint = new SKPaint();
 
             //currently Skia does not measure properly with Utf8 !!!
@@ -22,48 +29,32 @@ namespace Avalonia.Skia
             _paint.TextEncoding = SKTextEncoding.Utf16;
             _paint.IsStroke = false;
             _paint.IsAntialias = true;
-            LineOffset = 0;
-
-            // Replace 0 characters with zero-width spaces (200B)
-            _text = _text.Replace((char)0, (char)0x200B);
-        }
+            _paint.Typeface = typeface;
+            _paint.TextSize = (float)fontSize;
+            _paint.TextAlign = textAlignment.ToSKTextAlign();
 
-        public static FormattedTextImpl Create(string text, string fontFamilyName, double fontSize, FontStyle fontStyle,
-            TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping)
-        {
-            var typeface = TypefaceCache.GetTypeface(fontFamilyName, fontStyle, fontWeight);
+            _wrapping = wrapping;
 
-            FormattedTextImpl instance = new FormattedTextImpl(text, wrapping);
-            instance._paint.Typeface = typeface;
-            instance._paint.TextSize = (float)fontSize;
-            instance._paint.TextAlign = textAlignment.ToSKTextAlign();
-            instance.Rebuild();
-            return instance;
+            Rebuild();
         }
 
-        private readonly SKPaint _paint;
-        private readonly string _text;
-        private readonly TextWrapping _wrapping;
-
-        private readonly List<FormattedTextLine> _lines = new List<FormattedTextLine>();
-        private readonly List<Rect> _rects = new List<Rect>();
+        public Size Constraint
+        {
+            get { return _constraint; }
+            set
+            {
+                if (_constraint == value)
+                    return;
 
-        private List<AvaloniaFormattedTextLine> _skiaLines;
-        private Size _size;
+                _constraint = value;
 
-        const float MAX_LINE_WIDTH = 10000;
-        private float LineOffset;
-        private float LineHeight;
+                Rebuild();
+            }
+        }
 
-        struct AvaloniaFormattedTextLine
+        public void Dispose()
         {
-            public float Top;
-            public int Start;
-            public int Length;
-            public int TextLength;
-            public float Height;
-            public float Width;
-        };
+        }
 
         public IEnumerable<FormattedTextLine> GetLines()
         {
@@ -95,10 +86,10 @@ namespace Avalonia.Skia
 
                 int offset = 0;
 
-                if (point.X >= line.Width / 2 && line.Length > 0)
+                if (point.X >= (rects[line.Start].X + line.Width) / 2 && line.Length > 0)
                 {
                     offset = line.TextLength > line.Length ?
-                                    line.Length : line.Length - 1;
+                                    line.Length : (line.Length - 1);
                 }
 
                 return new TextHitTestResult
@@ -126,13 +117,12 @@ namespace Avalonia.Skia
             if (index < 0 || index >= rects.Count)
             {
                 var r = rects.LastOrDefault();
-                return new Rect(r.X + r.Width, r.Y, 0, LineHeight);
+                return new Rect(r.X + r.Width, r.Y, 0, _lineHeight);
             }
 
             if (rects.Count == 0)
             {
-                //empty text
-                return new Rect(0, 0, 1, LineHeight);
+                return new Rect(0, 0, 1, _lineHeight);
             }
 
             if (index == rects.Count)
@@ -174,163 +164,27 @@ namespace Avalonia.Skia
 
         public void SetForegroundBrush(IBrush brush, int startIndex, int length)
         {
-            // TODO: we need an implementation here to properly support FormattedText
-        }
-
-        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;
-            var mAscent = metrics.Ascent;
-            var lastLineDescent = mBottom - mDescent;
-            // This seems like the best measure of full vertical extent
-            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);
-
-                // lineHeight = hh;
-                line.Start = curOff;
-                line.Length = measured - trailingnumber;
-                line.Width = lineWidth;
-                line.Height = LineHeight;
-                line.Top = curY;
-
-                _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;
-            }
-
-            // 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)
+            var key = new FBrushRange(startIndex, length);
+            if (brush == null)
             {
-                _lines.Add(new FormattedTextLine(0, LineHeight));
-                _size = new Size(0, LineHeight + lastLineDescent);
+                if (_foregroundBrushes.ContainsKey(key))
+                    _foregroundBrushes.Remove(key);
             }
             else
             {
-                var lastLine = _skiaLines[_skiaLines.Count - 1];
-                _size = new Size(maxX, lastLine.Top + lastLine.Height + lastLineDescent);
-            }
-        }
-
-        private List<Rect> GetRects()
-        {
-            if (_text.Length > _rects.Count)
-            {
-                BuildRects();
+                _foregroundBrushes[key] = brush;
             }
-
-            return _rects;
         }
 
-        private void BuildRects()
+        public override string ToString()
         {
-            // Build character rects
-            var fm = _paint.FontMetrics;
-
-            float width = (float)(Constraint.Width > 0 && !double.IsPositiveInfinity(Constraint.Width) ?
-                                            Constraint.Width :
-                                            _size.Width);
-
-            for (int li = 0; li < _skiaLines.Count; li++)
-            {
-                var line = _skiaLines[li];
-                float prevRight = 0;
-
-                switch (_paint.TextAlign)
-                {
-                    case SKTextAlign.Center: prevRight = (width - line.Width) / 2; break;
-                    case SKTextAlign.Right: prevRight = width - line.Width; break;
-                }
-
-                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;
-                }
-            }
+            return _text;
         }
 
-        internal void Draw(SKCanvas canvas, SKPoint origin, DrawingContextImpl.PaintWrapper foreground)
+        internal void Draw(SKCanvas canvas, SKPoint origin,
+                            DrawingContextImpl.PaintWrapper foreground,
+                            Func<IBrush, Size, DrawingContextImpl.PaintWrapper> brushFactory)
         {
-            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.
@@ -356,67 +210,90 @@ namespace Avalonia.Skia
                 }
                 ctx->Canvas->restore();
             */
+            SKPaint paint = _paint;
+            IDisposable currd = null;
+            var currentWrapper = foreground;
 
-            using (foreground.ApplyTo(paint))
+            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];
-                    var subString = _text.Substring(line.Start, line.Length);
-
-                    float x = 0;
 
-                    //this is a quick fix so we have skia rendering
-                    //properly right and center align
-                    //TODO: find a better implementation including
-                    //hittesting and text selection working properly
+                    float x = TransformX(origin.X, 0, paint.TextAlign);
 
-                    //paint.TextAlign = SKTextAlign.Right;
-                    if (paint.TextAlign == SKTextAlign.Left)
+                    if (!hasCusomFGBrushes)
                     {
-                        x = origin.X;
+                        var subString = _text.Substring(line.Start, line.Length);
+                        canvas.DrawText(subString, x, origin.Y + line.Top + _lineOffset, paint);
                     }
                     else
                     {
-                        double width = Constraint.Width > 0 && !double.IsPositiveInfinity(Constraint.Width) ?
-                                        Constraint.Width :
-                                        _size.Width;
+                        float currX = x;
+                        string subStr;
+                        int len;
 
-                        switch (_paint.TextAlign)
+                        for (int i = line.Start; i < line.Start + line.Length;)
                         {
-                            case SKTextAlign.Center: x = origin.X + (float)width / 2; break;
-                            case SKTextAlign.Right: x = origin.X + (float)width; break;
-                        }
-                    }
+                            var fb = GetNextForegroundBrush(ref line, i, out len);
 
-                    canvas.DrawText(subString, x, origin.Y + line.Top + LineOffset, paint);
-                }
-            }
-        }
+                            if (fb != null)
+                            {
+                                //TODO: figure out how to get the brush size
+                                currentWrapper = brushFactory(fb, new Size());
+                            }
+                            else
+                            {
+                                if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose();
+                                currentWrapper = foreground;
+                            }
 
-        Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity);
+                            subStr = _text.Substring(i, len);
 
-        public Size Constraint
-        {
-            get { return _constraint; }
-            set
-            {
-                if (_constraint == value)
-                    return;
+                            if (currFGPaint != currentWrapper.Paint)
+                            {
+                                currFGPaint = ApplyWrapperTo(ref currentWrapper, ref currd, paint);
+                            }
 
-                _constraint = value;
+                            canvas.DrawText(subStr, currX, origin.Y + line.Top + _lineOffset, paint);
 
-                Rebuild();
+                            i += len;
+                            currX += paint.MeasureText(subStr);
+                        }
+                    }
+                }
+            }
+            finally
+            {
+                if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose();
+                currd?.Dispose();
             }
         }
 
-        public override string ToString()
-        {
-            return _text;
-        }
+        private const float MAX_LINE_WIDTH = 10000;
 
-        public void Dispose()
+        private readonly Dictionary<FBrushRange, IBrush> _foregroundBrushes =
+                                                new Dictionary<FBrushRange, IBrush>();
+        private readonly List<FormattedTextLine> _lines = new List<FormattedTextLine>();
+        private readonly SKPaint _paint;
+        private readonly List<Rect> _rects = new List<Rect>();
+        private readonly string _text;
+        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)
@@ -533,5 +410,228 @@ namespace Avalonia.Skia
 
             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 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;
+        }
+
+        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.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);
+            }
+        }
+
+        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 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;
+        }
     }
 }

+ 1 - 1
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -20,7 +20,7 @@ namespace Avalonia.Skia
         public IFormattedTextImpl CreateFormattedText(string text, string fontFamilyName, double fontSize, FontStyle fontStyle,
             TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping)
         {
-            return FormattedTextImpl.Create(text, fontFamilyName, fontSize, fontStyle, textAlignment, fontWeight, wrapping);
+            return new FormattedTextImpl(text, fontFamilyName, fontSize, fontStyle, textAlignment, fontWeight, wrapping);
         }
 
         public IStreamGeometryImpl CreateStreamGeometry()