Browse Source

Ported immutable media impls from scenegraph.

The deferred renderer requires immutable objects for drawing as it
renders on a separate thread.
Steven Kirk 8 years ago
parent
commit
e6cb529878
34 changed files with 656 additions and 570 deletions
  1. 13 14
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  2. 8 9
      src/Avalonia.Controls/Primitives/AccessText.cs
  3. 9 12
      src/Avalonia.Controls/TextBlock.cs
  4. 5 1
      src/Avalonia.HtmlRenderer/Adapters/GraphicsAdapter.cs
  5. 73 103
      src/Avalonia.Visuals/Media/FormattedText.cs
  6. 21 0
      src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs
  7. 19 5
      src/Avalonia.Visuals/Media/Geometry.cs
  8. 35 0
      src/Avalonia.Visuals/Media/Typeface.cs
  9. 8 17
      src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs
  10. 27 3
      src/Avalonia.Visuals/Platform/IGeometryImpl.cs
  11. 7 9
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  12. 6 9
      src/Avalonia.Visuals/Rendering/RendererMixin.cs
  13. 12 6
      src/Gtk/Avalonia.Cairo/CairoPlatform.cs
  14. 39 46
      src/Gtk/Avalonia.Cairo/Media/FormattedTextImpl.cs
  15. 9 0
      src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs
  16. 21 11
      src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs
  17. 1 1
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  18. 44 43
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  19. 8 3
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  20. 32 31
      src/Skia/Avalonia.Skia/StreamGeometryImpl.cs
  21. 1 0
      src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
  22. 11 6
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  23. 1 1
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  24. 40 33
      src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
  25. 38 61
      src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
  26. 12 24
      src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs
  27. 24 0
      src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs
  28. 16 1
      tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs
  29. 11 7
      tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
  30. 60 76
      tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs
  31. 5 5
      tests/Avalonia.UnitTests/TestServices.cs
  32. 0 28
      tests/Avalonia.Visuals.UnitTests/Media/FormattedTextTests.cs
  33. 21 0
      tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs
  34. 19 5
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

+ 13 - 14
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -127,11 +127,11 @@ namespace Avalonia.Controls.Presenters
             base.Render(context);
 
             if (selectionStart == selectionEnd)
-            {                
+            {
                 var backgroundColor = (((Control)TemplatedParent).GetValue(BackgroundProperty) as SolidColorBrush)?.Color;
                 var caretBrush = Brushes.Black;
 
-                if(backgroundColor.HasValue)
+                if (backgroundColor.HasValue)
                 {
                     byte red = (byte)~(backgroundColor.Value.R);
                     byte green = (byte)~(backgroundColor.Value.G);
@@ -139,7 +139,7 @@ namespace Avalonia.Controls.Presenters
 
                     caretBrush = new SolidColorBrush(Color.FromRgb(red, green, blue));
                 }
-                
+
                 if (_caretBlink)
                 {
                     var charPos = FormattedText.HitTestTextPosition(CaretIndex);
@@ -212,7 +212,10 @@ namespace Avalonia.Controls.Presenters
 
             if (length > 0)
             {
-                result.SetForegroundBrush(Brushes.White, start, length);
+                result.Spans = new[]
+                {
+                    new FormattedTextStyleSpan(start, length, foregroundBrush: Brushes.White),
+                };
             }
 
             return result;
@@ -228,17 +231,13 @@ namespace Avalonia.Controls.Presenters
             }
             else
             {
-                // TODO: Pretty sure that measuring "X" isn't the right way to do this...
-                using (var formattedText = new FormattedText(
-                    "X",
-                    FontFamily,
-                    FontSize,
-                    FontStyle,
-                    TextAlignment,
-                    FontWeight))
+                return new FormattedText
                 {
-                    return formattedText.Measure();
-                }
+                    Text = "X",
+                    Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight),
+                    TextAlignment = TextAlignment,
+                    Constraint = availableSize,
+                }.Measure();
             }
         }
 

+ 8 - 9
src/Avalonia.Controls/Primitives/AccessText.cs

@@ -85,15 +85,14 @@ namespace Avalonia.Controls.Primitives
         /// <returns>A <see cref="FormattedText"/> object.</returns>
         protected override FormattedText CreateFormattedText(Size constraint)
         {
-            var result = new FormattedText(
-                StripAccessKey(Text),
-                FontFamily,
-                FontSize,
-                FontStyle,
-                TextAlignment,
-                FontWeight);
-            result.Constraint = constraint;
-            return result;
+            return new FormattedText
+            {
+                Constraint = constraint,
+                Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight),
+                Text = StripAccessKey(Text),
+                TextAlignment = TextAlignment,
+                Wrapping = TextWrapping,
+            };
         }
 
         /// <summary>

+ 9 - 12
src/Avalonia.Controls/TextBlock.cs

@@ -116,7 +116,7 @@ namespace Avalonia.Controls
                 this.GetObservable(TextAlignmentProperty).Select(_ => Unit.Default),
                 this.GetObservable(FontSizeProperty).Select(_ => Unit.Default),
                 this.GetObservable(FontStyleProperty).Select(_ => Unit.Default),
