Browse Source

Merge branch 'master' into net6-ios

Max Katz 3 years ago
parent
commit
a44340e02a
29 changed files with 799 additions and 382 deletions
  1. 11 0
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  2. 10 0
      src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs
  3. 12 0
      src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs
  4. 17 0
      src/Avalonia.Controls/Documents/Bold.cs
  5. 71 0
      src/Avalonia.Controls/Documents/Inline.cs
  6. 123 0
      src/Avalonia.Controls/Documents/InlineCollection.cs
  7. 17 0
      src/Avalonia.Controls/Documents/Italic.cs
  8. 44 0
      src/Avalonia.Controls/Documents/LineBreak.cs
  9. 86 0
      src/Avalonia.Controls/Documents/Run.cs
  10. 95 0
      src/Avalonia.Controls/Documents/Span.cs
  11. 129 0
      src/Avalonia.Controls/Documents/TextElement.cs
  12. 15 0
      src/Avalonia.Controls/Documents/Underline.cs
  13. 1 1
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  14. 1 0
      src/Avalonia.Controls/Properties/AssemblyInfo.cs
  15. 63 6
      src/Avalonia.Controls/TextBlock.cs
  16. 0 2
      src/Avalonia.Controls/TopLevel.cs
  17. 0 344
      src/Avalonia.Controls/ValidatingToplevel.cs
  18. 5 6
      src/Avalonia.Controls/Window.cs
  19. 4 5
      src/Avalonia.Controls/WindowBase.cs
  20. 3 0
      src/Avalonia.Visuals/Avalonia.Visuals.csproj
  21. 1 1
      src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs
  22. 7 13
      src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
  23. 6 0
      src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
  24. 8 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs
  25. 44 0
      tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
  26. 1 1
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  27. 2 2
      tests/Avalonia.LeakTests/ControlTests.cs
  28. 22 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
  29. 1 1
      tests/Avalonia.UnitTests/MockTextShaperImpl.cs

+ 11 - 0
samples/ControlCatalog/Pages/TextBlockPage.xaml

@@ -117,6 +117,17 @@
           <TextBlock Text="👪 👨‍👩‍👧 👨‍👩‍👧‍👦" />
           <TextBlock Text="👪 👨‍👩‍👧 👨‍👩‍👧‍👦" />
         </StackPanel>
         </StackPanel>
       </Border>
       </Border>
+      <Border>
+        <TextBlock Margin="10" TextWrapping="Wrap">
+          This <Span FontWeight="Bold">is</Span> a
+          <Span Background="Silver" Foreground="Maroon">TextBlock</Span>
+          with <Span TextDecorations="Underline">several</Span>
+          <Span FontStyle="Italic">Span</Span> elements,
+          <Span Foreground="Blue">
+            using a <Bold>variety</Bold> of <Italic>styles</Italic>
+          </Span>.
+        </TextBlock>
+      </Border>
     </WrapPanel>
     </WrapPanel>
   </StackPanel>
   </StackPanel>
 </UserControl>
 </UserControl>

+ 10 - 0
src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs

@@ -0,0 +1,10 @@
+using System;
+
+namespace Avalonia.Metadata
+{
+    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+    public class TrimSurroundingWhitespaceAttribute : Attribute
+    {
+
+    }
+}

+ 12 - 0
src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs

@@ -0,0 +1,12 @@
+using System;
+
+namespace Avalonia.Metadata
+{
+    /// <summary>
+    /// Indicates that a collection type should be processed as being whitespace significant by a XAML processor.
+    /// </summary>
+    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+    public class WhitespaceSignificantCollectionAttribute : Attribute
+    {
+    }
+}

+ 17 - 0
src/Avalonia.Controls/Documents/Bold.cs

@@ -0,0 +1,17 @@
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Documents
+{
+    /// <summary>
+    /// Bold element - markup helper for indicating bolded content.
+    /// Equivalent to a Span with FontWeight property set to FontWeights.Bold.
+    /// Can contain other inline elements.
+    /// </summary>
+    public sealed class Bold : Span
+    {
+        static Bold()
+        {
+            FontWeightProperty.OverrideDefaultValue<Bold>(FontWeight.Bold);
+        }
+    }
+}

+ 71 - 0
src/Avalonia.Controls/Documents/Inline.cs

@@ -0,0 +1,71 @@
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Media;
+using Avalonia.Media.TextFormatting;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls.Documents 
+{
+    /// <summary>
+    /// Inline element.
+    /// </summary>
+    public abstract class Inline : TextElement
+    {
+        /// <summary>
+        /// AvaloniaProperty for <see cref="TextDecorations" /> property.
+        /// </summary>
+        public static readonly StyledProperty<TextDecorationCollection> TextDecorationsProperty =
+            AvaloniaProperty.Register<Inline, TextDecorationCollection>(
+                nameof(TextDecorations));
+
+        /// <summary>
+        /// AvaloniaProperty for <see cref="BaselineAlignment" /> property.
+        /// </summary>
+        public static readonly StyledProperty<BaselineAlignment> BaselineAlignmentProperty =
+            AvaloniaProperty.Register<Inline, BaselineAlignment>(
+                nameof(BaselineAlignment),
+                BaselineAlignment.Baseline);
+
+        /// <summary>
+        /// The TextDecorations property specifies decorations that are added to the text of an element.
+        /// </summary>
+        public TextDecorationCollection TextDecorations
+        {
+            get { return GetValue(TextDecorationsProperty); }
+            set { SetValue(TextDecorationsProperty, value); }
+        }
+
+        /// <summary>
+        /// Describes how the baseline for a text-based element is positioned on the vertical axis,
+        /// relative to the established baseline for text.
+        /// </summary>
+        public BaselineAlignment BaselineAlignment
+        {
+            get { return GetValue(BaselineAlignmentProperty); }
+            set { SetValue(BaselineAlignmentProperty, value); }
+        }
+
+        internal abstract int BuildRun(StringBuilder stringBuilder, IList<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex);
+
+        internal abstract int AppendText(StringBuilder stringBuilder);
+
+        protected TextRunProperties CreateTextRunProperties()
+        {
+            return new GenericTextRunProperties(new Typeface(FontFamily, FontStyle, FontWeight), FontSize,
+                TextDecorations, Foreground, Background, BaselineAlignment);
+        }
+        
+        protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
+        {
+            base.OnPropertyChanged(change);
+
+            switch (change.Property.Name)
+            {
+                case nameof(TextDecorations):
+                case nameof(BaselineAlignment):
+                    Invalidate();
+                    break;
+            }
+        }
+    }
+}

