Browse Source

Merge remote-tracking branch 'origin/master' into feature/tray-icon-support

Dan Walmsley 4 years ago
parent
commit
912d3f4f95
80 changed files with 1241 additions and 147 deletions
  1. 1 0
      Documentation/build.md
  2. 14 2
      packages/Avalonia/AvaloniaBuildTasks.targets
  3. 1 0
      src/Avalonia.Base/Metadata/TemplateContent.cs
  4. 15 0
      src/Avalonia.Controls/Design.cs
  5. 5 5
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  6. 61 37
      src/Avalonia.Controls/ItemsSourceView.cs
  7. 3 2
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  8. 1 0
      src/Avalonia.Controls/Primitives/Popup.cs
  9. 4 5
      src/Avalonia.Controls/Templates/IControlTemplate.cs
  10. 20 0
      src/Avalonia.Controls/Templates/TemplateResult.cs
  11. 9 3
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  12. 2 2
      src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
  13. 2 2
      src/Avalonia.Dialogs/ManagedFileChooser.cs
  14. 1 1
      src/Avalonia.Dialogs/ManagedFileChooserSources.cs
  15. 2 0
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  16. 6 2
      src/Avalonia.Input/Gestures.cs
  17. 2 0
      src/Avalonia.Input/MouseDevice.cs
  18. 1 0
      src/Avalonia.Themes.Default/Expander.xaml
  19. 1 1
      src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml
  20. 3 3
      src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml
  21. 3 1
      src/Avalonia.Visuals/ApiCompatBaseline.txt
  22. 170 0
      src/Avalonia.Visuals/Media/CombinedGeometry.cs
  23. 1 1
      src/Avalonia.Visuals/Media/FormattedText.cs
  24. 37 0
      src/Avalonia.Visuals/Media/GeometryCollection.cs
  25. 80 0
      src/Avalonia.Visuals/Media/GeometryGroup.cs
  26. 8 2
      src/Avalonia.Visuals/Media/PathMarkupParser.cs
  27. 49 3
      src/Avalonia.Visuals/Media/RotateTransform.cs
  28. 17 0
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  29. 2 2
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  30. 5 2
      src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
  31. 6 1
      src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs
  32. 3 1
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  33. 1 1
      src/Avalonia.Visuals/Vector.cs
  34. 1 1
      src/Avalonia.X11/X11CursorFactory.cs
  35. 0 5
      src/Avalonia.X11/X11Window.cs
  36. 4 5
      src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
  37. 4 3
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  38. 14 0
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatformOptions.cs
  39. 0 46
      src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs
  40. 70 0
      src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevBackBuffer.cs
  41. 6 1
      src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs
  42. 0 3
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  43. 6 1
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs
  44. 1 1
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  45. 12 0
      src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs
  46. 11 1
      src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs
  47. 5 1
      src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs
  48. 35 0
      src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs
  49. 36 0
      src/Skia/Avalonia.Skia/GeometryGroupImpl.cs
  50. 10 0
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  51. 2 0
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  52. 36 0
      src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs
  53. 33 0
      src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs
  54. 25 0
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  55. 6 0
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  56. 10 0
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  57. 63 0
      tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs
  58. 62 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/GenericTemplateTests.cs
  59. 89 0
      tests/Avalonia.RenderTests/Media/CombinedGeometryTests.cs
  60. 95 0
      tests/Avalonia.RenderTests/Media/GeometryGroupTests.cs
  61. 10 0
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  62. 26 0
      tests/Avalonia.Visuals.UnitTests/Media/GeometryGroupTests.cs
  63. 23 0
      tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs
  64. 10 0
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
  65. BIN
      tests/TestFiles/Direct2D1/Media/CombinedGeometry/Geometry1_Transform.expected.png
  66. BIN
      tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png
  67. BIN
      tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png
  68. BIN
      tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png
  69. BIN
      tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png
  70. BIN
      tests/TestFiles/Direct2D1/Media/GeometryGroup/Child_Transform.expected.png
  71. BIN
      tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_Stroke_EvenOdd.expected.png
  72. BIN
      tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_Stroke_NonZero.expected.png
  73. BIN
      tests/TestFiles/Skia/Media/CombinedGeometry/Geometry1_Transform.expected.png
  74. BIN
      tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png
  75. BIN
      tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png
  76. BIN
      tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png
  77. BIN
      tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png
  78. BIN
      tests/TestFiles/Skia/Media/GeometryGroup/Child_Transform.expected.png
  79. BIN
      tests/TestFiles/Skia/Media/GeometryGroup/FillRule_Stroke_EvenOdd.expected.png
  80. BIN
      tests/TestFiles/Skia/Media/GeometryGroup/FillRule_Stroke_NonZero.expected.png

+ 1 - 0
Documentation/build.md

@@ -6,6 +6,7 @@ Avalonia requires at least Visual Studio 2019 and .NET Core SDK 3.1 to build on
 
 ```
 git clone https://github.com/AvaloniaUI/Avalonia.git
+cd Avalonia
 git submodule update --init
 ```
 

+ 14 - 2
packages/Avalonia/AvaloniaBuildTasks.targets

@@ -42,12 +42,24 @@
   </Target>
   
   <PropertyGroup>
-    <BuildAvaloniaResourcesDependsOn>$(BuildAvaloniaResourcesDependsOn);AddAvaloniaResources;ResolveReferences</BuildAvaloniaResourcesDependsOn>
+    <BuildAvaloniaResourcesDependsOn>$(BuildAvaloniaResourcesDependsOn);AddAvaloniaResources;ResolveReferences;_GenerateAvaloniaResourcesDependencyCache</BuildAvaloniaResourcesDependsOn>
   </PropertyGroup>
   
+  <Target Name="_GenerateAvaloniaResourcesDependencyCache" BeforeTargets="GenerateAvaloniaResources">
+    <ItemGroup>
+      <CustomAdditionalGenerateAvaloniaResourcesInputs Include="$(IntermediateOutputPath)/Avalonia/Resources.Inputs.cache" />
+    </ItemGroup>
+    
+    <Hash ItemsToHash="@(AvaloniaResource);@(AvaloniaXaml);$(MSBuildAllProjects)">
+      <Output TaskParameter="HashResult" PropertyName="AvaloniaResourcesDependencyHash" />
+    </Hash>
+
+    <WriteLinesToFile Overwrite="true" File="$(IntermediateOutputPath)/Avalonia/Resources.Inputs.cache" Lines="$(AvaloniaResourcesDependencyHash)" WriteOnlyWhenDifferent="True" />
+  </Target>
+  
   <Target Name="GenerateAvaloniaResources" 
           BeforeTargets="CoreCompile;CoreResGen"
-          Inputs="@(AvaloniaResource);@(AvaloniaXaml);$(MSBuildAllProjects)"
+          Inputs="@(AvaloniaResource);@(AvaloniaXaml);@(CustomAdditionalGenerateAvaloniaResourcesInputs);$(MSBuildAllProjects)"
           Outputs="$(AvaloniaResourcesTemporaryFilePath)"
 	  DependsOnTargets="$(BuildAvaloniaResourcesDependsOn)">
     <ItemGroup>

+ 1 - 0
src/Avalonia.Base/Metadata/TemplateContent.cs

@@ -8,5 +8,6 @@ namespace Avalonia.Metadata
     [AttributeUsage(AttributeTargets.Property)]
     public class TemplateContentAttribute : Attribute
     {
+        public Type TemplateResultType { get; set; }
     }
 }

+ 15 - 0
src/Avalonia.Controls/Design.cs

@@ -60,6 +60,19 @@ namespace Avalonia.Controls
             return target.GetValue(PreviewWithProperty);
         }
 
+        public static readonly AttachedProperty<IStyle> DesignStyleProperty = AvaloniaProperty
+            .RegisterAttached<Control, IStyle>("DesignStyle", typeof(Design));
+
+        public static void SetDesignStyle(Control control, IStyle value)
+        {
+            control.SetValue(DesignStyleProperty, value);
+        }
+
+        public static IStyle GetDesignStyle(Control control)
+        {
+            return control.GetValue(DesignStyleProperty);
+        }
+
         public static void ApplyDesignModeProperties(Control target, Control source)
         {
             if (source.IsSet(WidthProperty))
@@ -68,6 +81,8 @@ namespace Avalonia.Controls
                 target.Height = source.GetValue(HeightProperty);
             if (source.IsSet(DataContextProperty))
                 target.DataContext = source.GetValue(DataContextProperty);
+            if (source.IsSet(DesignStyleProperty))
+                target.Styles.Add(source.GetValue(DesignStyleProperty));
         }
     }
 }

+ 5 - 5
src/Avalonia.Controls/Flyouts/FlyoutBase.cs

@@ -215,11 +215,6 @@ namespace Avalonia.Controls.Primitives
                 }
             }
 
-            if (CancelOpening())
-            {
-                return false;
-            }
-
             if (Popup.Parent != null && Popup.Parent != placementTarget)
             {
                 ((ISetLogicalParent)Popup).SetParent(null);
@@ -236,6 +231,11 @@ namespace Avalonia.Controls.Primitives
                 Popup.Child = CreatePresenter();
             }
 
+            if (CancelOpening())
+            {
+                return false;
+            }
+
             PositionPopup(showAtPointer);
             IsOpen = Popup.IsOpen = true;
             OnOpened();

+ 61 - 37
src/Avalonia.Controls/ItemsSourceView.cs

@@ -32,8 +32,8 @@ namespace Avalonia.Controls
         /// </summary>
         public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty<object>());
 
