Browse Source

Make IFormattedTextImpl immutable.

Previously `IFormattedTextImpl` had `SetForegroundBrush` which set
mutable state. Make `FormattedText` fully mutable (before it was kinda
mutable, kinda immutable) and create immutable `IFormattedTextImpl`s on
demand.
Steven Kirk 8 years ago
parent
commit
376535b198

+ 11 - 10
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -212,7 +212,10 @@ namespace Avalonia.Controls.Presenters
 
 
             if (length > 0)
             if (length > 0)
             {
             {
-                result.SetForegroundBrush(Brushes.White, start, length);
+                result.Spans = new[]
+                {
+                    new FormattedTextStyleSpan(start, length, foregroundBrush: Brushes.White),
+                };
             }
             }
 
 
             return result;
             return result;
@@ -228,15 +231,13 @@ namespace Avalonia.Controls.Presenters
             }
             }
             else
             else
             {
             {
-                // TODO: Pretty sure that measuring "X" isn't the right way to do this...
-                return new FormattedText(
-                    "X",
-                    FontFamily,
-                    FontSize,
-                    availableSize,
-                    FontStyle,
-                    TextAlignment,
-                    FontWeight).Measure();
+                return new FormattedText
+                {
+                    Text = "X",
+                    Typeface = new Typeface(FontFamily, FontSize, FontStyle, FontWeight),
+                    TextAlignment = TextAlignment,
+                    Constraint = availableSize,
+                }.Measure();
             }
             }
         }
         }
 
 

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