-                this.GetObservable(FontWeightProperty).Select(_=>Unit.Default))
+                this.GetObservable(FontWeightProperty).Select(_ => Unit.Default))
                 .Subscribe(_ =>
                 {
                     InvalidateFormattedText();
@@ -350,16 +350,14 @@ namespace Avalonia.Controls
         /// <returns>A <see cref="FormattedText"/> object.</returns>
         protected virtual FormattedText CreateFormattedText(Size constraint)
         {
-            var result = new FormattedText(
-                Text ?? string.Empty,
-                FontFamily,
-                FontSize,
-                FontStyle,
-                TextAlignment,
-                FontWeight,
-                TextWrapping);
-            result.Constraint = constraint;
-            return result;
+            return new FormattedText
+            {
+                Constraint = constraint,
+                Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight),
+                Text = Text ?? string.Empty,
+                TextAlignment = TextAlignment,
+                Wrapping = TextWrapping,
+            };
         }
 
         /// <summary>
@@ -370,7 +368,6 @@ namespace Avalonia.Controls
             if (_formattedText != null)
             {
                 _constraint = _formattedText.Constraint;
-                _formattedText.Dispose();
                 _formattedText = null;
             }
 

+ 5 - 1
src/Avalonia.HtmlRenderer/Adapters/GraphicsAdapter.cs

@@ -117,7 +117,11 @@ namespace TheArtOfDev.HtmlRenderer.Avalonia.Adapters
         FormattedText GetText(string str, RFont font)
         {
             var f = ((FontAdapter)font);
-            return new FormattedText(str, f.Name, font.Size, f.FontStyle, TextAlignment.Left, f.Weight);
+            return new FormattedText
+            {
+                Text = str,
+                Typeface = new Typeface(f.Name, font.Size, f.FontStyle, f.Weight),
+            };
         }
 
         public override void MeasureString(string str, RFont font, double maxWidth, out int charFit, out double charFitWidth)

+ 73 - 103
src/Avalonia.Visuals/Media/FormattedText.cs

@@ -10,63 +10,32 @@ namespace Avalonia.Media
     /// <summary>
     /// Represents a piece of text with formatting.
     /// </summary>
-    public class FormattedText : AvaloniaDisposable
+    public class FormattedText
     {
+        private readonly IPlatformRenderInterface _platform;
+        private Size _constraint = Size.Infinity;
+        private IFormattedTextImpl _platformImpl;
+        private IReadOnlyList<FormattedTextStyleSpan> _spans;
+        private Typeface _typeface;
+        private string _text;
+        private TextAlignment _textAlignment;
+        private TextWrapping _wrapping;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="FormattedText"/> class.
         /// </summary>
-        /// <param name="text">The text.</param>
-        /// <param name="fontFamilyName">The font family.</param>
-        /// <param name="fontSize">The font size.</param>
-        /// <param name="fontStyle">The font style.</param>
-        /// <param name="textAlignment">The text alignment.</param>
-        /// <param name="fontWeight">The font weight.</param>
-        /// <param name="wrapping">The text wrapping mode.</param>
-        public FormattedText(
-            string text,
-            string fontFamilyName,
-            double fontSize,
-            FontStyle fontStyle = FontStyle.Normal,
-            TextAlignment textAlignment = TextAlignment.Left,
-            FontWeight fontWeight = FontWeight.Normal,
-            TextWrapping wrapping = TextWrapping.Wrap)
+        public FormattedText()
         {
-            Contract.Requires<ArgumentNullException>(text != null);
-            Contract.Requires<ArgumentNullException>(fontFamilyName != null);
-
-            if (fontSize <= 0)
-            {
-                throw new ArgumentException("FontSize must be greater than 0");
-            }
-
-            if (fontWeight <= 0)
-            {
-                throw new ArgumentException("FontWeight must be greater than 0");
-            }
-
-            Text = text;
-            FontFamilyName = fontFamilyName;
-            FontSize = fontSize;
-            FontStyle = fontStyle;
-            FontWeight = fontWeight;
-            TextAlignment = textAlignment;
-            Wrapping = wrapping;
-
-            var platform = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
-
-            if (platform == null)
-            {
-                throw new Exception("Could not create FormattedText: IPlatformRenderInterface not registered.");
-            }
+            _platform = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
+        }
 
-            PlatformImpl = platform.CreateFormattedText(
-                text,
-                fontFamilyName,
-                fontSize,
-                fontStyle,
-                textAlignment,
-                fontWeight,
-                wrapping);
+        /// <summary>
+        /// Initializes a new instance of the <see cref="FormattedText"/> class.
+        /// </summary>
+        /// <param name="platform">The platform render interface.</param>
+        public FormattedText(IPlatformRenderInterface platform)
+        {
+            _platform = platform;
         }
 
         /// <summary>
@@ -74,64 +43,76 @@ namespace Avalonia.Media
         /// </summary>
         public Size Constraint
         {
-            get
-            {
-                CheckDisposed();
-                return PlatformImpl.Constraint;
-            }
-            set
-            {
-                CheckDisposed();
-                PlatformImpl.Constraint = value;
-            }
+            get => _constraint;
+            set => Set(ref _constraint, value);
         }
 
         /// <summary>
-        /// Gets the font family.
+        /// Gets or sets the base typeface.
         /// </summary>
-        public string FontFamilyName { get; }
+        public Typeface Typeface
+        {
+            get => _typeface;
+            set => Set(ref _typeface, value);
+        }
 
         /// <summary>
-        /// Gets the font size.
+        /// Gets or sets a collection of spans that describe the formatting of subsections of the
+        /// text.
         /// </summary>
-        public double FontSize { get; }
+        public IReadOnlyList<FormattedTextStyleSpan> Spans
+        {
+            get => _spans;
+            set => Set(ref _spans, value);
+        }
 
         /// <summary>
-        /// Gets the font style.
+        /// Gets or sets the text.
         /// </summary>
-        public FontStyle FontStyle { get; }
+        public string Text
+        {
+            get => _text;
+            set => Set(ref _text, value);
+        }
 
         /// <summary>
-        /// Gets the font weight.
+        /// Gets or sets the aligment of the text.
         /// </summary>
-        public FontWeight FontWeight { get; }
+        public TextAlignment TextAlignment
+        {
+            get => _textAlignment;
+            set => Set(ref _textAlignment, value);
+        }
 
         /// <summary>
-        /// Gets the text.
+        /// Gets or sets the text wrapping.
         /// </summary>
-        public string Text { get; }
+        public TextWrapping Wrapping
+        {
+            get => _wrapping;
+            set => Set(ref _wrapping, value);
+        }
 
         /// <summary>
         /// Gets platform-specific platform implementation.
         /// </summary>
-        public IFormattedTextImpl PlatformImpl { get; }
-
-        /// <summary>
-        /// Gets the text alignment.
-        /// </summary>
-        public TextAlignment TextAlignment { get; }
-
-        /// <summary>
-        /// Gets the text wrapping.
-        /// </summary>
-        public TextWrapping Wrapping { get; }
-
-        /// <summary>
-        /// Disposes of unmanaged resources associated with the formatted text.
-        /// </summary>
-        protected override void DoDispose()
+        public IFormattedTextImpl PlatformImpl
         {
-            PlatformImpl.Dispose();
+            get
+            {
+                if (_platformImpl == null)
+                {
+                    _platformImpl = _platform.CreateFormattedText(
+                        _text,
+                        _typeface,
+                        _textAlignment,
+                        _wrapping,
+                        _constraint,
+                        _spans);
+                }
+
+                return _platformImpl;
+            }
         }
 
         /// <summary>
@@ -142,7 +123,6 @@ namespace Avalonia.Media
         /// </returns>
         public IEnumerable<FormattedTextLine> GetLines()
         {
-            CheckDisposed();
             return PlatformImpl.GetLines();
         }
 
@@ -155,7 +135,6 @@ namespace Avalonia.Media
         /// </returns>
         public TextHitTestResult HitTestPoint(Point point)
         {
-            CheckDisposed();
             return PlatformImpl.HitTestPoint(point);
         }
 
@@ -166,7 +145,6 @@ namespace Avalonia.Media
         /// <returns>The character bounds.</returns>
         public Rect HitTestTextPosition(int index)
         {
-            CheckDisposed();
             return PlatformImpl.HitTestTextPosition(index);
         }
 
@@ -178,7 +156,6 @@ namespace Avalonia.Media
         /// <returns>The character bounds.</returns>
         public IEnumerable<Rect> HitTestTextRange(int index, int length)
         {
-            CheckDisposed();
             return PlatformImpl.HitTestTextRange(index, length);
         }
 
@@ -188,20 +165,13 @@ namespace Avalonia.Media
         /// <returns>The bounds box of the text.</returns>
         public Size Measure()
         {
-            CheckDisposed();
-            return PlatformImpl.Measure();
+            return PlatformImpl.Size;
         }
 
-        /// <summary>
-        /// Sets the foreground brush for the specified text range.
-        /// </summary>
-        /// <param name="brush">The brush.</param>
-        /// <param name="startIndex">The start of the text range.</param>
-        /// <param name="length">The length of the text range.</param>
-        public void SetForegroundBrush(IBrush brush, int startIndex, int length)
+        private void Set<T>(ref T field, T value)
         {
-            CheckDisposed();
-            PlatformImpl.SetForegroundBrush(brush, startIndex, length);
+            field = value;
+            _platformImpl = null;
         }
     }
 }

+ 21 - 0
src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs

@@ -0,0 +1,21 @@
+using System;
+
+namespace Avalonia.Media
+{
+    public class FormattedTextStyleSpan
+    {
+        public FormattedTextStyleSpan(
+            int startIndex,
+            int length,
+            IBrush foregroundBrush = null)
+        {
+            StartIndex = startIndex;
+            Length = length;
+            ForegroundBrush = foregroundBrush;
+        }
+
+        public int StartIndex { get; }
+        public int Length { get; }
+        public IBrush ForegroundBrush { get; }
+    }
+}

+ 19 - 5
src/Avalonia.Visuals/Media/Geometry.cs

@@ -22,10 +22,7 @@ namespace Avalonia.Media
         /// </summary>
         static Geometry()
         {
-            TransformProperty.Changed.Subscribe(x =>
-            {
-                ((Geometry)x.Sender).PlatformImpl.Transform = ((Transform)x.NewValue).Value;
-            });
+            TransformProperty.Changed.AddClassHandler<Geometry>(x => x.TransformChanged);
         }
 
         /// <summary>
@@ -68,7 +65,7 @@ namespace Avalonia.Media
         }
 
         /// <summary>
-        /// Indicates whether the geometry contains the specified point.
+        /// Indicates whether the geometry's fill contains the specified point.
         /// </summary>
         /// <param name="point">The point.</param>
         /// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
@@ -76,5 +73,22 @@ namespace Avalonia.Media
         {
             return PlatformImpl.FillContains(point);
         }
+
+        /// <summary>
+        /// Indicates whether the geometry's stroke contains the specified point.
+        /// </summary>
+        /// <param name="pen">The pen to use.</param>
+        /// <param name="point">The point.</param>
+        /// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
+        public bool StrokeContains(Pen pen, Point point)
+        {
+            return PlatformImpl.StrokeContains(pen, point);
+        }
+
+        private void TransformChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            var transform = (Transform)e.NewValue;
+            PlatformImpl = PlatformImpl.WithTransform(transform.Value);
+        }
     }
 }