-        private protected readonly IList _inner;
-        private INotifyCollectionChanged? _notifyCollectionChanged;
+        private IList? _inner;
+        private NotifyCollectionChangedEventHandler? _collectionChanged;
 
         /// <summary>
         /// Initializes a new instance of the ItemsSourceView class for the specified data source.
@@ -42,27 +42,22 @@ namespace Avalonia.Controls
         public ItemsSourceView(IEnumerable source)
         {
             source = source ?? throw new ArgumentNullException(nameof(source));
-
-            if (source is IList list)
-            {
-                _inner = list;
-            }
-            else if (source is IEnumerable<object> objectEnumerable)
+            _inner = source switch
             {
-                _inner = new List<object>(objectEnumerable);
-            }
-            else
-            {
-                _inner = new List<object>(source.Cast<object>());
-            }
-
-            ListenToCollectionChanges();
+                ItemsSourceView _ => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)),
+                IList list => list,
+                INotifyCollectionChanged _ => throw new ArgumentException(
+                    "Collection implements INotifyCollectionChanged by not IList.",
+                    nameof(source)),
+                IEnumerable<object> iObj => new List<object>(iObj),
+                _ => new List<object>(source.Cast<object>())
+            };
         }
 
         /// <summary>
         /// Gets the number of items in the collection.
         /// </summary>
-        public int Count => _inner.Count;
+        public int Count => Inner.Count;
 
         /// <summary>
         /// Gets a value that indicates whether the items source can provide a unique key for each item.
@@ -72,6 +67,19 @@ namespace Avalonia.Controls
         /// </remarks>
         public bool HasKeyIndexMapping => false;
 
+        /// <summary>
+        /// Gets the inner collection.
+        /// </summary>
+        public IList Inner
+        {
+            get
+            {
+                if (_inner is null)
+                    ThrowDisposed();
+                return _inner!;
+            }
+        }
+
         /// <summary>
         /// Retrieves the item at the specified index.
         /// </summary>
@@ -82,15 +90,38 @@ namespace Avalonia.Controls
         /// <summary>
         /// Occurs when the collection has changed to indicate the reason for the change and which items changed.
         /// </summary>
-        public event NotifyCollectionChangedEventHandler? CollectionChanged;
+        public event NotifyCollectionChangedEventHandler? CollectionChanged
+        {
+            add
+            {
+                if (_collectionChanged is null && Inner is INotifyCollectionChanged incc)
+                {
+                    incc.CollectionChanged += OnCollectionChanged;
+                }
+
+                _collectionChanged += value;
+            }
+
+            remove
+            {
+                _collectionChanged -= value;
+
+                if (_collectionChanged is null && Inner is INotifyCollectionChanged incc)
+                {
+                    incc.CollectionChanged -= OnCollectionChanged;
+                }
+            }
+        }
 
         /// <inheritdoc/>
         public void Dispose()
         {
-            if (_notifyCollectionChanged != null)
+            if (_inner is INotifyCollectionChanged incc)
             {
-                _notifyCollectionChanged.CollectionChanged -= OnCollectionChanged;
+                incc.CollectionChanged -= OnCollectionChanged;
             }
+
+            _inner = null;
         }
 
         /// <summary>
@@ -98,9 +129,9 @@ namespace Avalonia.Controls
         /// </summary>
         /// <param name="index">The index.</param>
         /// <returns>The item.</returns>
-        public object? GetAt(int index) => _inner[index];
+        public object? GetAt(int index) => Inner[index];
 
-        public int IndexOf(object? item) => _inner.IndexOf(item);
+        public int IndexOf(object? item) => Inner.IndexOf(item);
 
         public static ItemsSourceView GetOrCreate(IEnumerable? items)
         {
@@ -146,7 +177,7 @@ namespace Avalonia.Controls
 
         internal void AddListener(ICollectionChangedListener listener)
         {
-            if (_inner is INotifyCollectionChanged incc)
+            if (Inner is INotifyCollectionChanged incc)
             {
                 CollectionChangedEventManager.Instance.AddListener(incc, listener);
             }
@@ -154,7 +185,7 @@ namespace Avalonia.Controls
 
         internal void RemoveListener(ICollectionChangedListener listener)
         {
-            if (_inner is INotifyCollectionChanged incc)
+            if (Inner is INotifyCollectionChanged incc)
             {
                 CollectionChangedEventManager.Instance.RemoveListener(incc, listener);
             }
@@ -162,22 +193,15 @@ namespace Avalonia.Controls
 
         protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args)
         {
-            CollectionChanged?.Invoke(this, args);
-        }
-
-        private void ListenToCollectionChanges()
-        {
-            if (_inner is INotifyCollectionChanged incc)
-            {
-                incc.CollectionChanged += OnCollectionChanged;
-                _notifyCollectionChanged = incc;
-            }
+            _collectionChanged?.Invoke(this, args);
         }
 
         private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
         {
             OnItemsSourceChanged(e);
         }
+
+        private void ThrowDisposed() => throw new ObjectDisposedException(nameof(ItemsSourceView));
     }
 
     public class ItemsSourceView<T> : ItemsSourceView, IReadOnlyList<T>
@@ -216,10 +240,10 @@ namespace Avalonia.Controls
         /// <param name="index">The index.</param>
         /// <returns>The item.</returns>
         [return: MaybeNull]
-        public new T GetAt(int index) => (T)_inner[index];
+        public new T GetAt(int index) => (T)Inner[index];
 
-        public IEnumerator<T> GetEnumerator() => _inner.Cast<T>().GetEnumerator();
-        IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator();
+        public IEnumerator<T> GetEnumerator() => Inner.Cast<T>().GetEnumerator();
+        IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator();
 
         public static new ItemsSourceView<T> GetOrCreate(IEnumerable? items)
         {

+ 3 - 2
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@@ -275,7 +275,7 @@ namespace Avalonia.Controls.Platform
                 return;
             }
 
-            if (item.HasSubMenu)
+            if (item.HasSubMenu && item.IsEffectivelyEnabled)
             {
                 Open(item, true);
             }
@@ -303,7 +303,8 @@ namespace Avalonia.Controls.Platform
                 {
                     item.Parent.SelectedItem.Close();
                     SelectItemAndAncestors(item);
-                    Open(item, false);
+                    if (item.HasSubMenu)
+                        Open(item, false);
                 }
                 else
                 {

+ 1 - 0
src/Avalonia.Controls/Primitives/Popup.cs

@@ -53,6 +53,7 @@ namespace Avalonia.Controls.Primitives
             AvaloniaProperty.Register<Popup, PopupPositionerConstraintAdjustment>(
                 nameof(PlacementConstraintAdjustment),
                 PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY |
+                PopupPositionerConstraintAdjustment.SlideX | PopupPositionerConstraintAdjustment.SlideY |
                 PopupPositionerConstraintAdjustment.ResizeX | PopupPositionerConstraintAdjustment.ResizeY);
 
         /// <summary>

+ 4 - 5
src/Avalonia.Controls/Templates/IControlTemplate.cs

@@ -1,3 +1,4 @@
+using System;
 using Avalonia.Controls.Primitives;
 using Avalonia.Styling;
 
@@ -10,18 +11,16 @@ namespace Avalonia.Controls.Templates
     {
     }
 
-    public class ControlTemplateResult
+    public class ControlTemplateResult : TemplateResult<IControl>
     {
         public IControl Control { get; }
-        public INameScope NameScope { get; }
 
-        public ControlTemplateResult(IControl control, INameScope nameScope)
+        public ControlTemplateResult(IControl control, INameScope nameScope) : base(control, nameScope)
         {
             Control = control;
-            NameScope = nameScope;
         }
 
-        public void Deconstruct(out IControl control, out INameScope scope)
+        public new void Deconstruct(out IControl control, out INameScope scope)
         {
             control = Control;
             scope = NameScope;

+ 20 - 0
src/Avalonia.Controls/Templates/TemplateResult.cs

@@ -0,0 +1,20 @@
+namespace Avalonia.Controls.Templates
+{
+    public class TemplateResult<T>
+    {
+        public T Result { get; }
+        public INameScope NameScope { get; }
+
+        public TemplateResult(T result, INameScope nameScope)
+        {
+            Result = result;
+            NameScope = nameScope;
+        }
+
+        public void Deconstruct(out T result, out INameScope scope)
+        {
+            result = Result;
+            scope = NameScope;
+        }
+    }
+}

+ 9 - 3
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs

@@ -160,13 +160,19 @@ namespace Avalonia.Diagnostics.Views
                 return;
             }
 
+            var root = Root;
+            if (root is null)
+            {
+                return;
+            }
+
             switch (e.Modifiers)
             {
                 case RawInputModifiers.Control | RawInputModifiers.Shift:
                 {
                     IControl? control = null;
 
-                    foreach (var popupRoot in GetPopupRoots(Root))
+                    foreach (var popupRoot in GetPopupRoots(root))
                     {
                         control = GetHoveredControl(popupRoot);
 
@@ -176,7 +182,7 @@ namespace Avalonia.Diagnostics.Views
                         }
                     }
 
-                    control ??= GetHoveredControl(Root);
+                    control ??= GetHoveredControl(root);
 
                     if (control != null)
                     {
@@ -190,7 +196,7 @@ namespace Avalonia.Diagnostics.Views
                 {
                     vm.FreezePopups = !vm.FreezePopups;
 
-                    foreach (var popupRoot in GetPopupRoots(Root))
+                    foreach (var popupRoot in GetPopupRoots(root))
                     {
                         if (popupRoot.Parent is Popup popup)
                         {

+ 2 - 2
src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs

@@ -30,13 +30,13 @@ namespace Avalonia.Dialogs
             }
             else
             {
-                using (Process process = Process.Start(new ProcessStartInfo
+                using Process process = Process.Start(new ProcessStartInfo
                 {
                     FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? url : "open",
                     Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"{url}" : "",
                     CreateNoWindow = true,
                     UseShellExecute = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
-                }));
+                });
             }
         }
 

+ 2 - 2
src/Avalonia.Dialogs/ManagedFileChooser.cs

@@ -1,13 +1,11 @@
 using System;
 using System.Linq;
 using System.Threading.Tasks;
-using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.LogicalTree;
-using Avalonia.Markup.Xaml;
 
 namespace Avalonia.Dialogs
 {
@@ -35,7 +33,9 @@ namespace Avalonia.Dialogs
             if (_quickLinksRoot != null)
             {
                 var isQuickLink = _quickLinksRoot.IsLogicalAncestorOf(e.Source as Control);
+#pragma warning disable CS0618 // Type or member is obsolete
                 if (e.ClickCount == 2 || isQuickLink)
+#pragma warning restore CS0618 // Type or member is obsolete
                 {
                     if (model.ItemType == ManagedFileChooserItemType.File)
                     {

+ 1 - 1
src/Avalonia.Dialogs/ManagedFileChooserSources.cs

@@ -67,7 +67,7 @@ namespace Avalonia.Dialogs
                        {
                            Directory.GetFiles(x.VolumePath);
                        }
-                       catch (Exception _)
+                       catch (Exception)
                        {
                            return null;
                        }

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

@@ -47,6 +47,8 @@ namespace Avalonia.Headless
         }
 
         public IStreamGeometryImpl CreateStreamGeometry() => new HeadlessStreamingGeometryStub();
+        public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children) => throw new NotImplementedException();
+        public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => throw new NotImplementedException();
 
         public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces) => new HeadlessRenderTarget();
 

+ 6 - 2
src/Avalonia.Input/Gestures.cs

@@ -81,17 +81,21 @@ namespace Avalonia.Input
                 var e = (PointerPressedEventArgs)ev;
                 var visual = (IVisual)ev.Source;
 
-                if (e.ClickCount <= 1)
+#pragma warning disable CS0618 // Type or member is obsolete
+                var clickCount = e.ClickCount;
+#pragma warning restore CS0618 // Type or member is obsolete
+                if (clickCount <= 1)
                 {
                     s_lastPress = new WeakReference<IInteractive>(ev.Source);
                 }
-                else if (s_lastPress != null && e.ClickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed)
+                else if (s_lastPress != null && clickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed)
                 {
                     if (s_lastPress.TryGetTarget(out var target) && target == e.Source)
                     {
                         e.Source.RaiseEvent(new TappedEventArgs(DoubleTappedEvent, e));
                     }
                 }
+
             }
         }
 

+ 2 - 0
src/Avalonia.Input/MouseDevice.cs

@@ -75,7 +75,9 @@ namespace Avalonia.Input
                 throw new InvalidOperationException("Control is not attached to visual tree.");
             }
 
+#pragma warning disable CS0618 // Type or member is obsolete
             var rootPoint = relativeTo.VisualRoot.PointToClient(Position);
+#pragma warning restore CS0618 // Type or member is obsolete
             var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo);
             return rootPoint * transform!.Value;
         }

+ 1 - 0
src/Avalonia.Themes.Default/Expander.xaml

@@ -101,6 +101,7 @@
                               Grid.Column="1" 
                               Background="Transparent" 
                               Content="{TemplateBinding Content}" 
+                              ContentTemplate="{Binding $parent[Expander].HeaderTemplate}"
                               VerticalAlignment="Center" 
                               HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                               VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"

+ 1 - 1
src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml

@@ -17,7 +17,7 @@
     <Setter Property="Template">
       <ControlTemplate>
         <LayoutTransformControl Name="PART_LayoutTransformControl" UseRenderTransform="True">
-          <Border CornerRadius="{DynamicResource ControlCornerRadius}" BoxShadow="0 6 8 0 #4F000000" Margin="5 5 5 10">
+          <Border CornerRadius="{TemplateBinding CornerRadius}" BoxShadow="0 6 8 0 #4F000000" Margin="5 5 5 10">
             <Border Background="{TemplateBinding Background}"
                     BorderBrush="{TemplateBinding BorderBrush}"
                     BorderThickness="{TemplateBinding BorderThickness}"

+ 3 - 3
src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml

@@ -23,11 +23,11 @@
         <Border x:Name="ProgressBarRoot" ClipToBounds="True" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}">          
             <Panel>
               <Panel x:Name="DeterminateRoot">