@@ -85,16 +85,14 @@ namespace Avalonia.Controls.Primitives
         /// <returns>A <see cref="FormattedText"/> object.</returns>
         /// <returns>A <see cref="FormattedText"/> object.</returns>
         protected override FormattedText CreateFormattedText(Size constraint)
         protected override FormattedText CreateFormattedText(Size constraint)
         {
         {
-            var result = new FormattedText(
-                StripAccessKey(Text),
-                FontFamily,
-                FontSize,
-                constraint,
-                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>
         /// <summary>

+ 8 - 11
src/Avalonia.Controls/TextBlock.cs

@@ -350,17 +350,14 @@ namespace Avalonia.Controls
         /// <returns>A <see cref="FormattedText"/> object.</returns>
         /// <returns>A <see cref="FormattedText"/> object.</returns>
         protected virtual FormattedText CreateFormattedText(Size constraint)
         protected virtual FormattedText CreateFormattedText(Size constraint)
         {
         {
-            var result = new FormattedText(
-                Text ?? string.Empty,
-                FontFamily,
-                FontSize,
-                constraint,
-                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>
         /// <summary>

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

@@ -117,7 +117,11 @@ namespace TheArtOfDev.HtmlRenderer.Avalonia.Adapters
         FormattedText GetText(string str, RFont font)
         FormattedText GetText(string str, RFont font)
         {
         {
             var f = ((FontAdapter)font);
             var f = ((FontAdapter)font);
-            return new FormattedText(str, f.Name, font.Size, Size.Infinity, 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)
         public override void MeasureString(string str, RFont font, double maxWidth, out int charFit, out double charFitWidth)

+ 1 - 0
src/Avalonia.Visuals/Avalonia.Visuals.csproj

@@ -2,6 +2,7 @@
   <PropertyGroup>
   <PropertyGroup>
     <TargetFramework>netstandard1.1</TargetFramework>
     <TargetFramework>netstandard1.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+    <RootNamespace>Avalonia</RootNamespace>
   </PropertyGroup>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
   <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
     <DebugSymbols>true</DebugSymbols>
     <DebugSymbols>true</DebugSymbols>

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

@@ -12,64 +12,30 @@ namespace Avalonia.Media
     /// </summary>
     /// </summary>
     public class FormattedText
     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>
         /// <summary>
         /// Initializes a new instance of the <see cref="FormattedText"/> class.
         /// Initializes a new instance of the <see cref="FormattedText"/> class.
         /// </summary>
         /// </summary>
-        /// <param name="text">The text.</param>
-        /// <param name="fontFamilyName">The font family.</param>
-        /// <param name="fontSize">The font size.</param>
-        /// <param name="constraint">The text layout constraints.</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,
-            Size constraint,
-            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,
-                constraint);
+        /// <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>
         /// <summary>
@@ -77,49 +43,77 @@ namespace Avalonia.Media
         /// </summary>
         /// </summary>
         public Size Constraint
         public Size Constraint
         {
         {
-            get { return PlatformImpl.Constraint; }
-            set { PlatformImpl = PlatformImpl.WithConstraint(value); }
+            get => _constraint;
+            set => Set(ref _constraint, value);
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Gets the font family.
+        /// Gets or sets the base typeface.
         /// </summary>
         /// </summary>
-        public string FontFamilyName { get; }
+        public Typeface Typeface
+        {
+            get => _typeface;
+            set => Set(ref _typeface, value);
+        }
 
 
         /// <summary>
         /// <summary>
-        /// Gets the font size.
+        /// Gets or sets a collection of spans that describe the formatting of subsections of the
+        /// text.
         /// </summary>
         /// </summary>
-        public double FontSize { get; }
+        public IReadOnlyList<FormattedTextStyleSpan> Spans
+        {
+            get => _spans;
+            set => Set(ref _spans, value);
+        }
 
 
         /// <summary>
         /// <summary>
-        /// Gets the font style.
+        /// Gets or sets the text.
         /// </summary>
         /// </summary>
-        public FontStyle FontStyle { get; }
+        public string Text
+        {
+            get => _text;
+            set => Set(ref _text, value);
+        }
 
 
         /// <summary>
         /// <summary>
-        /// Gets the font weight.
+        /// Gets or sets the aligment of the text.
         /// </summary>
         /// </summary>
-        public FontWeight FontWeight { get; }
+        public TextAlignment TextAlignment
+        {
+            get => _textAlignment;
+            set => Set(ref _textAlignment, value);
+        }
 
 
         /// <summary>
         /// <summary>
-        /// Gets the text.
+        /// Gets or sets the text wrapping.
         /// </summary>
         /// </summary>
-        public string Text { get; }
+        public TextWrapping Wrapping
+        {
+            get => _wrapping;
+            set => Set(ref _wrapping, value);
+        }
 
 
         /// <summary>
         /// <summary>
         /// Gets platform-specific platform implementation.
         /// Gets platform-specific platform implementation.
         /// </summary>
         /// </summary>
-        public IFormattedTextImpl PlatformImpl { get; private set; }
-
-        /// <summary>
-        /// Gets the text alignment.
-        /// </summary>
-        public TextAlignment TextAlignment { get; }
-
-        /// <summary>
-        /// Gets the text wrapping.
-        /// </summary>
-        public TextWrapping Wrapping { get; }
+        public IFormattedTextImpl PlatformImpl
+        {
+            get
+            {
+                if (_platformImpl == null)
+                {
+                    _platformImpl = _platform.CreateFormattedText(
+                        _text,
+                        _typeface,
+                        _textAlignment,
+                        _wrapping,
+                        _constraint,
+                        _spans);
+                }
+
+                return _platformImpl;
+            }
+        }
 
 
         /// <summary>
         /// <summary>
         /// Gets the lines in the text.
         /// Gets the lines in the text.
@@ -174,15 +168,10 @@ namespace Avalonia.Media
             return PlatformImpl.Size;
             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)
         {
         {
-            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; }
+    }
+}

+ 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; }
+    }
+}

+ 0 - 15
src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs

@@ -58,20 +58,5 @@ namespace Avalonia.Platform
         /// <param name="length">The number of characters in the text range.</param>
         /// <param name="length">The number of characters in the text range.</param>
         /// <returns>The character bounds.</returns>
         /// <returns>The character bounds.</returns>
         IEnumerable<Rect> HitTestTextRange(int index, int length);
         IEnumerable<Rect> HitTestTextRange(int index, int length);
-
-        /// <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);
-
-        /// <summary>
-        /// Makes a clone of the formatted text with the specified constraint.
-        /// </summary>
-        /// <param name="constraint">The constraint.</param>
-        /// <returns>The cloned formatted text.</returns>
-        IFormattedTextImpl WithConstraint(Size constraint);
     }
     }
 }
 }

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

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