+ 123 - 0
src/Avalonia.Controls/Documents/InlineCollection.cs

@@ -0,0 +1,123 @@
+using System;
+using System.Text;
+using Avalonia.Collections;
+using Avalonia.LogicalTree;
+using Avalonia.Metadata;
+
+namespace Avalonia.Controls.Documents
+{
+    /// <summary>
+    /// A collection of <see cref="Inline"/>s.
+    /// </summary>
+    [WhitespaceSignificantCollection]
+    public class InlineCollection : AvaloniaList<Inline>
+    {
+        private string? _text = string.Empty;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="InlineCollection"/> class.
+        /// </summary>
+        public InlineCollection(ILogical parent) : base(0)
+        {
+            ResetBehavior = ResetBehavior.Remove;
+            
+            this.ForEachItem(
+                x =>
+                {
+                    ((ISetLogicalParent)x).SetParent(parent);
+                    x.Invalidated += Invalidate;
+                    Invalidate();
+                },
+                x =>
+                {
+                    ((ISetLogicalParent)x).SetParent(null);
+                    x.Invalidated -= Invalidate;
+                    Invalidate();
+                },
+                () => throw new NotSupportedException());
+        }
+
+        public bool HasComplexContent => Count > 0;
+
+        /// <summary>
+        /// Gets or adds the text held by the inlines collection.
+        /// <remarks>
+        /// Can be null for complex content.
+        /// </remarks>
+        /// </summary>
+        public string? Text
+        {
+            get
+            {
+                if (!HasComplexContent)
+                {
+                    return _text;
+                }
+                
+                var builder = new StringBuilder();
+
+                foreach(var inline in this)
+                {
+                    inline.AppendText(builder);
+                }
+
+                return builder.ToString();
+            }
+            set
+            {
+                if (HasComplexContent)
+                {
+                    Add(new Run(value));
+                }
+                else
+                {
+                    _text = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Add a text segment to the collection.
+        /// <remarks>
+        /// For non complex content this appends the text to the end of currently held text.
+        /// For complex content this adds a <see cref="Run"/> to the collection.
+        /// </remarks>
+        /// </summary>
+        /// <param name="text"></param>
+        public void Add(string text)
+        {
+            if (HasComplexContent)
+            {
+                Add(new Run(text));
+            }
+            else
+            {
+                _text += text;
+            }
+        }
+
+        public override void Add(Inline item)
+        {
+            if (!HasComplexContent)
+            {
+                base.Add(new Run(_text));
+                
+                _text = string.Empty;
+            }
+            
+            base.Add(item);
+        }
+
+        /// <summary>
+        /// Raised when an inline in the collection changes.
+        /// </summary>
+        public event EventHandler? Invalidated;
+        
+        /// <summary>
+        /// Raises the <see cref="Invalidated"/> event.
+        /// </summary>
+        protected void Invalidate() => Invalidated?.Invoke(this, EventArgs.Empty);
+
+        private void Invalidate(object? sender, EventArgs e) => Invalidate();
+    }
+}

+ 17 - 0
src/Avalonia.Controls/Documents/Italic.cs

@@ -0,0 +1,17 @@
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Documents
+{
+    /// <summary>
+    /// Italic element - markup helper for indicating italicized content.
+    /// Equivalent to a Span with FontStyle property set to FontStyles.Italic.
+    /// Can contain other inline elements.
+    /// </summary>
+    public sealed class Italic : Span
+    {
+        static Italic()
+        {
+            FontStyleProperty.OverrideDefaultValue<Italic>(FontStyle.Italic);
+        }
+    }
+}

+ 44 - 0
src/Avalonia.Controls/Documents/LineBreak.cs

@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Media.TextFormatting;
+using Avalonia.Metadata;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls.Documents 
+{
+    /// <summary>
+    /// LineBreak element that forces a line breaking. 
+    /// </summary>
+    [TrimSurroundingWhitespace]
+    public class LineBreak : Inline
+    {
+        /// <summary>
+        /// Creates a new LineBreak instance.
+        /// </summary>
+        public LineBreak()
+        {
+        }
+
+        internal override int BuildRun(StringBuilder stringBuilder,
+            IList<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex)
+        {
+            var length = AppendText(stringBuilder);
+
+            textStyleOverrides.Add(new ValueSpan<TextRunProperties>(firstCharacterIndex, length,
+                CreateTextRunProperties()));
+
+            return length;
+        }
+
+        internal override int AppendText(StringBuilder stringBuilder)
+        {
+            var text = Environment.NewLine;
+
+            stringBuilder.Append(text);
+
+            return text.Length;
+        }
+    }
+}
+

+ 86 - 0
src/Avalonia.Controls/Documents/Run.cs

@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Data;
+using Avalonia.Media.TextFormatting;
+using Avalonia.Metadata;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls.Documents
+{
+    /// <summary>
+    /// A terminal element in text flow hierarchy - contains a uniformatted run of unicode characters
+    /// </summary>
+    public class Run : Inline
+    {
+        /// <summary>
+        /// Initializes an instance of Run class.
+        /// </summary>
+        public Run()
+        {
+        }
+
+        /// <summary>
+        /// Initializes an instance of Run class specifying its text content.
+        /// </summary>
+        /// <param name="text">
+        /// Text content assigned to the Run.
+        /// </param>
+        public Run(string? text)
+        {
+            Text = text;
+        }
+
+        /// <summary>
+        /// Dependency property backing Text.
+        /// </summary>
+        /// <remarks>
+        /// Note that when a TextRange that intersects with this Run gets modified (e.g. by editing 
+        /// a selection in RichTextBox), we will get two changes to this property since we delete 
+        /// and then insert when setting the content of a TextRange.
+        /// </remarks>
+        public static readonly StyledProperty<string?> TextProperty = AvaloniaProperty.Register<Run, string?> (
+            nameof (Text), defaultBindingMode: BindingMode.TwoWay);
+
+        /// <summary>
+        /// The content spanned by this TextElement.
+        /// </summary>
+        [Content]
+        public string? Text {
+            get { return GetValue (TextProperty); }
+            set { SetValue (TextProperty, value); }
+        }
+
+        internal override int BuildRun(StringBuilder stringBuilder,
+            IList<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex)
+        {
+            var length = AppendText(stringBuilder);
+
+            textStyleOverrides.Add(new ValueSpan<TextRunProperties>(firstCharacterIndex, length,
+                CreateTextRunProperties()));
+
+            return length;
+        }
+
+        internal override int AppendText(StringBuilder stringBuilder)
+        {
+            var text = Text ?? "";
+
+            stringBuilder.Append(text);
+
+            return text.Length;
+        }
+
+        protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
+        {
+            base.OnPropertyChanged(change);
+
+            switch (change.Property.Name)
+            {
+                case nameof(Text):
+                    Invalidate();
+                    break;
+            }
+        }
+    }
+}

+ 95 - 0
src/Avalonia.Controls/Documents/Span.cs

@@ -0,0 +1,95 @@
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Media.TextFormatting;
+using Avalonia.Metadata;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls.Documents
+{
+    /// <summary>
+    /// Span element used for grouping other Inline elements.
+    /// </summary>
+    public class Span : Inline
+    {
+        /// <summary>
+        /// Defines the <see cref="Inlines"/> property.
+        /// </summary>
+        public static readonly DirectProperty<Span, InlineCollection> InlinesProperty =
+            AvaloniaProperty.RegisterDirect<Span, InlineCollection>(
+                nameof(Inlines),
+                o => o.Inlines);
+
+        /// <summary>
+        /// Initializes a new instance of a Span element.
+        /// </summary>
+        public Span()
+        {
+            Inlines = new InlineCollection(this);
+
+            Inlines.Invalidated += (s, e) => Invalidate();
+        }
+
+        /// <summary>
+        /// Gets or sets the inlines.
+        /// </summary>
+        [Content]
+        public InlineCollection Inlines { get; }
+
+        internal override int BuildRun(StringBuilder stringBuilder, IList<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex)
+        {
+            var length = 0;
+
+            if (Inlines.HasComplexContent)
+            {
+                foreach (var inline in Inlines)
+                {
+                    var inlineLength = inline.BuildRun(stringBuilder, textStyleOverrides, firstCharacterIndex);
+
+                    firstCharacterIndex += inlineLength;
+
+                    length += inlineLength;
+                }
+            }
+            else
+            {
+                if (Inlines.Text == null)
+                {
+                    return length;
+                }
+                
+                stringBuilder.Append(Inlines.Text);
+
+                length = Inlines.Text.Length;
+
+                textStyleOverrides.Add(new ValueSpan<TextRunProperties>(firstCharacterIndex, length,
+                    CreateTextRunProperties()));
+            }
+
+            return length;
+        }
+
+        internal override int AppendText(StringBuilder stringBuilder)
+        {
+            if (Inlines.HasComplexContent)
+            {
+                var length = 0;
+
+                foreach (var inline in Inlines)
+                {
+                    length += inline.AppendText(stringBuilder);
+                }
+
+                return length;
+            }
+
+            if (Inlines.Text == null)
+            {
+                return 0;
+            }
+         
+            stringBuilder.Append(Inlines.Text);
+
+            return Inlines.Text.Length;
+        }
+    }
+}

+ 129 - 0
src/Avalonia.Controls/Documents/TextElement.cs

@@ -0,0 +1,129 @@
+using System;
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Documents
+{
+    /// <summary>
+    /// TextElement is an  base class for content in text based controls.
+    /// TextElements span other content, applying property values or providing structural information.
+    /// </summary>
+    public abstract class TextElement : StyledElement
+    {
+        /// <summary>
+        /// Defines the <see cref="Background"/> property.
+        /// </summary>
+        public static readonly StyledProperty<IBrush?> BackgroundProperty =
+            AvaloniaProperty.Register<TextElement, IBrush?>(nameof(Background), inherits: true);
+
+        /// <summary>
+        /// Defines the <see cref="FontFamily"/> property.
+        /// </summary>
+        public static readonly AttachedProperty<FontFamily> FontFamilyProperty =
+            TextBlock.FontFamilyProperty.AddOwner<TextElement>();
+
+        /// <summary>
+        /// Defines the <see cref="FontSize"/> property.
+        /// </summary>
+        public static readonly AttachedProperty<double> FontSizeProperty =
+            TextBlock.FontSizeProperty.AddOwner<TextElement>();
+
+        /// <summary>
+        /// Defines the <see cref="FontStyle"/> property.
+        /// </summary>
+        public static readonly AttachedProperty<FontStyle> FontStyleProperty =
+            TextBlock.FontStyleProperty.AddOwner<TextElement>();
+
+        /// <summary>
+        /// Defines the <see cref="FontWeight"/> property.
+        /// </summary>
+        public static readonly AttachedProperty<FontWeight> FontWeightProperty =
+            TextBlock.FontWeightProperty.AddOwner<TextElement>();
+
+        /// <summary>
+        /// Defines the <see cref="Foreground"/> property.
+        /// </summary>
+        public static readonly AttachedProperty<IBrush?> ForegroundProperty =
+            TextBlock.ForegroundProperty.AddOwner<TextElement>();
+
+        /// <summary>
+        /// Gets or sets a brush used to paint the control's background.
+        /// </summary>
+        public IBrush? Background
+        {
+            get { return GetValue(BackgroundProperty); }
+            set { SetValue(BackgroundProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the font family.
+        /// </summary>
+        public FontFamily FontFamily
+        {
+            get { return GetValue(FontFamilyProperty); }
+            set { SetValue(FontFamilyProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the font size.
+        /// </summary>
+        public double FontSize
+        {
+            get { return GetValue(FontSizeProperty); }
+            set { SetValue(FontSizeProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the font style.
+        /// </summary>
+        public FontStyle FontStyle
+        {
+            get { return GetValue(FontStyleProperty); }
+            set { SetValue(FontStyleProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the font weight.
+        /// </summary>
+        public FontWeight FontWeight
+        {
+            get { return GetValue(FontWeightProperty); }
+            set { SetValue(FontWeightProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets a brush used to paint the text.
+        /// </summary>
+        public IBrush? Foreground
+        {
+            get { return GetValue(ForegroundProperty); }
+            set { SetValue(ForegroundProperty, value); }
+        }
+        
+        /// <summary>
+        /// Raised when the visual representation of the text element changes.
+        /// </summary>
+        public event EventHandler? Invalidated;
+
+        protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
+        {
+            base.OnPropertyChanged(change);
+
+            switch (change.Property.Name)
+            {
+                case nameof(Background):
+                case nameof(FontFamily):
+                case nameof(FontSize):
+                case nameof(FontStyle):
+                case nameof(FontWeight):
+                case nameof(Foreground):
+                    Invalidate();
+                    break;
+            }
+        }
+
+        /// <summary>
+        /// Raises the <see cref="Invalidate"/> event.
+        /// </summary>
+        protected void Invalidate() => Invalidated?.Invoke(this, EventArgs.Empty);
+    }
+}

+ 15 - 0
src/Avalonia.Controls/Documents/Underline.cs

@@ -0,0 +1,15 @@
+namespace Avalonia.Controls.Documents
+{
+    /// <summary>
+    /// Underline element - markup helper for indicating superscript content.
+    /// Equivalent to a Span with TextDecorations property set to TextDecorations.Underlined.
+    /// Can contain other inline elements.
+    /// </summary>
+    public sealed class Underline : Span
+    {
+        static Underline()
+        {
+            TextDecorationsProperty.OverrideDefaultValue<Underline>(Media.TextDecorations.Underline);
+        }
+    }
+}

+ 1 - 1
src/Avalonia.Controls/Primitives/PopupRoot.cs

@@ -44,7 +44,7 @@ namespace Avalonia.Controls.Primitives
         /// The dependency resolver to use. If null the default dependency resolver will be used.
         /// The dependency resolver to use. If null the default dependency resolver will be used.
         /// </param>
         /// </param>
         public PopupRoot(TopLevel parent, IPopupImpl impl, IAvaloniaDependencyResolver? dependencyResolver)
         public PopupRoot(TopLevel parent, IPopupImpl impl, IAvaloniaDependencyResolver? dependencyResolver)
-            : base(ValidatingPopupImpl.Wrap(impl), dependencyResolver)
+            : base(impl, dependencyResolver)
         {
         {
             _parent = parent;
             _parent = parent;
         }
         }

+ 1 - 0
src/Avalonia.Controls/Properties/AssemblyInfo.cs

@@ -14,3 +14,4 @@ using Avalonia.Metadata;
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Templates")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Templates")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Notifications")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Notifications")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Chrome")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Chrome")]
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Documents")]

+ 63 - 6
src/Avalonia.Controls/TextBlock.cs

@@ -1,5 +1,8 @@
 using System;
 using System;
+using System.Collections.Generic;
 using System.Reactive.Linq;
 using System.Reactive.Linq;
+using System.Text;
+using Avalonia.Controls.Documents;
 using Avalonia.LogicalTree;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
 using Avalonia.Media;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Media.TextFormatting;
@@ -108,6 +111,14 @@ namespace Avalonia.Controls
                 o => o.Text,
                 o => o.Text,
                 (o, v) => o.Text = v);
                 (o, v) => o.Text = v);
 
 
+        /// <summary>
+        /// Defines the <see cref="Inlines"/> property.
+        /// </summary>
+        public static readonly DirectProperty<TextBlock, InlineCollection> InlinesProperty =
+            AvaloniaProperty.RegisterDirect<TextBlock, InlineCollection>(
+                nameof(Inlines),
+                o => o.Inlines);
+
         /// <summary>
         /// <summary>
         /// Defines the <see cref="TextAlignment"/> property.
         /// Defines the <see cref="TextAlignment"/> property.
         /// </summary>
         /// </summary>
@@ -132,7 +143,6 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty =
         public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty =
             AvaloniaProperty.Register<TextBlock, TextDecorationCollection?>(nameof(TextDecorations));
             AvaloniaProperty.Register<TextBlock, TextDecorationCollection?>(nameof(TextDecorations));
 
 
-        private string? _text;
         private TextLayout? _textLayout;
         private TextLayout? _textLayout;
         private Size _constraint;
         private Size _constraint;
 
 
@@ -151,7 +161,9 @@ namespace Avalonia.Controls
         /// </summary>
         /// </summary>
         public TextBlock()
         public TextBlock()
         {
         {
-            _text = string.Empty;
+            Inlines = new InlineCollection(this);
+
+            Inlines.Invalidated += InlinesChanged;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -186,13 +198,30 @@ namespace Avalonia.Controls
         /// <summary>
         /// <summary>
         /// Gets or sets the text.
         /// Gets or sets the text.
         /// </summary>
         /// </summary>
-        [Content]
         public string? Text
         public string? Text
         {
         {
-            get { return _text; }
-            set { SetAndRaise(TextProperty, ref _text, value); }
+            get => Inlines.Text;
+            set
+            {
+                var old = Text;
+
+                if (value == old)
+                {
+                    return;
+                }
+
+                Inlines.Text = value;
+
+                RaisePropertyChanged(TextProperty, old, value);
+            }
         }
         }
 
 
+        /// <summary>
+        /// Gets or sets the inlines.
+        /// </summary>
+        [Content]
+        public InlineCollection Inlines { get; }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the font family.
         /// Gets or sets the font family.
         /// </summary>
         /// </summary>
@@ -463,6 +492,23 @@ namespace Avalonia.Controls
         /// <returns>A <see cref="TextLayout"/> object.</returns>
         /// <returns>A <see cref="TextLayout"/> object.</returns>
         protected virtual TextLayout CreateTextLayout(Size constraint, string? text)
         protected virtual TextLayout CreateTextLayout(Size constraint, string? text)
         {
         {
+            List<ValueSpan<TextRunProperties>>? textStyleOverrides = null;
+
+            if (Inlines.HasComplexContent)
+            {
+                textStyleOverrides = new List<ValueSpan<TextRunProperties>>(Inlines.Count);
+
+                var textPosition = 0;
+                var stringBuilder = new StringBuilder();
+
+                foreach (var inline in Inlines)
+                {
+                    textPosition += inline.BuildRun(stringBuilder, textStyleOverrides, textPosition);
+                }
+
+                text = stringBuilder.ToString();
+            }
+
             return new TextLayout(
             return new TextLayout(
                 text ?? string.Empty,
                 text ?? string.Empty,
                 new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
                 new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
@@ -476,7 +522,8 @@ namespace Avalonia.Controls
                 constraint.Width,
                 constraint.Width,
                 constraint.Height,
                 constraint.Height,
                 maxLines: MaxLines,
                 maxLines: MaxLines,
-                lineHeight: LineHeight);
+                lineHeight: LineHeight,
+                textStyleOverrides: textStyleOverrides);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -491,6 +538,11 @@ namespace Avalonia.Controls
 
 
         protected override Size MeasureOverride(Size availableSize)
         protected override Size MeasureOverride(Size availableSize)
         {
         {
+            if (!Inlines.HasComplexContent && string.IsNullOrEmpty(Text))
+            {
+                return new Size();
+            }
+
             var padding = Padding;
             var padding = Padding;
             
             
             _constraint = availableSize.Deflate(padding);
             _constraint = availableSize.Deflate(padding);
@@ -552,6 +604,11 @@ namespace Avalonia.Controls
                     break;
                     break;
                 }
                 }
             }
             }
+        }
+
+ 		private void InlinesChanged(object? sender, EventArgs e)
+        {
+            InvalidateTextLayout();
         }
         }
     }
     }
 }
 }

+ 0 - 2
src/Avalonia.Controls/TopLevel.cs

@@ -134,8 +134,6 @@ namespace Avalonia.Controls
                     "Could not create window implementation: maybe no windowing subsystem was initialized?");
                     "Could not create window implementation: maybe no windowing subsystem was initialized?");
             }
             }
 
 
-            impl = ValidatingToplevelImpl.Wrap(impl);
-            
             PlatformImpl = impl;
             PlatformImpl = impl;
 
 
             _actualTransparencyLevel = PlatformImpl.TransparencyLevel;            
             _actualTransparencyLevel = PlatformImpl.TransparencyLevel;            

+ 0 - 344
src/Avalonia.Controls/ValidatingToplevel.cs

@@ -1,344 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Avalonia.Controls.Platform;
-using Avalonia.Controls.Primitives.PopupPositioning;
-using Avalonia.Input;
-using Avalonia.Input.Raw;
-using Avalonia.Input.TextInput;
-using Avalonia.Platform;
-using Avalonia.Rendering;
-
-namespace Avalonia.Controls;
-
-internal class ValidatingToplevelImpl : ITopLevelImpl, ITopLevelImplWithNativeControlHost,
-    ITopLevelImplWithNativeMenuExporter, ITopLevelImplWithTextInputMethod
-{
-    private readonly ITopLevelImpl _impl;
-    private bool _disposed;
-
-    public ValidatingToplevelImpl(ITopLevelImpl impl)
-    {
-        _impl = impl ?? throw new InvalidOperationException(
-            "Could not create TopLevel implementation: maybe no windowing subsystem was initialized?");
-    }
-
-    public void Dispose()
-    {
-        _disposed = true;
-        _impl.Dispose();
-    }
-
-    protected void CheckDisposed()
-    {
-        if (_disposed)
-            throw new ObjectDisposedException(_impl.GetType().FullName);
-    }
-
-    protected ITopLevelImpl Inner
-    {
-        get
-        {
-            CheckDisposed();
-            return _impl;
-        }
-    }
-
-    public static ITopLevelImpl Wrap(ITopLevelImpl impl)
-    {
-#if DEBUG
-        if (impl is ValidatingToplevelImpl)
-            return impl;
-        return new ValidatingToplevelImpl(impl);
-#else
-        return impl;
-#endif
-    }
-
-    public Size ClientSize => Inner.ClientSize;
-    public Size? FrameSize => Inner.FrameSize;
-    public double RenderScaling => Inner.RenderScaling;
-    public IEnumerable<object> Surfaces => Inner.Surfaces;
-
-    public Action<RawInputEventArgs>? Input
-    {
-        get => Inner.Input;
-        set => Inner.Input = value;
-    }
-
-    public Action<Rect>? Paint
-    {
-        get => Inner.Paint;
-        set => Inner.Paint = value;
-    }
-
-    public Action<Size, PlatformResizeReason>? Resized
-    {
-        get => Inner.Resized;
-        set => Inner.Resized = value;
-    }
-
-    public Action<double>? ScalingChanged
-    {
-        get => Inner.ScalingChanged;
-        set => Inner.ScalingChanged = value;
-    }
-
-    public Action<WindowTransparencyLevel>? TransparencyLevelChanged
-    {
-        get => Inner.TransparencyLevelChanged;
-        set => Inner.TransparencyLevelChanged = value;
-    }
-
-    public IRenderer CreateRenderer(IRenderRoot root) => Inner.CreateRenderer(root);
-
-    public void Invalidate(Rect rect) => Inner.Invalidate(rect);
-
-    public void SetInputRoot(IInputRoot inputRoot) => Inner.SetInputRoot(inputRoot);
-
-    public Point PointToClient(PixelPoint point) => Inner.PointToClient(point);
-
-    public PixelPoint PointToScreen(Point point) => Inner.PointToScreen(point);
-
-    public void SetCursor(ICursorImpl? cursor) => Inner.SetCursor(cursor);
-
-    public Action? Closed
-    {
-        get => Inner.Closed;
-        set => Inner.Closed = value;
-    }
-
-    public Action? LostFocus
-    {
-        get => Inner.LostFocus;
-        set => Inner.LostFocus = value;
-    }
-
-    // Exception: for some reason we are notifying platform mouse device from TopLevel.cs
-    public IMouseDevice MouseDevice => _impl.MouseDevice;
-    public IPopupImpl? CreatePopup() => Inner.CreatePopup();
-
-    public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) =>
-        Inner.SetTransparencyLevelHint(transparencyLevel);
-
-
-    public WindowTransparencyLevel TransparencyLevel => Inner.TransparencyLevel;
-    public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => Inner.AcrylicCompensationLevels;
-    public INativeControlHostImpl? NativeControlHost => (Inner as ITopLevelImplWithNativeControlHost)?.NativeControlHost;
-
-    public ITopLevelNativeMenuExporter? NativeMenuExporter =>
-        (Inner as ITopLevelImplWithNativeMenuExporter)?.NativeMenuExporter;
-
-    public ITextInputMethodImpl? TextInputMethod => (Inner as ITopLevelImplWithTextInputMethod)?.TextInputMethod;
-}
-
-internal class ValidatingWindowBaseImpl : ValidatingToplevelImpl, IWindowBaseImpl
-{
-    private readonly IWindowBaseImpl _impl;
-
-    public ValidatingWindowBaseImpl(IWindowBaseImpl impl) : base(impl)
-    {
-        _impl = impl;
-    }
-
-    protected new IWindowBaseImpl Inner
-    {
-        get
-        {
-            CheckDisposed();
-            return _impl;
-        }
-    }
-    
-    public static IWindowBaseImpl Wrap(IWindowBaseImpl impl)
-    {
-#if DEBUG
-        if (impl is ValidatingToplevelImpl)
-            return impl;
-        return new ValidatingWindowBaseImpl(impl);
-#else
-        return impl;
-#endif
-    }
-
-    public void Show(bool activate, bool isDialog) => Inner.Show(activate, isDialog);
-
-    public void Hide() => Inner.Hide();
-
-    public double DesktopScaling => Inner.DesktopScaling;
-    public PixelPoint Position => Inner.Position;
-
-    public Action<PixelPoint>? PositionChanged
-    {
-        get => Inner.PositionChanged;
-        set => Inner.PositionChanged = value;
-    }
-
-    public void Activate() => Inner.Activate();
-
-    public Action? Deactivated
-    {
-        get => Inner.Deactivated;
-        set => Inner.Deactivated = value;
-    }
-
-    public Action? Activated
-    {
-        get => Inner.Activated;
-        set => Inner.Activated = value;
-    }
-
-    public IPlatformHandle Handle => Inner.Handle;
-    public Size MaxAutoSizeHint => Inner.MaxAutoSizeHint;
-    public void SetTopmost(bool value) => Inner.SetTopmost(value);
-    public IScreenImpl Screen => Inner.Screen;
-}
-
-internal class ValidatingWindowImpl : ValidatingWindowBaseImpl, IWindowImpl
-{
-    private readonly IWindowImpl _impl;
-
-    public ValidatingWindowImpl(IWindowImpl impl) : base(impl)
-    {
-        _impl = impl;
-    }
-
-    protected new IWindowImpl Inner
-    {
-        get
-        {
-            CheckDisposed();
-            return _impl;
-        }
-    }
-    
-    public static IWindowImpl Unwrap(IWindowImpl impl)
-    {
-        if (impl is ValidatingWindowImpl v)
-            return v.Inner;
-        return impl;
-    }
-
-    public static IWindowImpl Wrap(IWindowImpl impl)
-    {
-#if DEBUG
-        if (impl is ValidatingToplevelImpl)
-            return impl;
-        return new ValidatingWindowImpl(impl);
-#else
-        return impl;
-#endif
-    }
-
-    public WindowState WindowState
-    {
-        get => Inner.WindowState;
-        set => Inner.WindowState = value;
-    }
-
-    public Action<WindowState> WindowStateChanged
-    {
-        get => Inner.WindowStateChanged;
-        set => Inner.WindowStateChanged = value;
-    }
-
-    public void SetTitle(string? title) => Inner.SetTitle(title);
-
-    public void SetParent(IWindowImpl parent)
-    {
-        //Workaround. SetParent will cast IWindowImpl to WindowImpl but  ValidatingWindowImpl isn't actual WindowImpl so it will fail with InvalidCastException.
-        if (parent is ValidatingWindowImpl validatingToplevelImpl)
-        {
-            Inner.SetParent(validatingToplevelImpl.Inner);
-        }
-        else
-        {
-            Inner.SetParent(parent);
-        }
-    }
-
-    public void SetEnabled(bool enable) => Inner.SetEnabled(enable);
-
-    public Action GotInputWhenDisabled
-    {
-        get => Inner.GotInputWhenDisabled;
-        set => Inner.GotInputWhenDisabled = value;
-    }
-
-    public void SetSystemDecorations(SystemDecorations enabled) => Inner.SetSystemDecorations(enabled);
-
-    public void SetIcon(IWindowIconImpl? icon) => Inner.SetIcon(icon);
-
-    public void ShowTaskbarIcon(bool value) => Inner.ShowTaskbarIcon(value);
-
-    public void CanResize(bool value) => Inner.CanResize(value);
-
-    public Func<bool> Closing
-    {
-        get => Inner.Closing;
-        set => Inner.Closing = value;
-    }
-
-    public bool IsClientAreaExtendedToDecorations => Inner.IsClientAreaExtendedToDecorations;
-
-    public Action<bool> ExtendClientAreaToDecorationsChanged
-    {
-        get => Inner.ExtendClientAreaToDecorationsChanged;
-        set => Inner.ExtendClientAreaToDecorationsChanged = value;
-    }
-
-    public bool NeedsManagedDecorations => Inner.NeedsManagedDecorations;
-    public Thickness ExtendedMargins => Inner.ExtendedMargins;
-    public Thickness OffScreenMargin => Inner.OffScreenMargin;
-    public void BeginMoveDrag(PointerPressedEventArgs e) => Inner.BeginMoveDrag(e);
-
-    public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => Inner.BeginResizeDrag(edge, e);
-
-    public void Resize(Size clientSize, PlatformResizeReason reason) =>
-        Inner.Resize(clientSize, reason);
-
-    public void Move(PixelPoint point) => Inner.Move(point);
-
-    public void SetMinMaxSize(Size minSize, Size maxSize) => Inner.SetMinMaxSize(minSize, maxSize);
-
-    public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint) =>
-        Inner.SetExtendClientAreaToDecorationsHint(extendIntoClientAreaHint);
-
-    public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) =>
-        Inner.SetExtendClientAreaChromeHints(hints);
-
-    public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) =>
-        Inner.SetExtendClientAreaTitleBarHeightHint(titleBarHeight);
-}
-
-internal class ValidatingPopupImpl : ValidatingWindowBaseImpl, IPopupImpl
-{
-    private readonly IPopupImpl _impl;
-
-    public ValidatingPopupImpl(IPopupImpl impl) : base(impl)
-    {
-        _impl = impl;
-    }
-
-    protected new IPopupImpl Inner
-    {
-        get
-        {
-            CheckDisposed();
-            return _impl;
-        }
-    }
-    
-    public static IPopupImpl Wrap(IPopupImpl impl)
-    {
-#if DEBUG
-        if (impl is ValidatingToplevelImpl)
-            return impl;
-        return new ValidatingPopupImpl(impl);
-#else
-        return impl;
-#endif
-    }
-
-    public IPopupPositioner PopupPositioner => Inner.PopupPositioner;
-    public void SetWindowManagerAddShadowHint(bool enabled) => Inner.SetWindowManagerAddShadowHint(enabled);
-}