-                <Border CornerRadius="{DynamicResource ControlCornerRadius}" x:Name="PART_Indicator" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Foreground}" />
+                <Border CornerRadius="{TemplateBinding CornerRadius}" x:Name="PART_Indicator" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Foreground}" />
               </Panel>
               <Panel x:Name="IndeterminateRoot">
-                <Border x:Name="IndeterminateProgressBarIndicator" CornerRadius="{DynamicResource ControlCornerRadius}" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Foreground}" />
-                <Border x:Name="IndeterminateProgressBarIndicator2" CornerRadius="{DynamicResource ControlCornerRadius}" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Foreground}" />
+                <Border x:Name="IndeterminateProgressBarIndicator" CornerRadius="{TemplateBinding CornerRadius}" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Foreground}" />
+                <Border x:Name="IndeterminateProgressBarIndicator2" CornerRadius="{TemplateBinding CornerRadius}" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Foreground}" />
               </Panel>
             </Panel>          
         </Border>

+ 3 - 1
src/Avalonia.Visuals/ApiCompatBaseline.txt

@@ -67,6 +67,8 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avaloni
 InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAndTangentAtDistance(System.Double, Avalonia.Point, Avalonia.Point)' is present in the implementation but not in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAtDistance(System.Double, Avalonia.Point)' is present in the implementation but not in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetSegment(System.Double, System.Double, System.Boolean, Avalonia.Platform.IGeometryImpl)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGeometryImpl Avalonia.Platform.IPlatformRenderInterface.CreateCombinedGeometry(Avalonia.Media.GeometryCombineMode, Avalonia.Media.Geometry, Avalonia.Media.Geometry)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGeometryImpl Avalonia.Platform.IPlatformRenderInterface.CreateGeometryGroup(Avalonia.Media.FillRule, System.Collections.Generic.IReadOnlyList<Avalonia.Media.Geometry>)' is present in the implementation but not in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun)' is present in the implementation but not in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' is present in the contract but not in the implementation.
 MembersMustExist : Member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' does not exist in the implementation but it does exist in the contract.
@@ -74,4 +76,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWr
 InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.String)' is present in the implementation but not in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToHeight(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToWidth(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract.
-Total Issues: 75
+Total Issues: 77

+ 170 - 0
src/Avalonia.Visuals/Media/CombinedGeometry.cs