+ 35 - 0
src/Avalonia.Visuals/Media/Typeface.cs

@@ -0,0 +1,35 @@
+using System;
+using Avalonia.Media;
+
+namespace Avalonia.Media
+{
+    public class Typeface
+    {
+        public Typeface(
+            string fontFamilyName,
+            double fontSize,
+            FontStyle style = FontStyle.Normal,
+            FontWeight weight = FontWeight.Normal)
+        {
+            if (fontSize <= 0)
+            {
+                throw new ArgumentException("Font size must be > 0.");
+            }
+
+            if (weight <= 0)
+            {
+                throw new ArgumentException("Font weight must be > 0.");
+            }
+
+            FontFamilyName = fontFamilyName;
+            FontSize = fontSize;
+            Style = style;
+            Weight = weight;
+        }
+
+        public string FontFamilyName { get; }
+        public double FontSize { get; }
+        public FontStyle Style { get; }
+        public FontWeight Weight { get; }
+    }
+}

+ 8 - 17
src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs

@@ -10,12 +10,17 @@ namespace Avalonia.Platform
     /// <summary>
     /// Defines the platform-specific interface for <see cref="FormattedText"/>.
     /// </summary>
-    public interface IFormattedTextImpl : IDisposable
+    public interface IFormattedTextImpl
     {
         /// <summary>
-        /// Gets or sets the constraint of the text.
+        /// Gets the constraint of the text.
         /// </summary>
-        Size Constraint { get; set; }
+        Size Constraint { get; }
+
+        /// <summary>
+        /// The measured size of the text.
+        /// </summary>
+        Size Size { get; }
 
         /// <summary>
         /// Gets the text.
@@ -53,19 +58,5 @@ namespace Avalonia.Platform
         /// <param name="length">The number of characters in the text range.</param>
         /// <returns>The character bounds.</returns>
         IEnumerable<Rect> HitTestTextRange(int index, int length);
-
-        /// <summary>
-        /// Gets the size of the text, taking <see cref="Constraint"/> into account.
-        /// </summary>
-        /// <returns>The bounds box of the text.</returns>
-        Size Measure();
-
-        /// <summary>
-        /// Sets the foreground brush for the specified text range.
-        /// </summary>
-        /// <param name="brush">The brush.</param>
-        /// <param name="startIndex">The start of the text range.</param>
-        /// <param name="length">The length of the text range.</param>
-        void SetForegroundBrush(IBrush brush, int startIndex, int length);
     }
 }

+ 27 - 3
src/Avalonia.Visuals/Platform/IGeometryImpl.cs

@@ -1,6 +1,8 @@
 // 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;
+
 namespace Avalonia.Platform
 {
     /// <summary>
@@ -14,9 +16,9 @@ namespace Avalonia.Platform
         Rect Bounds { get; }
 
         /// <summary>
-        /// Gets or sets a transform to apply to the geometry.
+        /// Gets the transform to applied to the geometry.
         /// </summary>
-        Matrix Transform { get; set; }
+        Matrix Transform { get; }
 
         /// <summary>
         /// Gets the geometry's bounding rectangle with the specified stroke thickness.
@@ -26,10 +28,32 @@ namespace Avalonia.Platform
         Rect GetRenderBounds(double strokeThickness);
 
         /// <summary>
-        /// Indicates whether the geometry contains the specified point.
+        /// Indicates whether the geometry's fill contains the specified point.
         /// </summary>
         /// <param name="point">The point.</param>
         /// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
         bool FillContains(Point point);
+
+        /// <summary>
+        /// Intersects the geometry with another geometry.
+        /// </summary>
+        /// <param name="geometry">The other geometry.</param>
+        /// <returns>A new <see cref="IGeometryImpl"/> representing the intersection.</returns>
+        IGeometryImpl Intersect(IGeometryImpl geometry);
+
+        /// <summary>
+        /// Indicates whether the geometry's stroke contains the specified point.
+        /// </summary>
+        /// <param name="pen">The stroke to use.</param>
+        /// <param name="point">The point.</param>
+        /// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
+        bool StrokeContains(Pen pen, Point point);
+
+        /// <summary>
+        /// Makes a clone of the geometry with the specified transform.
+        /// </summary>
+        /// <param name="transform">The transform.</param>
+        /// <returns>The cloned geometry.</returns>
+        IGeometryImpl WithTransform(Matrix transform);
     }
 }

+ 7 - 9
src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs

@@ -17,21 +17,19 @@ namespace Avalonia.Platform
         /// Creates a formatted text implementation.
         /// </summary>
         /// <param name="text">The text.</param>
-        /// <param name="fontFamilyName">The font family.</param>
-        /// <param name="fontSize">The font size.</param>
-        /// <param name="fontStyle">The font style.</param>
+        /// <param name="typeface">The base typeface.</param>
         /// <param name="textAlignment">The text alignment.</param>
-        /// <param name="fontWeight">The font weight.</param>
         /// <param name="wrapping">The text wrapping mode.</param>
+        /// <param name="constraint">The text layout constraints.</param>
+        /// <param name="spans">The style spans.</param>
         /// <returns>An <see cref="IFormattedTextImpl"/>.</returns>
         IFormattedTextImpl CreateFormattedText(
             string text,
-            string fontFamilyName,
-            double fontSize,
-            FontStyle fontStyle,
+            Typeface typeface,
             TextAlignment textAlignment,
-            FontWeight fontWeight,
-            TextWrapping wrapping);
+            TextWrapping wrapping,
+            Size constraint,
+            IReadOnlyList<FormattedTextStyleSpan> spans);
 
         /// <summary>
         /// Creates a stream geometry implementation.

+ 6 - 9
src/Avalonia.Visuals/Rendering/RendererMixin.cs

@@ -58,16 +58,13 @@ namespace Avalonia.Rendering
                         s_lastMeasure = now;
                     }
                     var pt = new Point(40, 40);
-                    using (
-                        var txt = new FormattedText("Frame #" + s_frameNum + " FPS: " + s_fps, "Arial", 18,
-                            FontStyle.Normal,
-                            TextAlignment.Left,
-                            FontWeight.Normal,
-                            TextWrapping.NoWrap))
+                    var txt = new FormattedText
                     {
-                        ctx.FillRectangle(Brushes.White, new Rect(pt, txt.Measure()));
-                        ctx.DrawText(Brushes.Black, pt, txt);
-                    }
+                        Text = "Frame #" + s_frameNum + " FPS: " + s_fps,
+                        Typeface = new Typeface("Arial", 18)
+                    };
+                    ctx.FillRectangle(Brushes.White, new Rect(pt, txt.Measure()));
+                    ctx.DrawText(Brushes.Black, pt, txt);
                 }
             }
         }

+ 12 - 6
src/Gtk/Avalonia.Cairo/CairoPlatform.cs

@@ -42,14 +42,20 @@ namespace Avalonia.Cairo
 
         public IFormattedTextImpl CreateFormattedText(
             string text,
-            string fontFamily,
-            double fontSize,
-            FontStyle fontStyle,
+            Typeface typeface,
             TextAlignment textAlignment,
-            Avalonia.Media.FontWeight fontWeight,
-            TextWrapping wrapping)
+            TextWrapping wrapping,
+            Size constraint,
+            IReadOnlyList<FormattedTextStyleSpan> spans)
         {
-            return new FormattedTextImpl(s_pangoContext, text, fontFamily, fontSize, fontStyle, textAlignment, fontWeight);
+            return new FormattedTextImpl(
+                s_pangoContext,
+                text,
+                typeface,
+                textAlignment,
+                wrapping,
+                constraint,
+                spans);
         }
 
         public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)

+ 39 - 46
src/Gtk/Avalonia.Cairo/Media/FormattedTextImpl.cs