+ 5 - 6
src/Avalonia.Controls/Window.cs

@@ -237,14 +237,13 @@ namespace Avalonia.Controls
         /// </summary>
         /// </summary>
         /// <param name="impl">The window implementation.</param>
         /// <param name="impl">The window implementation.</param>
         public Window(IWindowImpl impl)
         public Window(IWindowImpl impl)
-            : base(ValidatingWindowImpl.Wrap(impl))
+            : base(impl)
         {
         {
-            var wrapped = (IWindowImpl)base.PlatformImpl!;
-            wrapped.Closing = HandleClosing;
-            wrapped.GotInputWhenDisabled = OnGotInputWhenDisabled;
-            wrapped.WindowStateChanged = HandleWindowStateChanged;
+            impl.Closing = HandleClosing;
+            impl.GotInputWhenDisabled = OnGotInputWhenDisabled;
+            impl.WindowStateChanged = HandleWindowStateChanged;
             _maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size);
             _maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size);
-            wrapped.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged;            
+            impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged;            
             this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, PlatformResizeReason.Application));
             this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, PlatformResizeReason.Application));
 
 
             PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar);
             PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar);

+ 4 - 5
src/Avalonia.Controls/WindowBase.cs

@@ -57,13 +57,12 @@ namespace Avalonia.Controls
         {
         {
         }
         }
 
 