@@ -0,0 +1,170 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Platform;
+
+#nullable enable
+
+namespace Avalonia.Media
+{
+    public enum GeometryCombineMode
+    {
+        /// <summary>
+        /// The two regions are combined by taking the union of both. The resulting geometry is
+        /// geometry A + geometry B.
+        /// </summary>
+        Union,
+
+        /// <summary>
+        /// The two regions are combined by taking their intersection. The new area consists of the
+        /// overlapping region between the two geometries.
+        /// </summary>
+        Intersect,
+
+        /// <summary>
+        /// The two regions are combined by taking the area that exists in the first region but not
+        /// the second and the area that exists in the second region but not the first. The new
+        /// region consists of (A-B) + (B-A), where A and B are geometries.
+        /// </summary>
+        Xor,
+
+        /// <summary>
+        /// The second region is excluded from the first. Given two geometries, A and B, the area of
+        /// geometry B is removed from the area of geometry A, producing a region that is A-B.
+        /// </summary>
+        Exclude,
+    }
+
+    /// <summary>
+    /// Represents a 2-D geometric shape defined by the combination of two Geometry objects.
+    /// </summary>
+    public class CombinedGeometry : Geometry
+    {
+        /// <summary>
+        /// Defines the <see cref="Geometry1"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Geometry?> Geometry1Property =
+            AvaloniaProperty.Register<CombinedGeometry, Geometry?>(nameof(Geometry1));
+
+        /// <summary>
+        /// Defines the <see cref="Geometry2"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Geometry?> Geometry2Property =
+            AvaloniaProperty.Register<CombinedGeometry, Geometry?>(nameof(Geometry2));
+        /// <summary>
+        /// Defines the <see cref="GeometryCombineMode"/> property.
+        /// </summary>
+        public static readonly StyledProperty<GeometryCombineMode> GeometryCombineModeProperty =
+            AvaloniaProperty.Register<CombinedGeometry, GeometryCombineMode>(nameof(GeometryCombineMode));
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CombinedGeometry"/> class.
+        /// </summary>
+        public CombinedGeometry()
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CombinedGeometry"/> class with the
+        /// specified <see cref="Geometry"/> objects.
+        /// </summary>
+        /// <param name="geometry1">The first geometry to combine.</param>
+        /// <param name="geometry2">The second geometry to combine.</param>
+        public CombinedGeometry(Geometry geometry1, Geometry geometry2)
+        {
+            Geometry1 = geometry1;
+            Geometry2 = geometry2;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CombinedGeometry"/> class with the
+        /// specified <see cref="Geometry"/> objects and <see cref="GeometryCombineMode"/>.
+        /// </summary>
+        /// <param name="combineMode">The method by which geometry1 and geometry2 are combined.</param>
+        /// <param name="geometry1">The first geometry to combine.</param>
+        /// <param name="geometry2">The second geometry to combine.</param>
+        public CombinedGeometry(GeometryCombineMode combineMode, Geometry? geometry1, Geometry? geometry2)
+        {
+            Geometry1 = geometry1;
+            Geometry2 = geometry2;
+            GeometryCombineMode = combineMode;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CombinedGeometry"/> class with the
+        /// specified <see cref="Geometry"/> objects, <see cref="GeometryCombineMode"/> and
+        /// <see cref="Transform"/>.
+        /// </summary>
+        /// <param name="combineMode">The method by which geometry1 and geometry2 are combined.</param>
+        /// <param name="geometry1">The first geometry to combine.</param>
+        /// <param name="geometry2">The second geometry to combine.</param>
+        /// <param name="transform">The transform applied to the geometry.</param>
+        public CombinedGeometry(
+            GeometryCombineMode combineMode,
+            Geometry? geometry1,
+            Geometry? geometry2,
+            Transform? transform)
+        {
+            Geometry1 = geometry1;
+            Geometry2 = geometry2;
+            GeometryCombineMode = combineMode;
+            Transform = transform;
+        }
+
+        /// <summary>
+        /// Gets or sets the first <see cref="Geometry"/> object of this
+        /// <see cref="CombinedGeometry"/> object.
+        /// </summary>
+        public Geometry? Geometry1
+        {
+            get => GetValue(Geometry1Property);
+            set => SetValue(Geometry1Property, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the second <see cref="Geometry"/> object of this
+        /// <see cref="CombinedGeometry"/> object.
+        /// </summary>
+        public Geometry? Geometry2
+        {
+            get => GetValue(Geometry2Property);
+            set => SetValue(Geometry2Property, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the method by which the two geometries (specified by the
+        /// <see cref="Geometry1"/> and <see cref="Geometry2"/> properties) are combined. The
+        /// default value is <see cref="GeometryCombineMode.Union"/>.
+        /// </summary>
+        public GeometryCombineMode GeometryCombineMode
+        {
+            get => GetValue(GeometryCombineModeProperty);
+            set => SetValue(GeometryCombineModeProperty, value);
+        }
+
+        public override Geometry Clone()
+        {
+            return new CombinedGeometry(GeometryCombineMode, Geometry1, Geometry2, Transform);
+        }
+
+        protected override IGeometryImpl? CreateDefiningGeometry()
+        {
+            var g1 = Geometry1;
+            var g2 = Geometry2;
+
+            if (g1 is object && g2 is object)
+            {
+                var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
+                return factory.CreateCombinedGeometry(GeometryCombineMode, g1, g2);
+            }
+            else if (GeometryCombineMode == GeometryCombineMode.Intersect)
+                return null;
+            else if (g1 is object)
+                return g1.PlatformImpl;
+            else if (g2 is object)
+                return g2.PlatformImpl;
+            else
+                return null;
+        }
+    }
+}

+ 1 - 1
src/Avalonia.Visuals/Media/FormattedText.cs

@@ -200,7 +200,7 @@ namespace Avalonia.Media
 
         private void Set<T>(ref T field, T value)
         {
-            if (field != null && field.Equals(value))
+            if (EqualityComparer<T>.Default.Equals(field, value))
             {
                 return;
             }

+ 37 - 0
src/Avalonia.Visuals/Media/GeometryCollection.cs

@@ -0,0 +1,37 @@
+using System.Collections;
+using System.Collections.Generic;
+using Avalonia.Animation;
+
+#nullable enable
+
+namespace Avalonia.Media
+{
+    public class GeometryCollection : Animatable, IList<Geometry>, IReadOnlyList<Geometry>
+    {
+        private List<Geometry> _inner;
+
+        public GeometryCollection() => _inner = new List<Geometry>();
+        public GeometryCollection(IEnumerable<Geometry> collection) => _inner = new List<Geometry>(collection);
+        public GeometryCollection(int capacity) => _inner = new List<Geometry>(capacity);
+
+        public Geometry this[int index] 
+        { 
+            get => _inner[index];
+            set => _inner[index] = value; 
+        }
+
+        public int Count => _inner.Count;
+        public bool IsReadOnly => false;
+
+        public void Add(Geometry item) => _inner.Add(item);
+        public void Clear() => _inner.Clear();
+        public bool Contains(Geometry item) => _inner.Contains(item);
+        public void CopyTo(Geometry[] array, int arrayIndex) => _inner.CopyTo(array, arrayIndex);
+        public IEnumerator<Geometry> GetEnumerator() => _inner.GetEnumerator();
+        public int IndexOf(Geometry item) => _inner.IndexOf(item);
+        public void Insert(int index, Geometry item) => _inner.Insert(index, item);
+        public bool Remove(Geometry item) => _inner.Remove(item);
+        public void RemoveAt(int index) => _inner.RemoveAt(index);
+        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+    }
+}

+ 80 - 0
src/Avalonia.Visuals/Media/GeometryGroup.cs

@@ -0,0 +1,80 @@
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Metadata;
+using Avalonia.Platform;
+
+#nullable enable
+
+namespace Avalonia.Media
+{
+    /// <summary>
+    /// Represents a composite geometry, composed of other <see cref="Geometry"/> objects.
+    /// </summary>
+    public class GeometryGroup : Geometry
+    {
+        public static readonly DirectProperty<GeometryGroup, GeometryCollection?> ChildrenProperty =
+            AvaloniaProperty.RegisterDirect<GeometryGroup, GeometryCollection?> (
+                nameof(Children),
+                o => o.Children,
+                (o, v) => o.Children = v);
+
+        public static readonly StyledProperty<FillRule> FillRuleProperty =
+            AvaloniaProperty.Register<GeometryGroup, FillRule>(nameof(FillRule));
+
+        private GeometryCollection? _children;
+        private bool _childrenSet;
+
+        /// <summary>
+        /// Gets or sets the collection that contains the child geometries.
+        /// </summary>
+        [Content]
+        public GeometryCollection? Children
+        {
+            get => _children ??= (!_childrenSet ? new GeometryCollection() : null);
+            set
+            {
+                SetAndRaise(ChildrenProperty, ref _children, value);
+                _childrenSet = true;
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets how the intersecting areas of the objects contained in this
+        /// <see cref="GeometryGroup"/> are combined. The default is <see cref="FillRule.EvenOdd"/>.
+        /// </summary>
+        public FillRule FillRule
+        {
+            get => GetValue(FillRuleProperty);
+            set => SetValue(FillRuleProperty, value);
+        }
+
+        public override Geometry Clone()
+        {
+            var result = new GeometryGroup { FillRule = FillRule, Transform = Transform };
+            if (_children?.Count > 0)
+                result.Children = new GeometryCollection(_children);
+            return result;
+        }
+
+        protected override IGeometryImpl? CreateDefiningGeometry()
+        {
+            if (_children?.Count > 0)
+            {
+                var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
+                return factory.CreateGeometryGroup(FillRule, _children);
+            }
+
+            return null;
+        }
+
+        protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
+        {
+            base.OnPropertyChanged(change);
+
+            if (change.Property == ChildrenProperty || change.Property == FillRuleProperty)
+            {
+                InvalidateGeometry();
+            }
+        }
+    }
+}

+ 8 - 2
src/Avalonia.Visuals/Media/PathMarkupParser.cs

@@ -496,12 +496,18 @@ namespace Avalonia.Media
 
         private bool ReadBool(ref ReadOnlySpan<char> span)
         {
-            if (!ReadArgument(ref span, out var boolValue) || boolValue.Length != 1)
+            span = SkipWhitespace(span);
+            
+            if (span.IsEmpty)
             {
                 throw new InvalidDataException("Invalid bool rule.");
             }
             
-            switch (boolValue[0])
+            var c = span[0];
+            
+            span = span.Slice(1);
+            
+            switch (c)
             {
                 case '0':
                     return false;

+ 49 - 3
src/Avalonia.Visuals/Media/RotateTransform.cs

@@ -14,6 +14,18 @@ namespace Avalonia.Media
         public static readonly StyledProperty<double> AngleProperty =
             AvaloniaProperty.Register<RotateTransform, double>(nameof(Angle));
 
+        /// <summary>
+        /// Defines the <see cref="CenterX"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> CenterXProperty =
+            AvaloniaProperty.Register<RotateTransform, double>(nameof(CenterX));
+
+        /// <summary>
+        /// Defines the <see cref="CenterY"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> CenterYProperty =
+            AvaloniaProperty.Register<RotateTransform, double>(nameof(CenterY));
+
         /// <summary>
         /// Initializes a new instance of the <see cref="RotateTransform"/> class.
         /// </summary>
@@ -32,18 +44,52 @@ namespace Avalonia.Media
             Angle = angle;
         }
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RotateTransform"/> class.
+        /// </summary>
+        /// <param name="angle">The angle, in degrees.</param>
+        /// <param name="centerX">The x-coordinate of the center point for the rotation.</param>
+        /// <param name="centerY">The y-coordinate of the center point for the rotation.</param>
+        public RotateTransform(double angle, double centerX, double centerY)
+            : this()
+        {
+            Angle = angle;
+            CenterX = centerX;
+            CenterY = centerY;
+        }
+
         /// <summary>
         /// Gets or sets the angle of rotation, in degrees.
         /// </summary>
         public double Angle
         {
-            get { return GetValue(AngleProperty); }
-            set { SetValue(AngleProperty, value); }
+            get => GetValue(AngleProperty);
+            set => SetValue(AngleProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the x-coordinate of the rotation center point. The default is 0.
+        /// </summary>
+        public double CenterX
+        {
+            get => GetValue(CenterXProperty);
+            set => SetValue(CenterXProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the y-coordinate of the rotation center point. The default is 0.
+        /// </summary>
+        public double CenterY
+        {
+            get => GetValue(CenterYProperty);
+            set => SetValue(CenterYProperty, value);
         }
 
         /// <summary>
         /// Gets the transform's <see cref="Matrix"/>.
         /// </summary>
-        public override Matrix Value => Matrix.CreateRotation(Matrix.ToRadians(Angle));
+        public override Matrix Value => Matrix.CreateTranslation(-CenterX, -CenterY) *
+            Matrix.CreateRotation(Matrix.ToRadians(Angle)) *
+            Matrix.CreateTranslation(CenterX, CenterY);
     }
 }

+ 17 - 0
src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs

@@ -59,6 +59,23 @@ namespace Avalonia.Platform
         /// <returns>An <see cref="IStreamGeometryImpl"/>.</returns>
         IStreamGeometryImpl CreateStreamGeometry();
 
+        /// <summary>
+        /// Creates a geometry group implementation.
+        /// </summary>
+        /// <param name="fillRule">The fill rule.</param>
+        /// <param name="children">The geometries to group.</param>
+        /// <returns>A combined geometry.</returns>
+        IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children);
+
+        /// <summary>
+        /// Creates a geometry group implementation.
+        /// </summary>
+        /// <param name="combineMode">The combine mode</param>
+        /// <param name="g1">The first geometry.</param>
+        /// <param name="g2">The second geometry.</param>
+        /// <returns>A combined geometry.</returns>
+        IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2);
+
         /// <summary>
         /// Creates a renderer.
         /// </summary>

+ 2 - 2
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@@ -279,13 +279,13 @@ namespace Avalonia.Rendering
         /// <inheritdoc/>
         Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
         {
-            return (_currentDraw.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty;
+            return (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty;
         }
 
         /// <inheritdoc/>
         void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush)
         {
-            var childScene = (_currentDraw.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual];
+            var childScene = (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual];
 
             if (childScene != null)
             {

+ 5 - 2
src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs

@@ -289,11 +289,14 @@ namespace Avalonia.Rendering
 
                 using (context.PushPostTransform(m))
                 using (context.PushOpacity(opacity))
-                using (clipToBounds 
-                    ? visual is IVisualWithRoundRectClip roundClipVisual 
+                using (clipToBounds
+#pragma warning disable CS0618 // Type or member is obsolete
+                    ? visual is IVisualWithRoundRectClip roundClipVisual
                         ? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius))
                         : context.PushClip(bounds) 
                     : default(DrawingContext.PushedState))
+#pragma warning restore CS0618 // Type or member is obsolete
+
                 using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default(DrawingContext.PushedState))
                 using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default(DrawingContext.PushedState))
                 using (context.PushTransformContainer())

+ 6 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs

@@ -17,7 +17,12 @@ namespace Avalonia.Rendering.SceneGraph
 
         public override bool HitTest(Point p)
         {
-            return Custom.HitTest(p * Transform);
+            if (Transform.HasInverse)
+            {
+                return Custom.HitTest(p * Transform.Invert());
+            }
+
+            return false;
         }
 
         public override void Render(IDrawingContextImpl context)

+ 3 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@@ -164,10 +164,12 @@ namespace Avalonia.Rendering.SceneGraph
             var visual = node.Visual;
             var opacity = visual.Opacity;
             var clipToBounds = visual.ClipToBounds;
+#pragma warning disable CS0618 // Type or member is obsolete
             var clipToBoundsRadius = visual is IVisualWithRoundRectClip roundRectClip ?
                 roundRectClip.ClipToBoundsRadius :
                 default;
-            
+#pragma warning restore CS0618 // Type or member is obsolete
+
             var bounds = new Rect(visual.Bounds.Size);
             var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl;
 

+ 1 - 1
src/Avalonia.Visuals/Vector.cs

@@ -175,7 +175,7 @@ namespace Avalonia
                    MathUtilities.AreClose(_y, other._y);
         }
 
-        public override bool Equals(object obj) => obj is Vector other && Equals(other);
+        public override bool Equals(object? obj) => obj is Vector other && Equals(other);
 
         public override int GetHashCode()
         {

+ 1 - 1
src/Avalonia.X11/X11CursorFactory.cs

@@ -25,7 +25,7 @@ namespace Avalonia.X11
             {
                 {StandardCursorType.Arrow, CursorFontShape.XC_top_left_arrow},
                 {StandardCursorType.Cross, CursorFontShape.XC_cross},
-                {StandardCursorType.Hand, CursorFontShape.XC_hand1},
+                {StandardCursorType.Hand, CursorFontShape.XC_hand2},
                 {StandardCursorType.Help, CursorFontShape.XC_question_arrow},
                 {StandardCursorType.Ibeam, CursorFontShape.XC_xterm},
                 {StandardCursorType.No, CursorFontShape.XC_X_cursor},

+ 0 - 5
src/Avalonia.X11/X11Window.cs

@@ -192,11 +192,6 @@ namespace Avalonia.X11
             if (platform.Options.UseDBusMenu)
                 NativeMenuExporter = DBusMenuExporter.TryCreateTopLevelNativeMenu(_handle);
             NativeControlHost = new X11NativeControlHost(_platform, this);
-            DispatcherTimer.Run(() =>
-            {
-                Paint?.Invoke(default);
-                return _handle != IntPtr.Zero;
-            }, TimeSpan.FromMilliseconds(100));
             InitializeIme();
         }
 

+ 4 - 5
src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs

@@ -30,10 +30,9 @@ namespace Avalonia.LinuxFramebuffer
 
         public IRenderer CreateRenderer(IRenderRoot root)
         {
-            return new DeferredRenderer(root, AvaloniaLocator.Current.GetService<IRenderLoop>())
-            {
-                
-            };
+            var factory = AvaloniaLocator.Current.GetService<IRendererFactory>();
+            var renderLoop = AvaloniaLocator.Current.GetService<IRenderLoop>();
+            return factory?.Create(root, renderLoop) ?? new DeferredRenderer(root, renderLoop);
         }
 
         public void Dispose()
@@ -41,7 +40,7 @@ namespace Avalonia.LinuxFramebuffer
             throw new NotSupportedException();
         }
 
-        
+
         public void Invalidate(Rect rect)
         {
         }

+ 4 - 3
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@@ -37,16 +37,17 @@ namespace Avalonia.LinuxFramebuffer
             Threading = new InternalPlatformThreadingInterface();
             if (_fb is IGlOutputBackend gl)
                 AvaloniaLocator.CurrentMutable.Bind<IPlatformOpenGlInterface>().ToConstant(gl.PlatformOpenGlInterface);
+            
+            var opts = AvaloniaLocator.Current.GetService<LinuxFramebufferPlatformOptions>();
+            
             AvaloniaLocator.CurrentMutable
                 .Bind<IPlatformThreadingInterface>().ToConstant(Threading)
-                .Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
+                .Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(opts?.Fps ?? 60))
                 .Bind<IRenderLoop>().ToConstant(new RenderLoop())
                 .Bind<ICursorFactory>().ToTransient<CursorFactoryStub>()
                 .Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice())
                 .Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
-                .Bind<IRenderLoop>().ToConstant(new RenderLoop())
                 .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
-
         }
 
        

+ 14 - 0
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatformOptions.cs

@@ -0,0 +1,14 @@
+namespace Avalonia.LinuxFramebuffer
+{
+    /// <summary>
+    /// Platform-specific options which apply to the Linux framebuffer.
+    /// </summary>
+    public class LinuxFramebufferPlatformOptions
+    {
+        /// <summary>
+        /// Gets or sets the number of frames per second at which the renderer should run.
+        /// Default 60.
+        /// </summary>
+        public int Fps { get; set; } = 60;
+    }
+}

+ 0 - 46
src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs

@@ -1,46 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-using Avalonia.Platform;
-
-namespace Avalonia.LinuxFramebuffer
-{
-    unsafe class LockedFramebuffer : ILockedFramebuffer
-    {
-        private readonly int _fb;
-        private readonly fb_fix_screeninfo _fixedInfo;
-        private fb_var_screeninfo _varInfo;
-        private readonly IntPtr _address;
-
-        public LockedFramebuffer(int fb, fb_fix_screeninfo fixedInfo, fb_var_screeninfo varInfo, IntPtr address, Vector dpi)
-        {
-            _fb = fb;
-            _fixedInfo = fixedInfo;
-            _varInfo = varInfo;
-            _address = address;
-            Dpi = dpi;
-            //Use double buffering to avoid flicker
-            Address = Marshal.AllocHGlobal(RowBytes * Size.Height);
-        }
-
-
-        void VSync()
-        {
-            NativeUnsafeMethods.ioctl(_fb, FbIoCtl.FBIO_WAITFORVSYNC, null);
-        }
-
-        public void Dispose()
-        {
-            VSync();
-            NativeUnsafeMethods.memcpy(_address, Address, new IntPtr(RowBytes * Size.Height));
-
-            Marshal.FreeHGlobal(Address);
-            Address = IntPtr.Zero;
-        }
-
-        public IntPtr Address { get; private set; }
-        public PixelSize Size => new PixelSize((int)_varInfo.xres, (int) _varInfo.yres);
-        public int RowBytes => (int) _fixedInfo.line_length;
-        public Vector Dpi { get; }
-        public PixelFormat Format => _varInfo.bits_per_pixel == 16 ? PixelFormat.Rgb565 : _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 : PixelFormat.Bgra8888;
-    }
-}

+ 70 - 0
src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevBackBuffer.cs

@@ -0,0 +1,70 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Threading;
+using Avalonia.Platform;
+
+namespace Avalonia.LinuxFramebuffer.Output
+{
+    internal unsafe class FbDevBackBuffer : IDisposable
+    {
+        private readonly int _fb;
+        private readonly fb_fix_screeninfo _fixedInfo;
+        private readonly fb_var_screeninfo _varInfo;
+        private readonly IntPtr _targetAddress;
+        private readonly object _lock = new object();
+
+        public FbDevBackBuffer(int fb, fb_fix_screeninfo fixedInfo, fb_var_screeninfo varInfo, IntPtr targetAddress)
+        {
+            _fb = fb;
+            _fixedInfo = fixedInfo;
+            _varInfo = varInfo;
+            _targetAddress = targetAddress;
+            Address = Marshal.AllocHGlobal(RowBytes * Size.Height);
+        }
+        
+
+        public void Dispose()
+        {
+            if (Address != IntPtr.Zero)
+            {
+                Marshal.FreeHGlobal(Address);
+                Address = IntPtr.Zero;
+            }
+        }
+
+        public ILockedFramebuffer Lock(Vector dpi)
+        {
+            Monitor.Enter(_lock);
+            try
+            {
+                return new LockedFramebuffer(Address,
+                    new PixelSize((int)_varInfo.xres, (int)_varInfo.yres),
+                    (int)_fixedInfo.line_length, dpi,
+                    _varInfo.bits_per_pixel == 16 ? PixelFormat.Rgb565
+                    : _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888
+                    : PixelFormat.Bgra8888,
+                    () =>
+                    {
+                        try
+                        {
+                            NativeUnsafeMethods.ioctl(_fb, FbIoCtl.FBIO_WAITFORVSYNC, null);
+                            NativeUnsafeMethods.memcpy(_targetAddress, Address, new IntPtr(RowBytes * Size.Height));
+                        }
+                        finally
+                        {
+                            Monitor.Exit(_lock);
+                        }
+                    });
+            }
+            catch
+            {
+                Monitor.Exit(_lock);
+                throw;
+            }
+        }
+
+        public IntPtr Address { get; private set; }
+        public PixelSize Size => new PixelSize((int)_varInfo.xres, (int) _varInfo.yres);
+        public int RowBytes => (int) _fixedInfo.line_length;
+    }
+}

+ 6 - 1
src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs

@@ -14,6 +14,7 @@ namespace Avalonia.LinuxFramebuffer
         private fb_var_screeninfo _varInfo;
         private IntPtr _mappedLength;
         private IntPtr _mappedAddress;
+        private FbDevBackBuffer _backBuffer;
         public double Scaling { get; set; }
 
         /// <summary>
@@ -146,7 +147,9 @@ namespace Avalonia.LinuxFramebuffer
         {
             if (_fd <= 0)
                 throw new ObjectDisposedException("LinuxFramebuffer");
-            return new LockedFramebuffer(_fd, _fixedInfo, _varInfo, _mappedAddress, new Vector(96, 96) * Scaling);
+            return (_backBuffer ??=
+                    new FbDevBackBuffer(_fd, _fixedInfo, _varInfo, _mappedAddress))
+                .Lock(new Vector(96, 96) * Scaling);
         }
 
 
@@ -165,6 +168,8 @@ namespace Avalonia.LinuxFramebuffer
 
         public void Dispose()
         {
+            _backBuffer?.Dispose();
+            _backBuffer = null;
             ReleaseUnmanagedResources();
             GC.SuppressFinalize(this);
         }

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

@@ -14,7 +14,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
 {
     class AvaloniaXamlIlCompiler : XamlILCompiler
     {
-        private readonly TransformerConfiguration _configuration;
         private readonly IXamlType _contextType;
         private readonly AvaloniaXamlIlDesignPropertiesTransformer _designTransformer;
         private readonly AvaloniaBindingExtensionTransformer _bindingTransformer;
@@ -22,8 +21,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
         private AvaloniaXamlIlCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult> emitMappings)
             : base(configuration, emitMappings, true)
         {
-            _configuration = configuration;
-
             void InsertAfter<T>(params IXamlAstTransformer[] t) 
                 => Transformers.InsertRange(Transformers.FindIndex(x => x is T) + 1, t);
 

+ 6 - 1
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs

@@ -49,8 +49,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
                 XmlNamespaceInfoProvider =
                     typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.IAvaloniaXamlIlXmlNamespaceInfoProvider"),
                 DeferredContentPropertyAttributes = {typeSystem.GetType("Avalonia.Metadata.TemplateContentAttribute")},
+                DeferredContentExecutorCustomizationDefaultTypeParameter = typeSystem.GetType("Avalonia.Controls.IControl"),
+                DeferredContentExecutorCustomizationTypeParameterDeferredContentAttributePropertyNames = new List<string>
+                {
+                    "TemplateResultType"
+                },
                 DeferredContentExecutorCustomization =
-                    runtimeHelpers.FindMethod(m => m.Name == "DeferredTransformationFactoryV1"),
+                    runtimeHelpers.FindMethod(m => m.Name == "DeferredTransformationFactoryV2"),
                 UsableDuringInitializationAttributes =
                 {
                     typeSystem.GetType("Avalonia.Metadata.UsableDuringInitializationAttribute"),

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github

@@ -1 +1 @@
-Subproject commit f4ac681b91a9dc7a7a095d1050a683de23d86b72
+Subproject commit 8e20d65eb5f1efbae08e49b18f39bfdce32df7b3

+ 12 - 0
src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs

@@ -7,6 +7,7 @@ namespace Avalonia.Markup.Xaml.Templates
     public static class TemplateContent
     {
         public static ControlTemplateResult Load(object templateContent)
+
         {
             if (templateContent is Func<IServiceProvider, object> direct)
             {
@@ -20,5 +21,16 @@ namespace Avalonia.Markup.Xaml.Templates
 
             throw new ArgumentException(nameof(templateContent));
         }
+
+        public static TemplateResult<T> Load<T>(object templateContent)
+        {
+            if (templateContent is Func<IServiceProvider, object> direct)
+                return (TemplateResult<T>)direct(null);
+
+            if (templateContent is null)
+                return null;
+
+            throw new ArgumentException(nameof(templateContent));
+        }
     }
 }

+ 11 - 1
src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs

@@ -15,6 +15,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
     {
         public static Func<IServiceProvider, object> DeferredTransformationFactoryV1(Func<IServiceProvider, object> builder,
             IServiceProvider provider)
+        {
+            return DeferredTransformationFactoryV2<IControl>(builder, provider);
+        }
+        
+        public static Func<IServiceProvider, object> DeferredTransformationFactoryV2<T>(Func<IServiceProvider, object> builder,
+            IServiceProvider provider)
         {
             var resourceNodes = provider.GetService<IAvaloniaXamlIlParentStackProvider>().Parents
                 .OfType<IResourceNode>().ToList();
@@ -25,7 +31,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
                 var scope = parentScope != null ? new ChildNameScope(parentScope) : (INameScope)new NameScope();
                 var obj = builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject, scope));
                 scope.Complete();
-                return new ControlTemplateResult((IControl)obj, scope);
+                
+                if(typeof(T) == typeof(IControl))
+                    return new ControlTemplateResult((IControl)obj, scope);
+
+                return new TemplateResult<T>((T)obj, scope);
             };
         }
 

+ 5 - 1
src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs

@@ -184,6 +184,9 @@ namespace Avalonia.Markup.Parsers
             
         }
 
+        // Don't need to override GetHashCode as the ISyntax objects will not be stored in a hash; the 
+        // only reason they have overridden Equals methods is for unit testing.
+#pragma warning disable CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode()
         public class PropertySyntax : ISyntax
         {
             public string Name { get; set; } = string.Empty;
@@ -205,7 +208,7 @@ namespace Avalonia.Markup.Parsers
                    && other.TypeName == TypeName
                    && other.TypeNamespace == TypeNamespace;
         }
-        
+
         public class ChildTraversalSyntax : ISyntax
         {
             public static ChildTraversalSyntax Instance { get;  } = new ChildTraversalSyntax();
@@ -231,5 +234,6 @@ namespace Avalonia.Markup.Parsers
                    && other.TypeName == TypeName
                    && other.TypeNamespace == TypeNamespace;
         }
+#pragma warning restore CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode()
     }
 }

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

@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using Avalonia.Media;
+using SkiaSharp;
+
+#nullable enable
+
+namespace Avalonia.Skia
+{
+    /// <summary>
+    /// A Skia implementation of a <see cref="Avalonia.Media.GeometryGroup"/>.
+    /// </summary>
+    internal class CombinedGeometryImpl : GeometryImpl
+    {
+        public CombinedGeometryImpl(GeometryCombineMode combineMode, Geometry g1, Geometry g2)
+        {
+            var path1 = ((GeometryImpl)g1.PlatformImpl).EffectivePath;
+            var path2 = ((GeometryImpl)g2.PlatformImpl).EffectivePath;
+            var op = combineMode switch
+            {
+                GeometryCombineMode.Intersect => SKPathOp.Intersect,
+                GeometryCombineMode.Xor => SKPathOp.Xor,
+                GeometryCombineMode.Exclude => SKPathOp.Difference,
+                _ => SKPathOp.Union,
+            };
+
+            var path = path1.Op(path2, op);
+
+            EffectivePath = path;
+            Bounds = path.Bounds.ToAvaloniaRect();
+        }
+
+        public override Rect Bounds { get; }
+        public override SKPath EffectivePath { get; }
+    }
+}

+ 36 - 0
src/Skia/Avalonia.Skia/GeometryGroupImpl.cs

@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using Avalonia.Media;
+using SkiaSharp;
+
+#nullable enable
+
+namespace Avalonia.Skia
+{
+    /// <summary>
+    /// A Skia implementation of a <see cref="Avalonia.Media.GeometryGroup"/>.
+    /// </summary>
+    internal class GeometryGroupImpl : GeometryImpl
+    {
+        public GeometryGroupImpl(FillRule fillRule, IReadOnlyList<Geometry> children)
+        {
+            var path = new SKPath
+            {
+                FillType = fillRule == FillRule.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd,
+            };
+
+            var count = children.Count;
+            
+            for (var i = 0; i < count; ++i)
+            {
+                if (children[i]?.PlatformImpl is GeometryImpl child)
+                    path.AddPath(child.EffectivePath);
+            }
+
+            EffectivePath = path;
+            Bounds = path.Bounds.ToAvaloniaRect();
+        }
+
+        public override Rect Bounds { get; }
+        public override SKPath EffectivePath { get; }
+    }
+}

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

@@ -62,6 +62,16 @@ namespace Avalonia.Skia
             return new StreamGeometryImpl();
         }
 
+        public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children)
+        {
+            return new GeometryGroupImpl(fillRule, children);
+        }
+
+        public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2)
+        {
+            return new CombinedGeometryImpl(combineMode, g1, g2);
+        }
+
         /// <inheritdoc />
         public IBitmapImpl LoadBitmap(string fileName)
         {

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

@@ -175,6 +175,8 @@ namespace Avalonia.Direct2D1
         public IGeometryImpl CreateLineGeometry(Point p1, Point p2) => new LineGeometryImpl(p1, p2);
         public IGeometryImpl CreateRectangleGeometry(Rect rect) => new RectangleGeometryImpl(rect);
         public IStreamGeometryImpl CreateStreamGeometry() => new StreamGeometryImpl();
+        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);
 
         /// <inheritdoc />
         public IBitmapImpl LoadBitmap(string fileName)

+ 36 - 0
src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs

@@ -0,0 +1,36 @@
+using SharpDX.Direct2D1;
+using AM = Avalonia.Media;
+
+namespace Avalonia.Direct2D1.Media
+{
+    /// <summary>
+    /// A Direct2D implementation of a <see cref="Avalonia.Media.CombinedGeometry"/>.
+    /// </summary>
+    internal class CombinedGeometryImpl : GeometryImpl
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
+        /// </summary>
+        public CombinedGeometryImpl(
+            AM.GeometryCombineMode combineMode,
+            AM.Geometry geometry1,
+            AM.Geometry geometry2)
+            : base(CreateGeometry(combineMode, geometry1, geometry2))
+        {
+        }
+
+        private static Geometry CreateGeometry(
+            AM.GeometryCombineMode combineMode,
+            AM.Geometry geometry1,
+            AM.Geometry geometry2)
+        {
+            var g1 = ((GeometryImpl)geometry1.PlatformImpl).Geometry;
+            var g2 = ((GeometryImpl)geometry2.PlatformImpl).Geometry;
+            var dest = new PathGeometry(Direct2D1Platform.Direct2D1Factory);
+            using var sink = dest.Open();
+            g1.Combine(g2, (CombineMode)combineMode, sink);
+            sink.Close();
+            return dest;
+        }
+    }
+}

+ 33 - 0
src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs

@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using SharpDX.Direct2D1;
+using AM = Avalonia.Media;
+
+namespace Avalonia.Direct2D1.Media
+{
+    /// <summary>
+    /// A Direct2D implementation of a <see cref="Avalonia.Media.GeometryGroup"/>.
+    /// </summary>
+    internal class GeometryGroupImpl : GeometryImpl
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
+        /// </summary>
+        public GeometryGroupImpl(AM.FillRule fillRule, IReadOnlyList<AM.Geometry> geometry)
+            : base(CreateGeometry(fillRule, geometry))
+        {
+        }
+
+        private static Geometry CreateGeometry(AM.FillRule fillRule, IReadOnlyList<AM.Geometry> children)
+        {
+            var count = children.Count;
+            var c = new Geometry[count];
+
+            for (var i = 0; i < count; ++i)
+            {
+                c[i] = ((GeometryImpl)children[i].PlatformImpl).Geometry;
+            }
+
+            return new GeometryGroup(Direct2D1Platform.Direct2D1Factory, (FillMode)fillRule, c);
+        }
+    }
+}