@@ -12,7 +12,7 @@ namespace Avalonia.Cairo.Media
 {
     public class FormattedTextImpl : IFormattedTextImpl
     {
-        private Size _size;
+        private Size _constraint;
 
         static double CorrectScale(double input)
         {
@@ -22,54 +22,64 @@ namespace Avalonia.Cairo.Media
         public FormattedTextImpl(
             Pango.Context context,
             string text,
-            string fontFamily,
-            double fontSize,
-            FontStyle fontStyle,
+            Typeface typeface,
             TextAlignment textAlignment,
-            FontWeight fontWeight)
+            TextWrapping wrapping,
+            Size constraint,
+            IReadOnlyList<FormattedTextStyleSpan> spans)
         {
             Contract.Requires<ArgumentNullException>(context != null);
             Contract.Requires<ArgumentNullException>(text != null);
+
             Layout = new Pango.Layout(context);
             Layout.SetText(text);
+
             Layout.FontDescription = new Pango.FontDescription
             {
-                Family = fontFamily,
-                Size = Pango.Units.FromDouble(CorrectScale(fontSize)),
-                Style = (Pango.Style)fontStyle,
-                Weight = fontWeight.ToCairo()
+                Family = typeface?.FontFamilyName ?? "monospace",
+                Size = Pango.Units.FromDouble(CorrectScale(typeface?.FontSize ?? 12)),
+                Style = (Pango.Style)(typeface?.Style ?? FontStyle.Normal),
+                Weight = (typeface?.Weight ?? FontWeight.Normal).ToCairo(),
             };
 
             Layout.Alignment = textAlignment.ToCairo();
             Layout.Attributes = new Pango.AttrList();
-        }
+            Layout.Width = double.IsPositiveInfinity(constraint.Width) ? -1 : Pango.Units.FromDouble(constraint.Width);
 
-        public string Text => Layout.Text;
-
-        public Size Constraint
-        {
-            get
+            if (spans != null)
             {
-                return _size;
+                foreach (var span in spans)
+                {
+                    if (span.ForegroundBrush is SolidColorBrush scb)
+                    {
+                        var color = new Pango.Color();
+                        color.Parse(string.Format("#{0}", scb.Color.ToString().Substring(3)));
+
+                        var brushAttr = new Pango.AttrForeground(color);
+                        brushAttr.StartIndex = (uint)TextIndexToPangoIndex(span.StartIndex);
+                        brushAttr.EndIndex = (uint)TextIndexToPangoIndex(span.StartIndex + span.Length);
+
+                        this.Layout.Attributes.Insert(brushAttr);
+                    }
+                }
             }
 
-            set
-            {
-                _size = value;
-                Layout.Width = double.IsPositiveInfinity(value.Width) ?
-                    -1 : Pango.Units.FromDouble(value.Width);
-            }
+            Size = Measure();
         }
 
-        public Pango.Layout Layout
+        public FormattedTextImpl(Pango.Layout layout)
         {
-            get;
+            Layout = layout;
+            Size = Measure();
         }
 
-        public void Dispose()
-        {
-            Layout.Dispose();
-        }
+        public string Text => Layout.Text;
+
+        public Size Constraint => _constraint;
+
+        public Size Size { get; }
+
+        public Pango.Layout Layout { get; }
 
         public IEnumerable<FormattedTextLine> GetLines()
         {
@@ -124,7 +134,7 @@ namespace Avalonia.Cairo.Media
             return ranges;
         }
 
-        public Size Measure()
+        private Size Measure()
         {
             int width;
             int height;
@@ -132,22 +142,5 @@ namespace Avalonia.Cairo.Media
 
             return new Size(width, height);
         }
-
-        public void SetForegroundBrush(IBrush brush, int startIndex, int count)
-        {
-            var scb = brush as SolidColorBrush;
-            if (scb != null)
-            {
-
-                var color = new Pango.Color();
-                color.Parse(string.Format("#{0}", scb.Color.ToString().Substring(3)));
-
-                var brushAttr = new Pango.AttrForeground(color);
-                brushAttr.StartIndex = (uint)TextIndexToPangoIndex(startIndex);
-                brushAttr.EndIndex = (uint)TextIndexToPangoIndex(startIndex + count);
-
-                Layout.Attributes.Insert(brushAttr);
-            }
-        }
     }
 }

+ 9 - 0
src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs

@@ -71,6 +71,15 @@ namespace Avalonia.Cairo.Media
             }
         }
 
+        internal bool StrokeContains(Pen pen, Point point)
+        {
+            using (var context = new Cairo.Context(new Cairo.ImageSurface(Cairo.Format.Argb32, 0, 0)))
+            {
+                context.AppendPath(Path);
+                return context.InStroke(point.X, point.Y);
+            }
+        }
+
         public void LineTo(Point point)
         {
             if (this.Path == null)

+ 21 - 11
src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs

@@ -35,19 +35,11 @@ namespace Avalonia.Cairo.Media
         private readonly StreamGeometryContextImpl _impl;
 
         private Matrix _transform = Matrix.Identity;
+
         public Matrix Transform
         {
             get { return _transform; }
-            set
-            {
-                if (value != Transform)
-                {
-                    if (!value.IsIdentity)
-                    {
-                        _transform = value;
-                    }
-                }
-            }
+            private set { _transform = value; }
         }
 
         public FillRule FillRule { get; set; }
@@ -60,7 +52,7 @@ namespace Avalonia.Cairo.Media
         public Rect GetRenderBounds(double strokeThickness)
         {
             // TODO: Calculate properly.
-			return Bounds.Inflate(strokeThickness);
+			return Bounds.TransformToAABB(Transform).Inflate(strokeThickness);
         }
 
         public IStreamGeometryContextImpl Open()
@@ -72,5 +64,23 @@ namespace Avalonia.Cairo.Media
         {
             return _impl.FillContains(point);
         }
+
+        public IGeometryImpl Intersect(IGeometryImpl geometry)
+        {
+            throw new NotImplementedException();
+        }
+
+        public bool StrokeContains(Pen pen, Point point)
+        {
+            return _impl.StrokeContains(pen, point);
+        }
+
+        /// <inheritdoc/>
+        public IGeometryImpl WithTransform(Matrix transform)
+        {
+            var result = (StreamGeometryImpl)Clone();
+            result.Transform = transform;
+            return result;
+        }
     }
 }

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

@@ -292,7 +292,7 @@ namespace Avalonia.Skia
 
         public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
         {
-            using (var paint = CreatePaint(foreground, text.Measure()))
+            using (var paint = CreatePaint(foreground, text.Size))
             {
                 var textImpl = (FormattedTextImpl)text;
                 textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint);

+ 44 - 43
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@@ -12,15 +12,23 @@ namespace Avalonia.Skia
 {
     public class FormattedTextImpl : IFormattedTextImpl
     {
-        public FormattedTextImpl(string text, string fontFamilyName, double fontSize, FontStyle fontStyle,
-                    TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping)
+        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 typeface = TypefaceCache.GetTypeface(fontFamilyName, fontStyle, fontWeight);
+            var skiaTypeface = TypefaceCache.GetTypeface(
+                typeface?.FontFamilyName ?? "monospace",
+                typeface?.Style ?? FontStyle.Normal,
+                typeface?.Weight ?? FontWeight.Normal);
 
             _paint = new SKPaint();
 
@@ -31,34 +39,31 @@ namespace Avalonia.Skia
             _paint.IsAntialias = true;            
             _paint.LcdRenderText = true;            
             _paint.SubpixelText = true;
-            _paint.Typeface = typeface;
-            _paint.TextSize = (float)fontSize;
+            _paint.Typeface = skiaTypeface;
+            _paint.TextSize = (float)(typeface?.FontSize ?? 12);
             _paint.TextAlign = textAlignment.ToSKTextAlign();
+            _paint.XferMode = SKXferMode.Src;
 
             _wrapping = wrapping;
+            _constraint = constraint;
 
-            Rebuild();
-        }
-
-        public Size Constraint
-        {
-            get { return _constraint; }
-            set
+            if (spans != null)
             {
-                if (_constraint == value)
-                    return;
-
-                _constraint = value;
-
-                Rebuild();
+                foreach (var span in spans)
+                {
+                    if (span.ForegroundBrush != null)
+                    {
+                        SetForegroundBrush(span.ForegroundBrush, span.StartIndex, span.Length);
+                    }
+                }
             }
+
+            Rebuild();
         }
 
-        public string Text { get; }
+        public Size Constraint => _constraint;
 
-        public void Dispose()
-        {
-        }
+        public Size Size => _size;
 
         public IEnumerable<FormattedTextLine> GetLines()
         {
@@ -161,27 +166,6 @@ namespace Avalonia.Skia
             return result;
         }
 
-        public Size Measure()
-        {
-            return _size;
-        }
-
-        public 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));
-            }
-        }
-
         public override string ToString()
         {
             return Text;
@@ -286,6 +270,7 @@ namespace Avalonia.Skia
         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;
@@ -619,6 +604,22 @@ namespace Avalonia.Skia
             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;

+ 8 - 3
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -17,10 +17,15 @@ namespace Avalonia.Skia
             return CreateRenderTargetBitmap(width, height);
         }
 