+ 13 - 10
src/Avalonia.Visuals/Rendering/RendererBase.cs

@@ -7,11 +7,21 @@ namespace Avalonia.Rendering
 {
 {
     public class RendererBase
     public class RendererBase
     {
     {
+        private static readonly Typeface s_fpsTypeface = new Typeface("Arial", 18);
         private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
         private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
         private int _framesThisSecond;
         private int _framesThisSecond;
         private int _fps;
         private int _fps;
+        private FormattedText _fpsText;
         private TimeSpan _lastFpsUpdate;
         private TimeSpan _lastFpsUpdate;
 
 
+        public RendererBase()
+        {
+            _fpsText = new FormattedText
+            {
+                Typeface = new Typeface(null, 18),
+            };
+        }
+
         protected void RenderFps(IDrawingContextImpl context, Rect clientRect, bool incrementFrameCount)
         protected void RenderFps(IDrawingContextImpl context, Rect clientRect, bool incrementFrameCount)
         {
         {
             var now = _stopwatch.Elapsed;
             var now = _stopwatch.Elapsed;
@@ -29,20 +39,13 @@ namespace Avalonia.Rendering
                 _lastFpsUpdate = now;
                 _lastFpsUpdate = now;
             }
             }
 
 
-            var txt = new FormattedText(
-                string.Format("FPS: {0:000}", _fps),
-                "Arial", 18,
-                Size.Infinity,
-                FontStyle.Normal,
-                TextAlignment.Left,
-                FontWeight.Normal,
-                TextWrapping.NoWrap);
-            var size = txt.Measure();
+            _fpsText.Text = string.Format("FPS: {0:000}", _fps);
+            var size = _fpsText.Measure();
             var rect = new Rect(clientRect.Right - size.Width, 0, size.Width, size.Height);
             var rect = new Rect(clientRect.Right - size.Width, 0, size.Width, size.Height);
 
 
             context.Transform = Matrix.Identity;
             context.Transform = Matrix.Identity;
             context.FillRectangle(Brushes.Black, rect);
             context.FillRectangle(Brushes.Black, rect);
-            context.DrawText(Brushes.White, rect.TopLeft, txt.PlatformImpl);
+            context.DrawText(Brushes.White, rect.TopLeft, _fpsText.PlatformImpl);
         }
         }
     }
     }
 }
 }

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

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

+ 37 - 74
src/Gtk/Avalonia.Cairo/Media/FormattedTextImpl.cs

@@ -22,25 +22,48 @@ namespace Avalonia.Cairo.Media
         public FormattedTextImpl(
         public FormattedTextImpl(
             Pango.Context context,
             Pango.Context context,
             string text,
             string text,
-            string fontFamily,
-            double fontSize,
-            FontStyle fontStyle,
+            Typeface typeface,
             TextAlignment textAlignment,
             TextAlignment textAlignment,
-            FontWeight fontWeight,
-            Size constraint)
+            TextWrapping wrapping,
+            Size constraint,
+            IReadOnlyList<FormattedTextStyleSpan> spans)
         {
         {
             Contract.Requires<ArgumentNullException>(context != null);
             Contract.Requires<ArgumentNullException>(context != null);
             Contract.Requires<ArgumentNullException>(text != null);
             Contract.Requires<ArgumentNullException>(text != null);
 
 
-            Layout = Create(
-                context,
-                text,
-                fontFamily,
-                fontSize,
-                (Pango.Style)fontStyle,
-                textAlignment.ToCairo(),
-                fontWeight.ToCairo(),
-                constraint);
+            Layout = new Pango.Layout(context);
+            Layout.SetText(text);
+
+            Layout.FontDescription = new Pango.FontDescription
+            {
+                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);
+
+            if (spans != null)
+            {
+                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);
+                    }
+                }
+            }
+
             Size = Measure();
             Size = Measure();
         }
         }
 
 