+ 25 - 0
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@@ -808,6 +808,31 @@ namespace Avalonia.Win32.Interop
             MNC_SELECT = 3
         }
 
+        public enum SysCommands
+        {
+            SC_SIZE = 0xF000,
+            SC_MOVE = 0xF010,
+            SC_MINIMIZE = 0xF020,
+            SC_MAXIMIZE = 0xF030,
+            SC_NEXTWINDOW = 0xF040,
+            SC_PREVWINDOW = 0xF050,
+            SC_CLOSE = 0xF060,
+            SC_VSCROLL = 0xF070,
+            SC_HSCROLL = 0xF080,
+            SC_MOUSEMENU = 0xF090,
+            SC_KEYMENU = 0xF100,
+            SC_ARRANGE = 0xF110,
+            SC_RESTORE = 0xF120,
+            SC_TASKLIST = 0xF130,
+            SC_SCREENSAVE = 0xF140,
+            SC_HOTKEY = 0xF150,
+            SC_DEFAULT = 0xF160,
+            SC_MONITORPOWER = 0xF170,
+            SC_CONTEXTHELP = 0xF180,
+            SC_SEPARATOR = 0xF00F,
+            SCF_ISSECURE = 0x00000001,
+        }
+
         [StructLayout(LayoutKind.Sequential)]
         public struct RGBQUAD
         {

+ 6 - 0
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@@ -123,6 +123,12 @@ namespace Avalonia.Win32
                         break;
                     }
 
