Forráskód Böngészése

Merge remote-tracking branch 'upstream/master'

Benedikt Stebner 3 éve
szülő
commit
a76f1473c0
32 módosított fájl, 620 hozzáadás és 74 törlés
  1. 9 8
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  2. 11 16
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  3. 2 1
      samples/ControlCatalog.Android/Properties/AndroidManifest.xml
  4. 1 0
      samples/ControlCatalog.Android/environment.device.txt
  5. 1 0
      samples/ControlCatalog.Android/environment.emulator.txt
  6. 6 6
      samples/RenderDemo/Pages/GlyphRunPage.xaml
  7. 86 27
      samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
  8. 12 2
      src/Avalonia.Base/Layout/LayoutManager.cs
  9. 19 0
      src/Avalonia.Base/Media/GlyphRun.cs
  10. 24 0
      src/Avalonia.Base/Media/PlatformGeometry.cs
  11. 8 0
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  12. 11 3
      src/Avalonia.Controls/GridSplitter.cs
  13. 17 0
      src/Avalonia.Controls/Templates/DataTemplates.cs
  14. 10 0
      src/Avalonia.Controls/Templates/ITypedDataTemplate.cs
  15. 7 0
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  16. 3 1
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  17. 73 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs
  18. 1 1
      src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
  19. 1 1
      src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
  20. 35 0
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  21. 28 0
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  22. 18 0
      tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs
  23. 5 0
      tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
  24. 5 0
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  25. 4 3
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs
  26. 4 4
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs
  27. 113 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs
  28. 18 1
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs
  29. 81 0
      tests/Avalonia.RenderTests/Media/GlyphRunTests.cs
  30. 7 0
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  31. BIN
      tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png
  32. BIN
      tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png

+ 9 - 8
native/Avalonia.Native/src/OSX/WindowBaseImpl.mm