@@ -111,66 +134,6 @@ namespace Avalonia.Cairo.Media
             return ranges;
             return ranges;
         }
         }
 
 
-        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);
-            }
-        }
-
-        public IFormattedTextImpl WithConstraint(Size constraint)
-        {
-            return new FormattedTextImpl(Create(
-                Layout.Context,
-                Layout.Text,
-                Layout.FontDescription.Family,
-                Layout.FontDescription.Size,
-                Layout.FontDescription.Style,
-                Layout.Alignment,
-                Layout.FontDescription.Weight,
-                constraint));
-        }
-
-        private Pango.Layout Create(
-            Pango.Context context,
-            string text,
-            string fontFamily,
-            double fontSize,
-            Pango.Style fontStyle,
-            Pango.Alignment textAlignment,
-            Pango.Weight fontWeight,
-            Size constraint)
-        {
-            Contract.Requires<ArgumentNullException>(context != null);
-            Contract.Requires<ArgumentNullException>(text != null);
-            var result = new Pango.Layout(context);
-
-            result.SetText(text);
-
-            result.FontDescription = new Pango.FontDescription
-            {
-                Family = fontFamily,
-                Size = Pango.Units.FromDouble(CorrectScale(fontSize)),
-                Style = (Pango.Style)fontStyle,
-                Weight = fontWeight
-            };
-
-            result.Alignment = textAlignment;
-            result.Attributes = new Pango.AttrList();
-            result.Width = double.IsPositiveInfinity(constraint.Width) ? -1 : Pango.Units.FromDouble(constraint.Width);
-            return result;
-        }
-
         private Size Measure()
         private Size Measure()
         {
         {
             int width;
             int width;

+ 37 - 38
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@@ -12,17 +12,23 @@ namespace Avalonia.Skia
 {
 {
     public class FormattedTextImpl : IFormattedTextImpl
     public class FormattedTextImpl : IFormattedTextImpl
     {
     {
-        public FormattedTextImpl(string text, string fontFamilyName, double fontSize, FontStyle fontStyle,
-                    TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping, Size constraint)
+        public FormattedTextImpl(
+            string text,
+            Typeface typeface,
+            TextAlignment textAlignment,
+            TextWrapping wrapping,
+            Size constraint,
+            IReadOnlyList<FormattedTextStyleSpan> spans)
         {
         {
             Text = text ?? string.Empty;
             Text = text ?? string.Empty;
-            _fontStyle = fontStyle;
-            _fontWeight = fontWeight;
 
 
             // Replace 0 characters with zero-width spaces (200B)
             // Replace 0 characters with zero-width spaces (200B)
             Text = Text.Replace((char)0, (char)0x200B);
             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();
             _paint = new SKPaint();
 
 
@@ -33,14 +39,22 @@ namespace Avalonia.Skia
             _paint.IsAntialias = true;            
             _paint.IsAntialias = true;            
             _paint.LcdRenderText = true;            
             _paint.LcdRenderText = true;            
             _paint.SubpixelText = 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.TextAlign = textAlignment.ToSKTextAlign();
             _paint.XferMode = SKXferMode.Src;
             _paint.XferMode = SKXferMode.Src;
 
 
             _wrapping = wrapping;
             _wrapping = wrapping;
             _constraint = constraint;
             _constraint = constraint;
 
 
+            foreach (var span in spans)
+            {
+                if (span.ForegroundBrush != null)
+                {
+                    SetForegroundBrush(span.ForegroundBrush, span.StartIndex, span.Length);
+                }
+            }
+
             Rebuild();
             Rebuild();
         }
         }
 
 
@@ -149,40 +163,11 @@ namespace Avalonia.Skia
             return result;
             return result;
         }
         }
 
 
-        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()
         public override string ToString()
         {
         {
             return Text;
             return Text;
         }
         }
 
 
-        public IFormattedTextImpl WithConstraint(Size constraint)
-        {
-            return new FormattedTextImpl(
-                Text,
-                _paint.Typeface.FamilyName,
-                _paint.TextSize,
-                _fontStyle,
-                _paint.TextAlign.ToAvalonia(),
-                _fontWeight,
-                _wrapping,
-                constraint);
-        }
-
         internal void Draw(DrawingContextImpl context,
         internal void Draw(DrawingContextImpl context,
                            SKCanvas canvas, SKPoint origin,
                            SKCanvas canvas, SKPoint origin,
                            DrawingContextImpl.PaintWrapper foreground)
                            DrawingContextImpl.PaintWrapper foreground)
@@ -284,8 +269,6 @@ namespace Avalonia.Skia
         private readonly List<Rect> _rects = new List<Rect>();
         private readonly List<Rect> _rects = new List<Rect>();
         public string Text { get; }
         public string Text { get; }
         private readonly TextWrapping _wrapping;
         private readonly TextWrapping _wrapping;
-        private readonly FontStyle _fontStyle;
-        private readonly FontWeight _fontWeight;
         private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity);
         private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity);
         private float _lineHeight = 0;
         private float _lineHeight = 0;
         private float _lineOffset = 0;
         private float _lineOffset = 0;
@@ -618,6 +601,22 @@ namespace Avalonia.Skia
             return x;
             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
         private struct AvaloniaFormattedTextLine
         {
         {
             public float Height;
             public float Height;

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

@@ -16,10 +16,15 @@ namespace Avalonia.Skia
             return CreateRenderTargetBitmap(width, height, 96, 96);
             return CreateRenderTargetBitmap(width, height, 96, 96);
         }
         }
 
 
-        public IFormattedTextImpl CreateFormattedText(string text, string fontFamilyName, double fontSize, FontStyle fontStyle,
-            TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping, Size constraint)
+        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, constraint);
+            return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans);
         }
         }
 
 
         public IStreamGeometryImpl CreateStreamGeometry()
         public IStreamGeometryImpl CreateStreamGeometry()