+                case WindowsMessage.WM_SYSCOMMAND:
+                    // Disable system handling of Alt/F10 menu keys.
+                    if ((SysCommands)wParam == SysCommands.SC_KEYMENU && HighWord(ToInt32(lParam)) <= 0)
+                        return IntPtr.Zero;
+                    break;
+
                 case WindowsMessage.WM_MENUCHAR:
                     {
                         // mute the system beep

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

@@ -36,6 +36,16 @@ namespace Avalonia.Benchmarks
             return new MockStreamGeometryImpl();
         }
 
+        public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children)
+        {
+            throw new NotImplementedException();
+        }
+
+        public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2)
+        {
+            throw new NotImplementedException();
+        }
+
         public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
         {
             throw new NotImplementedException();

+ 63 - 0
tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs

@@ -0,0 +1,63 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Text;
+using Avalonia.Collections;
+using Avalonia.Diagnostics;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests
+{
+    public class ItemsSourceViewTests
+    {
+        [Fact]
+        public void Only_Subscribes_To_Source_CollectionChanged_When_CollectionChanged_Subscribed()
+        {
+            var source = new AvaloniaList<string>();
+            var target = new ItemsSourceView<string>(source);
+            var debug = (INotifyCollectionChangedDebug)source;
+
+            Assert.Null(debug.GetCollectionChangedSubscribers());
+
+            void Handler(object sender, NotifyCollectionChangedEventArgs e) { }
+            target.CollectionChanged += Handler;
+
+            Assert.NotNull(debug.GetCollectionChangedSubscribers());
+            Assert.Equal(1, debug.GetCollectionChangedSubscribers().Length);
+
+            target.CollectionChanged -= Handler;
+
+            Assert.Null(debug.GetCollectionChangedSubscribers());
+        }
+
+        [Fact]
+        public void Cannot_Wrap_An_ItemsSourceView_In_Another()
+        {
+            var source = new ItemsSourceView<string>(new string[0]);
+            Assert.Throws<ArgumentException>(() => new ItemsSourceView<string>(source));
+        }
+
+        [Fact]
+        public void Cannot_Create_ItemsSourceView_With_Collection_That_Implements_INCC_But_Not_List()
+        {
+            var source = new InvalidCollection();
+            Assert.Throws<ArgumentException>(() => new ItemsSourceView<string>(source));
+        }
+
+        private class InvalidCollection : INotifyCollectionChanged, IEnumerable<string>
+        {
+            public event NotifyCollectionChangedEventHandler CollectionChanged;
+
+            public IEnumerator<string> GetEnumerator()
+            {
+                yield break;
+            }
+
+            IEnumerator IEnumerable.GetEnumerator()
+            {
+                yield break;
+            }
+        }
+    }
+}

+ 62 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/GenericTemplateTests.cs

@@ -0,0 +1,62 @@
+using System.Collections.Generic;
+using Avalonia.Controls;
+using Avalonia.Controls.Presenters;
+using Avalonia.Markup.Xaml.Templates;
+using Avalonia.Metadata;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Markup.Xaml.UnitTests.Xaml
+{
+    public class SampleTemplatedObject : StyledElement
+    {
+        [Content] public List<SampleTemplatedObject> Content { get; set; } = new List<SampleTemplatedObject>();
+        public string Foo { get; set; }
+    }
+    
+    public class SampleTemplatedObjectTemplate
+    {
+        [Content]
+        [TemplateContent(TemplateResultType = typeof(SampleTemplatedObject))]
+        public object Content { get; set; }
+    }
+
+    public class SampleTemplatedObjectContainer
+    {
+        public SampleTemplatedObjectTemplate Template { get; set; }
+    }
+    
+    public class GenericTemplateTests
+    {
+        [Fact]
+        public void DataTemplate_Can_Be_Empty()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<s:SampleTemplatedObjectContainer xmlns='https://github.com/avaloniaui'
+        xmlns:sys='clr-namespace:System;assembly=netstandard'
+        xmlns:s='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <s:SampleTemplatedObjectContainer.Template>
+        <s:SampleTemplatedObjectTemplate>
+            <s:SampleTemplatedObject x:Name='root'>
+                <s:SampleTemplatedObject x:Name='child1' Foo='foo' />
+                <s:SampleTemplatedObject x:Name='child2' Foo='bar' />
+            </s:SampleTemplatedObject>
+        </s:SampleTemplatedObjectTemplate>
+    </s:SampleTemplatedObjectContainer.Template>
+</s:SampleTemplatedObjectContainer>";
+                var container =
+                    (SampleTemplatedObjectContainer)AvaloniaRuntimeXamlLoader.Load(xaml,
+                        typeof(GenericTemplateTests).Assembly);
+                var res = TemplateContent.Load<SampleTemplatedObject>(container.Template.Content);
+                Assert.Equal(res.Result, res.NameScope.Find("root"));
+                Assert.Equal(res.Result.Content[0], res.NameScope.Find("child1"));
+                Assert.Equal(res.Result.Content[1], res.NameScope.Find("child2"));
+                Assert.Equal("foo", res.Result.Content[0].Foo);
+                Assert.Equal("bar", res.Result.Content[1].Foo);
+            }
+        }
+    }
+}