@@ -298,14 +298,15 @@ HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reaso
         }
 
         @try {
-            lastSize = NSSize {x, y};
-
-            if (!_shown) {
-                BaseEvents->Resized(AvnSize{x, y}, reason);
-            }
-            else if(Window != nullptr) {
-                [Window setContentSize:lastSize];
-                [Window invalidateShadow];
+            if(x != lastSize.width || y != lastSize.height) {
+                lastSize = NSSize{x, y};
+
+                if (!_shown) {
+                    BaseEvents->Resized(AvnSize{x, y}, reason);
+                } else if (Window != nullptr) {
+                    [Window setContentSize:lastSize];
+                    [Window invalidateShadow];
+                }
             }
         }
         @finally {

+ 11 - 16
samples/ControlCatalog.Android/ControlCatalog.Android.csproj

@@ -9,42 +9,37 @@
     <ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
     <AndroidPackageFormat>apk</AndroidPackageFormat>
     <MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
+    <RuntimeIdentifiers>android-arm64;android-x64</RuntimeIdentifiers>
   </PropertyGroup>
-  <ItemGroup>
-    <None Remove="Assets\AboutAssets.txt" />
-  </ItemGroup>
   <ItemGroup>
     <AndroidResource Include="..\..\build\Assets\Icon.png">
       <Link>Resources\drawable\Icon.png</Link>
     </AndroidResource>
   </ItemGroup>
 
-  <PropertyGroup Condition="'$(Configuration)'=='Release' and '$(TF_BUILD)' == ''">
-    <DebugSymbols>False</DebugSymbols>
-    <UseInterpreter>False</UseInterpreter>
+  <PropertyGroup Condition="'$(RunAOTCompilation)'=='' and '$(Configuration)'=='Release' and '$(TF_BUILD)'==''">
     <RunAOTCompilation>True</RunAOTCompilation>
+  </PropertyGroup>
+  
+  <PropertyGroup Condition="'$(RunAOTCompilation)'=='True'">
     <EnableLLVM>True</EnableLLVM>
     <AndroidAotAdditionalArguments>no-write-symbols,nodebug</AndroidAotAdditionalArguments>
     <AndroidAotMode>Hybrid</AndroidAotMode>
     <AndroidGenerateJniMarshalMethods>True</AndroidGenerateJniMarshalMethods>
   </PropertyGroup>
 
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
-    <EmbedAssembliesIntoApk>False</EmbedAssembliesIntoApk>
-    <RunAOTCompilation>False</RunAOTCompilation>
-  </PropertyGroup>
-
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
-    <EmbedAssembliesIntoApk>True</EmbedAssembliesIntoApk>
+  <PropertyGroup Condition="'$(AndroidEnableProfiler)'=='True'">
+    <IsEmulator Condition="'$(IsEmulator)' == ''">True</IsEmulator>
+    <DebugSymbols>True</DebugSymbols>
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.3.1.3" />
-    <PackageReference Include="Xamarin.AndroidX.Lifecycle.ViewModel" Version="2.3.1.3" />
+    <AndroidEnvironment Condition="'$(IsEmulator)'=='True'" Include="environment.emulator.txt" />
+    <AndroidEnvironment Condition="'$(IsEmulator)'!='True'" Include="environment.device.txt" />
   </ItemGroup>
 
   <ItemGroup>
     <ProjectReference Include="..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj" />
     <ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
   </ItemGroup>
-</Project>
+</Project>

+ 2 - 1
samples/ControlCatalog.Android/Properties/AndroidManifest.xml

@@ -1,4 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
-	<application android:label="ControlCatalog.Android" android:icon="@drawable/Icon"></application>
+  <application android:label="ControlCatalog.Android" android:icon="@drawable/Icon"></application>
+  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 </manifest>

+ 1 - 0
samples/ControlCatalog.Android/environment.device.txt

@@ -0,0 +1 @@
+DOTNET_DiagnosticPorts=127.0.0.1:9000,suspend

+ 1 - 0
samples/ControlCatalog.Android/environment.emulator.txt

@@ -0,0 +1 @@
+DOTNET_DiagnosticPorts=10.0.2.2:9001,suspend

+ 6 - 6
samples/RenderDemo/Pages/GlyphRunPage.xaml

@@ -2,13 +2,13 @@
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:local="clr-namespace:RenderDemo.Pages"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              x:Class="RenderDemo.Pages.GlyphRunPage">
-  <Border
+  <Grid
+    ColumnDefinitions="*,*"
     Background="White">
-    <Image
-      x:Name="imageControl"
-      Stretch="None">
-    </Image>
-  </Border>
+    <local:GlyphRunControl Grid.Column="0"/>
+    <local:GlyphRunGeometryControl Grid.Column="1"/>
+  </Grid>
 </UserControl>

+ 86 - 27
samples/RenderDemo/Pages/GlyphRunPage.xaml.cs

@@ -9,14 +9,6 @@ namespace RenderDemo.Pages
 {
     public class GlyphRunPage : UserControl
     {
-        private Image _imageControl;
-        private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
-        private readonly Random _rand = new Random();
-        private ushort[] _glyphIndices = new ushort[1];
-        private char[] _characters = new char[1];
-        private float _fontSize = 20;
-        private int _direction = 10;
-
         public GlyphRunPage()
         {
             this.InitializeComponent();
@@ -25,19 +17,43 @@ namespace RenderDemo.Pages
         private void InitializeComponent()
         {
             AvaloniaXamlLoader.Load(this);
+        }
+    }
+
+    public class GlyphRunControl : Control
+    {
+        private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
+        private readonly Random _rand = new Random();
+        private ushort[] _glyphIndices = new ushort[1];
+        private char[] _characters = new char[1];
+        private float _fontSize = 20;
+        private int _direction = 10;
 
-            _imageControl = this.FindControl<Image>("imageControl");
-            _imageControl.Source = new DrawingImage();
+        private DispatcherTimer _timer;
 
-            DispatcherTimer.Run(() =>
+        protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            _timer = new DispatcherTimer
+            {
+                Interval = TimeSpan.FromSeconds(1)
+            };
+
+            _timer.Tick += (s,e) =>
             {
-                UpdateGlyphRun();
+                InvalidateVisual();
+            };
+
+            _timer.Start();
+        }
+
+        protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            _timer.Stop();
 
-                return true;
-            }, TimeSpan.FromSeconds(1));
+            _timer = null;
         }
 
-        private void UpdateGlyphRun()
+        public override void Render(DrawingContext context)
         {
             var c = (char)_rand.Next(65, 90);
 
@@ -57,27 +73,70 @@ namespace RenderDemo.Pages
 
             _characters[0] = c;
 
-            var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight;
+            var glyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices);
 
-            var drawingGroup = new DrawingGroup();
+            context.DrawGlyphRun(Brushes.Black, glyphRun);
+        }
+    }
 
-            var glyphRunDrawing = new GlyphRunDrawing
+    public class GlyphRunGeometryControl : Control
+    {
+        private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
+        private readonly Random _rand = new Random();
+        private ushort[] _glyphIndices = new ushort[1];
+        private char[] _characters = new char[1];
+        private float _fontSize = 20;
+        private int _direction = 10;
+
+        private DispatcherTimer _timer;
+
+        protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            _timer = new DispatcherTimer
             {
-                Foreground = Brushes.Black,
-                GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices)
+                Interval = TimeSpan.FromSeconds(1)
             };
 