-        public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver? dependencyResolver) : base(ValidatingWindowBaseImpl.Wrap(impl), dependencyResolver)
+        public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver? dependencyResolver) : base(impl, dependencyResolver)
         {
         {
             Screens = new Screens(PlatformImpl?.Screen);
             Screens = new Screens(PlatformImpl?.Screen);
-            var wrapped = PlatformImpl!;
-            wrapped.Activated = HandleActivated;
-            wrapped.Deactivated = HandleDeactivated;
-            wrapped.PositionChanged = HandlePositionChanged;
+            impl.Activated = HandleActivated;
+            impl.Deactivated = HandleDeactivated;
+            impl.PositionChanged = HandlePositionChanged;
         }
         }
 
 
         /// <summary>
         /// <summary>

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

@@ -12,6 +12,9 @@
     <ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
     <ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
     <ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" />
     <ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" />
   </ItemGroup>
   </ItemGroup>
+  <ItemGroup>
+    <Compile Remove="Media\TextFormatting\TextRunCache.cs" />
+  </ItemGroup>
   <Import Project="..\..\build\Rx.props" />
   <Import Project="..\..\build\Rx.props" />
   <Import Project="..\..\build\System.Memory.props" />
   <Import Project="..\..\build\System.Memory.props" />
   <Import Project="..\..\build\ApiDiff.props" />
   <Import Project="..\..\build\ApiDiff.props" />