+ 7 - 10
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@@ -90,25 +90,22 @@ namespace Avalonia.Direct2D1
 
 
         public IFormattedTextImpl CreateFormattedText(
         public IFormattedTextImpl CreateFormattedText(
             string text,
             string text,
-            string fontFamily,
-            double fontSize,
-            FontStyle fontStyle,
+            Typeface typeface,
             TextAlignment textAlignment,
             TextAlignment textAlignment,
-            FontWeight fontWeight,
             TextWrapping wrapping,
             TextWrapping wrapping,
-            Size constraint)
+            Size constraint,
+            IReadOnlyList<FormattedTextStyleSpan> spans)
         {
         {
             return new FormattedTextImpl(
             return new FormattedTextImpl(
                 text,
                 text,
-                fontFamily,
-                fontSize,
-                fontStyle,
+                typeface,
                 textAlignment,
                 textAlignment,
-                fontWeight,
                 wrapping,
                 wrapping,
-                constraint);
+                constraint,
+                spans);
         }
         }
 
 
+
         public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
         public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
         {
         {
             var nativeWindow = surfaces?.OfType<IPlatformHandle>().FirstOrDefault();
             var nativeWindow = surfaces?.OfType<IPlatformHandle>().FirstOrDefault();

+ 42 - 71
src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs

@@ -14,32 +14,45 @@ namespace Avalonia.Direct2D1.Media
     {
     {
         public FormattedTextImpl(
         public FormattedTextImpl(
             string text,
             string text,
-            string fontFamily,
-            double fontSize,
-            FontStyle fontStyle,
+            Typeface typeface,
             TextAlignment textAlignment,
             TextAlignment textAlignment,
-            FontWeight fontWeight,
             TextWrapping wrapping,
             TextWrapping wrapping,
-            Size constraint)
+            Size constraint,
+            IReadOnlyList<FormattedTextStyleSpan> spans)
         {
         {
             Text = text;
             Text = text;
-            TextLayout = Create(
-                text,
-                fontFamily,
-                fontSize,
-                (DWrite.FontStyle)fontStyle,
-                textAlignment.ToDirect2D(),
-                (DWrite.FontWeight)fontWeight,
-                wrapping == TextWrapping.Wrap ? DWrite.WordWrapping.Wrap : DWrite.WordWrapping.NoWrap,
-                (float)constraint.Width,
-                (float)constraint.Height);
-            Size = Measure();
-        }
+            var factory = AvaloniaLocator.Current.GetService<DWrite.Factory>();
+
+            using (var format = new DWrite.TextFormat(
+                factory,
+                typeface?.FontFamilyName ?? "Courier New",
+                (DWrite.FontWeight)(typeface?.Weight ?? FontWeight.Normal),
+                (DWrite.FontStyle)(typeface?.Style ?? FontStyle.Normal),
+                (float)(typeface?.FontSize ?? 12)))
+            {
+                format.WordWrapping = wrapping == TextWrapping.Wrap ? 
+                    DWrite.WordWrapping.Wrap :
+                    DWrite.WordWrapping.NoWrap;
+
+                TextLayout = new DWrite.TextLayout(
+                    factory,
+                    text ?? string.Empty,
+                    format,
+                    (float)constraint.Width,
+                    (float)constraint.Height)
+                {
+                    TextAlignment = textAlignment.ToDirect2D()
+                };
+            }
+
+            if (spans != null)
+            {
+                foreach (var span in spans)
+                {
+                    ApplySpan(span);
+                }
+            }
 
 
-        public FormattedTextImpl(string text, DWrite.TextLayout textLayout)
-        {
-            Text = text;
-            TextLayout = textLayout;
             Size = Measure();
             Size = Measure();
         }
         }
 
 
@@ -101,58 +114,16 @@ namespace Avalonia.Direct2D1.Media
             return result.Select(x => new Rect(x.Left, x.Top, x.Width, x.Height));
             return result.Select(x => new Rect(x.Left, x.Top, x.Width, x.Height));
         }
         }
 
 
-        public void SetForegroundBrush(IBrush brush, int startIndex, int count)
+        private void ApplySpan(FormattedTextStyleSpan span)
         {
         {
-            TextLayout.SetDrawingEffect(
-                new BrushWrapper(brush),
-                new DWrite.TextRange(startIndex, count));
-        }
-
-        public IFormattedTextImpl WithConstraint(Size constraint)
-        {
-            var factory = AvaloniaLocator.Current.GetService<DWrite.Factory>();
-            return new FormattedTextImpl(Text, Create(
-                Text,
-                TextLayout.FontFamilyName,
-                TextLayout.FontSize,
-                TextLayout.FontStyle,
-                TextLayout.TextAlignment,
-                TextLayout.FontWeight,
-                TextLayout.WordWrapping,
-                (float)constraint.Width,
-                (float)constraint.Height));
-        }
-
-        private static DWrite.TextLayout Create(
-            string text,
-            string fontFamily,
-            double fontSize,
-            DWrite.FontStyle fontStyle,
-            DWrite.TextAlignment textAlignment,
-            DWrite.FontWeight fontWeight,
-            DWrite.WordWrapping wrapping,
-            float constraintX,
-            float constraintY)
-        {
-            var factory = AvaloniaLocator.Current.GetService<DWrite.Factory>();
-
-            using (var format = new DWrite.TextFormat(
-                factory,
-                fontFamily,
-                fontWeight,
-                fontStyle,
-                (float)fontSize))
+            if (span.Length > 0)
             {
             {
-                format.WordWrapping = wrapping;
-
-                var result = new DWrite.TextLayout(
-                    factory,
-                    text ?? string.Empty,
-                    format,
-                    constraintX,
-                    constraintY);
-                result.TextAlignment = textAlignment;
-                return result;
+                if (span.ForegroundBrush != null)
+                {
+                    TextLayout.SetDrawingEffect(
+                        new BrushWrapper(span.ForegroundBrush),
+                        new DWrite.TextRange(span.StartIndex, span.Length));
+                }
             }
             }
         }
         }
 
 

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

@@ -157,23 +157,20 @@ namespace Avalonia.Layout.UnitTests
             public IEnumerable<Rect> HitTestTextRange(int index, int length) => new Rect[0];
             public IEnumerable<Rect> HitTestTextRange(int index, int length) => new Rect[0];
 
 
             public Size Measure() => Constraint;
             public Size Measure() => Constraint;
-
-            public void SetForegroundBrush(IBrush brush, int startIndex, int length)
-            {
-            }
-
-            public IFormattedTextImpl WithConstraint(Size constraint)
-            {
-                return this;
-            }
         }
         }
 
 
         private void RegisterServices()
         private void RegisterServices()
         {
         {
             var globalStyles = new Mock<IGlobalStyles>();
             var globalStyles = new Mock<IGlobalStyles>();
             var renderInterface = new Mock<IPlatformRenderInterface>();
             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>(), It.IsAny<Size>()))