-            drawingGroup.Children.Add(glyphRunDrawing);
-
-            var geometryDrawing = new GeometryDrawing
+            _timer.Tick += (s, e) =>
             {
-                Pen = new Pen(Brushes.Black),
-                Geometry = new RectangleGeometry { Rect = new Rect(glyphRunDrawing.GlyphRun.Size) }
+                InvalidateVisual();
             };
 
-            drawingGroup.Children.Add(geometryDrawing);
+            _timer.Start();
+        }
+
+        protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            _timer.Stop();
+
+            _timer = null;
+        }
+
+        public override void Render(DrawingContext context)
+        {
+            var c = (char)_rand.Next(65, 90);
+
+            if (_fontSize + _direction > 200)
+            {
+                _direction = -10;
+            }
+
+            if (_fontSize + _direction < 20)
+            {
+                _direction = 10;
+            }
+
+            _fontSize += _direction;
+
+            _glyphIndices[0] = _glyphTypeface.GetGlyph(c);
+
+            _characters[0] = c;
+
+            var glyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices);
+
+            var geometry = glyphRun.BuildGeometry();          
 
-            (_imageControl.Source as DrawingImage).Drawing = drawingGroup;
+            context.DrawGeometry(Brushes.Green, null, geometry);          
         }
     }
 }

+ 12 - 2
src/Avalonia.Base/Layout/LayoutManager.cs

@@ -28,7 +28,7 @@ namespace Avalonia.Layout
         public LayoutManager(ILayoutRoot owner)
         {
             _owner = owner ?? throw new ArgumentNullException(nameof(owner));
-            _executeLayoutPass = ExecuteLayoutPass;
+            _executeLayoutPass = ExecuteQueuedLayoutPass;
         }
 
         public virtual event EventHandler? LayoutUpdated;
@@ -94,6 +94,16 @@ namespace Avalonia.Layout
             QueueLayoutPass();
         }
 
+        private void ExecuteQueuedLayoutPass()
+        {
+            if (!_queued)
+            {
+                return;
+            }
+            
+            ExecuteLayoutPass();
+        }
+
         /// <inheritdoc/>
         public virtual void ExecuteLayoutPass()
         {
@@ -319,8 +329,8 @@ namespace Avalonia.Layout
         {
             if (!_queued && !_running)
             {
-                Dispatcher.UIThread.Post(_executeLayoutPass, DispatcherPriority.Layout);
                 _queued = true;
+                Dispatcher.UIThread.Post(_executeLayoutPass, DispatcherPriority.Layout);
             }
         }
 

+ 19 - 0
src/Avalonia.Base/Media/GlyphRun.cs

@@ -194,6 +194,25 @@ namespace Avalonia.Media
             }
         }
 
+        /// <summary>
+        /// Obtains geometry for the glyph run.
+        /// </summary>
+        /// <returns>The geometry returned contains the combined geometry of all glyphs in the glyph run.</returns>
+        public Geometry BuildGeometry()
+        {
+            var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
+
+            var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this, out var scale);
+
+            var geometry = new PlatformGeometry(geometryImpl);
+
+            var transform = new MatrixTransform(Matrix.CreateTranslation(geometry.Bounds.Left, -geometry.Bounds.Top) * scale);
+
+            geometry.Transform = transform;
+
+            return geometry;
+        }
+
         /// <summary>
         /// Retrieves the offset from the leading edge of the <see cref="GlyphRun"/>
         /// to the leading or trailing edge of a caret stop containing the specified character hit.

+ 24 - 0
src/Avalonia.Base/Media/PlatformGeometry.cs

@@ -0,0 +1,24 @@
+using Avalonia.Platform;
+
+namespace Avalonia.Media
+{
+    internal class PlatformGeometry : Geometry
+    {
+        private readonly IGeometryImpl _geometryImpl;
+
+        public PlatformGeometry(IGeometryImpl geometryImpl)
+        {
+            _geometryImpl = geometryImpl;
+        }
+
+        public override Geometry Clone()
+        {
+            return new PlatformGeometry(_geometryImpl);
+        }
+
+        protected override IGeometryImpl? CreateDefiningGeometry()
+        {
+           return _geometryImpl;
+        }
+    }
+}

+ 8 - 0
src/Avalonia.Base/Platform/IPlatformRenderInterface.cs

@@ -58,6 +58,14 @@ namespace Avalonia.Platform
         /// <returns>A combined geometry.</returns>
         IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2);
 