+ 1 - 1
src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs

@@ -69,7 +69,7 @@ namespace Avalonia.Media.TextFormatting
 
 
                 var textRange = new TextRange(propertiesOverride.Start, propertiesOverride.Length);
                 var textRange = new TextRange(propertiesOverride.Start, propertiesOverride.Length);
 
 
-                if (textRange.Start + textRange.Length < text.Start)
+                if (textRange.Start + textRange.Length <= text.Start)
                 {
                 {
                     continue;
                     continue;
                 }
                 }

+ 7 - 13
src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs

@@ -79,14 +79,12 @@ namespace Avalonia.Media.TextFormatting
                     if(TryGetShapeableLength(text, previousTypeface.Value, out var fallbackCount, out _))
                     if(TryGetShapeableLength(text, previousTypeface.Value, out var fallbackCount, out _))
                     {
                     {
                         return new ShapeableTextCharacters(text.Take(fallbackCount),
                         return new ShapeableTextCharacters(text.Take(fallbackCount),
-                            new GenericTextRunProperties(previousTypeface.Value, defaultProperties.FontRenderingEmSize,
-                                defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
+                            defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
                     }
                     }
                 }
                 }
 
 
-                return new ShapeableTextCharacters(text.Take(count),
-                    new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
-                        defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
+                return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
+                    biDiLevel);
             }
             }
             
             
             if (previousTypeface is not null)
             if (previousTypeface is not null)