+            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"));
                 .Returns(new FormattedTextMock("TEST"));
                 
                 
             var windowImpl = new Mock<IWindowImpl>();
             var windowImpl = new Mock<IWindowImpl>();

+ 16 - 31
tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs

@@ -50,38 +50,40 @@ namespace Avalonia.Direct2D1.RenderTests.Media
             FontStyle fontStyle,
             FontStyle fontStyle,
             TextAlignment textAlignment,
             TextAlignment textAlignment,
             FontWeight fontWeight,
             FontWeight fontWeight,
-            TextWrapping wrapping)
+            TextWrapping wrapping,
+            double widthConstraint)
         {
         {
             var r = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
             var r = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
             return r.CreateFormattedText(text,
             return r.CreateFormattedText(text,
-                fontFamily,
-                fontSize,
-                fontStyle,
+                new Typeface(fontFamily, fontSize, fontStyle, fontWeight),
                 textAlignment,
                 textAlignment,
-                fontWeight,
                 wrapping,
                 wrapping,
-                Size.Infinity);
+                widthConstraint == -1 ? Size.Infinity : new Size(widthConstraint, double.PositiveInfinity),
+                null);
         }
         }
 
 
         private IFormattedTextImpl Create(string text, double fontSize)
         private IFormattedTextImpl Create(string text, double fontSize)
         {
         {
             return Create(text, FontName, fontSize,
             return Create(text, FontName, fontSize,
                 FontStyle.Normal, TextAlignment.Left,
                 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,
             return Create(text, FontName, fontSize,
                 FontStyle.Normal, alignment,
                 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,
             return Create(text, FontName, fontSize,
                 FontStyle.Normal, TextAlignment.Left,
                 FontStyle.Normal, TextAlignment.Left,
-                FontWeight.Normal, wrap);
+                FontWeight.Normal, wrap,
+                widthConstraint);
         }
         }
 
 
 #if AVALONIA_CAIRO
 #if AVALONIA_CAIRO
@@ -134,14 +136,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media
                                                             double widthConstraint,
                                                             double widthConstraint,
                                                             TextWrapping wrap)
                                                             TextWrapping wrap)
         {
         {
-            var fmt = Create(input, FontSize, wrap);
+            var fmt = Create(input, FontSize, wrap, widthConstraint);
             var constrained = fmt;
             var constrained = fmt;
 
 
-            if (widthConstraint != -1)
-            {
-                constrained = fmt.WithConstraint(new Size(widthConstraint, 10000));
-            }
-
             var lines = constrained.GetLines().ToArray();
             var lines = constrained.GetLines().ToArray();
             Assert.Equal(linesCount, lines.Count());
             Assert.Equal(linesCount, lines.Count());
         }
         }