+        /// <summary>
+        /// Created a geometry implementation for the glyph run.
+        /// </summary>
+        /// <param name="glyphRun">The glyph run to build a geometry from.</param>
+        /// <param name="scale">The scaling of the produces geometry.</param>
+        /// <returns>The geometry returned contains the combined geometry of all glyphs in the glyph run.</returns>
+        IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale);
+
         /// <summary>
         /// Creates a renderer.
         /// </summary>

+ 11 - 3
src/Avalonia.Controls/GridSplitter.cs

@@ -221,7 +221,8 @@ namespace Avalonia.Controls
                     ShowsPreview = showsPreview,
                     ResizeDirection = resizeDirection,
                     SplitterLength = Math.Min(Bounds.Width, Bounds.Height),
-                    ResizeBehavior = GetEffectiveResizeBehavior(resizeDirection)
+                    ResizeBehavior = GetEffectiveResizeBehavior(resizeDirection),
+                    Scaling = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1,
                 };
 
                 // Store the rows and columns to resize on drag events.
@@ -630,13 +631,17 @@ namespace Avalonia.Controls
             {
                 double actualLength1 = GetActualLength(definition1);
                 double actualLength2 = GetActualLength(definition2);
+                double pixelLength = 1 / _resizeData.Scaling;
+                double epsilon = pixelLength + LayoutHelper.LayoutEpsilon;
 
                 // When splitting, Check to see if the total pixels spanned by the definitions 
-                // is the same as before starting resize. If not cancel the drag.
+                // is the same as before starting resize. If not cancel the drag. We need to account for
+                // layout rounding here, so ignore differences of less than a device pixel to avoid problems
+                // that WPF has, such as https://stackoverflow.com/questions/28464843.
                 if (_resizeData.SplitBehavior == SplitBehavior.Split &&
                     !MathUtilities.AreClose(
                         actualLength1 + actualLength2,
-                        _resizeData.OriginalDefinition1ActualLength + _resizeData.OriginalDefinition2ActualLength, LayoutHelper.LayoutEpsilon))
+                        _resizeData.OriginalDefinition1ActualLength + _resizeData.OriginalDefinition2ActualLength, epsilon))
                 {
                     CancelResize();
 
@@ -798,6 +803,9 @@ namespace Avalonia.Controls
             // The minimum of Width/Height of Splitter.  Used to ensure splitter 
             // isn't hidden by resizing a row/column smaller than the splitter.
             public double SplitterLength;
+
+            // The current layout scaling factor.
+            public double Scaling;
         }
     }
 

+ 17 - 0
src/Avalonia.Controls/Templates/DataTemplates.cs

@@ -1,3 +1,4 @@
+using System;
 using Avalonia.Collections;
 
 namespace Avalonia.Controls.Templates
@@ -13,6 +14,22 @@ namespace Avalonia.Controls.Templates
         public DataTemplates()
         {
             ResetBehavior = ResetBehavior.Remove;
+            
+            Validate += ValidateDataTemplate;
+        }
+
+        private static void ValidateDataTemplate(IDataTemplate template)
+        {
+            var valid = template switch
+            {
+                ITypedDataTemplate typed => typed.DataType is not null,
+                _ => true
+            };
+            
+            if (!valid)
+            {
+                throw new InvalidOperationException("DataTemplate inside of DataTemplates must have a DataType set. Set DataType property or use ItemTemplate with single template instead.");
+            }
         }
     }
 }

+ 10 - 0
src/Avalonia.Controls/Templates/ITypedDataTemplate.cs

@@ -0,0 +1,10 @@
+using System;
+using Avalonia.Metadata;
+
+namespace Avalonia.Controls.Templates;
+
+public interface ITypedDataTemplate : IDataTemplate
+{
+    [DataType]
+    Type? DataType { get; }
+}

+ 7 - 0
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@@ -114,6 +114,13 @@ namespace Avalonia.Headless
             return new HeadlessGlyphRunStub();
         }
 