+ 89 - 0
tests/Avalonia.RenderTests/Media/CombinedGeometryTests.cs

@@ -0,0 +1,89 @@
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Controls.Shapes;
+using Avalonia.Media;
+using Xunit;
+
+#if AVALONIA_SKIA
+namespace Avalonia.Skia.RenderTests
+#else
+namespace Avalonia.Direct2D1.RenderTests.Media
+#endif
+{
+    public class CombinedGeometryTests : TestBase
+    {
+        public CombinedGeometryTests()
+            : base(@"Media\CombinedGeometry")
+        {
+        }
+
+        [Theory]
+        [InlineData(Avalonia.Media.GeometryCombineMode.Union)]
+        [InlineData(Avalonia.Media.GeometryCombineMode.Intersect)]
+        [InlineData(Avalonia.Media.GeometryCombineMode.Xor)]
+        [InlineData(Avalonia.Media.GeometryCombineMode.Exclude)]
+        public async Task GeometryCombineMode(GeometryCombineMode mode)
+        {
+            var target = new Border
+            {
+                Width = 200,
+                Height = 200,
+                Background = Brushes.White,
+                Child = new Path
+                {
+                    Data = new CombinedGeometry
+                    {
+                        GeometryCombineMode = mode,
+                        Geometry1 = new RectangleGeometry(new Rect(25, 25, 100, 100)),
+                        Geometry2 = new EllipseGeometry
+                        {
+                            Center = new Point(125, 125),
+                            RadiusX = 50,
+                            RadiusY = 50,
+                        }
+                    },
+                    Fill = Brushes.Blue,
+                    Stroke = Brushes.Red,
+                    StrokeThickness = 1,
+                }
+            };
+
+            var testName = $"{nameof(GeometryCombineMode)}_{mode}";
+            await RenderToFile(target, testName);
+            CompareImages(testName);
+        }
+
+        [Fact]
+        public async Task Geometry1_Transform()
+        {
+            var target = new Border
+            {
+                Width = 200,
+                Height = 200,
+                Background = Brushes.White,
+                Child = new Path
+                {
+                    Data = new CombinedGeometry
+                    {
+                        Geometry1 = new RectangleGeometry(new Rect(25, 25, 100, 100))
+                        {
+                            Transform = new RotateTransform(45, 75, 75)
+                        },
+                        Geometry2 = new EllipseGeometry
+                        {
+                            Center = new Point(125, 125),
+                            RadiusX = 50,
+                            RadiusY = 50,
+                        }
+                    },
+                    Fill = Brushes.Blue,
+                    Stroke = Brushes.Red,
+                    StrokeThickness = 1,
+                }
+            };
+
+            await RenderToFile(target);
+            CompareImages();
+        }
+    }
+}