-        public IFormattedTextImpl CreateFormattedText(string text, string fontFamilyName, double fontSize, FontStyle fontStyle,
-            TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping)
+        public IFormattedTextImpl CreateFormattedText(
+            string text,
+            Typeface typeface,
+            TextAlignment textAlignment,
+            TextWrapping wrapping,
+            Size constraint,
+            IReadOnlyList<FormattedTextStyleSpan> spans)
         {
-            return new FormattedTextImpl(text, fontFamilyName, fontSize, fontStyle, textAlignment, fontWeight, wrapping);
+            return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans);
         }
 
         public IStreamGeometryImpl CreateStreamGeometry()

+ 32 - 31
src/Skia/Avalonia.Skia/StreamGeometryImpl.cs

@@ -13,16 +13,15 @@ namespace Avalonia.Skia
     class StreamGeometryImpl : IStreamGeometryImpl
     {
         SKPath _path;
-        SKPath _transformedPath;
 
         private Matrix _transform = Matrix.Identity;
 
-        public SKPath EffectivePath => (_transformedPath ?? _path);
+        public SKPath EffectivePath => _path;
 
         public Rect GetRenderBounds(double strokeThickness)
         {
             // TODO: Calculate properly.
-            return Bounds.Inflate(strokeThickness);
+            return Bounds.TransformToAABB(Transform).Inflate(strokeThickness);
         }
 
         public Rect Bounds { get; private set; }
@@ -30,32 +29,6 @@ namespace Avalonia.Skia
         public Matrix Transform
         {
             get { return _transform; }
-            set
-            {
-                if (_transform == value)
-                    return;
-
-                _transform = value;
-                ApplyTransform();
-            }
-        }
-
-        void ApplyTransform()
-        {
-            if (_path == null)
-                return;
-
-            if (_transformedPath != null)
-            {
-                _transformedPath.Dispose();
-                _transformedPath = null;
-            }
-
-            if (!Transform.IsIdentity)
-            {
-                _transformedPath = new SKPath(_path);
-                _transformedPath.Transform(Transform.ToSKMatrix());
-            }
         }
 
         public IStreamGeometryImpl Clone()
@@ -63,7 +36,6 @@ namespace Avalonia.Skia
             return new StreamGeometryImpl
             {
                 _path = _path?.Clone(),
-                _transformedPath = _transformedPath?.Clone(),
                 _transform = Transform,
                 Bounds = Bounds
             };
@@ -84,6 +56,36 @@ namespace Avalonia.Skia
             return GetRenderBounds(0).Contains(point);
         }
 
+        public bool StrokeContains(Pen pen, Point point)
+        {
+            // TODO: Not supported by SkiaSharp yet, so use expanded Rect
+            // return EffectivePath.Contains(point.X, point.Y);
+            return GetRenderBounds(0).Contains(point);
+        }
+
+        public IGeometryImpl Intersect(IGeometryImpl geometry)
+        {
+            throw new NotImplementedException();
+        }
+
+        public IGeometryImpl WithTransform(Matrix transform)
+        {
+            var result = (StreamGeometryImpl)Clone();
+
+            if (result.Transform != Matrix.Identity)
+            {
+                result._path.Transform(result.Transform.Invert().ToSKMatrix());
+            }
+
+            if (transform != Matrix.Identity)
+            {
+                result._path.Transform(transform.ToSKMatrix());
+            }
+
+            result._transform = transform;
+            return result;
+        }
+
         class StreamContext : IStreamGeometryContextImpl
         {
             private readonly StreamGeometryImpl _geometryImpl;
@@ -100,7 +102,6 @@ namespace Avalonia.Skia
             {
                 SKRect rc;
                 _path.GetBounds(out rc);
-                _geometryImpl.ApplyTransform();
                 _geometryImpl.Bounds = rc.ToAvaloniaRect();
             }
 

+ 1 - 0
src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj

@@ -68,6 +68,7 @@
     <Compile Include="Media\GeometryImpl.cs" />
     <Compile Include="Media\StreamGeometryImpl.cs" />
     <Compile Include="Media\FormattedTextImpl.cs" />
+    <Compile Include="Media\TransformedGeometryImpl.cs" />
     <Compile Include="PrimitiveExtensions.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="RenderTarget.cs" />

+ 11 - 6
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@@ -88,14 +88,19 @@ namespace Avalonia.Direct2D1
 
         public IFormattedTextImpl CreateFormattedText(
             string text,
-            string fontFamily,
-            double fontSize,
-            FontStyle fontStyle,
+            Typeface typeface,
             TextAlignment textAlignment,
-            FontWeight fontWeight,
-            TextWrapping wrapping)
+            TextWrapping wrapping,
+            Size constraint,
+            IReadOnlyList<FormattedTextStyleSpan> spans)
         {
-            return new FormattedTextImpl(text, fontFamily, fontSize, fontStyle, textAlignment, fontWeight, wrapping);
+            return new FormattedTextImpl(
+                text,
+                typeface,
+                textAlignment,
+                wrapping,
+                constraint,
+                spans);
         }
 
         public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop)

+ 1 - 1
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@@ -207,7 +207,7 @@ namespace Avalonia.Direct2D1.Media
             {
                 var impl = (FormattedTextImpl)text;
 
-                using (var brush = CreateBrush(foreground, impl.Measure()))
+                using (var brush = CreateBrush(foreground, impl.Size))
                 using (var renderer = new AvaloniaTextRenderer(this, _renderTarget, brush.PlatformBrush))
                 {
                     if (brush.PlatformBrush != null)

+ 40 - 33
src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs

@@ -14,51 +14,52 @@ namespace Avalonia.Direct2D1.Media
     {
         public FormattedTextImpl(
             string text,
-            string fontFamily,
-            double fontSize,
-            FontStyle fontStyle,
+            Typeface typeface,
             TextAlignment textAlignment,
-            FontWeight fontWeight,
-            TextWrapping wrapping)
+            TextWrapping wrapping,
+            Size constraint,
+            IReadOnlyList<FormattedTextStyleSpan> spans)
         {
+            Text = text;
             var factory = AvaloniaLocator.Current.GetService<DWrite.Factory>();
 
             using (var format = new DWrite.TextFormat(
                 factory,
-                fontFamily,
-                (DWrite.FontWeight)fontWeight,
-                (DWrite.FontStyle)fontStyle,
-                (float)fontSize))
+                typeface?.FontFamilyName ?? "Courier New",
+                (DWrite.FontWeight)(typeface?.Weight ?? FontWeight.Normal),
+                (DWrite.FontStyle)(typeface?.Style ?? FontStyle.Normal),
+                (float)(typeface?.FontSize ?? 12)))
             {
-                Text = text;
                 format.WordWrapping = wrapping == TextWrapping.Wrap ? 
-                    DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap;
+                    DWrite.WordWrapping.Wrap :
+                    DWrite.WordWrapping.NoWrap;
 
                 TextLayout = new DWrite.TextLayout(
                     factory,
                     text ?? string.Empty,
                     format,
-                    float.MaxValue,
-                    float.MaxValue);
+                    (float)constraint.Width,
+                    (float)constraint.Height)
+                {
+                    TextAlignment = textAlignment.ToDirect2D()
+                };
             }
 
-            TextLayout.TextAlignment = textAlignment.ToDirect2D();
-        }
-
-        public Size Constraint
-        {
-            get
+            if (spans != null)
             {
-                return new Size(TextLayout.MaxWidth, TextLayout.MaxHeight);
+                foreach (var span in spans)
+                {
+                    ApplySpan(span);
+                }
             }
 
-            set
-            {
-                TextLayout.MaxWidth = (float)value.Width;
-                TextLayout.MaxHeight = (float)value.Height;
-            }
+            Size = Measure();
         }
 