+        public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale)
+        {
+            scale = Matrix.Identity;
+
+            return new HeadlessGeometryStub(new Rect(glyphRun.Size));
+        }
+
         class HeadlessGeometryStub : IGeometryImpl
         {
             public HeadlessGeometryStub(Rect bounds)

+ 3 - 1
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs

@@ -35,7 +35,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
             Transformers.Insert(2, _designTransformer = new AvaloniaXamlIlDesignPropertiesTransformer());
             Transformers.Insert(3, _bindingTransformer = new AvaloniaBindingExtensionTransformer());
             
-            
             // Targeted
             InsertBefore<PropertyReferenceResolver>(
                 new AvaloniaXamlIlResolveClassesPropertiesTransformer(),
@@ -57,6 +56,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
                 new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer()
             );
 
+            InsertAfter<TypeReferenceResolver>(
+                new XDataTypeTransformer());
+
             // After everything else
             InsertBefore<NewObjectTransformer>(
                 new AddNameScopeRegistration(),

+ 73 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs

@@ -0,0 +1,73 @@
+using System.Collections.Generic;
+using System.Linq;
+using XamlX;
+using XamlX.Ast;
+using XamlX.Transform;
+using XamlX.Transform.Transformers;
+using XamlX.TypeSystem;
+
+namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
+{
+    internal class XDataTypeTransformer : IXamlAstTransformer
+    {
+        private const string DataTypePropertyName = "DataType";
+        
+        /// <summary>
+        /// Converts x:DataType directives to regular DataType assignments if property with Avalonia.Metadata.DataTypeAttribute exists.
+        /// </summary>
+        /// <returns></returns>
+        public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
+        {
+            if (node is XamlAstObjectNode on)
+            {
+                for (var c = 0; c < on.Children.Count; c++)
+                {
+                    var ch = on.Children[c];
+                    if (ch is XamlAstXmlDirective { Namespace: XamlNamespaces.Xaml2006, Name: DataTypePropertyName } d)
+                    {
+                        if (on.Children.OfType<XamlAstXamlPropertyValueNode>()
+                            .Any(p => ((XamlAstNamePropertyReference)p.Property)?.Name == DataTypePropertyName))
+                        {
+                            // Break iteration if any DataType property was already set by user code.
+                            break;
+                        }
+                        
+                        var templateDataTypeAttribute = context.GetAvaloniaTypes().DataTypeAttribute;
+
+                        var clrType = (on.Type as XamlAstClrTypeReference)?.Type;
+                        if (clrType is null)
+                        {
+                            break;
+                        }
+
+                        // Technically it's possible to map "x:DataType" to a property with [DataType] attribute regardless of its name,
+                        // but we go explicitly strict here and check the name as well.
+                        var (declaringType, dataTypeProperty) = GetAllProperties(clrType)
+                            .FirstOrDefault(t => t.property.Name == DataTypePropertyName && t.property.CustomAttributes
+                                .Any(a => a.Type == templateDataTypeAttribute));
+                       
+                        if (dataTypeProperty is not null)
+                        {
+                            on.Children[c] = new XamlAstXamlPropertyValueNode(d,
+                                new XamlAstNamePropertyReference(d,
+                                    new XamlAstClrTypeReference(ch, declaringType, false), dataTypeProperty.Name,
+                                    on.Type),
+                                d.Values);
+                        }
+                    }
+                }
+            }
+
+            return node;
+        }
+        
+        private static IEnumerable<(IXamlType declaringType, IXamlProperty property)> GetAllProperties(IXamlType t)
+        {
+            foreach (var p in t.Properties)
+                yield return (t, p);
+            if(t.BaseType!=null)
+                foreach (var tuple in GetAllProperties(t.BaseType))
+                    yield return tuple;
+        }
+    }
+}

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs

@@ -5,7 +5,7 @@ using Avalonia.Metadata;
 
 namespace Avalonia.Markup.Xaml.Templates
 {
-    public class DataTemplate : IRecyclingDataTemplate
+    public class DataTemplate : IRecyclingDataTemplate, ITypedDataTemplate
     {
         [DataType]
         public Type DataType { get; set; }

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs

@@ -9,7 +9,7 @@ using Avalonia.Metadata;
 
 namespace Avalonia.Markup.Xaml.Templates
 {
-    public class TreeDataTemplate : ITreeDataTemplate
+    public class TreeDataTemplate : ITreeDataTemplate, ITypedDataTemplate
     {
         [DataType]
         public Type DataType { get; set; }

+ 35 - 0
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -62,6 +62,41 @@ namespace Avalonia.Skia
             return new CombinedGeometryImpl(combineMode, g1, g2);
         }
 
+        public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale)
+        {
+            if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface)
+            {
+                throw new InvalidOperationException("PlatformImpl can't be null.");
+            }
+
+            var fontRenderingEmSize = (float)glyphRun.FontRenderingEmSize;
+            var skFont = new SKFont(glyphTypeface.Typeface, fontRenderingEmSize)
+            {
+                Size = fontRenderingEmSize,
+                Edging = SKFontEdging.Antialias,
+                Hinting = SKFontHinting.None,
+                LinearMetrics = true
+            };
+
+            SKPath path = new SKPath();
+            var matrix = SKMatrix.Identity;
+
+            var currentX = 0f;
+
+            foreach (var glyph in glyphRun.GlyphIndices)
+            {
+                var p = skFont.GetGlyphPath(glyph);
+
+                path.AddPath(p, currentX, 0);
+
+                currentX += p.Bounds.Right;
+            }
+
+            scale = Matrix.CreateScale(matrix.ScaleX, matrix.ScaleY);
+
+            return new StreamGeometryImpl(path);
+        }
+
         /// <inheritdoc />
         public IBitmapImpl LoadBitmap(string fileName)
         {

+ 28 - 0
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@@ -159,6 +159,34 @@ namespace Avalonia.Direct2D1
         public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children) => new GeometryGroupImpl(fillRule, children);
         public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2);
 
+        public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale)
+        {
+            if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface)
+            {
+                throw new InvalidOperationException("PlatformImpl can't be null.");
+            }
+
+            var pathGeometry = new SharpDX.Direct2D1.PathGeometry(Direct2D1Factory);
+
+            using (var sink = pathGeometry.Open())
+            {
+                var glyphs = new short[glyphRun.GlyphIndices.Count];
+
+                for (int i = 0; i < glyphRun.GlyphIndices.Count; i++)
+                {
+                    glyphs[i] = (short)glyphRun.GlyphIndices[i];
+                }
+
+                glyphTypeface.FontFace.GetGlyphRunOutline((float)glyphRun.FontRenderingEmSize, glyphs, null, null, false, !glyphRun.IsLeftToRight, sink);
+
+                sink.Close();
+            }
+
+            scale = Matrix.Identity;
+
+            return new StreamGeometryImpl(pathGeometry);
+        }      
+
         /// <inheritdoc />
         public IBitmapImpl LoadBitmap(string fileName)
         {

+ 18 - 0
tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs

@@ -2,6 +2,7 @@ using System.Collections.Generic;
 using System.Linq;
 using Avalonia.Controls;
 using Avalonia.Layout;
+using Avalonia.Threading;
 using Xunit;
 
 namespace Avalonia.Base.UnitTests.Layout
@@ -421,5 +422,22 @@ namespace Avalonia.Base.UnitTests.Layout
             Assert.Equal(new Size(200, 200), control.Bounds.Size);
             Assert.Equal(new Size(200, 200), control.DesiredSize);
         }