+ 95 - 0
tests/Avalonia.RenderTests/Media/GeometryGroupTests.cs

@@ -0,0 +1,95 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+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 GeometryGroupTests : TestBase
+    {
+        public GeometryGroupTests()
+            : base(@"Media\GeometryGroup")
+        {
+        }
+
+        [Theory]
+        [InlineData(FillRule.EvenOdd)]
+        [InlineData(FillRule.NonZero)]
+        public async Task FillRule_Stroke(FillRule fillRule)
+        {
+            var target = new Border
+            {
+                Width = 200,
+                Height = 200,
+                Background = Brushes.White,
+                Child = new Path
+                {
+                    Data = new GeometryGroup
+                    {
+                        FillRule = fillRule,
+                        Children =
+                        {
+                            new RectangleGeometry(new Rect(25, 25, 100, 100)),
+                            new EllipseGeometry
+                            {
+                                Center = new Point(125, 125),
+                                RadiusX = 50,
+                                RadiusY = 50,
+                            },
+                        }
+                    },
+                    Fill = Brushes.Blue,
+                    Stroke = Brushes.Red,
+                    StrokeThickness = 1,
+                }
+            };
+
+            var testName = $"{nameof(FillRule_Stroke)}_{fillRule}";
+            await RenderToFile(target, testName);
+            CompareImages(testName);
+        }
+
+        [Fact]
+        public async Task Child_Transform()
+        {
+            var target = new Border
+            {
+                Width = 200,
+                Height = 200,
+                Background = Brushes.White,
+                Child = new Path
+                {
+                    Data = new GeometryGroup
+                    {
+                        Children =
+                        {
+                            new RectangleGeometry(new Rect(25, 25, 100, 100))
+                            {
+                                Transform = new RotateTransform(45, 75, 75)
+                            },
+                            new EllipseGeometry
+                            {
+                                Center = new Point(125, 125),
+                                RadiusX = 50,
+                                RadiusY = 50,
+                            },
+                        }
+                    },
+                    Fill = Brushes.Blue,
+                    Stroke = Brushes.Red,
+                    StrokeThickness = 1,
+                }
+            };
+
+            await RenderToFile(target);
+            CompareImages();
+        }
+    }
+}

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

@@ -52,6 +52,16 @@ namespace Avalonia.UnitTests
             return new MockStreamGeometryImpl();
         }
 
+        public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children)
+        {
+            return Mock.Of<IGeometryImpl>();
+        }
+
+        public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2)
+        {
+            return Mock.Of<IGeometryImpl>();
+        }
+
         public IWriteableBitmapImpl CreateWriteableBitmap(
             PixelSize size,
             Vector dpi,

+ 26 - 0
tests/Avalonia.Visuals.UnitTests/Media/GeometryGroupTests.cs

@@ -0,0 +1,26 @@
+using Avalonia.Media;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media
+{
+    public class GeometryGroupTests
+    {
+        [Fact]
+        public void Children_Should_Have_Initial_Collection()
+        {
+            var target = new GeometryGroup();
+
+            Assert.NotNull(target.Children);
+        }
+
+        [Fact]
+        public void Children_Can_Be_Set_To_Null()
+        {
+            var target = new GeometryGroup();
+
+            target.Children = null;
+
+            Assert.Null(target.Children);
+        }
+    }
+}

+ 23 - 0
tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs

@@ -297,5 +297,28 @@ namespace Avalonia.Visuals.UnitTests.Media
                 Assert.Equal(new Point(20, 20), figure.StartPoint);
             }
         }
+
+        [Fact]
+        public void Should_Parse_Flags_Without_Separator()
+        {
+            var pathGeometry = new PathGeometry();
+            using (var context = new PathGeometryContext(pathGeometry))
+            using (var parser = new PathMarkupParser(context))
+            {
+                parser.Parse("a.898.898 0 01.27.188");
+
+                var figure = pathGeometry.Figures[0];
+
+                var segments = figure.Segments;
+                
+                Assert.NotNull(segments);
+                
+                Assert.Equal(1, segments.Count);
+
+                var arcSegment = segments[0];
+
+                Assert.IsType<ArcSegment>(arcSegment);
+            }
+        }
     }
 }

+ 10 - 0
tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

@@ -37,6 +37,16 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
             return new MockStreamGeometry();
         }
 
+        public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children)
+        {
+            throw new NotImplementedException();
+        }
+
+        public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2)
+        {
+            throw new NotImplementedException();
+        }
+
         public IBitmapImpl LoadBitmap(Stream stream)
         {
             throw new NotImplementedException();

BIN
tests/TestFiles/Direct2D1/Media/CombinedGeometry/Geometry1_Transform.expected.png


BIN
tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png


BIN
tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png


BIN
tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png


BIN
tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png


BIN
tests/TestFiles/Direct2D1/Media/GeometryGroup/Child_Transform.expected.png


BIN
tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_Stroke_EvenOdd.expected.png


BIN
tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_Stroke_NonZero.expected.png


BIN
tests/TestFiles/Skia/Media/CombinedGeometry/Geometry1_Transform.expected.png


BIN
tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png


BIN
tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png


BIN
tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png


BIN
tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png


BIN
tests/TestFiles/Skia/Media/GeometryGroup/Child_Transform.expected.png


BIN
tests/TestFiles/Skia/Media/GeometryGroup/FillRule_Stroke_EvenOdd.expected.png


BIN
tests/TestFiles/Skia/Media/GeometryGroup/FillRule_Stroke_NonZero.expected.png