+        public Size Constraint => new Size(TextLayout.MaxWidth, TextLayout.MaxHeight);
+
+        public Size Size { get; }
+
         public string Text { get; }
 
         public DWrite.TextLayout TextLayout { get; }
@@ -113,7 +114,20 @@ namespace Avalonia.Direct2D1.Media
             return result.Select(x => new Rect(x.Left, x.Top, x.Width, x.Height));
         }
 
-        public Size Measure()
+        private void ApplySpan(FormattedTextStyleSpan span)
+        {
+            if (span.Length > 0)
+            {
+                if (span.ForegroundBrush != null)
+                {
+                    TextLayout.SetDrawingEffect(
+                        new BrushWrapper(span.ForegroundBrush),
+                        new DWrite.TextRange(span.StartIndex, span.Length));
+                }
+            }
+        }
+
+        private Size Measure()
         {
             var metrics = TextLayout.Metrics;
             var width = metrics.WidthIncludingTrailingWhitespace;
@@ -125,12 +139,5 @@ namespace Avalonia.Direct2D1.Media
 
             return new Size(width, TextLayout.Metrics.Height);
         }
-
-        public void SetForegroundBrush(IBrush brush, int startIndex, int count)
-        {
-            TextLayout.SetDrawingEffect(
-                new BrushWrapper(brush),
-                new DWrite.TextRange(startIndex, count));
-        }
     }
 }

+ 38 - 61
src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs

@@ -1,6 +1,7 @@
 // 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 System;
 using Avalonia.Platform;
 using SharpDX.Direct2D1;
 
@@ -11,85 +12,61 @@ namespace Avalonia.Direct2D1.Media
     /// </summary>
     public abstract class GeometryImpl : IGeometryImpl
     {
-        private TransformedGeometry _transformed;
-
-        /// <summary>
-        /// Gets the geometry's bounding rectangle.
-        /// </summary>
-        public abstract Rect Bounds
+        public GeometryImpl(Geometry geometry)
         {
-            get;
+            Geometry = geometry;
         }
 
-        /// <summary>
-        /// Gets the geomentry without any transforms applied.
-        /// </summary>
-        public abstract Geometry DefiningGeometry
+        /// <inheritdoc/>
+        public Rect Bounds => Geometry.GetWidenedBounds(0).ToAvalonia();
+
+        /// <inheritdoc/>
+        public Geometry Geometry { get; }
+
+        /// <inheritdoc/>
+        public virtual Matrix Transform => Matrix.Identity;
+
+        /// <inheritdoc/>
+        public Rect GetRenderBounds(double strokeThickness)
         {
-            get;
+            return Geometry.GetWidenedBounds((float)strokeThickness).ToAvalonia();
         }
 
-        /// <summary>
-        /// Gets the Direct2D <see cref="Geometry"/>.
-        /// </summary>
-        public Geometry Geometry => _transformed ?? DefiningGeometry;
+        /// <inheritdoc/>
+        public bool FillContains(Point point)
+        {
+            return Geometry.FillContainsPoint(point.ToSharpDX());
+        }
 
-        /// <summary>
-        /// Gets or sets the transform for the geometry.
-        /// </summary>
-        public Matrix Transform
+        /// <inheritdoc/>
+        public IGeometryImpl Intersect(IGeometryImpl geometry)
         {
-            get
-            {
-                return _transformed != null ?
-                    _transformed.Transform.ToAvalonia() :
-                    Matrix.Identity;
-            }
+            var result = new PathGeometry(Geometry.Factory);
 
-            set
+            using (var sink = result.Open())
             {
-                if (value != Transform)
-                {
-                    if (_transformed != null)
-                    {
-                        _transformed.Dispose();
-                        _transformed = null;
-                    }
-
-                    if (!value.IsIdentity)
-                    {
-                        Factory factory = AvaloniaLocator.Current.GetService<Factory>();
-                        _transformed = new TransformedGeometry(
-                            factory,
-                            DefiningGeometry,
-                            value.ToDirect2D());
-                    }
-                }
+                Geometry.Combine(((GeometryImpl)geometry).Geometry, CombineMode.Intersect, sink);
+                return new StreamGeometryImpl(result);
             }
         }
 
-        /// <summary>
-        /// Gets the geometry's bounding rectangle with the specified stroke thickness.
-        /// </summary>
-        /// <param name="strokeThickness">The stroke thickness.</param>
-        /// <returns>The bounding rectangle.</returns>
-        public Rect GetRenderBounds(double strokeThickness)
+        /// <inheritdoc/>
+        public bool StrokeContains(Avalonia.Media.Pen pen, Point point)
         {
-            if (_transformed != null)
-            {
-                return _transformed.GetWidenedBounds((float)strokeThickness).ToAvalonia();
-            }
-            else
-            {
-                return DefiningGeometry.GetWidenedBounds((float)strokeThickness).ToAvalonia();
-            }
+            return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)pen.Thickness);
         }
 
-
-        public bool FillContains(Point point)
+        /// <inheritdoc/>
+        public IGeometryImpl WithTransform(Matrix transform)
         {
-            return Geometry.FillContainsPoint(point.ToSharpDX());
+            var factory = AvaloniaLocator.Current.GetService<Factory>();
+            return new TransformedGeometryImpl(
+                new TransformedGeometry(
+                    factory,
+                    GetSourceGeometry(),
+                    transform.ToDirect2D()));
         }
 
+        protected virtual Geometry GetSourceGeometry() => Geometry;
     }
 }

+ 12 - 24
src/Windows/Avalonia.Direct2D1/Media/StreamGeometryImpl.cs

@@ -3,7 +3,6 @@
 
 using Avalonia.Platform;
 using SharpDX.Direct2D1;
-using D2DGeometry = SharpDX.Direct2D1.Geometry;
 
 namespace Avalonia.Direct2D1.Media
 {
@@ -12,55 +11,44 @@ namespace Avalonia.Direct2D1.Media
     /// </summary>
     public class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl
     {
-        private readonly PathGeometry _path;
-
         /// <summary>
         /// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
         /// </summary>
         public StreamGeometryImpl()
+            : base(CreateGeometry())
         {
-            Factory factory = AvaloniaLocator.Current.GetService<Factory>();
-            _path = new PathGeometry(factory);
         }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
         /// </summary>
         /// <param name="geometry">An existing Direct2D <see cref="PathGeometry"/>.</param>
-        protected StreamGeometryImpl(PathGeometry geometry)
+        public StreamGeometryImpl(PathGeometry geometry)
+            : base(geometry)
         {
-            _path = geometry;
         }
 
         /// <inheritdoc/>
-        public override Rect Bounds => _path.GetWidenedBounds(0).ToAvalonia();
-
-        /// <inheritdoc/>
-        public override D2DGeometry DefiningGeometry => _path;
-
-        /// <summary>
-        /// Clones the geometry.
-        /// </summary>
-        /// <returns>A cloned geometry.</returns>
         public IStreamGeometryImpl Clone()
         {
             Factory factory = AvaloniaLocator.Current.GetService<Factory>();
             var result = new PathGeometry(factory);
             var sink = result.Open();
-            _path.Stream(sink);
+            ((PathGeometry)Geometry).Stream(sink);
             sink.Close();
             return new StreamGeometryImpl(result);
         }
 
-        /// <summary>
-        /// Opens the geometry to start defining it.
-        /// </summary>
-        /// <returns>
-        /// An <see cref="Avalonia.Platform.IStreamGeometryContextImpl"/> which can be used to define the geometry.
-        /// </returns>
+        /// <inheritdoc/>
         public IStreamGeometryContextImpl Open()
         {
-            return new StreamGeometryContextImpl(_path.Open());
+            return new StreamGeometryContextImpl(((PathGeometry)Geometry).Open());
+        }
+
+        private static Geometry CreateGeometry()
+        {
+            Factory factory = AvaloniaLocator.Current.GetService<Factory>();
+            return new PathGeometry(factory);
         }
     }
 }

+ 24 - 0
src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs

@@ -0,0 +1,24 @@
+// 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 SharpDX.Direct2D1;
+
+namespace Avalonia.Direct2D1.Media
+{
+    public class TransformedGeometryImpl : GeometryImpl
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
+        /// </summary>
+        /// <param name="geometry">An existing Direct2D <see cref="TransformedGeometry"/>.</param>
+        public TransformedGeometryImpl(TransformedGeometry geometry)
+            : base(geometry)
+        {
+        }
+
+        /// <inheritdoc/>
+        public override Matrix Transform => ((TransformedGeometry)Geometry).Transform.ToAvalonia();
+
+        protected override Geometry GetSourceGeometry() => ((TransformedGeometry)Geometry).SourceGeometry;
+    }
+}

+ 16 - 1
tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs

@@ -335,7 +335,7 @@ namespace Avalonia.Input.UnitTests
 
         class MockRenderInterface : IPlatformRenderInterface
         {
-            public IFormattedTextImpl CreateFormattedText(string text, string fontFamilyName, double fontSize, FontStyle fontStyle, TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping)
+            public IFormattedTextImpl CreateFormattedText(string text, Typeface typeface, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, IReadOnlyList<FormattedTextStyleSpan> spans)
             {
                 throw new NotImplementedException();
             }
@@ -414,11 +414,26 @@ namespace Avalonia.Input.UnitTests
                     throw new NotImplementedException();
                 }
 
+                public IGeometryImpl Intersect(IGeometryImpl geometry)
+                {
+                    throw new NotImplementedException();
+                }
+
                 public IStreamGeometryContextImpl Open()
                 {
                     return _impl;
                 }
 
+                public bool StrokeContains(Pen pen, Point point)
+                {
+                    throw new NotImplementedException();
+                }
+
+                public IGeometryImpl WithTransform(Matrix transform)
+                {
+                    throw new NotImplementedException();
+                }
+
                 class MockStreamGeometryContext : IStreamGeometryContextImpl
                 {
                     private List<Point> points = new List<Point>();

+ 11 - 7
tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs

@@ -142,6 +142,8 @@ namespace Avalonia.Layout.UnitTests
 
             public string Text { get; }
 
+            public Size Size => new Size();
+
             public void Dispose()
             {
             }
@@ -155,20 +157,22 @@ namespace Avalonia.Layout.UnitTests
             public IEnumerable<Rect> HitTestTextRange(int index, int length) => new Rect[0];
 
             public Size Measure() => Constraint;
-
-            public void SetForegroundBrush(IBrush brush, int startIndex, int length)
-            {
-            }
         }
 
         private void RegisterServices()
         {
             var globalStyles = new Mock<IGlobalStyles>();
             var renderInterface = new Mock<IPlatformRenderInterface>();
-            renderInterface.Setup(x => x.CreateFormattedText(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>(), It.IsAny<FontStyle>(),
-                It.IsAny<TextAlignment>(), It.IsAny<FontWeight>(), It.IsAny<TextWrapping>()))
+            renderInterface.Setup(x =>
+                x.CreateFormattedText(
+                    It.IsAny<string>(),
+                    It.IsAny<Typeface>(),
+                    It.IsAny<TextAlignment>(),
+                    It.IsAny<TextWrapping>(),
+                    It.IsAny<Size>(),
+                    It.IsAny<IReadOnlyList<FormattedTextStyleSpan>>()))
                 .Returns(new FormattedTextMock("TEST"));
-                
+
             var windowImpl = new Mock<IWindowImpl>();
 
             Size clientSize = default(Size);

+ 60 - 76
tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs

@@ -50,37 +50,40 @@ namespace Avalonia.Direct2D1.RenderTests.Media
             FontStyle fontStyle,
             TextAlignment textAlignment,
             FontWeight fontWeight,
-            TextWrapping wrapping)
+            TextWrapping wrapping,
+            double widthConstraint)
         {
             var r = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
             return r.CreateFormattedText(text,
-                fontFamily,
-                fontSize,
-                fontStyle,
+                new Typeface(fontFamily, fontSize, fontStyle, fontWeight),
                 textAlignment,
-                fontWeight,
-                wrapping);
+                wrapping,
+                widthConstraint == -1 ? Size.Infinity : new Size(widthConstraint, double.PositiveInfinity),
+                null);
         }
 
         private IFormattedTextImpl Create(string text, double fontSize)
         {
             return Create(text, FontName, fontSize,
                 FontStyle.Normal, TextAlignment.Left,
-                FontWeight.Normal, TextWrapping.NoWrap);
+                FontWeight.Normal, TextWrapping.NoWrap,
+                -1);
         }
 
-        private IFormattedTextImpl Create(string text, double fontSize, TextAlignment alignment)
+        private IFormattedTextImpl Create(string text, double fontSize, TextAlignment alignment, double widthConstraint)
         {
             return Create(text, FontName, fontSize,
                 FontStyle.Normal, alignment,
-                FontWeight.Normal, TextWrapping.NoWrap);
+                FontWeight.Normal, TextWrapping.NoWrap,
+                widthConstraint);
         }
 
-        private IFormattedTextImpl Create(string text, double fontSize, TextWrapping wrap)
+        private IFormattedTextImpl Create(string text, double fontSize, TextWrapping wrap, double widthConstraint)
         {
             return Create(text, FontName, fontSize,
                 FontStyle.Normal, TextAlignment.Left,
-                FontWeight.Normal, wrap);
+                FontWeight.Normal, wrap,
+                widthConstraint);
         }
 
 #if AVALONIA_CAIRO
@@ -101,17 +104,15 @@ namespace Avalonia.Direct2D1.RenderTests.Media
         [InlineData(stringmiddlenewlines, FontSize, 72.01, 4 * FontSizeHeight)]
         public void Should_Measure_String_Correctly(string input, double fontSize, double expWidth, double expHeight)
         {
-            using (var fmt = Create(input, fontSize))
-            {
-                var size = fmt.Measure();
+            var fmt = Create(input, fontSize);
+            var size = fmt.Size;
 
-                Assert.Equal(expWidth, size.Width, 2);
-                Assert.Equal(expHeight, size.Height, 2);
+            Assert.Equal(expWidth, size.Width, 2);
+            Assert.Equal(expHeight, size.Height, 2);
 
-                var linesHeight = fmt.GetLines().Sum(l => l.Height);
+            var linesHeight = fmt.GetLines().Sum(l => l.Height);
 
-                Assert.Equal(expHeight, linesHeight, 2);
-            }
+            Assert.Equal(expHeight, linesHeight, 2);
         }
 
 #if AVALONIA_CAIRO
@@ -135,16 +136,11 @@ namespace Avalonia.Direct2D1.RenderTests.Media
                                                             double widthConstraint,
                                                             TextWrapping wrap)
         {
-            using (var fmt = Create(input, FontSize, wrap))
-            {
-                if (widthConstraint != -1)
-                {
-                    fmt.Constraint = new Size(widthConstraint, 10000);
-                }
+            var fmt = Create(input, FontSize, wrap, widthConstraint);
+            var constrained = fmt;
 
-                var lines = fmt.GetLines().ToArray();
-                Assert.Equal(linesCount, lines.Count());
-            }
+            var lines = constrained.GetLines().ToArray();
+            Assert.Equal(linesCount, lines.Count());
         }
 
 #if AVALONIA_CAIRO
@@ -178,14 +174,12 @@ namespace Avalonia.Direct2D1.RenderTests.Media
                                     double x, double y,
                                     bool isInside, bool isTrailing, int pos)
         {
-            using (var fmt = Create(input, FontSize))
-            {
-                var htRes = fmt.HitTestPoint(new Point(x, y));
+            var fmt = Create(input, FontSize);
+            var htRes = fmt.HitTestPoint(new Point(x, y));
 
-                Assert.Equal(pos, htRes.TextPosition);
-                Assert.Equal(isInside, htRes.IsInside);
-                Assert.Equal(isTrailing, htRes.IsTrailing);
-            }
+            Assert.Equal(pos, htRes.TextPosition);
+            Assert.Equal(isInside, htRes.IsInside);
+            Assert.Equal(isTrailing, htRes.IsTrailing);
         }
 
 #if AVALONIA_CAIRO