+        
+        [Fact]
+        public void LayoutManager_Execute_Layout_Pass_Should_Clear_Queued_LayoutPasses()
+        {
+            var control = new LayoutTestControl();
+            var root = new LayoutTestRoot { Child = control };
+
+            int layoutCount = 0;
+            root.LayoutUpdated += (_, _) => layoutCount++;
+
+            root.LayoutManager.InvalidateArrange(control);
+            root.LayoutManager.ExecuteInitialLayoutPass();
+            
+            Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout);
+            
+            Assert.Equal(1, layoutCount);
+        }
     }
 }

+ 5 - 0
tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs

@@ -121,6 +121,11 @@ namespace Avalonia.Base.UnitTests.VisualTree
             throw new NotImplementedException();
         }
 
+        public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale)
+        {
+            throw new NotImplementedException();
+        }
+
         class MockStreamGeometry : IStreamGeometryImpl
         {
             private MockStreamGeometryContext _impl = new MockStreamGeometryContext();

+ 5 - 0
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@@ -117,6 +117,11 @@ namespace Avalonia.Benchmarks
             return new NullGlyphRun();
         }
 
+        public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale)
+        {
+            throw new NotImplementedException();
+        }
+
         public bool SupportsIndividualRoundRects => true;
 
         public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul;

+ 4 - 3
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

@@ -17,6 +17,7 @@ using Avalonia.Markup.Xaml.Templates;
 using Avalonia.Media;
 using Avalonia.Metadata;
 using Avalonia.UnitTests;
+using JetBrains.Annotations;
 using XamlX;
 using Xunit;
 
@@ -413,11 +414,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
         xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
         x:DataType='local:TestDataContext'>
     <ItemsControl Items='{CompiledBinding ListProperty}' Name='target'>
-        <ItemsControl.DataTemplates>
+        <ItemsControl.ItemTemplate>
             <DataTemplate>
                 <TextBlock Text='{CompiledBinding}' Name='textBlock' />
             </DataTemplate>
-        </ItemsControl.DataTemplates>
+        </ItemsControl.ItemTemplate>
     </ItemsControl>
 </Window>";
                 var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