@@ -94,8 +92,7 @@ namespace Avalonia.Media.TextFormatting
                 if(TryGetShapeableLength(text, previousTypeface.Value, out count, out _))
                 if(TryGetShapeableLength(text, previousTypeface.Value, out count, out _))
                 {
                 {
                     return new ShapeableTextCharacters(text.Take(count),
                     return new ShapeableTextCharacters(text.Take(count),
-                        new GenericTextRunProperties(previousTypeface.Value, defaultProperties.FontRenderingEmSize,
-                            defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
+                        defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
                 }
                 }
             }
             }
 
 
@@ -124,9 +121,8 @@ namespace Avalonia.Media.TextFormatting
             if (matchFound && TryGetShapeableLength(text, currentTypeface, out count, out _))
             if (matchFound && TryGetShapeableLength(text, currentTypeface, out count, out _))
             {
             {
                 //Fallback found
                 //Fallback found
-                return new ShapeableTextCharacters(text.Take(count),
-                    new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
-                    defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
+                return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
+                    biDiLevel);
             }
             }
 
 
             // no fallback found
             // no fallback found
@@ -148,9 +144,7 @@ namespace Avalonia.Media.TextFormatting
                 count += grapheme.Text.Length;
                 count += grapheme.Text.Length;
             }
             }
 
 