@@ -205,15 +199,13 @@ namespace Avalonia.Direct2D1.RenderTests.Media
         public void Should_HitTestPosition_Correctly(string input,
                     int index, double x, double y, double width, double height)
         {
-            using (var fmt = Create(input, FontSize))
-            {
-                var r = fmt.HitTestTextPosition(index);
+            var fmt = Create(input, FontSize);
+            var r = fmt.HitTestTextPosition(index);
 
-                Assert.Equal(x, r.X, 2);
-                Assert.Equal(y, r.Y, 2);
-                Assert.Equal(width, r.Width, 2);
-                Assert.Equal(height, r.Height, 2);
-            }
+            Assert.Equal(x, r.X, 2);
+            Assert.Equal(y, r.Y, 2);
+            Assert.Equal(width, r.Width, 2);
+            Assert.Equal(height, r.Height, 2);
         }
 
 #if AVALONIA_CAIRO
@@ -229,17 +221,14 @@ namespace Avalonia.Direct2D1.RenderTests.Media
                                                     double x, double y, double width, double height)
         {
             //parse expected
-            using (var fmt = Create(input, FontSize, TextAlignment.Right))
-            {
-                fmt.Constraint = new Size(widthConstraint, 100);
-
-                var r = fmt.HitTestTextPosition(index);
+            var fmt = Create(input, FontSize, TextAlignment.Right, widthConstraint);
+            var constrained = fmt;
+            var r = constrained.HitTestTextPosition(index);
 
-                Assert.Equal(x, r.X, 2);
-                Assert.Equal(y, r.Y, 2);
-                Assert.Equal(width, r.Width, 2);
-                Assert.Equal(height, r.Height, 2);
-            }
+            Assert.Equal(x, r.X, 2);
+            Assert.Equal(y, r.Y, 2);
+            Assert.Equal(width, r.Width, 2);
+            Assert.Equal(height, r.Height, 2);
         }
 
 #if AVALONIA_CAIRO
@@ -255,17 +244,14 @@ namespace Avalonia.Direct2D1.RenderTests.Media
                                                     double x, double y, double width, double height)
         {
             //parse expected
-            using (var fmt = Create(input, FontSize, TextAlignment.Center))
-            {
-                fmt.Constraint = new Size(widthConstraint, 100);
+            var fmt = Create(input, FontSize, TextAlignment.Center, widthConstraint);
+            var constrained = fmt;
+            var r = constrained.HitTestTextPosition(index);
 
-                var r = fmt.HitTestTextPosition(index);
-
-                Assert.Equal(x, r.X, 2);
-                Assert.Equal(y, r.Y, 2);
-                Assert.Equal(width, r.Width, 2);
-                Assert.Equal(height, r.Height, 2);
-            }
+            Assert.Equal(x, r.X, 2);
+            Assert.Equal(y, r.Y, 2);
+            Assert.Equal(width, r.Width, 2);
+            Assert.Equal(height, r.Height, 2);
         }
 
 #if AVALONIA_CAIRO
@@ -291,22 +277,20 @@ namespace Avalonia.Direct2D1.RenderTests.Media
                 return new Rect(v[0], v[1], v[2], v[3]);
             }).ToArray();
 
-            using (var fmt = Create(input, FontSize))
-            {
-                var htRes = fmt.HitTestTextRange(index, length).ToArray();
+            var fmt = Create(input, FontSize);
+            var htRes = fmt.HitTestTextRange(index, length).ToArray();
 
-                Assert.Equal(rects.Length, htRes.Length);
+            Assert.Equal(rects.Length, htRes.Length);
 
-                for (int i = 0; i < rects.Length; i++)
-                {
-                    var exr = rects[i];
-                    var r = htRes[i];
+            for (int i = 0; i < rects.Length; i++)
+            {
+                var exr = rects[i];
+                var r = htRes[i];
 
-                    Assert.Equal(exr.X, r.X, 2);
-                    Assert.Equal(exr.Y, r.Y, 2);
-                    Assert.Equal(exr.Width, r.Width, 2);
-                    Assert.Equal(exr.Height, r.Height, 2);
-                }
+                Assert.Equal(exr.X, r.X, 2);
+                Assert.Equal(exr.Y, r.Y, 2);
+                Assert.Equal(exr.Width, r.Width, 2);
+                Assert.Equal(exr.Height, r.Height, 2);
             }
         }
     }

+ 5 - 5
tests/Avalonia.UnitTests/TestServices.cs

@@ -13,6 +13,7 @@ using Avalonia.Styling;
 using Avalonia.Themes.Default;
 using Avalonia.Rendering;
 using System.Reactive.Concurrency;
+using System.Collections.Generic;
 
 namespace Avalonia.UnitTests
 {
@@ -163,12 +164,11 @@ namespace Avalonia.UnitTests
             return Mock.Of<IPlatformRenderInterface>(x => 
                 x.CreateFormattedText(
                     It.IsAny<string>(),
-                    It.IsAny<string>(),
-                    It.IsAny<double>(),
-                    It.IsAny<FontStyle>(),
+                    It.IsAny<Typeface>(),
                     It.IsAny<TextAlignment>(),
-                    It.IsAny<FontWeight>(),
-                    It.IsAny<TextWrapping>()) == Mock.Of<IFormattedTextImpl>() &&
+                    It.IsAny<TextWrapping>(),
+                    It.IsAny<Size>(),
+                    It.IsAny<IReadOnlyList<FormattedTextStyleSpan>>()) == Mock.Of<IFormattedTextImpl>() &&
                 x.CreateStreamGeometry() == Mock.Of<IStreamGeometryImpl>(
                     y => y.Open() == Mock.Of<IStreamGeometryContextImpl>()));
         }

+ 0 - 28
tests/Avalonia.Visuals.UnitTests/Media/FormattedTextTests.cs

@@ -1,28 +0,0 @@
-using System;
-using Avalonia.Media;
-using Xunit;
-
-namespace Avalonia.Visuals.UnitTests.Media
-{
-    public class FormattedTextTests
-    {
-        [Fact]
-        public void Exception_Should_Be_Thrown_If_FontSize_0()
-        {
-            Assert.Throws<ArgumentException>(() => new FormattedText(
-                "foo",
-                "Ariel",
-                0));
-        }
-
-        [Fact]
-        public void Exception_Should_Be_Thrown_If_FontWeight_0()
-        {
-            Assert.Throws<ArgumentException>(() => new FormattedText(
-                "foo",
-                "Ariel",
-                12,
-                fontWeight: 0));
-        }
-    }
-}

+ 21 - 0
tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs

@@ -0,0 +1,21 @@
+using System;
+using Avalonia.Media;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media
+{
+    public class TypefaceTests
+    {
+        [Fact]
+        public void Exception_Should_Be_Thrown_If_FontSize_0()
+        {
+            Assert.Throws<ArgumentException>(() => new Typeface("foo", 0));
+        }
+
+        [Fact]
+        public void Exception_Should_Be_Thrown_If_FontWeight_0()
+        {
+            Assert.Throws<ArgumentException>(() => new Typeface("foo", 12, weight: 0));
+        }
+    }
+}

+ 19 - 5
tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

@@ -10,12 +10,11 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
     {
         public IFormattedTextImpl CreateFormattedText(
             string text,
-            string fontFamilyName,
-            double fontSize,
-            FontStyle fontStyle,
+            Typeface typeface,
             TextAlignment textAlignment,
-            FontWeight fontWeight,
-            TextWrapping wrapping)
+            TextWrapping wrapping,
+            Size constraint,
+            IReadOnlyList<FormattedTextStyleSpan> spans)
         {
             throw new NotImplementedException();
         }
@@ -94,11 +93,26 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
                 throw new NotImplementedException();
             }
 
+            public IGeometryImpl Intersect(IGeometryImpl geometry)
+            {
+                throw new NotImplementedException();
+            }
+
             public IStreamGeometryContextImpl Open()
             {
                 return _impl;
             }
 
+            public bool StrokeContains(Pen pen, Point point)
+            {
+                throw new NotImplementedException();
+            }
+
+            public IGeometryImpl WithTransform(Matrix transform)
+            {
+                throw new NotImplementedException();
+            }
+
             class MockStreamGeometryContext : IStreamGeometryContextImpl
             {
                 private List<Point> points = new List<Point>();