@@ -1527,7 +1528,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
         [TemplateContent]
         public object Content { get; set; }
 
-        public bool Match(object data) => FancyDataType.IsInstanceOfType(data);
+        public bool Match(object data) => FancyDataType?.IsInstanceOfType(data) ?? true;
 
         public IControl Build(object data) => TemplateContent.Load(Content)?.Control;
     }

+ 4 - 4
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs

@@ -74,18 +74,18 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
 <Window xmlns='https://github.com/avaloniaui'>
     <DockPanel>
         <TabStrip Name='strip' DockPanel.Dock='Top' Items='{Binding Items}' SelectedIndex='0'>
-          <TabStrip.DataTemplates>
+          <TabStrip.ItemTemplate>
             <DataTemplate>
               <TextBlock Text='{Binding Header}'/>
             </DataTemplate>
-          </TabStrip.DataTemplates>
+          </TabStrip.ItemTemplate>
         </TabStrip>
         <Carousel Name='carousel' Items='{Binding Items}' SelectedIndex='{Binding #strip.SelectedIndex}'>
-          <Carousel.DataTemplates>
+          <Carousel.ItemTemplate>
             <DataTemplate>
               <TextBlock Text='{Binding Detail}'/>
             </DataTemplate>
-          </Carousel.DataTemplates>
+          </Carousel.ItemTemplate>
         </Carousel>
     </DockPanel>
 </Window>";

+ 113 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs

@@ -1,5 +1,11 @@
+using System;
+using System.Linq;
 using Avalonia.Controls;
 using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Templates;
+using Avalonia.Markup.Xaml.Templates;
+using Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;
+using Avalonia.Metadata;
 using Avalonia.UnitTests;
 using Xunit;
 
@@ -89,6 +95,93 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
             }
         }
 
+        [Fact]
+        public void XDataType_Should_Be_Assigned_To_Clr_Property()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:sys='clr-namespace:System;assembly=netstandard'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.DataTemplates>
+        <DataTemplate x:DataType='sys:String'>
+            <Canvas Name='foo'/>
+        </DataTemplate>
+    </Window.DataTemplates>
+    <ContentControl Name='target' Content='Foo'/>
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var target = window.FindControl<ContentControl>("target");
+                var template = (DataTemplate)window.DataTemplates.First();
+                
+                window.ApplyTemplate();
+                target.ApplyTemplate();
+                ((ContentPresenter)target.Presenter).UpdateChild();
+                
+                Assert.Equal(typeof(string), template.DataType);
+                Assert.IsType<Canvas>(target.Presenter.Child);
+            }
+        }
+        
+        [Fact]
+        public void XDataType_Should_Be_Ignored_If_DataType_Already_Set()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:sys='clr-namespace:System;assembly=netstandard'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.DataTemplates>
+        <DataTemplate DataType='sys:String' x:DataType='UserControl'>
+            <Canvas Name='foo'/>
+        </DataTemplate>
+    </Window.DataTemplates>
+    <ContentControl Name='target' Content='Foo'/>
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var target = window.FindControl<ContentControl>("target");
+
+                window.ApplyTemplate();
+                target.ApplyTemplate();
+                ((ContentPresenter)target.Presenter).UpdateChild();
+
+                Assert.IsType<Canvas>(target.Presenter.Child);
+            }
+        }
+        
+        [Fact]
+        public void XDataType_Should_Be_Ignored_If_DataType_Has_Non_Standard_Name()
+        {
+            // We don't want DataType to be mapped to FancyDataType, avoid possible confusion.
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:sys='clr-namespace:System;assembly=netstandard'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'>
+    <ContentControl Name='target' Content='Foo'>
+        <ContentControl.ContentTemplate>
+            <local:CustomDataTemplate x:DataType='local:TestDataContext'>
+                <TextBlock Text='{CompiledBinding StringProperty}' Name='textBlock' />
+            </local:CustomDataTemplate>
+        </ContentControl.ContentTemplate>
+    </ContentControl>
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var target = window.FindControl<ContentControl>("target");
+                
+                window.ApplyTemplate();
+                target.ApplyTemplate();
+                ((ContentPresenter)target.Presenter).UpdateChild();
+
+                var dataTemplate = (CustomDataTemplate)target.ContentTemplate;
+                Assert.Null(dataTemplate.FancyDataType);
+            }
+        }
+        
         [Fact]
         public void Can_Set_DataContext_In_DataTemplate()
         {
@@ -132,5 +225,25 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
                 Assert.Same(viewModel.Child.Child, canvas.DataContext);
             }
         }