-            return new ShapeableTextCharacters(text.Take(count),
-                new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
-                    defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
+            return new ShapeableTextCharacters(text.Take(count), defaultProperties, biDiLevel);
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 6 - 0
src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs

@@ -90,5 +90,11 @@ namespace Avalonia.Media.TextFormatting
         {
         {
             return !Equals(left, right);
             return !Equals(left, right);
         }
         }
+        
+        internal TextRunProperties WithTypeface(Typeface typeface)
+        {
+            return new GenericTextRunProperties(typeface, FontRenderingEmSize,
+                TextDecorations, ForegroundBrush, BackgroundBrush, BaselineAlignment);
+        }
     }
     }
 }
 }

+ 8 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs

@@ -39,6 +39,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
                 {
                 {
                     typeSystem.GetType("Avalonia.Metadata.ContentAttribute")
                     typeSystem.GetType("Avalonia.Metadata.ContentAttribute")
                 },
                 },
+                WhitespaceSignificantCollectionAttributes =
+                {
+                     typeSystem.GetType("Avalonia.Metadata.WhitespaceSignificantCollectionAttribute")
+                },
+                TrimSurroundingWhitespaceAttributes =
+                {
+                    typeSystem.GetType("Avalonia.Metadata.TrimSurroundingWhitespaceAttribute")
+                },
                 ProvideValueTarget = typeSystem.GetType("Avalonia.Markup.Xaml.IProvideValueTarget"),
                 ProvideValueTarget = typeSystem.GetType("Avalonia.Markup.Xaml.IProvideValueTarget"),
                 RootObjectProvider = typeSystem.GetType("Avalonia.Markup.Xaml.IRootObjectProvider"),
                 RootObjectProvider = typeSystem.GetType("Avalonia.Markup.Xaml.IRootObjectProvider"),
                 RootObjectProviderIntermediateRootPropertyName = "IntermediateRootObject",
                 RootObjectProviderIntermediateRootPropertyName = "IntermediateRootObject",