@@ -224,14 +221,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
                                                     double x, double y, double width, double height)
                                                     double x, double y, double width, double height)
         {
         {
             //parse expected
             //parse expected
-            var fmt = Create(input, FontSize, TextAlignment.Right);
+            var fmt = Create(input, FontSize, TextAlignment.Right, widthConstraint);
             var constrained = fmt;
             var constrained = fmt;
-
-            if (widthConstraint != -1)
-            {
-                constrained = fmt.WithConstraint(new Size(widthConstraint, 100));
-            }
-
             var r = constrained.HitTestTextPosition(index);
             var r = constrained.HitTestTextPosition(index);
 
 
             Assert.Equal(x, r.X, 2);
             Assert.Equal(x, r.X, 2);
@@ -253,14 +244,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
                                                     double x, double y, double width, double height)
                                                     double x, double y, double width, double height)
         {
         {
             //parse expected
             //parse expected
-            var fmt = Create(input, FontSize, TextAlignment.Center);
+            var fmt = Create(input, FontSize, TextAlignment.Center, widthConstraint);
             var constrained = fmt;
             var constrained = fmt;
-
-            if (widthConstraint != -1)
-            {
-                constrained = fmt.WithConstraint(new Size(widthConstraint, 100));
-            }
-
             var r = constrained.HitTestTextPosition(index);
             var r = constrained.HitTestTextPosition(index);
 
 
             Assert.Equal(x, r.X, 2);
             Assert.Equal(x, r.X, 2);

+ 4 - 8
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@@ -11,17 +11,13 @@ namespace Avalonia.UnitTests
     {
     {
         public IFormattedTextImpl CreateFormattedText(
         public IFormattedTextImpl CreateFormattedText(
             string text,
             string text,
-            string fontFamilyName,
-            double fontSize,
-            FontStyle fontStyle,
+            Typeface typeface,
             TextAlignment textAlignment,
             TextAlignment textAlignment,
-            FontWeight fontWeight,
             TextWrapping wrapping,
             TextWrapping wrapping,
-            Size constraint)
+            Size constraint,
+            IReadOnlyList<FormattedTextStyleSpan> spans)
         {
         {
-            var result = new Mock<IFormattedTextImpl>();
-            result.Setup(x => x.WithConstraint(It.IsAny<Size>())).Returns(() => result.Object);
-            return result.Object;
+            return Mock.Of<IFormattedTextImpl>();
         }
         }
 
 
         public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
         public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)

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

@@ -1,30 +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,
-                Size.Infinity));
-        }
-
-        [Fact]
-        public void Exception_Should_Be_Thrown_If_FontWeight_0()
-        {
-            Assert.Throws<ArgumentException>(() => new FormattedText(
-                "foo",
-                "Ariel",
-                12,
-                Size.Infinity,
-                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));
+        }
+    }
+}

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

@@ -10,13 +10,11 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
     {
     {
         public IFormattedTextImpl CreateFormattedText(
         public IFormattedTextImpl CreateFormattedText(
             string text,
             string text,
-            string fontFamilyName,
-            double fontSize,
-            FontStyle fontStyle,
+            Typeface typeface,
             TextAlignment textAlignment,
             TextAlignment textAlignment,
-            FontWeight fontWeight,
             TextWrapping wrapping,
             TextWrapping wrapping,
-            Size constraint)
+            Size constraint,
+            IReadOnlyList<FormattedTextStyleSpan> spans)
         {
         {
             throw new NotImplementedException();
             throw new NotImplementedException();
         }
         }