+        
+        [Fact]
+        public void DataTemplates_Without_Type_Should_Throw()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:sys='clr-namespace:System;assembly=netstandard'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.DataTemplates>
+        <DataTemplate>
+            <Canvas Name='foo'/>
+        </DataTemplate>
+    </Window.DataTemplates>
+    <ContentControl Name='target' Content='Foo'/>
+</Window>";
+                Assert.Throws<InvalidOperationException>(() => (Window)AvaloniaRuntimeXamlLoader.Load(xaml));
+            }
+        }
     }
 }

+ 18 - 1
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs

@@ -14,12 +14,29 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
         {
             using (UnitTestApplication.Start(TestServices.MockPlatformWrapper))
             {
-                var xaml = "<DataTemplates xmlns='https://github.com/avaloniaui'><TreeDataTemplate ItemsSource='{Binding}'/></DataTemplates>";
+                var xaml = "<DataTemplates xmlns='https://github.com/avaloniaui'><TreeDataTemplate DataType='Control' ItemsSource='{Binding}'/></DataTemplates>";
                 var templates = (DataTemplates)AvaloniaRuntimeXamlLoader.Load(xaml);
                 var template = (TreeDataTemplate)(templates.First());
 
                 Assert.IsType<Binding>(template.ItemsSource);
             }                
         }
+        
+        [Fact]
+        public void XDataType_Should_Be_Assigned_To_Clr_Property()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformWrapper))
+            {
+                var xaml = @"
+<DataTemplates xmlns='https://github.com/avaloniaui'
+               xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <TreeDataTemplate x:DataType='x:String' />
+</DataTemplates>";
+                var templates = (DataTemplates)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var template = (TreeDataTemplate)(templates.First());
+
+                Assert.Equal(typeof(string), template.DataType);
+            }                
+        }
     }
 }

+ 81 - 0
tests/Avalonia.RenderTests/Media/GlyphRunTests.cs

@@ -0,0 +1,81 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Controls.Documents;
+using Avalonia.Controls.Shapes;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Xunit;
+
+#if AVALONIA_SKIA
+namespace Avalonia.Skia.RenderTests
+#else
+namespace Avalonia.Direct2D1.RenderTests.Media
+#endif
+{
+    public class GlyphRunTests : TestBase
+    {
+        public GlyphRunTests()
+            : base(@"Media\GlyphRun")
+        {
+        }
+         
+        [Fact]
+        public async Task Should_Render_GlyphRun_Geometry()
+        {
+            Decorator target = new Decorator
+            {
+                Padding = new Thickness(8),
+                Width = 200,
+                Height = 100,
+                Child = new GlyphRunGeometryControl
+                {
+                    [TextElement.ForegroundProperty] = new LinearGradientBrush
+                    {
+                        StartPoint = new RelativePoint(0, 0.5, RelativeUnit.Relative),
+                        EndPoint = new RelativePoint(1, 0.5, RelativeUnit.Relative),
+                        GradientStops =
+                        {
+                            new GradientStop { Color = Colors.Red, Offset = 0 },
+                            new GradientStop { Color = Colors.Blue, Offset = 1 }
+                        }
+                    }
+                }
+            };
+
+            await RenderToFile(target);
+
+            CompareImages();
+        }
+
+        public class GlyphRunGeometryControl : Control
+        {
+            private readonly Geometry _geometry;
+
+            public GlyphRunGeometryControl()
+            {
+                var glyphTypeface = new Typeface(TestFontFamily).GlyphTypeface;
+
+                var glyphIndices = new[] { glyphTypeface.GetGlyph('A'), glyphTypeface.GetGlyph('B'), glyphTypeface.GetGlyph('C') };
+
+                var characters = new[] { 'A', 'B', 'C' };
+
+                var glyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices);
+
+                _geometry = glyphRun.BuildGeometry();
+            }
+
+            protected override Size MeasureOverride(Size availableSize)
+            {
+                return _geometry.Bounds.Size;
+            }
+
+            public override void Render(DrawingContext context)
+            {
+                var foreground = TextElement.GetForeground(this);
+
+                context.DrawGeometry(foreground, null, _geometry);
+            }
+        }
+    }
+}

+ 7 - 0
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@@ -122,6 +122,13 @@ namespace Avalonia.UnitTests
             return Mock.Of<IGlyphRunImpl>();
         }
 
+        public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale)
+        {
+            scale = Matrix.Identity;
+
+            return Mock.Of<IGeometryImpl>();
+        }
+
         public bool SupportsIndividualRoundRects { get; set; }
 
         public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul;

BIN
tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png


BIN
tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png