+ 44 - 0
tests/Avalonia.Controls.UnitTests/TextBlockTests.cs

@@ -1,3 +1,5 @@
+using System;
+using Avalonia.Controls.Documents;
 using Avalonia.Data;
 using Avalonia.Data;
 using Avalonia.Media;
 using Avalonia.Media;
 using Avalonia.Rendering;
 using Avalonia.Rendering;
@@ -60,5 +62,47 @@ namespace Avalonia.Controls.UnitTests
 
 
             renderer.Verify(x => x.AddDirty(target), Times.Once);
             renderer.Verify(x => x.AddDirty(target), Times.Once);
         }
         }
+
+        [Fact]
+        public void Changing_InlinesCollection_Should_Invalidate_Measure()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            {
+                var target = new TextBlock();
+
+                target.Measure(Size.Infinity);
+           
+                Assert.True(target.IsMeasureValid);
+           
+                target.Inlines.Add(new Run("Hello"));
+           
+                Assert.False(target.IsMeasureValid);
+           
+                target.Measure(Size.Infinity);
+           
+                Assert.True(target.IsMeasureValid);
+            }
+        }
+        
+        [Fact]
+        public void Changing_Inlines_Properties_Should_Invalidate_Measure()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            {
+                var target = new TextBlock();
+
+                var inline = new Run("Hello");
+                
+                target.Inlines.Add(inline);
+
+                target.Measure(Size.Infinity);
+           
+                Assert.True(target.IsMeasureValid);
+
+                inline.Text = "1337";
+                
+                Assert.False(target.IsMeasureValid);
+            }
+        }
     }
     }
 }
 }

+ 1 - 1
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@@ -821,7 +821,7 @@ namespace Avalonia.Controls.UnitTests
                     target.Width = 410;
                     target.Width = 410;
                     target.LayoutManager.ExecuteLayoutPass();
                     target.LayoutManager.ExecuteLayoutPass();
 
 
-                    var windowImpl = Mock.Get(ValidatingWindowImpl.Unwrap(target.PlatformImpl));
+                    var windowImpl = Mock.Get(target.PlatformImpl);
                     windowImpl.Verify(x => x.Resize(new Size(410, 800), PlatformResizeReason.Application));
                     windowImpl.Verify(x => x.Resize(new Size(410, 800), PlatformResizeReason.Application));
                     Assert.Equal(410, target.Width);
                     Assert.Equal(410, target.Width);
                 }
                 }

+ 2 - 2
tests/Avalonia.LeakTests/ControlTests.cs

@@ -496,7 +496,7 @@ namespace Avalonia.LeakTests
                 
                 
                 AttachShowAndDetachContextMenu(window);
                 AttachShowAndDetachContextMenu(window);
 
 
-                Mock.Get(ValidatingWindowImpl.Unwrap(window.PlatformImpl)).Invocations.Clear();
+                Mock.Get(window.PlatformImpl).Invocations.Clear();
                 dotMemory.Check(memory =>
                 dotMemory.Check(memory =>
                     Assert.Equal(initialMenuCount, memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount));
                     Assert.Equal(initialMenuCount, memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount));
                 dotMemory.Check(memory =>
                 dotMemory.Check(memory =>
@@ -541,7 +541,7 @@ namespace Avalonia.LeakTests
                 BuildAndShowContextMenu(window);
                 BuildAndShowContextMenu(window);
                 BuildAndShowContextMenu(window);
                 BuildAndShowContextMenu(window);
 
 
-                Mock.Get(ValidatingWindowImpl.Unwrap(window.PlatformImpl)).Invocations.Clear();
+                Mock.Get(window.PlatformImpl).Invocations.Clear();
                 dotMemory.Check(memory =>
                 dotMemory.Check(memory =>
                     Assert.Equal(initialMenuCount, memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount));
                     Assert.Equal(initialMenuCount, memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount));
                 dotMemory.Check(memory =>
                 dotMemory.Check(memory =>

+ 22 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs

@@ -935,6 +935,28 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
             }
             }
         }
         }
 
 
+        [Fact]
+        public void Should_Parse_Tip_With_Comment()
+        {
+            var xaml = @"
+                <TextBlock xmlns='https://github.com/avaloniaui' Text='TextBlock with tooltip'>
+                    <ToolTip.Tip>
+                        <!--Comment-->
+                        <ToolTip>
+                            Foo
+                        </ToolTip>
+                    </ToolTip.Tip>
+                </TextBlock>";
+
+            var textBlock = AvaloniaRuntimeXamlLoader.Parse<TextBlock>(xaml);
+
+            var toolTip = ToolTip.GetTip(textBlock) as ToolTip;
+
+            Assert.NotNull(toolTip);
+
+            Assert.Equal("Foo", toolTip.Content);
+        }
+
         private class SelectedItemsViewModel : INotifyPropertyChanged
         private class SelectedItemsViewModel : INotifyPropertyChanged
         {
         {
             public string[] Items { get; set; }
             public string[] Items { get; set; }

+ 1 - 1
tests/Avalonia.UnitTests/MockTextShaperImpl.cs

@@ -21,7 +21,7 @@ namespace Avalonia.UnitTests
 
 
                 var glyphIndex = typeface.GetGlyph(codepoint);
                 var glyphIndex = typeface.GetGlyph(codepoint);
 
 
-                shapedBuffer[i] = new GlyphInfo(glyphIndex, glyphCluster);
+                shapedBuffer[i] = new GlyphInfo(glyphIndex, glyphCluster, 10);
 
 
                 i += count;
                 i += count;
             }
             }