فهرست منبع

Merge branch 'master' into refactor/itemcontainergenerator

Steven Kirk 2 سال پیش
والد
کامیت
a8df4863f3
100فایلهای تغییر یافته به همراه1681 افزوده شده و 425 حذف شده
  1. 2 0
      .editorconfig
  2. 0 1
      Avalonia.sln
  3. 1 0
      Directory.Build.props
  4. 31 0
      NOTICE.md
  5. 8 8
      azure-pipelines-integrationtests.yml
  6. 12 12
      azure-pipelines.yml
  7. 1 1
      build/Base.props
  8. 0 5
      build/JetBrains.Annotations.props
  9. 0 1
      build/SharedVersion.props
  10. 1 1
      build/System.Memory.props
  11. 2 4
      global.json
  12. 0 1
      nukebuild/DotNetConfigHelper.cs
  13. 1 1
      samples/ControlCatalog.Android/MainActivity.cs
  14. 4 0
      samples/ControlCatalog.Android/Resources/values/styles.xml
  15. 2 0
      samples/ControlCatalog.Android/SplashActivity.cs
  16. 13 0
      samples/ControlCatalog.Browser/Properties/launchSettings.json
  17. 1 1
      samples/ControlCatalog.Desktop/Program.cs
  18. 4 2
      samples/ControlCatalog/MainView.xaml
  19. 16 7
      samples/ControlCatalog/MainView.xaml.cs
  20. 0 1
      samples/ControlCatalog/MainWindow.xaml
  21. 1 1
      samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs
  22. 212 0
      samples/ControlCatalog/Pages/GesturePage.cs
  23. 117 0
      samples/ControlCatalog/Pages/GesturePage.xaml
  24. 4 1
      samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs
  25. 1 1
      samples/ControlCatalog/Pages/PointerCanvas.cs
  26. 29 29
      samples/ControlCatalog/Pages/ScreenPage.cs
  27. 1 1
      samples/ControlCatalog/Pages/TabControlPage.xaml.cs
  28. 1 1
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  29. 0 7
      samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
  30. 2 5
      samples/ControlCatalog/ViewModels/ContextPageViewModel.cs
  31. 1 1
      samples/ControlCatalog/ViewModels/CursorPageViewModel.cs
  32. 0 9
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  33. 3 1
      samples/ControlCatalog/ViewModels/PlatformInformationViewModel.cs
  34. 1 1
      samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs
  35. 2 3
      samples/Directory.Build.props
  36. 1 1
      src/Android/Avalonia.Android/AndroidPlatform.cs
  37. 10 4
      src/Android/Avalonia.Android/AvaloniaSplashActivity.cs
  38. 2 0
      src/Android/Avalonia.Android/AvaloniaView.cs
  39. 1 0
      src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs
  40. 83 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  41. 1 1
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidMotionEventsHelper.cs
  42. 0 1
      src/Avalonia.Base/Animation/AnimationInstance`1.cs
  43. 3 1
      src/Avalonia.Base/Avalonia.Base.csproj
  44. 1 1
      src/Avalonia.Base/AvaloniaObject.cs
  45. 26 0
      src/Avalonia.Base/Compatibility/OperatingSystem.cs
  46. 0 34
      src/Avalonia.Base/Contract.cs
  47. 13 8
      src/Avalonia.Base/Controls/NameScopeExtensions.cs
  48. 1 1
      src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs
  49. 1 3
      src/Avalonia.Base/Data/InstancedBinding.cs
  50. 1 2
      src/Avalonia.Base/Input/Cursor.cs
  51. 128 0
      src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs
  52. 14 19
      src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs
  53. 49 16
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  54. 375 0
      src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs
  55. 128 1
      src/Avalonia.Base/Input/Gestures.cs
  56. 51 0
      src/Avalonia.Base/Input/HoldingRoutedEventArgs.cs
  57. 15 1
      src/Avalonia.Base/Input/InputElement.cs
  58. 24 0
      src/Avalonia.Base/Input/PinchEventArgs.cs
  59. 2 4
      src/Avalonia.Base/Input/Raw/RawInputEventArgs.cs
  60. 0 6
      src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
  61. 1 1
      src/Avalonia.Base/Layout/WrapLayout/UvMeasure.cs
  62. 5 0
      src/Avalonia.Base/Logging/LogArea.cs
  63. 1 1
      src/Avalonia.Base/Media/DrawingContext.cs
  64. 6 5
      src/Avalonia.Base/Media/DrawingGroup.cs
  65. 1 4
      src/Avalonia.Base/Media/FontManager.cs
  66. 1 1
      src/Avalonia.Base/Media/ImmediateDrawingContext.cs
  67. 6 21
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs
  68. 1 1
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  69. 1 1
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  70. 6 6
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  71. 11 11
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  72. 1 1
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  73. 14 14
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  74. 2 2
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  75. 20 20
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  76. 2 5
      src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs
  77. 1 1
      src/Avalonia.Base/Media/TextFormatting/TextRun.cs
  78. 1 4
      src/Avalonia.Base/Media/TextFormatting/TextShaper.cs
  79. 67 1
      src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs
  80. 10 4
      src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs
  81. 1 3
      src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
  82. 8 8
      src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs
  83. 1 2
      src/Avalonia.Base/Media/Typeface.cs
  84. 0 2
      src/Avalonia.Base/PixelVector.cs
  85. 2 0
      src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
  86. 2 0
      src/Avalonia.Base/Platform/IPlatformSettings.cs
  87. 0 19
      src/Avalonia.Base/Platform/IRuntimePlatform.cs
  88. 6 38
      src/Avalonia.Base/Platform/StandardRuntimePlatform.cs
  89. 4 9
      src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs
  90. 1 1
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  91. 1 1
      src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs
  92. 2 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs
  93. 26 6
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  94. 3 3
      src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs
  95. 2 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs
  96. 4 4
      src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs
  97. 1 1
      src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs
  98. 51 2
      src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs
  99. 3 3
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  100. 1 2
      src/Avalonia.Base/Rendering/RenderLoop.cs

+ 2 - 0
.editorconfig

@@ -169,6 +169,8 @@ dotnet_diagnostic.CA1828.severity = warning
 dotnet_diagnostic.CA1829.severity = warning
 #CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters
 dotnet_diagnostic.CA1847.severity = warning
+#CACA2211:Non-constant fields should not be visible
+dotnet_diagnostic.CA2211.severity = error
 
 # Wrapping preferences
 csharp_wrap_before_ternary_opsigns = false

+ 0 - 1
Avalonia.sln

@@ -100,7 +100,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
 		build\EmbedXaml.props = build\EmbedXaml.props
 		build\HarfBuzzSharp.props = build\HarfBuzzSharp.props
 		build\ImageSharp.props = build\ImageSharp.props
-		build\JetBrains.Annotations.props = build\JetBrains.Annotations.props
 		build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props
 		build\Microsoft.CSharp.props = build\Microsoft.CSharp.props
 		build\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props

+ 1 - 0
Directory.Build.props

@@ -7,5 +7,6 @@
       <AddSyntheticProjectReferencesForSolutionDependencies>false</AddSyntheticProjectReferencesForSolutionDependencies>
       <MSBuildEnableWorkloadResolver>false</MSBuildEnableWorkloadResolver>
       <RunApiCompat>False</RunApiCompat>
+      <LangVersion>11</LangVersion>
   </PropertyGroup>
 </Project>

+ 31 - 0
NOTICE.md

@@ -303,3 +303,34 @@ https://github.com/chromium/chromium
 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# Flutter
+
+https://github.com/flutter/flutter
+
+//Copyright 2014 The Flutter Authors. All rights reserved.
+
+//Redistribution and use in source and binary forms, with or without modification,
+//are permitted provided that the following conditions are met:
+
+//    * Redistributions of source code must retain the above copyright
+//      notice, this list of conditions and the following disclaimer.
+//    * Redistributions in binary form must reproduce the above
+//      copyright notice, this list of conditions and the following
+//      disclaimer in the documentation and/or other materials provided
+//      with the distribution.
+//    * Neither the name of Google Inc. nor the names of its
+//      contributors may be used to endorse or promote products derived
+//      from this software without specific prior written permission.
+
+//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+//LOSS OF USE, DATA, OR PROFITS;
+//OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+//ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 8 - 8
azure-pipelines-integrationtests.yml

@@ -13,14 +13,14 @@ jobs:
 
   steps:
   - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 6.0.401'
+    displayName: 'Use .NET Core SDK 6.0.404'
     inputs:
-      version: 6.0.401
+      version: 6.0.404
 
   - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 7.0.100'
+    displayName: 'Use .NET Core SDK 7.0.101'
     inputs:
-      version: 7.0.100
+      version: 7.0.101
       
   - script: system_profiler SPDisplaysDataType |grep Resolution
   
@@ -51,14 +51,14 @@ jobs:
 
   steps:
   - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 6.0.401'
+    displayName: 'Use .NET Core SDK 6.0.404'
     inputs:
-      version: 6.0.401
+      version: 6.0.404
 
   - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 7.0.100'
+    displayName: 'Use .NET Core SDK 7.0.101'
     inputs:
-      version: 7.0.100
+      version: 7.0.101
 
   - task: Windows Application Driver@0
     inputs:

+ 12 - 12
azure-pipelines.yml

@@ -30,14 +30,14 @@ jobs:
     vmImage: 'ubuntu-20.04'
   steps:
   - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 6.0.401'
+    displayName: 'Use .NET Core SDK 6.0.404'
     inputs:
-      version: 6.0.401
+      version: 6.0.404
 
   - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 7.0'
+    displayName: 'Use .NET Core SDK 7.0.101'
     inputs:
-      version: 7.0.100
+      version: 7.0.101
 
   - task: CmdLine@2
     displayName: 'Install Workloads'
@@ -67,14 +67,14 @@ jobs:
     vmImage: 'macos-12'
   steps:
   - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 6.0.401'
+    displayName: 'Use .NET Core SDK 6.0.404'
     inputs:
-      version: 6.0.401
+      version: 6.0.404
 
   - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 7.0.100'
+    displayName: 'Use .NET Core SDK 7.0.101'
     inputs:
-      version: 7.0.100
+      version: 7.0.101
 
   - task: CmdLine@2
     displayName: 'Install Workloads'
@@ -138,14 +138,14 @@ jobs:
     SolutionDir: '$(Build.SourcesDirectory)'
   steps:
   - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 6.0.401'
+    displayName: 'Use .NET Core SDK 6.0.404'
     inputs:
-      version: 6.0.401
+      version: 6.0.404
 
   - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 7.0.100'
+    displayName: 'Use .NET Core SDK 7.0.101'
     inputs:
-      version: 7.0.100
+      version: 7.0.101
 
   - task: CmdLine@2
     displayName: 'Install Workloads'

+ 1 - 1
build/Base.props

@@ -1,5 +1,5 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-  <ItemGroup>
+  <ItemGroup Condition="'$(TargetFramework)' != 'net6'">
     <PackageReference Include="System.ValueTuple" Version="4.5.0" />
     <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.6.0" />
   </ItemGroup>

+ 0 - 5
build/JetBrains.Annotations.props

@@ -1,5 +0,0 @@
-<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-  <ItemGroup>
-    <PackageReference Include="JetBrains.Annotations" Version="10.3.0" />
-  </ItemGroup>
-</Project>

+ 0 - 1
build/SharedVersion.props

@@ -8,7 +8,6 @@
     <RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <NoWarn>$(NoWarn);CS1591</NoWarn>
-    <LangVersion>preview</LangVersion>
     <PackageLicenseExpression>MIT</PackageLicenseExpression>
     <PackageIcon>Icon.png</PackageIcon>
     <PackageDescription>Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.</PackageDescription>

+ 1 - 1
build/System.Memory.props

@@ -1,5 +1,5 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-  <ItemGroup>
+  <ItemGroup Condition="'$(TargetFramework)' != 'net6'">
     <PackageReference Include="System.Memory" Version="4.5.3" />
   </ItemGroup>
 </Project>

+ 2 - 4
global.json

@@ -1,11 +1,9 @@
 {
     "sdk": {
-        "version": "7.0.100",
+        "version": "7.0.101",
         "rollForward": "latestFeature"
     },
     "msbuild-sdks": {
-        "Microsoft.Build.Traversal": "1.0.43",
-        "MSBuild.Sdk.Extras": "3.0.22",
-        "AggregatePackage.NuGet.Sdk" : "0.1.12"
+        "Microsoft.Build.Traversal": "3.2.0"
     }
 }

+ 0 - 1
nukebuild/DotNetConfigHelper.cs

@@ -1,5 +1,4 @@
 using System.Globalization;
-using JetBrains.Annotations;
 using Nuke.Common.Tools.DotNet;
 // ReSharper disable ReturnValueOfPureMethodIsNotUsed
 

+ 1 - 1
samples/ControlCatalog.Android/MainActivity.cs

@@ -5,7 +5,7 @@ using Avalonia.Android;
 
 namespace ControlCatalog.Android
 {
-    [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)]
+    [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.Main", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)]
     public class MainActivity : AvaloniaMainActivity
     {
     }

+ 4 - 0
samples/ControlCatalog.Android/Resources/values/styles.xml

@@ -14,4 +14,8 @@
     <item name="android:windowContentOverlay">@null</item>
   </style>
 
+  <style name="MyTheme.Main" parent ="MyTheme.NoActionBar">
+    <item name="android:windowIsTranslucent">true</item>
+  </style>
+
 </resources>

+ 2 - 0
samples/ControlCatalog.Android/SplashActivity.cs

@@ -28,6 +28,8 @@ namespace ControlCatalog.Android
             base.OnResume();
 
             StartActivity(new Intent(Application.Context, typeof(MainActivity)));
+
+            Finish();
         }
     }
 }

+ 13 - 0
samples/ControlCatalog.Browser/Properties/launchSettings.json

@@ -0,0 +1,13 @@
+{
+  "profiles": {
+    "ControlCatalog.Browser": {
+      "commandName": "Project",
+      "launchBrowser": true,
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      },
+      "applicationUrl": "https://localhost:5001;http://localhost:5000",
+      "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/debug?browser={browserInspectUri}"
+    }
+  }
+}

+ 1 - 1
samples/ControlCatalog.Desktop/Program.cs

@@ -23,7 +23,7 @@ namespace ControlCatalog
         private static void ConfigureAssetAssembly(AppBuilder builder)
         {
             AvaloniaLocator.CurrentMutable
-                .GetService<IAssetLoader>()
+                .GetRequiredService<IAssetLoader>()
                 .SetDefaultAssembly(typeof(App).Assembly);
         }
     }

+ 4 - 2
samples/ControlCatalog/MainView.xaml

@@ -92,6 +92,9 @@
       <TabItem Header="Flyouts">
         <pages:FlyoutsPage />
       </TabItem>
+      <TabItem Header="Gestures">
+        <pages:GesturePage />
+      </TabItem>
       <TabItem Header="Image"
                ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                ScrollViewer.VerticalScrollBarVisibility="Disabled">
@@ -206,8 +209,7 @@
               </ComboBox.Items>
             </ComboBox>
             <ComboBox x:Name="TransparencyLevels"
-                      HorizontalAlignment="Stretch"
-                      SelectedIndex="{Binding TransparencyLevel}">
+                      HorizontalAlignment="Stretch">
               <ComboBox.Items>
                 <WindowTransparencyLevel>None</WindowTransparencyLevel>
                 <WindowTransparencyLevel>Transparent</WindowTransparencyLevel>

+ 16 - 7
samples/ControlCatalog/MainView.xaml.cs

@@ -6,6 +6,7 @@ using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Markup.Xaml;
 using Avalonia.Media;
 using Avalonia.Media.Immutable;
+using Avalonia.VisualTree;
 using ControlCatalog.Models;
 using ControlCatalog.Pages;
 
@@ -59,17 +60,25 @@ namespace ControlCatalog
             };
 
             var transparencyLevels = this.Get<ComboBox>("TransparencyLevels");
-            IDisposable? backgroundSetter = null, paneBackgroundSetter = null;
+            IDisposable? topLevelBackgroundSideSetter = null, sideBarBackgroundSetter = null, paneBackgroundSetter = null;
             transparencyLevels.SelectionChanged += (sender, e) =>
             {
-                backgroundSetter?.Dispose();
+                topLevelBackgroundSideSetter?.Dispose();
+                sideBarBackgroundSetter?.Dispose();
                 paneBackgroundSetter?.Dispose();
-                if (transparencyLevels.SelectedItem is WindowTransparencyLevel selected
-                    && selected != WindowTransparencyLevel.None)
+                if (transparencyLevels.SelectedItem is WindowTransparencyLevel selected)
                 {
-                    var semiTransparentBrush = new ImmutableSolidColorBrush(Colors.Gray, 0.5);
-                    backgroundSetter = sideBar.SetValue(BackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style);
-                    paneBackgroundSetter = sideBar.SetValue(SplitView.PaneBackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style);
+                    var topLevel = (TopLevel)this.GetVisualRoot()!;
+                    topLevel.TransparencyLevelHint = selected;
+
+                    if (selected != WindowTransparencyLevel.None)
+                    {
+                        var transparentBrush = new ImmutableSolidColorBrush(Colors.White, 0);
+                        var semiTransparentBrush = new ImmutableSolidColorBrush(Colors.Gray, 0.2);
+                        topLevelBackgroundSideSetter = topLevel.SetValue(BackgroundProperty, transparentBrush, Avalonia.Data.BindingPriority.Style);
+                        sideBarBackgroundSetter = sideBar.SetValue(BackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style);
+                        paneBackgroundSetter = sideBar.SetValue(SplitView.PaneBackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style);
+                    }
                 }
             };
         }

+ 0 - 1
samples/ControlCatalog/MainWindow.xaml

@@ -10,7 +10,6 @@
         ExtendClientAreaToDecorationsHint="{Binding ExtendClientAreaEnabled}"
         ExtendClientAreaChromeHints="{Binding ChromeHints}"
         ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}"
-        TransparencyLevelHint="{Binding TransparencyLevel}"        
         x:Name="MainWindow"
         Background="Transparent"
         x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}"

+ 1 - 1
samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs

@@ -17,7 +17,7 @@ namespace ControlCatalog.Pages
             this.Get<TextBlock>("TimePickerDesc").Text = "Use a TimePicker to let users set a time in your app, for example " +
                 "to set a reminder. The TimePicker displays three controls for hour, minute, and AM / PM(if necessary).These controls " +
                 "are easy to use with touch or mouse, and they can be styled and configured in several different ways. " +
-                "12 - hour or 24 - hour clock and visiblility of AM / PM is dynamically set based on user time settings, or can be overridden.";
+                "12 - hour or 24 - hour clock and visibility of AM / PM is dynamically set based on user time settings, or can be overridden.";
 
 
         }

+ 212 - 0
samples/ControlCatalog/Pages/GesturePage.cs

@@ -0,0 +1,212 @@
+using System;
+using System.Numerics;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.LogicalTree;
+using Avalonia.Markup.Xaml;
+using Avalonia.Rendering.Composition;
+
+namespace ControlCatalog.Pages
+{
+    public class GesturePage : UserControl
+    {
+        private bool _isInit;
+        private float _currentScale;
+
+        public GesturePage()
+        {
+            this.InitializeComponent();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToVisualTree(e);
+
+            if(_isInit)
+            {
+                return;
+            }
+
+            _isInit = true;
+
+            SetPullHandlers(this.Find<Border>("TopPullZone"), false);
+            SetPullHandlers(this.Find<Border>("BottomPullZone"), true);
+            SetPullHandlers(this.Find<Border>("RightPullZone"), true);
+            SetPullHandlers(this.Find<Border>("LeftPullZone"), false);
+
+            var image = this.Find<Image>("PinchImage");
+            SetPinchHandlers(image);
+
+            var reset = this.Find<Button>("ResetButton");
+
+            reset!.Click += (s, e) =>
+            {
+                var compositionVisual = ElementComposition.GetElementVisual(image);
+
+                if(compositionVisual!= null)
+                {
+                    _currentScale = 1;
+                    compositionVisual.Scale = new Vector3(1,1,1);
+                    image.InvalidateMeasure();
+                }
+            };
+                
+        }
+
+        private void SetPinchHandlers(Control? control)
+        {
+            if (control == null)
+            {
+                return;
+            }
+
+            _currentScale = 1;
+            Vector3 currentOffset = default;
+            bool isZooming = false;
+
+            CompositionVisual? compositionVisual = null;
+
+            void InitComposition(Control visual)
+            {
+                if (compositionVisual != null)
+                {
+                    return;
+                }
+
+                compositionVisual = ElementComposition.GetElementVisual(visual);
+            }
+
+            control.LayoutUpdated += (s, e) =>
+            {
+                InitComposition(control!);
+                if (compositionVisual != null)
+                {
+                    compositionVisual.Scale = new(_currentScale, _currentScale, 1);
+
+                    if(currentOffset == default)
+                    {
+                        currentOffset = compositionVisual.Offset;
+                    }
+                }
+            };
+
+            control.AddHandler(Gestures.PinchEvent, (s, e) =>
+            {
+                InitComposition(control!);
+
+                isZooming = true;
+
+                if(compositionVisual != null)
+                {
+                    var scale = _currentScale * (float)e.Scale;
+
+                    compositionVisual.Scale = new(scale, scale, 1);
+                }
+            });
+
+            control.AddHandler(Gestures.PinchEndedEvent, (s, e) =>
+            {
+                InitComposition(control!);
+
+                isZooming = false;
+
+                if (compositionVisual != null)
+                {
+                    _currentScale = compositionVisual.Scale.X;
+                }
+            });
+
+            control.AddHandler(Gestures.ScrollGestureEvent, (s, e) =>
+            {
+                InitComposition(control!);
+
+                if (compositionVisual != null && !isZooming)
+                {
+                    currentOffset -= new Vector3((float)e.Delta.X, (float)e.Delta.Y, 0);
+
+                    compositionVisual.Offset = currentOffset;
+                }
+            });
+        }
+
+        private void SetPullHandlers(Control? control, bool inverse)
+        {
+            if (control == null)
+            {
+                return;
+            }
+
+            var ball = control.FindLogicalDescendantOfType<Border>();
+
+            Vector3 defaultOffset = default;
+
+            CompositionVisual? ballCompositionVisual = null;
+
+            if (ball != null)
+            {
+                InitComposition(ball);
+            }
+            else
+            {
+                return;
+            }
+
+            control.LayoutUpdated += (s, e) =>
+            {
+                InitComposition(ball!);
+                if (ballCompositionVisual != null)
+                {
+                    defaultOffset = ballCompositionVisual.Offset;
+                }
+            };
+
+            control.AddHandler(Gestures.PullGestureEvent, (s, e) =>
+            {
+                Vector3 center = new((float)control.Bounds.Center.X, (float)control.Bounds.Center.Y, 0);
+                InitComposition(ball!);
+                if (ballCompositionVisual != null)
+                {
+                    ballCompositionVisual.Offset = defaultOffset + new System.Numerics.Vector3((float)e.Delta.X * 0.4f, (float)e.Delta.Y * 0.4f, 0) * (inverse ? -1 : 1);
+                }
+            });
+
+            control.AddHandler(Gestures.PullGestureEndedEvent, (s, e) =>
+            {
+                InitComposition(ball!);
+                if (ballCompositionVisual != null)
+                {
+                    ballCompositionVisual.Offset = defaultOffset;
+                }
+            });
+
+            void InitComposition(Control control)
+            {
+                if (ballCompositionVisual != null)
+                {
+                    return;
+                }
+
+                ballCompositionVisual = ElementComposition.GetElementVisual(ball);
+
+                if (ballCompositionVisual != null)
+                {
+                    var offsetAnimation = ballCompositionVisual.Compositor.CreateVector3KeyFrameAnimation();
+                    offsetAnimation.Target = "Offset";
+                    offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
+                    offsetAnimation.Duration = TimeSpan.FromMilliseconds(100);
+
+                    var implicitAnimations = ballCompositionVisual.Compositor.CreateImplicitAnimationCollection();
+                    implicitAnimations["Offset"] = offsetAnimation;
+
+                    ballCompositionVisual.ImplicitAnimations = implicitAnimations;
+                }
+            }
+        }
+    }
+}

+ 117 - 0
samples/ControlCatalog/Pages/GesturePage.xaml

@@ -0,0 +1,117 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             d:DesignHeight="800"
+             d:DesignWidth="400"
+             x:Class="ControlCatalog.Pages.GesturePage">
+  <StackPanel Orientation="Vertical"
+              Spacing="4">
+    <TextBlock FontWeight="Bold"
+               FontSize="18"
+               Margin="5">Pull Gexture (Touch / Pen)</TextBlock>
+    <TextBlock Margin="5">Pull from colored rectangles</TextBlock>
+    <Border>
+      <DockPanel HorizontalAlignment="Stretch"
+                 ClipToBounds="True"
+                 Margin="5"
+                 Height="200">
+        <Border DockPanel.Dock="Top"
+                Margin="2"
+                Name="TopPullZone"
+                Background="Transparent"
+                BorderBrush="Red"
+                HorizontalAlignment="Stretch"
+                Height="50"
+                BorderThickness="1">
+          <Border.GestureRecognizers>
+            <PullGestureRecognizer PullDirection="TopToBottom"/>
+          </Border.GestureRecognizers>
+          <Border Width="10"
+                  Height="10"
+                  HorizontalAlignment="Center"
+                  VerticalAlignment="Center"
+                  CornerRadius="5"
+                  Name="TopBall"
+                  Background="Green"/>
+        </Border>
+        <Border DockPanel.Dock="Bottom"
+                BorderBrush="Green"
+                Margin="2"
+                Background="Transparent"
+                Name="BottomPullZone"
+                HorizontalAlignment="Stretch"
+                Height="50"
+                BorderThickness="1">
+          <Border.GestureRecognizers>
+            <PullGestureRecognizer PullDirection="BottomToTop"/>
+          </Border.GestureRecognizers>
+          <Border Width="10"
+                  Name="BottomBall"
+                  HorizontalAlignment="Center"
+                  VerticalAlignment="Center"
+                  Height="10"
+                  CornerRadius="5"
+                  Background="Green"/>
+        </Border>
+        <Border DockPanel.Dock="Right"
+                Margin="2"
+                Background="Transparent"
+                Name="RightPullZone"
+                BorderBrush="Blue"
+                HorizontalAlignment="Right"
+                VerticalAlignment="Stretch"
+                Width="50"
+                BorderThickness="1">
+          <Border.GestureRecognizers>
+            <PullGestureRecognizer PullDirection="RightToLeft"/>
+          </Border.GestureRecognizers>
+          <Border Width="10"
+                  Height="10"
+                  Name="RightBall"
+                  HorizontalAlignment="Center"
+                  VerticalAlignment="Center"
+                  CornerRadius="5"
+                  Background="Green"/>
+
+        </Border>
+        <Border DockPanel.Dock="Left"
+                Margin="2"
+                Background="Transparent"
+                Name="LeftPullZone"
+                BorderBrush="Orange"
+                HorizontalAlignment="Left"
+                VerticalAlignment="Stretch"
+                Width="50"
+                BorderThickness="1">
+          <Border.GestureRecognizers>
+            <PullGestureRecognizer PullDirection="LeftToRight"/>
+          </Border.GestureRecognizers>
+          <Border Width="10"
+                  Height="10"
+                  Name="LeftBall"
+                  HorizontalAlignment="Center"
+                  VerticalAlignment="Center"
+                  CornerRadius="5"
+                  Background="Green"/>
+
+        </Border>
+      </DockPanel>
+    </Border>
+
+    <TextBlock FontWeight="Bold"
+               FontSize="18"
+               Margin="5">Pinch/Zoom Gexture (Multi Touch)</TextBlock>
+    <Border ClipToBounds="True">
+      <Image Stretch="UniformToFill"
+             Margin="5"
+             Name="PinchImage"
+             Source="/Assets/delicate-arch-896885_640.jpg">
+        <Image.GestureRecognizers>
+          <PinchGestureRecognizer/>
+          <ScrollGestureRecognizer CanHorizontallyScroll="True" CanVerticallyScroll="True"/>
+        </Image.GestureRecognizers>
+      </Image>
+    </Border>
+    <Button HorizontalAlignment="Center" Name="ResetButton">Reset</Button>
+  </StackPanel>
+</UserControl>

+ 4 - 1
samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs

@@ -1,5 +1,8 @@
-using Avalonia.Controls;
+using System;
+using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
+using Avalonia.Markup.Xaml.MarkupExtensions;
+using Avalonia.Media.Immutable;
 using ControlCatalog.ViewModels;
 
 namespace ControlCatalog.Pages

+ 1 - 1
samples/ControlCatalog/Pages/PointerCanvas.cs

@@ -24,7 +24,7 @@ public class PointerCanvas : Control
     {
         struct CanvasPoint
         {
-            public IBrush Brush;
+            public IBrush? Brush;
             public Point Point;
             public double Radius;
             public double? Pressure;

+ 29 - 29
samples/ControlCatalog/Pages/ScreenPage.cs

@@ -36,44 +36,44 @@ namespace ControlCatalog.Pages
 
             var drawBrush = Brushes.Black;
             Pen p = new Pen(drawBrush);
-            if (screens != null)
-                foreach (Screen screen in screens)
+
+            foreach (Screen screen in screens)
+            {
+                if (screen.Bounds.X / 10f < _leftMost)
                 {
-                    if (screen.Bounds.X / 10f < _leftMost)
-                    {
-                        _leftMost = screen.Bounds.X / 10f;
-                        Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
-                        return;
-                    }
+                    _leftMost = screen.Bounds.X / 10f;
+                    Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
+                    return;
+                }
 
-                    Rect boundsRect = new Rect(screen.Bounds.X / 10f + Math.Abs(_leftMost), screen.Bounds.Y / 10f, screen.Bounds.Width / 10f,
-                                      screen.Bounds.Height / 10f);
-                    Rect workingAreaRect = new Rect(screen.WorkingArea.X / 10f + Math.Abs(_leftMost), screen.WorkingArea.Y / 10f, screen.WorkingArea.Width / 10f,
-                                       screen.WorkingArea.Height / 10f);
-                    
-                    context.DrawRectangle(p, boundsRect);
-                    context.DrawRectangle(p, workingAreaRect);
+                Rect boundsRect = new Rect(screen.Bounds.X / 10f + Math.Abs(_leftMost), screen.Bounds.Y / 10f, screen.Bounds.Width / 10f,
+                                  screen.Bounds.Height / 10f);
+                Rect workingAreaRect = new Rect(screen.WorkingArea.X / 10f + Math.Abs(_leftMost), screen.WorkingArea.Y / 10f, screen.WorkingArea.Width / 10f,
+                                   screen.WorkingArea.Height / 10f);
 
+                context.DrawRectangle(p, boundsRect);
+                context.DrawRectangle(p, workingAreaRect);
 
-                    var formattedText = CreateFormattedText($"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}");
-                    context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height));
 
-                    formattedText =
-                        CreateFormattedText($"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}");
-                    context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 20));
+                var formattedText = CreateFormattedText($"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}");
+                context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height));
 
-                    formattedText = CreateFormattedText($"Scaling: {screen.Scaling * 100}%");
-                    context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 40));
+                formattedText =
+                    CreateFormattedText($"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}");
+                context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 20));
 
-                    formattedText = CreateFormattedText($"IsPrimary: {screen.IsPrimary}");
+                formattedText = CreateFormattedText($"Scaling: {screen.Scaling * 100}%");
+                context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 40));
 
-                    context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 60));
+                formattedText = CreateFormattedText($"IsPrimary: {screen.IsPrimary}");
 
-                    formattedText =
-                        CreateFormattedText(
-                            $"Current: {screen.Equals(w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))))}");
-                    context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 80));
-                }
+                context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 60));
+
+                formattedText =
+                    CreateFormattedText(
+                        $"Current: {screen.Equals(w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))))}");
+                context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 80));
+            }
 
             context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10f, w.Bounds.Width / 10, w.Bounds.Height / 10));
         }

+ 1 - 1
samples/ControlCatalog/Pages/TabControlPage.xaml.cs

@@ -51,7 +51,7 @@ namespace ControlCatalog.Pages
 
         private static IBitmap LoadBitmap(string uri)
         {
-            var assets = AvaloniaLocator.Current!.GetService<IAssetLoader>()!;
+            var assets = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
             return new Bitmap(assets.Open(new Uri(uri)));
         }
     }

+ 1 - 1
samples/ControlCatalog/Pages/TextBoxPage.xaml

@@ -38,7 +38,7 @@
                  UseFloatingWatermark="True"
                  PasswordChar="*"
                  Text="Password" />
-        <TextBox Width="200" Text="Left aligned text" TextAlignment="Left" />
+        <TextBox Width="200" Text="Left aligned text" TextAlignment="Left" AcceptsTab="True" />
         <TextBox Width="200" Text="Center aligned text" TextAlignment="Center" />
         <TextBox Width="200" Text="Right aligned text" TextAlignment="Right" />
         <TextBox Width="200" Text="Custom selection brush"

+ 0 - 7
samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml

@@ -11,12 +11,5 @@
     <CheckBox Content="Title Bar" IsChecked="{Binding SystemTitleBarEnabled}" />    
     <CheckBox Content="Prefer System Chrome" IsChecked="{Binding PreferSystemChromeEnabled}" />
     <Slider Minimum="-1" Maximum="200" Value="{Binding TitleBarHeight}" />
-    <ComboBox x:Name="TransparencyLevels" SelectedIndex="{Binding TransparencyLevel}">
-      <ComboBoxItem>None</ComboBoxItem>
-      <ComboBoxItem>Transparent</ComboBoxItem>
-      <ComboBoxItem>Blur</ComboBoxItem>
-      <ComboBoxItem>AcrylicBlur</ComboBoxItem>
-      <ComboBoxItem>Mica</ComboBoxItem>
-    </ComboBox>
   </StackPanel>
 </UserControl>

+ 2 - 5
samples/ControlCatalog/ViewModels/ContextPageViewModel.cs

@@ -56,12 +56,9 @@ namespace ControlCatalog.ViewModels
 
             var result = await window.StorageProvider.OpenFilePickerAsync(new Avalonia.Platform.Storage.FilePickerOpenOptions() { AllowMultiple = true });
 
-            if (result != null)
+            foreach (var file in result)
             {
-                foreach (var file in result)
-                {
-                    System.Diagnostics.Debug.WriteLine($"Opened: {file.Name}");
-                }
+                System.Diagnostics.Debug.WriteLine($"Opened: {file.Name}");
             }
         }
 

+ 1 - 1
samples/ControlCatalog/ViewModels/CursorPageViewModel.cs

@@ -18,7 +18,7 @@ namespace ControlCatalog.ViewModels
                 .Select(x => new StandardCursorModel(x))
                 .ToList();
 
-            var loader = AvaloniaLocator.Current!.GetService<IAssetLoader>()!;
+            var loader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
             var s = loader.Open(new Uri("avares://ControlCatalog/Assets/avalonia-32.png"));
             var bitmap = new Bitmap(s);
             CustomCursor = new Cursor(bitmap, new PixelPoint(16, 16));

+ 0 - 9
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@@ -12,12 +12,9 @@ namespace ControlCatalog.ViewModels
 {
     class MainWindowViewModel : ViewModelBase
     {
-        private IManagedNotificationManager _notificationManager;
-
         private bool _isMenuItemChecked = true;
         private WindowState _windowState;
         private WindowState[] _windowStates = Array.Empty<WindowState>();
-        private int _transparencyLevel;
         private ExtendClientAreaChromeHints _chromeHints = ExtendClientAreaChromeHints.PreferSystemChrome;
         private bool _extendClientAreaEnabled;
         private bool _systemTitleBarEnabled;
@@ -77,12 +74,6 @@ namespace ControlCatalog.ViewModels
             TitleBarHeight = -1;
         }        
 
-        public int TransparencyLevel
-        {
-            get { return _transparencyLevel; }
-            set { this.RaiseAndSetIfChanged(ref _transparencyLevel, value); }
-        }        
-
         public ExtendClientAreaChromeHints ChromeHints
         {
             get { return _chromeHints; }

+ 3 - 1
samples/ControlCatalog/ViewModels/PlatformInformationViewModel.cs

@@ -1,3 +1,5 @@
+using System;
+using System.Runtime.InteropServices;
 using Avalonia;
 using Avalonia.Platform;
 using MiniMvvm;
@@ -13,7 +15,7 @@ public class PlatformInformationViewModel : ViewModelBase
 
         if (runtimeInfo is { } info)
         {
-            if (info.IsBrowser)
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER")))
             {
                 if (info.IsDesktop)
                 {

+ 1 - 1
samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs

@@ -19,7 +19,7 @@ namespace ControlCatalog.ViewModels
     {
         public TransitioningContentControlPageViewModel()
         {
-            var assetLoader = AvaloniaLocator.Current?.GetService<IAssetLoader>()!;
+            var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
 
             var images = new string[] 
             { 

+ 2 - 3
samples/Directory.Build.props

@@ -2,9 +2,8 @@
   <PropertyGroup>
       <IsPackable>false</IsPackable>
       <AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)..\src\tools\Avalonia.Designer.HostApp\bin\Debug\netcoreapp2.0\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
+      <EnableNETAnalyzers>false</EnableNETAnalyzers>
+      <LangVersion>11</LangVersion>
   </PropertyGroup>
   <Import Project="..\build\SharedVersion.props" />
-  <PropertyGroup>
-    <EnableNETAnalyzers>false</EnableNETAnalyzers>
-  </PropertyGroup>  
 </Project>

+ 1 - 1
src/Android/Avalonia.Android/AndroidPlatform.cs

@@ -15,7 +15,7 @@ namespace Avalonia
 {
     public static class AndroidApplicationExtensions
     {
-        public static T UseAndroid<T>(this T builder) where T : AppBuilderBase<T>, new()
+        public static AppBuilder UseAndroid(this AppBuilder builder)
         {
             return builder
                 .UseWindowingSubsystem(() => AndroidPlatform.Initialize(), "Android")

+ 10 - 4
src/Android/Avalonia.Android/AvaloniaSplashActivity.cs

@@ -1,6 +1,5 @@
 using Android.OS;
 using AndroidX.AppCompat.App;
-using AndroidX.Lifecycle;
 
 namespace Avalonia.Android
 {
@@ -8,15 +7,22 @@ namespace Avalonia.Android
     {
         protected abstract AppBuilder CreateAppBuilder();
 
+        private static AppBuilder s_appBuilder;
+
         protected override void OnCreate(Bundle? savedInstanceState)
         {
             base.OnCreate(savedInstanceState);
 
-            var builder = CreateAppBuilder();
+            if (s_appBuilder == null)
+            {
+                var builder = CreateAppBuilder();
+
+                var lifetime = new SingleViewLifetime();
 
-            var lifetime = new SingleViewLifetime();
+                builder.SetupWithLifetime(lifetime);
 
-            builder.SetupWithLifetime(lifetime);
+                s_appBuilder = builder;
+            }
         }
     }
 

+ 2 - 0
src/Android/Avalonia.Android/AvaloniaView.cs

@@ -24,6 +24,8 @@ namespace Avalonia.Android
 
             _root = new EmbeddableControlRoot(_view);
             _root.Prepare();
+
+            this.SetBackgroundColor(global::Android.Graphics.Color.Transparent);
         }
 
         internal TopLevelImpl TopLevelImpl => _view;

+ 1 - 0
src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs

@@ -22,6 +22,7 @@ namespace Avalonia.Android
         public InvalidationAwareSurfaceView(Context context) : base(context)
         {
             Holder.AddCallback(this);
+            Holder.SetFormat(global::Android.Graphics.Format.Transparent);
             _handler = new Handler(context.MainLooper);
         }
 

+ 83 - 2
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@@ -26,6 +26,7 @@ using Avalonia.Rendering.Composition;
 using Java.Lang;
 using Math = System.Math;
 using AndroidRect = Android.Graphics.Rect;
+using Android.Graphics.Drawables;
 
 namespace Avalonia.Android.Platform.SkiaPlatform
 {
@@ -283,7 +284,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
         public Action LostFocus { get; set; }
         public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; }
 
-        public WindowTransparencyLevel TransparencyLevel => WindowTransparencyLevel.None;
+        public WindowTransparencyLevel TransparencyLevel { get; private set; }
 
         public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1);
 
@@ -301,7 +302,87 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
         public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
         {
-            throw new NotImplementedException();
+            if (TransparencyLevel != transparencyLevel)
+            {
+                bool isBelowR = Build.VERSION.SdkInt < BuildVersionCodes.R;
+                bool isAboveR = Build.VERSION.SdkInt > BuildVersionCodes.R;
+                if (_view.Context is AvaloniaMainActivity activity)
+                {
+                    switch (transparencyLevel)
+                    {
+                        case WindowTransparencyLevel.AcrylicBlur:
+                        case WindowTransparencyLevel.ForceAcrylicBlur:
+                        case WindowTransparencyLevel.Mica:
+                        case WindowTransparencyLevel.None:
+                            if (!isBelowR)
+                            {
+                                activity.SetTranslucent(false);
+                            }
+                            if (isAboveR)
+                            {
+                                activity.Window?.ClearFlags(WindowManagerFlags.BlurBehind);
+
+                                var attr = activity.Window?.Attributes;
+                                if (attr != null)
+                                {
+                                    attr.BlurBehindRadius = 0;
+
+                                    activity.Window.Attributes = attr;
+                                }
+                            }
+                            activity.Window.SetBackgroundDrawable(new ColorDrawable(Color.White));
+
+                            if(transparencyLevel != WindowTransparencyLevel.None)
+                            {
+                                return;
+                            }
+                            break;
+                        case WindowTransparencyLevel.Transparent:
+                            if (!isBelowR)
+                            {
+                                activity.SetTranslucent(true);
+                            }
+                            if (isAboveR)
+                            {
+                                activity.Window?.ClearFlags(WindowManagerFlags.BlurBehind);
+
+                                var attr = activity.Window?.Attributes;
+                                if (attr != null)
+                                {
+                                    attr.BlurBehindRadius = 0;
+
+                                    activity.Window.Attributes = attr;
+                                }
+                            }
+                            activity.Window.SetBackgroundDrawable(new ColorDrawable(Color.Transparent));
+                            break;
+                        case WindowTransparencyLevel.Blur:
+                            if (isAboveR)
+                            {
+                                activity.SetTranslucent(true);
+                                activity.Window?.AddFlags(WindowManagerFlags.BlurBehind);
+
+                                var attr = activity.Window?.Attributes;
+                                if (attr != null)
+                                {
+                                    attr.BlurBehindRadius = 120;
+
+                                    activity.Window.Attributes = attr;
+                                }
+                                activity.Window.SetBackgroundDrawable(new ColorDrawable(Color.Transparent));
+                            }
+                            else
+                            {
+                                activity.Window?.ClearFlags(WindowManagerFlags.BlurBehind);
+                                activity.Window.SetBackgroundDrawable(new ColorDrawable(Color.White));
+
+                                return;
+                            }
+                            break;
+                    }
+                    TransparencyLevel = transparencyLevel;
+                }
+            }
         }
     }
 

+ 1 - 1
src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidMotionEventsHelper.cs

@@ -38,7 +38,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers
                 return null;
             }
 
-            var eventTime = (ulong)DateTime.Now.Millisecond;
+            var eventTime = (ulong)e.EventTime;
             var inputRoot = _view.InputRoot;
             var actionMasked = e.ActionMasked;
             var modifiers = GetModifiers(e.MetaState, e.ButtonState);

+ 0 - 1
src/Avalonia.Base/Animation/AnimationInstance`1.cs

@@ -5,7 +5,6 @@ using Avalonia.Animation.Animators;
 using Avalonia.Animation.Utils;
 using Avalonia.Data;
 using Avalonia.Reactive;
-using JetBrains.Annotations;
 
 namespace Avalonia.Animation
 {

+ 3 - 1
src/Avalonia.Base/Avalonia.Base.csproj

@@ -15,7 +15,6 @@
   <Import Project="..\..\build\Base.props" />
   <Import Project="..\..\build\Binding.props" />
   <Import Project="..\..\build\Rx.props" />
-  <Import Project="..\..\build\JetBrains.Annotations.props" />
   <Import Project="..\..\build\System.Memory.props" />
   <Import Project="..\..\build\ApiDiff.props" />
   <Import Project="..\..\build\NullableEnable.props" />
@@ -29,10 +28,13 @@
 
   <ItemGroup Label="InternalsVisibleTo">
     <InternalsVisibleTo Include="Avalonia.Base.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.Desktop, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Controls, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Markup, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Markup.Xaml, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.OpenGL, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.Skia, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Controls.ColorPicker, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Controls.DataGrid, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Controls.UnitTests, PublicKey=$(AvaloniaPublicKey)" />

+ 1 - 1
src/Avalonia.Base/AvaloniaObject.cs

@@ -621,7 +621,7 @@ namespace Avalonia
         /// <param name="oldValue">The old property value.</param>
         /// <param name="newValue">The new property value.</param>
         /// <param name="priority">The priority of the binding that produced the value.</param>
-        private protected void RaisePropertyChanged<T>(
+        protected void RaisePropertyChanged<T>(
             DirectPropertyBase<T> property,
             Optional<T> oldValue,
             BindingValue<T> newValue,

+ 26 - 0
src/Avalonia.Base/Compatibility/OperatingSystem.cs

@@ -0,0 +1,26 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.Compatibility
+{
+    internal sealed class OperatingSystemEx
+    {
+#if NET6_0_OR_GREATER
+        public static bool IsWindows() => OperatingSystem.IsWindows();
+        public static bool IsMacOS() => OperatingSystem.IsMacOS();
+        public static bool IsLinux() => OperatingSystem.IsLinux();
+        public static bool IsAndroid() => OperatingSystem.IsAndroid();
+        public static bool IsIOS() => OperatingSystem.IsIOS();
+        public static bool IsBrowser() => OperatingSystem.IsBrowser();
+        public static bool IsOSPlatform(string platform) => OperatingSystem.IsOSPlatform(platform);
+#else
+        public static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+        public static bool IsMacOS() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
+        public static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+        public static bool IsAndroid() => IsOSPlatform("ANDROID");
+        public static bool IsIOS() => IsOSPlatform("IOS");
+        public static bool IsBrowser() => IsOSPlatform("BROWSER");
+        public static bool IsOSPlatform(string platform) => RuntimeInformation.IsOSPlatform(OSPlatform.Create(platform));
+#endif
+    }
+}

+ 0 - 34
src/Avalonia.Base/Contract.cs

@@ -1,34 +0,0 @@
-using System;
-using System.Runtime.CompilerServices;
-using JetBrains.Annotations;
-
-namespace Avalonia
-{
-    /// <summary>
-    /// A stub of Code Contract's Contract class.
-    /// </summary>
-    /// <remarks>
-    /// It would be nice to use Code Contracts on Avalonia but last time I tried it slowed things
-    /// to a crawl and often crashed. Instead use the same signature for checking preconditions
-    /// in the hope that it might become usable at some point.
-    /// </remarks>
-    public static class Contract
-    {
-        /// <summary>
-        /// Specifies a precondition.
-        /// </summary>
-        /// <typeparam name="TException">
-        /// The exception to throw if <paramref name="condition"/> is false.
-        /// </typeparam>
-        /// <param name="condition">The precondition.</param>
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        [ContractAnnotation("condition:false=>stop")]
-        public static void Requires<TException>(bool condition) where TException : Exception, new()
-        {
-            if (!condition)
-            {
-                throw new TException();
-            }
-        }
-    }
-}

+ 13 - 8
src/Avalonia.Base/Controls/NameScopeExtensions.cs

@@ -25,13 +25,18 @@ namespace Avalonia.Controls
 
             var result = nameScope.Find(name);
 
-            if (result != null && !(result is T))
+            if (result == null)
             {
-                throw new InvalidOperationException(
-                    $"Expected control '{name}' to be '{typeof(T)} but it was '{result.GetType()}'.");
+                return null;
             }
 
-            return (T?)result;
+            if (result is T typed)
+            {
+                return typed;
+            }
+
+            throw new InvalidOperationException(
+                $"Expected control '{name}' to be '{typeof(T)} but it was '{result.GetType()}'.");
         }
 
         /// <summary>
@@ -74,13 +79,13 @@ namespace Avalonia.Controls
                 throw new KeyNotFoundException($"Could not find control '{name}'.");
             }
 
-            if (!(result is T))
+            if (result is T typed)
             {
-                throw new InvalidOperationException(
-                    $"Expected control '{name}' to be '{typeof(T)} but it was '{result.GetType()}'.");
+                return typed;
             }
 
-            return (T)result;
+            throw new InvalidOperationException(
+                $"Expected control '{name}' to be '{typeof(T)} but it was '{result.GetType()}'.");
         }
 
         /// <summary>

+ 1 - 1
src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs

@@ -38,7 +38,7 @@ namespace Avalonia.Data.Converters
                     }
                     else if (Equals(obj, default(TIn)))
                     {
-                        yield return default(TIn);
+                        yield return default;
                     }
                 }
             }

+ 1 - 3
src/Avalonia.Base/Data/InstancedBinding.cs

@@ -28,11 +28,9 @@ namespace Avalonia.Data
         /// </remarks>
         public InstancedBinding(ISubject<object?> subject, BindingMode mode, BindingPriority priority)
         {
-            Contract.Requires<ArgumentNullException>(subject != null);
-
             Mode = mode;
             Priority = priority;
-            Value = subject;
+            Value = subject ?? throw new ArgumentNullException(nameof(subject));
         }
 
         private InstancedBinding(object? value, BindingMode mode, BindingPriority priority)

+ 1 - 2
src/Avalonia.Base/Input/Cursor.cs

@@ -71,8 +71,7 @@ namespace Avalonia.Input
 
         private static ICursorFactory GetCursorFactory()
         {
-            return AvaloniaLocator.Current.GetService<ICursorFactory>() ??
-                throw new Exception("Could not create Cursor: ICursorFactory not registered.");
+            return AvaloniaLocator.Current.GetRequiredService<ICursorFactory>();
         }
     }
 }

+ 128 - 0
src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs

@@ -0,0 +1,128 @@
+using Avalonia.Input.GestureRecognizers;
+
+namespace Avalonia.Input
+{
+    public class PinchGestureRecognizer : StyledElement, IGestureRecognizer
+    {
+        private IInputElement? _target;
+        private IGestureRecognizerActionsDispatcher? _actions;
+        private float _initialDistance;
+        private IPointer? _firstContact;
+        private Point _firstPoint;
+        private IPointer? _secondContact;
+        private Point _secondPoint;
+        private Point _origin;
+
+        public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions)
+        {
+            _target = target;
+            _actions = actions;
+        }
+
+        private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
+        {
+            PointerPressed(e);
+        }
+
+        private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
+        {
+            PointerReleased(e);
+        }
+
+        public void PointerCaptureLost(IPointer pointer)
+        {
+            RemoveContact(pointer);
+        }
+
+        public void PointerMoved(PointerEventArgs e)
+        {
+            if (_target != null && _target is Visual visual)
+            {
+                if(_firstContact == e.Pointer)
+                {
+                    _firstPoint = e.GetPosition(visual);
+                }
+                else if (_secondContact == e.Pointer)
+                {
+                    _secondPoint = e.GetPosition(visual);
+                }
+                else
+                {
+                    return;
+                }
+
+                if (_firstContact != null && _secondContact != null)
+                {
+                    var distance = GetDistance(_firstPoint, _secondPoint);
+
+                    var scale = distance / _initialDistance;
+
+                    _target?.RaiseEvent(new PinchEventArgs(scale, _origin));
+                }
+            }
+        }
+
+        public void PointerPressed(PointerPressedEventArgs e)
+        {
+            if (_target != null && _target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen))
+            {
+                if (_firstContact == null)
+                {
+                    _firstContact = e.Pointer;
+                    _firstPoint = e.GetPosition(visual);
+
+                    return;
+                }
+                else if (_secondContact == null && _firstContact != e.Pointer)
+                {
+                    _secondContact = e.Pointer;
+                    _secondPoint = e.GetPosition(visual);
+                }
+                else
+                {
+                    return;
+                }
+
+                if (_firstContact != null && _secondContact != null)
+                {
+                    _initialDistance = GetDistance(_firstPoint, _secondPoint);
+
+                    _origin = new Point((_firstPoint.X + _secondPoint.X) / 2.0f, (_firstPoint.Y + _secondPoint.Y) / 2.0f);
+
+                    _actions!.Capture(_firstContact, this);
+                    _actions!.Capture(_secondContact, this);
+                }
+            }
+        }
+
+        public void PointerReleased(PointerReleasedEventArgs e)
+        {
+            RemoveContact(e.Pointer);
+        }
+
+        private void RemoveContact(IPointer pointer)
+        {
+            if (_firstContact == pointer || _secondContact == pointer)
+            {
+                if (_secondContact == pointer)
+                {
+                    _secondContact = null;
+                }
+
+                if (_firstContact == pointer)
+                {
+                    _firstContact = _secondContact;
+
+                    _secondContact = null;
+                }
+                _target?.RaiseEvent(new PinchEndedEventArgs());
+            }
+        }
+
+        private float GetDistance(Point a, Point b)
+        {
+            var length = _secondPoint - _firstPoint;
+            return (float)new Vector(length.X, length.Y).Length;
+        }
+    }
+}

+ 14 - 19
src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs

@@ -1,15 +1,19 @@
-using Avalonia.Input.GestureRecognizers;
+using System;
+using Avalonia.Input.GestureRecognizers;
 
 namespace Avalonia.Input
 {
     public class PullGestureRecognizer : StyledElement, IGestureRecognizer
     {
+        internal static int MinPullDetectionSize = 50;
+
         private IInputElement? _target;
         private IGestureRecognizerActionsDispatcher? _actions;
         private Point _initialPosition;
         private int _gestureId;
         private IPointer? _tracking;
         private PullDirection _pullDirection;
+        private bool _pullInProgress;
 
         /// <summary>
         /// Defines the <see cref="PullDirection"/> property.
@@ -31,23 +35,12 @@ namespace Avalonia.Input
             PullDirection = pullDirection;
         }
 
+        public PullGestureRecognizer() { }
+
         public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions)
         {
             _target = target;
             _actions = actions;
-
-            _target?.AddHandler(InputElement.PointerPressedEvent, OnPointerPressed, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble);
-            _target?.AddHandler(InputElement.PointerReleasedEvent, OnPointerReleased, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble);
-        }
-
-        private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
-        {
-            PointerPressed(e);
-        }
-
-        private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
-        {
-            PointerReleased(e);
         }
 
         public void PointerCaptureLost(IPointer pointer)
@@ -94,6 +87,7 @@ namespace Avalonia.Input
                         break;
                 }
 
+                _pullInProgress = true;
                 _target?.RaiseEvent(new PullGestureEventArgs(_gestureId, delta, PullDirection));
             }
         }
@@ -111,16 +105,16 @@ namespace Avalonia.Input
                 switch (PullDirection)
                 {
                     case PullDirection.TopToBottom:
-                        canPull = position.Y < bounds.Height * 0.1;
+                        canPull = position.Y < Math.Max(MinPullDetectionSize, bounds.Height * 0.1);
                         break;
                     case PullDirection.BottomToTop:
-                        canPull = position.Y > bounds.Height - (bounds.Height * 0.1);
+                        canPull = position.Y > Math.Min(bounds.Height - MinPullDetectionSize, bounds.Height - (bounds.Height * 0.1));
                         break;
                     case PullDirection.LeftToRight:
-                        canPull = position.X < bounds.Width * 0.1;
+                        canPull = position.X < Math.Max(MinPullDetectionSize, bounds.Width * 0.1);
                         break;
                     case PullDirection.RightToLeft:
-                        canPull = position.X > bounds.Width - (bounds.Width * 0.1);
+                        canPull = position.X > Math.Min(bounds.Width - MinPullDetectionSize, bounds.Width - (bounds.Width * 0.1));
                         break;
                 }
 
@@ -135,7 +129,7 @@ namespace Avalonia.Input
 
         public void PointerReleased(PointerReleasedEventArgs e)
         {
-            if (_tracking == e.Pointer)
+            if (_tracking == e.Pointer && _pullInProgress)
             {
                 EndPull();
             }
@@ -145,6 +139,7 @@ namespace Avalonia.Input
         {
             _tracking = null;
             _initialPosition = default;
+            _pullInProgress = false;
 
             _target?.RaiseEvent(new PullGestureEndedEventArgs(_gestureId, PullDirection));
         }

+ 49 - 16
src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs

@@ -16,7 +16,10 @@ namespace Avalonia.Input.GestureRecognizers
         private bool _canHorizontallyScroll;
         private bool _canVerticallyScroll;
         private int _gestureId;
-        
+        private int _scrollStartDistance = 30;
+        private Point _pointerPressedPoint;
+        private VelocityTracker? _velocityTracker;
+
         // Movement per second
         private Vector _inertia;
         private ulong? _lastMoveTimestamp;
@@ -38,6 +41,15 @@ namespace Avalonia.Input.GestureRecognizers
                 nameof(CanVerticallyScroll),
                 o => o.CanVerticallyScroll,
                 (o, v) => o.CanVerticallyScroll = v);
+
+        /// <summary>
+        /// Defines the <see cref="ScrollStartDistance"/> property.
+        /// </summary>
+        public static readonly DirectProperty<ScrollGestureRecognizer, int> ScrollStartDistanceProperty =
+            AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, int>(
+                nameof(ScrollStartDistance),
+                o => o.ScrollStartDistance,
+                (o, v) => o.ScrollStartDistance = v);
         
         /// <summary>
         /// Gets or sets a value indicating whether the content can be scrolled horizontally.
@@ -56,6 +68,15 @@ namespace Avalonia.Input.GestureRecognizers
             get => _canVerticallyScroll;
             set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value);
         }
+
+        /// <summary>
+        /// Gets or sets a value indicating the distance the pointer moves before scrolling is started
+        /// </summary>
+        public int ScrollStartDistance
+        {
+            get => _scrollStartDistance;
+            set => SetAndRaise(ScrollStartDistanceProperty, ref _scrollStartDistance, value);
+        }
         
 
         public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions)
@@ -72,12 +93,9 @@ namespace Avalonia.Input.GestureRecognizers
                 EndGesture();
                 _tracking = e.Pointer;
                 _gestureId = ScrollGestureEventArgs.GetNextFreeId();
-                _trackedRootPoint = e.GetPosition((Visual?)_target);
+                _trackedRootPoint = _pointerPressedPoint = e.GetPosition((Visual?)_target);
             }
         }
-
-        // Arbitrary chosen value, probably need to move that to platform settings or something
-        private const double ScrollStartDistance = 30;
         
         // Pixels per second speed that is considered to be the stop of inertial scroll
         private const double InertialScrollSpeedEnd = 5;
@@ -95,6 +113,13 @@ namespace Avalonia.Input.GestureRecognizers
                         _scrolling = true;
                     if (_scrolling)
                     {
+                        _velocityTracker = new VelocityTracker();
+                        
+                        // Correct _trackedRootPoint with ScrollStartDistance, so scrolling does not start with a skip of ScrollStartDistance
+                        _trackedRootPoint = new Point(
+                            _trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? _scrollStartDistance : -_scrollStartDistance),
+                            _trackedRootPoint.Y - (_trackedRootPoint.Y >= rootPoint.Y ? _scrollStartDistance : -_scrollStartDistance));
+
                         _actions!.Capture(e.Pointer, this);
                     }
                 }
@@ -102,14 +127,11 @@ namespace Avalonia.Input.GestureRecognizers
                 if (_scrolling)
                 {
                     var vector = _trackedRootPoint - rootPoint;
-                    var elapsed = _lastMoveTimestamp.HasValue && _lastMoveTimestamp < e.Timestamp ?
-                        TimeSpan.FromMilliseconds(e.Timestamp - _lastMoveTimestamp.Value) :
-                        TimeSpan.Zero;
-                    
+
+                    _velocityTracker?.AddPosition(TimeSpan.FromMilliseconds(e.Timestamp), _pointerPressedPoint - rootPoint);
+
                     _lastMoveTimestamp = e.Timestamp;
                     _trackedRootPoint = rootPoint;
-                    if (elapsed.TotalSeconds > 0)
-                        _inertia = vector / elapsed.TotalSeconds;
                     _target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector));
                     e.Handled = true;
                 }
@@ -134,12 +156,14 @@ namespace Avalonia.Input.GestureRecognizers
             }
             
         }
-        
-        
+
+
         public void PointerReleased(PointerReleasedEventArgs e)
         {
             if (e.Pointer == _tracking && _scrolling)
             {
+                _inertia = _velocityTracker?.GetFlingVelocity().PixelsPerSecond ?? Vector.Zero;
+
                 e.Handled = true;
                 if (_inertia == default
                     || e.Timestamp == 0
@@ -167,9 +191,18 @@ namespace Avalonia.Input.GestureRecognizers
                         var distance = speed * elapsedSinceLastTick.TotalSeconds;
                         _target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance));
 
-
-
-                        if (Math.Abs(speed.X) < InertialScrollSpeedEnd || Math.Abs(speed.Y) <= InertialScrollSpeedEnd)
+                        // EndGesture using InertialScrollSpeedEnd only in the direction of scrolling
+                        if (CanVerticallyScroll && CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd && Math.Abs(speed.Y) <= InertialScrollSpeedEnd)
+                        {
+                            EndGesture();
+                            return false;
+                        }
+                        else if (CanVerticallyScroll && Math.Abs(speed.Y) <= InertialScrollSpeedEnd)
+                        {
+                            EndGesture();
+                            return false;
+                        }
+                        else if (CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd)
                         {
                             EndGesture();
                             return false;

+ 375 - 0
src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs

@@ -0,0 +1,375 @@
+// Code in this file is derived from
+// https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/gestures/velocity_tracker.dart
+
+using System;
+using System.Diagnostics;
+using Avalonia.Utilities;
+
+namespace Avalonia.Input.GestureRecognizers
+{
+    // Possible enhancement: add Flutter's 'IOSScrollViewFlingVelocityTracker' and 'MacOSScrollViewFlingVelocityTracker'?
+
+    internal readonly record struct Velocity(Vector PixelsPerSecond)
+    {
+        public Velocity ClampMagnitude(double minValue, double maxValue)
+        {
+            Debug.Assert(minValue >= 0.0);
+            Debug.Assert(maxValue >= 0.0 && maxValue >= minValue);
+            double valueSquared = PixelsPerSecond.SquaredLength;
+            if (valueSquared > maxValue * maxValue)
+            {
+                double length = PixelsPerSecond.Length;
+                return new Velocity(length != 0.0 ? (PixelsPerSecond / length) * maxValue : Vector.Zero);
+                // preventing double.NaN in Vector PixelsPerSecond is important -- if a NaN eventually gets into a
+                // ScrollGestureEventArgs it results in runtime errors.
+            }
+            if (valueSquared < minValue * minValue)
+            {
+                double length = PixelsPerSecond.Length;
+                return new Velocity(length != 0.0 ? (PixelsPerSecond / length) * minValue : Vector.Zero);
+            }
+            return this;
+        }
+    }
+
+    /// A two dimensional velocity estimate.
+    ///
+    /// VelocityEstimates are computed by [VelocityTracker.getVelocityEstimate]. An
+    /// estimate's [confidence] measures how well the velocity tracker's position
+    /// data fit a straight line, [duration] is the time that elapsed between the
+    /// first and last position sample used to compute the velocity, and [offset]
+    /// is similarly the difference between the first and last positions.
+    ///
+    /// See also:
+    ///
+    ///  * [VelocityTracker], which computes [VelocityEstimate]s.
+    ///  * [Velocity], which encapsulates (just) a velocity vector and provides some
+    ///    useful velocity operations.
+    internal record VelocityEstimate(Vector PixelsPerSecond, double Confidence, TimeSpan Duration, Vector Offset);
+
+    internal record struct PointAtTime(bool Valid, Vector Point, TimeSpan Time);
+
+    /// Computes a pointer's velocity based on data from [PointerMoveEvent]s.
+    ///
+    /// The input data is provided by calling [addPosition]. Adding data is cheap.
+    ///
+    /// To obtain a velocity, call [getVelocity] or [getVelocityEstimate]. This will
+    /// compute the velocity based on the data added so far. Only call these when
+    /// you need to use the velocity, as they are comparatively expensive.
+    ///
+    /// The quality of the velocity estimation will be better if more data points
+    /// have been received.
+    internal class VelocityTracker
+    {
+        private const int AssumePointerMoveStoppedMilliseconds = 40;
+        private const int HistorySize = 20;
+        private const int HorizonMilliseconds = 100;
+        private const int MinSampleSize = 3;
+        private const double MinFlingVelocity = 50.0; // Logical pixels / second
+        private const double MaxFlingVelocity = 8000.0;
+
+        private readonly PointAtTime[] _samples = new PointAtTime[HistorySize];
+        private int _index = 0;
+
+        /// <summary>
+        /// Adds a position as the given time to the tracker.
+        /// </summary>
+        /// <param name="time"></param>
+        /// <param name="position"></param>
+        public void AddPosition(TimeSpan time, Vector position)
+        {
+            _index++;
+            if (_index == HistorySize)
+            {
+                _index = 0;
+            }
+            _samples[_index] = new PointAtTime(true, position, time);
+        }
+
+        /// Returns an estimate of the velocity of the object being tracked by the
+        /// tracker given the current information available to the tracker.
+        ///
+        /// Information is added using [addPosition].
+        ///
+        /// Returns null if there is no data on which to base an estimate.
+        protected virtual VelocityEstimate? GetVelocityEstimate()
+        {
+            Span<double> x = stackalloc double[HistorySize];
+            Span<double> y = stackalloc double[HistorySize];
+            Span<double> w = stackalloc double[HistorySize];
+            Span<double> time = stackalloc double[HistorySize];
+            int sampleCount = 0;
+            int index = _index;
+
+            var newestSample = _samples[index];
+            if (!newestSample.Valid)
+            {
+                return null;
+            }
+
+            var previousSample = newestSample;
+            var oldestSample = newestSample;
+
+            // Starting with the most recent PointAtTime sample, iterate backwards while
+            // the samples represent continuous motion.
+            do
+            {
+                var sample = _samples[index];
+                if (!sample.Valid)
+                {
+                    break;
+                }
+
+                double age = (newestSample.Time - sample.Time).TotalMilliseconds;
+                double delta = Math.Abs((sample.Time - previousSample.Time).TotalMilliseconds);
+                previousSample = sample;
+                if (age > HorizonMilliseconds || delta > AssumePointerMoveStoppedMilliseconds)
+                {
+                    break;
+                }
+
+                oldestSample = sample;
+                var position = sample.Point;
+                x[sampleCount] = position.X;
+                y[sampleCount] = position.Y;
+                w[sampleCount] = 1.0;
+                time[sampleCount] = -age;
+                index = (index == 0 ? HistorySize : index) - 1;
+
+                sampleCount++;
+            } while (sampleCount < HistorySize);
+
+            if (sampleCount >= MinSampleSize)
+            {
+                var xFit = LeastSquaresSolver.Solve(2, time.Slice(0, sampleCount), x.Slice(0, sampleCount), w.Slice(0, sampleCount));
+                if (xFit != null)
+                {
+                    var yFit = LeastSquaresSolver.Solve(2, time.Slice(0, sampleCount), y.Slice(0, sampleCount), w.Slice(0, sampleCount));
+                    if (yFit != null)
+                    {
+                        return new VelocityEstimate( // convert from pixels/ms to pixels/s
+                          PixelsPerSecond: new Vector(xFit.Coefficients[1] * 1000, yFit.Coefficients[1] * 1000),
+                          Confidence: xFit.Confidence * yFit.Confidence,
+                          Duration: newestSample.Time - oldestSample.Time,
+                          Offset: newestSample.Point - oldestSample.Point
+                        );
+                    }
+                }
+            }
+
+            // We're unable to make a velocity estimate but we did have at least one
+            // valid pointer position.
+            return new VelocityEstimate(
+              PixelsPerSecond: Vector.Zero,
+              Confidence: 1.0,
+              Duration: newestSample.Time - oldestSample.Time,
+              Offset: newestSample.Point - oldestSample.Point
+            );
+        }
+
+        /// <summary>
+        /// Computes the velocity of the pointer at the time of the last
+        /// provided data point.
+        ///
+        /// This can be expensive. Only call this when you need the velocity.
+        ///
+        /// Returns [Velocity.zero] if there is no data from which to compute an
+        /// estimate or if the estimated velocity is zero./// 
+        /// </summary>
+        /// <returns></returns>
+        internal Velocity GetVelocity()
+        {
+            var estimate = GetVelocityEstimate();
+            if (estimate == null || estimate.PixelsPerSecond.IsDefault)
+            {
+                return new Velocity(Vector.Zero);
+            }
+            return new Velocity(estimate.PixelsPerSecond);
+        }
+
+        internal virtual Velocity GetFlingVelocity()
+        {
+            return GetVelocity().ClampMagnitude(MinFlingVelocity, MaxFlingVelocity);
+        }
+    }
+
+    /// An nth degree polynomial fit to a dataset.
+    internal class PolynomialFit
+    {
+        /// Creates a polynomial fit of the given degree.
+        ///
+        /// There are n + 1 coefficients in a fit of degree n.
+        internal PolynomialFit(int degree)
+        {
+            Coefficients = new double[degree + 1];
+        }
+
+        /// The polynomial coefficients of the fit.
+        public double[] Coefficients { get; }
+
+        /// An indicator of the quality of the fit.
+        ///
+        /// Larger values indicate greater quality.
+        public double Confidence { get; set; }
+    }
+
+    internal class LeastSquaresSolver
+    {
+        private const double PrecisionErrorTolerance = 1e-10;
+
+        /// <summary>
+        /// Fits a polynomial of the given degree to the data points.
+        /// When there is not enough data to fit a curve null is returned.
+        /// </summary>
+        public static PolynomialFit? Solve(int degree, ReadOnlySpan<double> x, ReadOnlySpan<double> y, ReadOnlySpan<double> w)
+        {
+            if (degree > x.Length)
+            {
+                // Not enough data to fit a curve.
+                return null;
+            }
+
+            PolynomialFit result = new PolynomialFit(degree);
+
+            // Shorthands for the purpose of notation equivalence to original C++ code.
+            int m = x.Length;
+            int n = degree + 1;
+
+            // Expand the X vector to a matrix A, pre-multiplied by the weights.
+            _Matrix a = new _Matrix(m, stackalloc double[n * m]);
+            for (int h = 0; h < m; h += 1)
+            {
+                a[0, h] = w[h];
+                for (int i = 1; i < n; i += 1)
+                {
+                    a[i, h] = a[i - 1, h] * x[h];
+                }
+            }
+
+            // Apply the Gram-Schmidt process to A to obtain its QR decomposition.
+
+            // Orthonormal basis, column-major order Vector.
+            _Matrix q = new _Matrix(m, stackalloc double[n * m]);
+            // Upper triangular matrix, row-major order.
+            _Matrix r = new _Matrix(n, stackalloc double[n * n]);
+            for (int j = 0; j < n; j += 1)
+            {
+                for (int h = 0; h < m; h += 1)
+                {
+                    q[j, h] = a[j, h];
+                }
+                for (int i = 0; i < j; i += 1)
+                {
+                    double dot = Multiply(q.GetRow(j), q.GetRow(i));
+                    for (int h = 0; h < m; h += 1)
+                    {
+                        q[j, h] = q[j, h] - dot * q[i, h];
+                    }
+                }
+
+                double norm = Norm(q.GetRow(j));
+                if (norm < PrecisionErrorTolerance)
+                {
+                    // Vectors are linearly dependent or zero so no solution.
+                    return null;
+                }
+
+                double inverseNorm = 1.0 / norm;
+                for (int h = 0; h < m; h += 1)
+                {
+                    q[j, h] = q[j, h] * inverseNorm;
+                }
+                for (int i = 0; i < n; i += 1)
+                {
+                    r[j, i] = i < j ? 0.0 : Multiply(q.GetRow(j), a.GetRow(i));
+                }
+            }
+
+            // Solve R B = Qt W Y to find B. This is easy because R is upper triangular.
+            // We just work from bottom-right to top-left calculating B's coefficients.
+            // "m" isn't expected to be bigger than HistorySize=20, so allocation on stack is safe.
+            Span<double> wy = stackalloc double[m];  
+            for (int h = 0; h < m; h += 1)
+            {
+                wy[h] = y[h] * w[h];
+            }
+            for (int i = n - 1; i >= 0; i -= 1)
+            {
+                result.Coefficients[i] = Multiply(q.GetRow(i), wy);
+                for (int j = n - 1; j > i; j -= 1)
+                {
+                    result.Coefficients[i] -= r[i, j] * result.Coefficients[j];
+                }
+                result.Coefficients[i] /= r[i, i];
+            }
+
+            // Calculate the coefficient of determination (confidence) as:
+            //   1 - (sumSquaredError / sumSquaredTotal)
+            // ...where sumSquaredError is the residual sum of squares (variance of the
+            // error), and sumSquaredTotal is the total sum of squares (variance of the
+            // data) where each has been weighted.
+            double yMean = 0.0;
+            for (int h = 0; h < m; h += 1)
+            {
+                yMean += y[h];
+            }
+            yMean /= m;
+
+            double sumSquaredError = 0.0;
+            double sumSquaredTotal = 0.0;
+            for (int h = 0; h < m; h += 1)
+            {
+                double term = 1.0;
+                double err = y[h] - result.Coefficients[0];
+                for (int i = 1; i < n; i += 1)
+                {
+                    term *= x[h];
+                    err -= term * result.Coefficients[i];
+                }
+                sumSquaredError += w[h] * w[h] * err * err;
+                double v = y[h] - yMean;
+                sumSquaredTotal += w[h] * w[h] * v * v;
+            }
+
+            result.Confidence = sumSquaredTotal <= PrecisionErrorTolerance ? 1.0 :
+                                  1.0 - (sumSquaredError / sumSquaredTotal);
+
+            return result;
+        }
+
+        private static double Multiply(Span<double> v1, Span<double> v2)
+        {
+            double result = 0.0;
+            for (int i = 0; i < v1.Length; i += 1)
+            {
+                result += v1[i] * v2[i];
+            }
+            return result;
+        }
+        
+        private static double Norm(Span<double> v)
+        {
+            return Math.Sqrt(Multiply(v, v));
+        }
+
+        private readonly ref struct _Matrix
+        {
+            private readonly int _columns;
+            private readonly Span<double> _elements;
+
+            internal _Matrix(int cols, Span<double> elements)
+            {
+                _columns = cols;
+                _elements = elements;
+            }
+
+            public double this[int row, int col]
+            {
+                get => _elements[row * _columns + col];
+                set => _elements[row * _columns + col] = value;
+            }
+
+            public Span<double> GetRow(int row) => _elements.Slice(row * _columns, _columns);
+        }
+    }
+}

+ 128 - 1
src/Avalonia.Base/Input/Gestures.cs

@@ -1,6 +1,8 @@
 using System;
+using System.Threading;
 using Avalonia.Interactivity;
 using Avalonia.Platform;
+using Avalonia.Threading;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Input
@@ -8,6 +10,21 @@ namespace Avalonia.Input
     public static class Gestures
     {
         private static bool s_isDoubleTapped = false;
+        private static bool s_isHolding;
+        private static CancellationTokenSource? s_holdCancellationToken;
+
+        /// <summary>
+        /// Defines the IsHoldingEnabled attached property.
+        /// </summary>
+        public static readonly AttachedProperty<bool> IsHoldingEnabledProperty =
+            AvaloniaProperty.RegisterAttached<StyledElement, bool>("IsHoldingEnabled", typeof(Gestures), true);
+
+        /// <summary>
+        /// Defines the IsHoldWithMouseEnabled attached property.
+        /// </summary>
+        public static readonly AttachedProperty<bool> IsHoldWithMouseEnabledProperty =
+            AvaloniaProperty.RegisterAttached<StyledElement, bool>("IsHoldWithMouseEnabled", typeof(Gestures), false);
+
         public static readonly RoutedEvent<TappedEventArgs> TappedEvent = RoutedEvent.Register<TappedEventArgs>(
             "Tapped",
             RoutingStrategies.Bubble,
@@ -45,19 +62,54 @@ namespace Avalonia.Input
 
         private static readonly WeakReference<object?> s_lastPress = new WeakReference<object?>(null);
         private static Point s_lastPressPoint;
+        private static IPointer? s_lastPointer;
+
+        public static readonly RoutedEvent<PinchEventArgs> PinchEvent =
+            RoutedEvent.Register<PinchEventArgs>(
+                "PinchEvent", RoutingStrategies.Bubble, typeof(Gestures));
+
+        public static readonly RoutedEvent<PinchEndedEventArgs> PinchEndedEvent =
+            RoutedEvent.Register<PinchEndedEventArgs>(
+                "PinchEndedEvent", RoutingStrategies.Bubble, typeof(Gestures));
 
         public static readonly RoutedEvent<PullGestureEventArgs> PullGestureEvent =
             RoutedEvent.Register<PullGestureEventArgs>(
                 "PullGesture", RoutingStrategies.Bubble, typeof(Gestures));
 
+        /// <summary>
+        /// Occurs when a user performs a press and hold gesture (with a single touch, mouse, or pen/stylus contact).
+        /// </summary>
+        public static readonly RoutedEvent<HoldingRoutedEventArgs> HoldingEvent =
+            RoutedEvent.Register<HoldingRoutedEventArgs>(
+                "Holding", RoutingStrategies.Bubble, typeof(Gestures));
+
         public static readonly RoutedEvent<PullGestureEndedEventArgs> PullGestureEndedEvent =
             RoutedEvent.Register<PullGestureEndedEventArgs>(
                 "PullGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
 
+        public static bool GetIsHoldingEnabled(StyledElement element)
+        {
+            return element.GetValue(IsHoldingEnabledProperty);
+        }
+        public static void SetIsHoldingEnabled(StyledElement element, bool value)
+        {
+            element.SetValue(IsHoldingEnabledProperty, value);
+        }
+
+        public static bool GetIsHoldWithMouseEnabled(StyledElement element)
+        {
+            return element.GetValue(IsHoldWithMouseEnabledProperty);
+        }
+        public static void SetIsHoldWithMouseEnabled(StyledElement element, bool value)
+        {
+            element.SetValue(IsHoldWithMouseEnabledProperty, value);
+        }
+
         static Gestures()
         {
             InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed);
             InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased);
+            InputElement.PointerMovedEvent.RouteFinished.Subscribe(PointerMoved);
         }
 
         public static void AddTappedHandler(Interactive element, EventHandler<RoutedEventArgs> handler)
@@ -102,11 +154,42 @@ namespace Avalonia.Input
                 var e = (PointerPressedEventArgs)ev;
                 var visual = (Visual)ev.Source;
 
+                if(s_lastPointer != null)
+                {
+                    if(s_isHolding && ev.Source is Interactive i)
+                    {
+                        i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Cancelled, s_lastPressPoint, s_lastPointer.Type));
+                    }
+                    s_holdCancellationToken?.Cancel();
+                    s_holdCancellationToken?.Dispose();
+                    s_holdCancellationToken = null;
+
+                    s_lastPointer = null;
+                }
+
+                s_isHolding = false;
+
                 if (e.ClickCount % 2 == 1)
                 {
                     s_isDoubleTapped = false;
                     s_lastPress.SetTarget(ev.Source);
+                    s_lastPointer = e.Pointer;
                     s_lastPressPoint = e.GetPosition((Visual)ev.Source);
+                    s_holdCancellationToken = new CancellationTokenSource();
+                    var token = s_holdCancellationToken.Token;
+                    var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>();
+
+                    if (settings != null)
+                    {
+                        DispatcherTimer.RunOnce(() =>
+                        {
+                            if (!token.IsCancellationRequested && e.Source is InputElement i && GetIsHoldingEnabled(i) && (e.Pointer.Type != PointerType.Mouse || GetIsHoldWithMouseEnabled(i)))
+                            {
+                                s_isHolding = true;
+                                i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Started, s_lastPressPoint, s_lastPointer.Type));
+                            }
+                        }, settings.HoldWaitDuration);
+                    }
                 }
                 else if (e.ClickCount % 2 == 0 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed)
                 {
@@ -140,7 +223,12 @@ namespace Avalonia.Input
 
                     if (tapRect.ContainsExclusive(point.Position))
                     {
-                        if (e.InitialPressMouseButton == MouseButton.Right)
+                        if(s_isHolding)
+                        {
+                            s_isHolding = false;
+                            i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Completed, s_lastPressPoint, s_lastPointer!.Type));
+                        }
+                        else if (e.InitialPressMouseButton == MouseButton.Right)
                         {
                             i.RaiseEvent(new TappedEventArgs(RightTappedEvent, e));
                         }
@@ -152,6 +240,45 @@ namespace Avalonia.Input
                         }
                     }
                 }
+
+                s_holdCancellationToken?.Cancel();
+                s_holdCancellationToken?.Dispose();
+                s_holdCancellationToken = null;
+                s_lastPointer = null;
+            }
+        }
+
+        private static void PointerMoved(RoutedEventArgs ev)
+        {
+            if (ev.Route == RoutingStrategies.Bubble)
+            {
+                var e = (PointerEventArgs)ev;
+                if (s_lastPress.TryGetTarget(out var target))
+                {
+                    if (e.Pointer == s_lastPointer)
+                    {
+                        var point = e.GetCurrentPoint((Visual)target);
+                        var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>();
+                        var tapSize = settings?.GetTapSize(point.Pointer.Type) ?? new Size(4, 4);
+                        var tapRect = new Rect(s_lastPressPoint, new Size())
+                            .Inflate(new Thickness(tapSize.Width, tapSize.Height));
+
+                        if (tapRect.ContainsExclusive(point.Position))
+                        {
+                            return;
+                        }
+
+                        if (s_isHolding && ev.Source is Interactive i)
+                        {
+                            i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Cancelled, s_lastPressPoint, s_lastPointer!.Type));
+                        }
+                    }
+                }
+
+                s_holdCancellationToken?.Cancel();
+                s_holdCancellationToken?.Dispose();
+                s_holdCancellationToken = null;
+                s_isHolding = false;
             }
         }
     }

+ 51 - 0
src/Avalonia.Base/Input/HoldingRoutedEventArgs.cs

@@ -0,0 +1,51 @@
+using System;
+using Avalonia.Interactivity;
+
+namespace Avalonia.Input
+{
+    public class HoldingRoutedEventArgs : RoutedEventArgs
+    {
+        /// <summary>
+        /// Gets the state of the <see cref="Gestures.HoldingEvent"/> event.
+        /// </summary>
+        public HoldingState HoldingState { get; }
+
+        /// <summary>
+        /// Gets the location of the touch, mouse, or pen/stylus contact.
+        /// </summary>
+        public Point Position { get; }
+
+        /// <summary>
+        /// Gets the pointer type of the input source.
+        /// </summary>
+        public PointerType PointerType { get; }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="HoldingRoutedEventArgs"/> class.
+        /// </summary>
+        public HoldingRoutedEventArgs(HoldingState holdingState, Point position, PointerType pointerType) : base(Gestures.HoldingEvent)
+        {
+            HoldingState = holdingState;
+            Position = position;
+            PointerType = pointerType;
+        }
+    }
+
+    public enum HoldingState
+    {
+        /// <summary>
+        /// A single contact has been detected and a time threshold is crossed without the contact being lifted, another contact detected, or another gesture started.
+        /// </summary>
+        Started,
+
+        /// <summary>
+        /// The single contact is lifted.
+        /// </summary>
+        Completed,
+
+        /// <summary>
+        /// An additional contact is detected or a subsequent gesture (such as a slide) is detected.
+        /// </summary>
+        Cancelled,
+    }
+}

+ 15 - 1
src/Avalonia.Base/Input/InputElement.cs

@@ -188,11 +188,16 @@ namespace Avalonia.Input
         /// </summary>
         public static readonly RoutedEvent<TappedEventArgs> TappedEvent = Gestures.TappedEvent;
 
+        /// <summary>
+        /// Defines the <see cref="Holding"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<HoldingRoutedEventArgs> HoldingEvent = Gestures.HoldingEvent;
+
         /// <summary>
         /// Defines the <see cref="DoubleTapped"/> event.
         /// </summary>
         public static readonly RoutedEvent<TappedEventArgs> DoubleTappedEvent = Gestures.DoubleTappedEvent;
-        
+
         private bool _isEffectivelyEnabled = true;
         private bool _isFocused;
         private bool _isKeyboardFocusWithin;
@@ -352,6 +357,15 @@ namespace Avalonia.Input
             add { AddHandler(TappedEvent, value); }
             remove { RemoveHandler(TappedEvent, value); }
         }
+        
+        /// <summary>
+        /// Occurs when a hold gesture occurs on the control.
+        /// </summary>
+        public event EventHandler<HoldingRoutedEventArgs>? Holding
+        {
+            add { AddHandler(HoldingEvent, value); }
+            remove { RemoveHandler(HoldingEvent, value); }
+        }
 
         /// <summary>
         /// Occurs when a double-tap gesture occurs on the control.

+ 24 - 0
src/Avalonia.Base/Input/PinchEventArgs.cs

@@ -0,0 +1,24 @@
+using Avalonia.Interactivity;
+
+namespace Avalonia.Input
+{
+    public class PinchEventArgs : RoutedEventArgs
+    {
+        public PinchEventArgs(double scale, Point scaleOrigin) :  base(Gestures.PinchEvent)
+        {
+            Scale = scale;
+            ScaleOrigin = scaleOrigin;
+        }
+
+        public double Scale { get; } = 1;
+
+        public Point ScaleOrigin { get; }
+    }
+
+    public class PinchEndedEventArgs : RoutedEventArgs
+    {
+        public PinchEndedEventArgs() :  base(Gestures.PinchEndedEvent)
+        {
+        }
+    }
+}

+ 2 - 4
src/Avalonia.Base/Input/Raw/RawInputEventArgs.cs

@@ -21,11 +21,9 @@ namespace Avalonia.Input.Raw
         /// <param name="root">The root from which the event originates.</param>
         public RawInputEventArgs(IInputDevice device, ulong timestamp, IInputRoot root)
         {
-            device = device ?? throw new ArgumentNullException(nameof(device));
-
-            Device = device;
+            Device = device ?? throw new ArgumentNullException(nameof(device));
             Timestamp = timestamp;
-            Root = root;
+            Root = root ?? throw new ArgumentNullException(nameof(root));
         }
 
         /// <summary>

+ 0 - 6
src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs

@@ -53,9 +53,6 @@ namespace Avalonia.Input.Raw
             RawInputModifiers inputModifiers)
             : base(device, timestamp, root)
         {
-            Contract.Requires<ArgumentNullException>(device != null);
-            Contract.Requires<ArgumentNullException>(root != null);
-
             Point = new RawPointerPoint();
             Position = position;
             Type = type;
@@ -80,9 +77,6 @@ namespace Avalonia.Input.Raw
             RawInputModifiers inputModifiers)
             : base(device, timestamp, root)
         {
-            Contract.Requires<ArgumentNullException>(device != null);
-            Contract.Requires<ArgumentNullException>(root != null);
-
             Point = point;
             Type = type;
             InputModifiers = inputModifiers;

+ 1 - 1
src/Avalonia.Base/Layout/WrapLayout/UvMeasure.cs

@@ -7,7 +7,7 @@ namespace Avalonia.Layout
 {    
     internal struct UvMeasure
     {
-        internal static readonly UvMeasure Zero = default(UvMeasure);
+        internal static readonly UvMeasure Zero = default;
 
         internal double U { get; set; }
 

+ 5 - 0
src/Avalonia.Base/Logging/LogArea.cs

@@ -35,6 +35,11 @@ namespace Avalonia.Logging
         /// </summary>
         public const string Control = nameof(Control);
 
+        /// <summary>
+        /// The log event comes from Win32 Platform.
+        /// </summary>
+        public const string Platform = nameof(Platform);
+        
         /// <summary>
         /// The log event comes from Win32 Platform.
         /// </summary>

+ 1 - 1
src/Avalonia.Base/Media/DrawingContext.cs

@@ -279,7 +279,7 @@ namespace Avalonia.Media
                 OpacityMask,
             }
 
-            public PushedState(DrawingContext context, PushedStateType type, Matrix matrix = default(Matrix))
+            public PushedState(DrawingContext context, PushedStateType type, Matrix matrix = default)
             {
                 if (context._states is null)
                     throw new ObjectDisposedException(nameof(DrawingContext));

+ 6 - 5
src/Avalonia.Base/Media/DrawingGroup.cs

@@ -76,8 +76,8 @@ namespace Avalonia.Media
         {
             using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity))
             using (context.PushOpacity(Opacity))
-            using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default(DrawingContext.PushedState))
-            using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, GetBounds()) : default(DrawingContext.PushedState))
+            using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default)
+            using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, GetBounds()) : default)
             {
                 foreach (var drawing in Children)
                 {
@@ -461,9 +461,10 @@ namespace Avalonia.Media
 
                 if (_rootDrawing == null)
                 {
-                    // When a DrawingGroup is set, it should be made the root if
-                    // a root drawing didnt exist.
-                    Contract.Requires<NotSupportedException>(_currentDrawingGroup == null);
+                    if (_currentDrawingGroup != null)
+                    {
+                        throw new NotSupportedException("When a DrawingGroup is set, it should be made the root if a root drawing didnt exist.");
+                    }
 
                     // If this is the first Drawing being added, avoid creating a DrawingGroup
                     // and set this drawing as the root drawing.  This optimizes the common

+ 1 - 4
src/Avalonia.Base/Media/FontManager.cs

@@ -47,10 +47,7 @@ namespace Avalonia.Media
                     return current;
                 }
 
-                var fontManagerImpl = AvaloniaLocator.Current.GetService<IFontManagerImpl>();
-
-                if (fontManagerImpl == null)
-                    throw new InvalidOperationException("No font manager implementation was registered.");
+                var fontManagerImpl = AvaloniaLocator.Current.GetRequiredService<IFontManagerImpl>();
 
                 current = new FontManager(fontManagerImpl);
 

+ 1 - 1
src/Avalonia.Base/Media/ImmediateDrawingContext.cs

@@ -218,7 +218,7 @@ namespace Avalonia.Media
                 OpacityMask,
             }
 
-            internal PushedState(ImmediateDrawingContext context, PushedStateType type, Matrix matrix = default(Matrix))
+            internal PushedState(ImmediateDrawingContext context, PushedStateType type, Matrix matrix = default)
             {
                 if (context._states is null)
                     throw new ObjectDisposedException(nameof(ImmediateDrawingContext));

+ 6 - 21
src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs

@@ -140,7 +140,7 @@ namespace Avalonia.Media.TextFormatting
                     throw new ArgumentOutOfRangeException(nameof(index));
                 }
 #endif
-                return Span[index];
+                return CharacterBufferReference.CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index];
             }
         }
 
@@ -163,27 +163,18 @@ namespace Avalonia.Media.TextFormatting
         /// <summary>
         /// Gets the character memory buffer
         /// </summary>
-        internal ReadOnlyMemory<char> CharacterBuffer
-        {
-            get { return CharacterBufferReference.CharacterBuffer; }
-        }
+        internal ReadOnlyMemory<char> CharacterBuffer => CharacterBufferReference.CharacterBuffer;
 
         /// <summary>
         /// Gets the character offset relative to the beginning of buffer to 
         /// the first character of the run
         /// </summary>
-        internal int OffsetToFirstChar
-        {
-            get { return CharacterBufferReference.OffsetToFirstChar; }
-        }
+        internal int OffsetToFirstChar => CharacterBufferReference.OffsetToFirstChar;
 
         /// <summary>
         /// Indicate whether the character buffer range is empty
         /// </summary>
-        internal bool IsEmpty
-        {
-            get { return CharacterBufferReference.CharacterBuffer.Length == 0 || Length <= 0; }
-        }
+        internal bool IsEmpty => CharacterBufferReference.CharacterBuffer.Length == 0 || Length <= 0;
 
         internal CharacterBufferRange Take(int length)
         {
@@ -280,14 +271,8 @@ namespace Avalonia.Media.TextFormatting
 
         int IReadOnlyCollection<char>.Count => Length;
 
-        public IEnumerator<char> GetEnumerator()
-        {
-            return new ImmutableReadOnlyListStructEnumerator<char>(this);
-        }
+        public IEnumerator<char> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<char>(this);
 
-        IEnumerator IEnumerable.GetEnumerator()
-        {
-            return GetEnumerator();
-        }
+        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
     }
 }

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

@@ -132,7 +132,7 @@ namespace Avalonia.Media.TextFormatting
             {
                 var grapheme = graphemeEnumerator.Current;
 
-                finalLength += grapheme.Text.Length;
+                finalLength += grapheme.Length;
 
                 if (finalLength >= length)
                 {

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs

@@ -91,7 +91,7 @@ namespace Avalonia.Media.TextFormatting
                     continue;
                 }
 
-                if (textRun is ShapedTextCharacters shapedText)
+                if (textRun is ShapedTextRun shapedText)
                 {
                     var glyphRun = shapedText.GlyphRun;
                     var shapedBuffer = shapedText.ShapedBuffer;

+ 6 - 6
src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs → src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs

@@ -6,11 +6,11 @@ namespace Avalonia.Media.TextFormatting
     /// <summary>
     /// A text run that holds shaped characters.
     /// </summary>
-    public sealed class ShapedTextCharacters : DrawableTextRun
+    public sealed class ShapedTextRun : DrawableTextRun
     {
         private GlyphRun? _glyphRun;
 
-        public ShapedTextCharacters(ShapedBuffer shapedBuffer, TextRunProperties properties)
+        public ShapedTextRun(ShapedBuffer shapedBuffer, TextRunProperties properties)
         {
             ShapedBuffer = shapedBuffer;
             CharacterBufferReference = shapedBuffer.CharacterBufferRange.CharacterBufferReference;
@@ -155,7 +155,7 @@ namespace Avalonia.Media.TextFormatting
             return length > 0;
         }
 
-        internal SplitResult<ShapedTextCharacters> Split(int length)
+        internal SplitResult<ShapedTextRun> Split(int length)
         {
             if (IsReversed)
             {
@@ -171,7 +171,7 @@ namespace Avalonia.Media.TextFormatting
             
             var splitBuffer = ShapedBuffer.Split(length);
 
-            var first = new ShapedTextCharacters(splitBuffer.First, Properties);
+            var first = new ShapedTextRun(splitBuffer.First, Properties);
 
             #if DEBUG
 
@@ -182,9 +182,9 @@ namespace Avalonia.Media.TextFormatting
 
 #endif
 
-            var second = new ShapedTextCharacters(splitBuffer.Second!, Properties);
+            var second = new ShapedTextRun(splitBuffer.Second!, Properties);
 
-            return new SplitResult<ShapedTextCharacters>(first, second);
+            return new SplitResult<ShapedTextRun>(first, second);
         }
 
         internal GlyphRun CreateGlyphRun()

+ 11 - 11
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@@ -91,12 +91,12 @@ namespace Avalonia.Media.TextFormatting
         public override TextRunProperties Properties { get; }
 
         /// <summary>
-        /// Gets a list of <see cref="ShapeableTextCharacters"/>.
+        /// Gets a list of <see cref="UnshapedTextRun"/>.
         /// </summary>
         /// <returns>The shapeable text characters.</returns>
-        internal IReadOnlyList<ShapeableTextCharacters> GetShapeableCharacters(CharacterBufferRange characterBufferRange, sbyte biDiLevel, ref TextRunProperties? previousProperties)
+        internal IReadOnlyList<UnshapedTextRun> GetShapeableCharacters(CharacterBufferRange characterBufferRange, sbyte biDiLevel, ref TextRunProperties? previousProperties)
         {
-            var shapeableCharacters = new List<ShapeableTextCharacters>(2);
+            var shapeableCharacters = new List<UnshapedTextRun>(2);
 
             while (characterBufferRange.Length > 0)
             {
@@ -120,7 +120,7 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="biDiLevel">The bidi level of the run.</param>
         /// <param name="previousProperties"></param>
         /// <returns>A list of shapeable text runs.</returns>
-        private static ShapeableTextCharacters CreateShapeableRun(CharacterBufferRange characterBufferRange,
+        private static UnshapedTextRun CreateShapeableRun(CharacterBufferRange characterBufferRange,
             TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties)
         {
             var defaultTypeface = defaultProperties.Typeface;
@@ -133,12 +133,12 @@ namespace Avalonia.Media.TextFormatting
                 {
                     if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, null, out var fallbackCount, out _))
                     {
-                        return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, fallbackCount,
+                        return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, fallbackCount,
                             defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
                     }
                 }
 
-                return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
+                return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
                     biDiLevel);
             }
 
@@ -146,7 +146,7 @@ namespace Avalonia.Media.TextFormatting
             {
                 if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, defaultTypeface, out count, out _))
                 {
-                    return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count,
+                    return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count,
                         defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
                 }
             }
@@ -176,7 +176,7 @@ namespace Avalonia.Media.TextFormatting
             if (matchFound && TryGetShapeableLength(characterBufferRange, currentTypeface, defaultTypeface, out count, out _))
             {
                 //Fallback found
-                return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
+                return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
                     biDiLevel);
             }
 
@@ -196,10 +196,10 @@ namespace Avalonia.Media.TextFormatting
                     break;
                 }
 
-                count += grapheme.Text.Length;
+                count += grapheme.Length;
             }
 
-            return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties, biDiLevel);
+            return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties, biDiLevel);
         }
 
         /// <summary>
@@ -264,7 +264,7 @@ namespace Avalonia.Media.TextFormatting
                     }
                 }
 
-                length += currentGrapheme.Text.Length;
+                length += currentGrapheme.Length;
             }
 
             return length > 0;

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@@ -31,7 +31,7 @@ namespace Avalonia.Media.TextFormatting
 
                 switch (currentRun)
                 {
-                    case ShapedTextCharacters shapedRun:
+                    case ShapedTextRun shapedRun:
                         {
                             currentWidth += shapedRun.Size.Width;
 

+ 14 - 14
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@@ -124,7 +124,7 @@ namespace Avalonia.Media.TextFormatting
 
                     var second = new List<DrawableTextRun>(secondCount);
 
-                    if (currentRun is ShapedTextCharacters shapedTextCharacters)
+                    if (currentRun is ShapedTextRun shapedTextCharacters)
                     {
                         var split = shapedTextCharacters.Split(length - currentLength);
 
@@ -206,16 +206,16 @@ namespace Avalonia.Media.TextFormatting
                             break;
                         }
 
-                    case ShapeableTextCharacters shapeableRun:
+                    case UnshapedTextRun shapeableRun:
                         {
-                            var groupedRuns = new List<ShapeableTextCharacters>(2) { shapeableRun };
+                            var groupedRuns = new List<UnshapedTextRun>(2) { shapeableRun };
                             var characterBufferReference = currentRun.CharacterBufferReference;
                             var length = currentRun.Length;
                             var offsetToFirstCharacter = characterBufferReference.OffsetToFirstChar;
 
                             while (index + 1 < processedRuns.Count)
                             {
-                                if (processedRuns[index + 1] is not ShapeableTextCharacters nextRun)
+                                if (processedRuns[index + 1] is not UnshapedTextRun nextRun)
                                 {
                                     break;
                                 }
@@ -258,10 +258,10 @@ namespace Avalonia.Media.TextFormatting
             return drawableTextRuns;
         }
 
-        private static IReadOnlyList<ShapedTextCharacters> ShapeTogether(
-            IReadOnlyList<ShapeableTextCharacters> textRuns, CharacterBufferReference text, int length, TextShaperOptions options)
+        private static IReadOnlyList<ShapedTextRun> ShapeTogether(
+            IReadOnlyList<UnshapedTextRun> textRuns, CharacterBufferReference text, int length, TextShaperOptions options)
         {
-            var shapedRuns = new List<ShapedTextCharacters>(textRuns.Count);
+            var shapedRuns = new List<ShapedTextRun>(textRuns.Count);
 
             var shapedBuffer = TextShaper.Current.ShapeText(text, length, options);
 
@@ -271,7 +271,7 @@ namespace Avalonia.Media.TextFormatting
 
                 var splitResult = shapedBuffer.Split(currentRun.Length);
 
-                shapedRuns.Add(new ShapedTextCharacters(splitResult.First, currentRun.Properties));
+                shapedRuns.Add(new ShapedTextRun(splitResult.First, currentRun.Properties));
 
                 shapedBuffer = splitResult.Second!;
             }
@@ -280,9 +280,9 @@ namespace Avalonia.Media.TextFormatting
         }
 
         /// <summary>
-        /// Coalesces ranges of the same bidi level to form <see cref="ShapeableTextCharacters"/>
+        /// Coalesces ranges of the same bidi level to form <see cref="UnshapedTextRun"/>
         /// </summary>
-        /// <param name="textCharacters">The text characters to form <see cref="ShapeableTextCharacters"/> from.</param>
+        /// <param name="textCharacters">The text characters to form <see cref="UnshapedTextRun"/> from.</param>
         /// <param name="levels">The bidi levels.</param>
         /// <returns></returns>
         private static IEnumerable<IReadOnlyList<TextRun>> CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ArraySlice<sbyte> levels)
@@ -474,7 +474,7 @@ namespace Avalonia.Media.TextFormatting
             {
                 switch (currentRun)
                 {
-                    case ShapedTextCharacters shapedTextCharacters:
+                    case ShapedTextRun shapedTextCharacters:
                         {
                             if(shapedTextCharacters.ShapedBuffer.Length > 0)
                             {
@@ -538,7 +538,7 @@ namespace Avalonia.Media.TextFormatting
             var shapedBuffer = new ShapedBuffer(characterBufferRange, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
                 (sbyte)flowDirection);
 
-            var textRuns = new List<DrawableTextRun> { new ShapedTextCharacters(shapedBuffer, properties) };
+            var textRuns = new List<DrawableTextRun> { new ShapedTextRun(shapedBuffer, properties) };
 
             return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine();
         }
@@ -744,7 +744,7 @@ namespace Avalonia.Media.TextFormatting
         /// <returns>
         /// The shaped symbol.
         /// </returns>
-        internal static ShapedTextCharacters CreateSymbol(TextRun textRun, FlowDirection flowDirection)
+        internal static ShapedTextRun CreateSymbol(TextRun textRun, FlowDirection flowDirection)
         {
             var textShaper = TextShaper.Current;
 
@@ -760,7 +760,7 @@ namespace Avalonia.Media.TextFormatting
 
             var shapedBuffer = textShaper.ShapeText(characterBuffer, textRun.Length, shaperOptions);
 
-            return new ShapedTextCharacters(shapedBuffer, textRun.Properties);
+            return new ShapedTextRun(shapedBuffer, textRun.Properties);
         }
     }
 }

+ 2 - 2
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@@ -65,7 +65,7 @@ namespace Avalonia.Media.TextFormatting
 
                 switch (currentRun)
                 {
-                    case ShapedTextCharacters shapedRun:
+                    case ShapedTextRun shapedRun:
                     {
                         currentWidth += currentRun.Size.Width;
 
@@ -118,7 +118,7 @@ namespace Avalonia.Media.TextFormatting
 
                                     switch (run)
                                     {
-                                        case ShapedTextCharacters endShapedRun:
+                                        case ShapedTextRun endShapedRun:
                                         {
                                             if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth,
                                                     out var suffixCount, out var suffixWidth))

+ 20 - 20
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@@ -192,14 +192,14 @@ namespace Avalonia.Media.TextFormatting
             {
                 var currentRun = _textRuns[i];
 
-                if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
+                if (currentRun is ShapedTextRun shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
                 {
                     var rightToLeftIndex = i;
                     currentPosition += currentRun.Length;
 
                     while (rightToLeftIndex + 1 <= _textRuns.Count - 1)
                     {
-                        var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextCharacters;
+                        var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextRun;
 
                         if (nextShaped == null || nextShaped.ShapedBuffer.IsLeftToRight)
                         {
@@ -255,7 +255,7 @@ namespace Avalonia.Media.TextFormatting
 
             switch (run)
             {
-                case ShapedTextCharacters shapedRun:
+                case ShapedTextRun shapedRun:
                     {
                         characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
 
@@ -303,7 +303,7 @@ namespace Avalonia.Media.TextFormatting
                 {
                     var currentRun = _textRuns[index];
 
-                    if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
+                    if (currentRun is ShapedTextRun shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
                     {
                         var i = index;
 
@@ -313,7 +313,7 @@ namespace Avalonia.Media.TextFormatting
                         {
                             var nextRun = _textRuns[i + 1];
 
-                            if (nextRun is ShapedTextCharacters nextShapedRun && !nextShapedRun.ShapedBuffer.IsLeftToRight)
+                            if (nextRun is ShapedTextRun nextShapedRun && !nextShapedRun.ShapedBuffer.IsLeftToRight)
                             {
                                 i++;
 
@@ -407,7 +407,7 @@ namespace Avalonia.Media.TextFormatting
 
             switch (currentRun)
             {
-                case ShapedTextCharacters shapedTextCharacters:
+                case ShapedTextRun shapedTextCharacters:
                     {
                         currentGlyphRun = shapedTextCharacters.GlyphRun;
 
@@ -476,7 +476,7 @@ namespace Avalonia.Media.TextFormatting
 
             switch (currentRun)
             {
-                case ShapedTextCharacters shapedRun:
+                case ShapedTextRun shapedRun:
                     {
                         nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
                         break;
@@ -550,7 +550,7 @@ namespace Avalonia.Media.TextFormatting
 
                 double combinedWidth;
 
-                if (currentRun is ShapedTextCharacters currentShapedRun)
+                if (currentRun is ShapedTextRun currentShapedRun)
                 {
                     var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster;
 
@@ -592,7 +592,7 @@ namespace Avalonia.Media.TextFormatting
                         var rightToLeftIndex = index;
                         var rightToLeftWidth = currentShapedRun.Size.Width;
 
-                        while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun)
+                        while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun)
                         {
                             if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
                             {
@@ -624,12 +624,12 @@ namespace Avalonia.Media.TextFormatting
 
                         for (int i = rightToLeftIndex - 1; i >= index; i--)
                         {
-                            if (TextRuns[i] is not ShapedTextCharacters)
+                            if (TextRuns[i] is not ShapedTextRun)
                             {
                                 continue;
                             }
 
-                            currentShapedRun = (ShapedTextCharacters)TextRuns[i];
+                            currentShapedRun = (ShapedTextRun)TextRuns[i];
 
                             currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
 
@@ -769,7 +769,7 @@ namespace Avalonia.Media.TextFormatting
                 var characterLength = 0;
                 var endX = startX;
 
-                if (currentRun is ShapedTextCharacters currentShapedRun)
+                if (currentRun is ShapedTextRun currentShapedRun)
                 {
                     var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
 
@@ -883,7 +883,7 @@ namespace Avalonia.Media.TextFormatting
             return result;
         }
 
-        private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextCharacters currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength)
+        private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextRun currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength)
         {
             var startX = endX;
 
@@ -945,7 +945,7 @@ namespace Avalonia.Media.TextFormatting
 
         private static sbyte GetRunBidiLevel(DrawableTextRun run, FlowDirection flowDirection)
         {
-            if (run is ShapedTextCharacters shapedTextCharacters)
+            if (run is ShapedTextRun shapedTextCharacters)
             {
                 return shapedTextCharacters.BidiLevel;
             }
@@ -1027,7 +1027,7 @@ namespace Avalonia.Media.TextFormatting
                 {
                     if (current.Level >= minLevelToReverse && current.Level % 2 != 0)
                     {
-                        if (current.Run is ShapedTextCharacters { IsReversed: false } shapedTextCharacters)
+                        if (current.Run is ShapedTextRun { IsReversed: false } shapedTextCharacters)
                         {
                             shapedTextCharacters.Reverse();
                         }
@@ -1145,7 +1145,7 @@ namespace Avalonia.Media.TextFormatting
 
                 switch (currentRun)
                 {
-                    case ShapedTextCharacters shapedRun:
+                    case ShapedTextRun shapedRun:
                         {
                             var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
 
@@ -1230,7 +1230,7 @@ namespace Avalonia.Media.TextFormatting
 
                 switch (currentRun)
                 {
-                    case ShapedTextCharacters shapedRun:
+                    case ShapedTextRun shapedRun:
                         {
                             var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
 
@@ -1294,7 +1294,7 @@ namespace Avalonia.Media.TextFormatting
 
                 switch (currentRun)
                 {
-                    case ShapedTextCharacters shapedRun:
+                    case ShapedTextRun shapedRun:
                         {
                             var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster;
 
@@ -1303,7 +1303,7 @@ namespace Avalonia.Media.TextFormatting
                                 break;
                             }
 
-                            if (previousRun is ShapedTextCharacters previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight)
+                            if (previousRun is ShapedTextRun previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight)
                             {
                                 if (shapedRun.ShapedBuffer.IsLeftToRight)
                                 {
@@ -1394,7 +1394,7 @@ namespace Avalonia.Media.TextFormatting
             {
                 switch (_textRuns[index])
                 {
-                    case ShapedTextCharacters textRun:
+                    case ShapedTextRun textRun:
                         {
                             var textMetrics =
                                 new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize);

+ 2 - 5
src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs

@@ -63,14 +63,11 @@
         {
             get { return 0; }
         }
-        
+
         /// <summary>
         /// Gets the default incremental tab width.
         /// </summary>
-        public virtual double DefaultIncrementalTab
-        {
-            get { return 4 * DefaultTextRunProperties.FontRenderingEmSize; }
-        }
+        public virtual double DefaultIncrementalTab => 0;
 
         /// <summary>
         /// Gets the letter spacing.

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/TextRun.cs

@@ -44,7 +44,7 @@ namespace Avalonia.Media.TextFormatting
 
                         fixed (char* charsPtr = characterBuffer.Span)
                         {
-                            return new string(charsPtr, 0, characterBuffer.Span.Length);
+                            return new string(charsPtr, _textRun.CharacterBufferReference.OffsetToFirstChar, _textRun.Length);
                         }
                     }
                 }

+ 1 - 4
src/Avalonia.Base/Media/TextFormatting/TextShaper.cs

@@ -29,10 +29,7 @@ namespace Avalonia.Media.TextFormatting
                     return current;
                 }
 
-                var textShaperImpl = AvaloniaLocator.Current.GetService<ITextShaperImpl>();
-
-                if (textShaperImpl == null)
-                    throw new InvalidOperationException("No text shaper implementation was registered.");
+                var textShaperImpl = AvaloniaLocator.Current.GetRequiredService<ITextShaperImpl>();
 
                 current = new TextShaper(textShaperImpl);
 

+ 67 - 1
src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs

@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using System.Runtime.CompilerServices;
 
 namespace Avalonia.Media.TextFormatting.Unicode
@@ -222,6 +223,71 @@ namespace Avalonia.Media.TextFormatting.Unicode
 
             return new Codepoint(code);
         }
+        
+        /// <summary>
+        /// Reads the <see cref="Codepoint"/> at specified position.
+        /// </summary>
+        /// <param name="text">The buffer to read from.</param>
+        /// <param name="index">The index to read at.</param>
+        /// <param name="count">The count of character that were read.</param>
+        /// <returns></returns>
+        public static Codepoint ReadAt(CharacterBufferRange text, int index, out int count)
+        {
+            count = 1;
+
+            if (index >= text.Length)
+            {
+                return ReplacementCodepoint;
+            }
+
+            var code = text[index];
+
+            ushort hi, low;
+
+            //# High surrogate
+            if (0xD800 <= code && code <= 0xDBFF)
+            {
+                hi = code;
+
+                if (index + 1 == text.Length)
+                {
+                    return ReplacementCodepoint;
+                }
+
+                low = text[index + 1];
+
+                if (0xDC00 <= low && low <= 0xDFFF)
+                {
+                    count = 2;
+                    return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000));
+                }
+
+                return ReplacementCodepoint;
+            }
+
+            //# Low surrogate
+            if (0xDC00 <= code && code <= 0xDFFF)
+            {
+                if (index == 0)
+                {
+                    return ReplacementCodepoint;
+                }
+
+                hi = text[index - 1];
+
+                low = code;
+
+                if (0xD800 <= hi && hi <= 0xDBFF)
+                {
+                    count = 2;
+                    return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000));
+                }
+
+                return ReplacementCodepoint;
+            }
+
+            return new Codepoint(code);
+        }
 
         /// <summary>
         /// Returns <see langword="true"/> if <paramref name="cp"/> is between

+ 10 - 4
src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs

@@ -7,10 +7,11 @@ namespace Avalonia.Media.TextFormatting.Unicode
     /// </summary>
     public readonly ref struct Grapheme
     {
-        public Grapheme(Codepoint firstCodepoint, ReadOnlySpan<char> text)
+        public Grapheme(Codepoint firstCodepoint, int offset, int length)
         {
             FirstCodepoint = firstCodepoint;
-            Text = text;
+            Offset = offset;
+            Length = length;
         }
 
         /// <summary>
@@ -19,8 +20,13 @@ namespace Avalonia.Media.TextFormatting.Unicode
         public Codepoint FirstCodepoint { get; }
 
         /// <summary>
-        /// The text that is representing the <see cref="Grapheme"/>.
+        /// The Offset to the FirstCodepoint
         /// </summary>
-        public ReadOnlySpan<char> Text { get; }
+        public int Offset { get; }
+
+        /// <summary>
+        /// The length of the grapheme cluster
+        /// </summary>
+        public int Length { get; }
     }
 }

+ 1 - 3
src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs

@@ -185,9 +185,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
 
             Return:
 
-            var text = _text.Take(processor.CurrentCodeUnitOffset);
-
-            Current = new Grapheme(firstCodepoint, text.Span);
+            Current = new Grapheme(firstCodepoint, _text.OffsetToFirstChar, processor.CurrentCodeUnitOffset);
 
             _text = _text.Skip(processor.CurrentCodeUnitOffset);
 

+ 8 - 8
src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs → src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs

@@ -5,9 +5,9 @@ namespace Avalonia.Media.TextFormatting
     /// <summary>
     /// A group of characters that can be shaped.
     /// </summary>
-    public sealed class ShapeableTextCharacters : TextRun
+    public sealed class UnshapedTextRun : TextRun
     {
-        public ShapeableTextCharacters(CharacterBufferReference characterBufferReference, int length,
+        public UnshapedTextRun(CharacterBufferReference characterBufferReference, int length,
             TextRunProperties properties, sbyte biDiLevel)
         {
             CharacterBufferReference = characterBufferReference;
@@ -24,30 +24,30 @@ namespace Avalonia.Media.TextFormatting
 
         public sbyte BidiLevel { get; }
 
-        public bool CanShapeTogether(ShapeableTextCharacters shapeableTextCharacters)
+        public bool CanShapeTogether(UnshapedTextRun unshapedTextRun)
         {
-            if (!CharacterBufferReference.Equals(shapeableTextCharacters.CharacterBufferReference))
+            if (!CharacterBufferReference.Equals(unshapedTextRun.CharacterBufferReference))
             {
                 return false;
             }
 
-            if (BidiLevel != shapeableTextCharacters.BidiLevel)
+            if (BidiLevel != unshapedTextRun.BidiLevel)
             {
                 return false;
             }
 
             if (!MathUtilities.AreClose(Properties.FontRenderingEmSize,
-                    shapeableTextCharacters.Properties.FontRenderingEmSize))
+                    unshapedTextRun.Properties.FontRenderingEmSize))
             {
                 return false;
             }
 
-            if (Properties.Typeface != shapeableTextCharacters.Properties.Typeface)
+            if (Properties.Typeface != unshapedTextRun.Properties.Typeface)
             {
                 return false;
             }
 
-            if (Properties.BaselineAlignment != shapeableTextCharacters.Properties.BaselineAlignment)
+            if (Properties.BaselineAlignment != unshapedTextRun.Properties.BaselineAlignment)
             {
                 return false;
             }

+ 1 - 2
src/Avalonia.Base/Media/Typeface.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Diagnostics;
-using JetBrains.Annotations;
 
 namespace Avalonia.Media
 {
@@ -17,7 +16,7 @@ namespace Avalonia.Media
         /// <param name="style">The font style.</param>
         /// <param name="weight">The font weight.</param>
         /// <param name="stretch">The font stretch.</param>
-        public Typeface([NotNull] FontFamily fontFamily,
+        public Typeface(FontFamily fontFamily,
             FontStyle style = FontStyle.Normal,
             FontWeight weight = FontWeight.Normal,
             FontStretch stretch = FontStretch.Normal)

+ 0 - 2
src/Avalonia.Base/PixelVector.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Globalization;
 using Avalonia.Animation.Animators;
-using JetBrains.Annotations;
 
 namespace Avalonia
 {
@@ -135,7 +134,6 @@ namespace Avalonia
         /// </summary>
         /// <param name="other">The other vector.</param>
         /// <returns>True if vectors are nearly equal.</returns>
-        [Pure]
         public bool NearlyEquals(PixelVector other)
         {
             const float tolerance = float.Epsilon;

+ 2 - 0
src/Avalonia.Base/Platform/DefaultPlatformSettings.cs

@@ -26,5 +26,7 @@ namespace Avalonia.Platform
             };
         }
         public TimeSpan GetDoubleTapTime(PointerType type) => TimeSpan.FromMilliseconds(500);
+
+        public TimeSpan HoldWaitDuration { get; set; } = TimeSpan.FromMilliseconds(300);
     }
 }

+ 2 - 0
src/Avalonia.Base/Platform/IPlatformSettings.cs

@@ -27,5 +27,7 @@ namespace Avalonia.Platform
         /// tap gesture.
         /// </summary>
         TimeSpan GetDoubleTapTime(PointerType type);
+
+        TimeSpan HoldWaitDuration { get; set; }
     }
 }

+ 0 - 19
src/Avalonia.Base/Platform/IRuntimePlatform.cs

@@ -23,29 +23,10 @@ namespace Avalonia.Platform
     [Unstable]
     public record struct RuntimePlatformInfo
     {
-        public OperatingSystemType OperatingSystem { get; set; }
-
         public FormFactorType FormFactor => IsDesktop ? FormFactorType.Desktop :
             IsMobile ? FormFactorType.Mobile : FormFactorType.Unknown;
         public bool IsDesktop { get; set; }
         public bool IsMobile { get; set; }
-        public bool IsBrowser { get; set; }
-        public bool IsCoreClr { get; set; }
-        public bool IsMono { get; set; }
-        public bool IsDotNetFramework { get; set; }
-        public bool IsUnix { get; set; }
-    }
-
-    [Unstable]
-    public enum OperatingSystemType
-    {
-        Unknown,
-        WinNT,
-        Linux,
-        OSX,
-        Android,
-        iOS,
-        Browser
     }
 
     [Unstable]

+ 6 - 38
src/Avalonia.Base/Platform/StandardRuntimePlatform.cs

@@ -1,6 +1,6 @@
 using System;
-using System.Runtime.InteropServices;
 using System.Threading;
+using Avalonia.Compatibility;
 using Avalonia.Platform.Internal;
 
 namespace Avalonia.Platform
@@ -14,45 +14,13 @@ namespace Avalonia.Platform
 
         public IUnmanagedBlob AllocBlob(int size) => new UnmanagedBlob(size);
         
-        private static readonly Lazy<RuntimePlatformInfo> Info = new(() =>
+        private static readonly RuntimePlatformInfo s_info = new()
         {
-            OperatingSystemType os;
+            IsDesktop = OperatingSystemEx.IsWindows() || OperatingSystemEx.IsMacOS() || OperatingSystemEx.IsLinux(),
+            IsMobile = OperatingSystemEx.IsAndroid() || OperatingSystemEx.IsIOS()
+        };
 
-            if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
-                os = OperatingSystemType.OSX;
-            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
-                os = OperatingSystemType.Linux;
-            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
-                os = OperatingSystemType.WinNT;
-            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("Android")))
-                os = OperatingSystemType.Android;
-            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("iOS")))
-                os = OperatingSystemType.iOS;
-            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("Browser")))
-                os = OperatingSystemType.Browser;
-            else
-                throw new Exception("Unknown OS platform " + RuntimeInformation.OSDescription);
 
-            // Source: https://github.com/dotnet/runtime/blob/main/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs
-            var isCoreClr = Environment.Version.Major >= 5 || RuntimeInformation.FrameworkDescription.StartsWith(".NET Core", StringComparison.OrdinalIgnoreCase);
-            var isMonoRuntime = Type.GetType("Mono.Runtime") != null;
-            var isFramework = !isCoreClr && RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework", StringComparison.OrdinalIgnoreCase);
-            
-            return new RuntimePlatformInfo
-            {
-                IsCoreClr = isCoreClr,
-                IsDotNetFramework = isFramework,
-                IsMono = isMonoRuntime,
-                
-                IsDesktop = os is OperatingSystemType.Linux or OperatingSystemType.OSX or OperatingSystemType.WinNT,
-                IsMobile = os is OperatingSystemType.Android or OperatingSystemType.iOS,
-                IsUnix = os is OperatingSystemType.Linux or OperatingSystemType.OSX or OperatingSystemType.Android,
-                IsBrowser = os == OperatingSystemType.Browser,
-                OperatingSystem = os,
-            };
-        });
-
-
-        public virtual RuntimePlatformInfo GetRuntimeInfo() => Info.Value;
+        public virtual RuntimePlatformInfo GetRuntimeInfo() => s_info;
     }
 }

+ 4 - 9
src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs

@@ -1,4 +1,5 @@
 using System.Reflection;
+using Avalonia.Compatibility;
 using Avalonia.Platform.Internal;
 using Avalonia.Platform.Interop;
 
@@ -18,15 +19,9 @@ namespace Avalonia.Platform
 #if NET6_0_OR_GREATER
                     new Net6Loader()
 #else
-                    standardPlatform.GetRuntimeInfo().OperatingSystem switch
-                    {
-                        OperatingSystemType.WinNT => (IDynamicLibraryLoader)new Win32Loader(),
-                        OperatingSystemType.OSX => new UnixLoader(),
-                        OperatingSystemType.Linux => new UnixLoader(),
-                        OperatingSystemType.Android => new UnixLoader(),
-                        // iOS, WASM, ...
-                        _ => new NotSupportedLoader()
-                    }
+                    OperatingSystemEx.IsWindows() ? (IDynamicLibraryLoader)new Win32Loader()
+                        : OperatingSystemEx.IsMacOS() || OperatingSystemEx.IsLinux() || OperatingSystemEx.IsAndroid() ? new UnixLoader()
+                        : new NotSupportedLoader()
 #endif
                 );
         }

+ 1 - 1
src/Avalonia.Base/Rendering/Composition/Compositor.cs

@@ -111,7 +111,7 @@ namespace Avalonia.Rendering.Composition
                 }
             }
             
-            batch.CommitedAt = Server.Clock.Elapsed;
+            batch.CommittedAt = Server.Clock.Elapsed;
             _server.EnqueueBatch(batch);
             
             lock (_pendingBatchLock)

+ 1 - 1
src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs

@@ -654,7 +654,7 @@ namespace Avalonia.Rendering.Composition.Expressions
                 }
             }
 
-            res = default(T);
+            res = default;
             return false;
         }
 

+ 2 - 2
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs

@@ -48,7 +48,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua
         }
     }
 
-    protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt)
+    protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
     {
         if (reader.Read<byte>() == 1)
         {
@@ -56,7 +56,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua
             _renderCommands = reader.ReadObject<CompositionDrawList?>();
             _contentBounds = null;
         }
-        base.DeserializeChangesCore(reader, commitedAt);
+        base.DeserializeChangesCore(reader, committedAt);
     }
 
     protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)

+ 26 - 6
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@@ -42,11 +42,18 @@ namespace Avalonia.Rendering.Composition.Server
 
             Root!.RenderedVisuals++;
             
+            if (Opacity != 1)
+                canvas.PushOpacity(Opacity);
+            if (AdornedVisual != null)
+            {
+                canvas.PostTransform = Matrix.Identity;
+                canvas.Transform = Matrix.Identity;
+                canvas.PushClip(AdornedVisual._combinedTransformedClipBounds);
+            }
             var transform = GlobalTransformMatrix;
             canvas.PostTransform = MatrixUtils.ToMatrix(transform);
             canvas.Transform = Matrix.Identity;
-            if (Opacity != 1)
-                canvas.PushOpacity(Opacity);
+            
             var boundsRect = new Rect(new Size(Size.X, Size.Y));
             if (ClipToBounds && !HandlesClipToBounds)
                 canvas.PushClip(Root!.SnapToDevicePixels(boundsRect));
@@ -67,6 +74,8 @@ namespace Avalonia.Rendering.Composition.Server
                 canvas.PopGeometryClip();
             if (ClipToBounds && !HandlesClipToBounds)
                 canvas.PopClip();
+            if (AdornedVisual != null)
+                canvas.PopClip();
             if(Opacity != 1)
                 canvas.PopOpacity();
         }
@@ -155,15 +164,25 @@ namespace Avalonia.Rendering.Composition.Server
                 
                 _clipSizeDirty = false;
             }
+
+            _combinedTransformedClipBounds =
+                AdornedVisual?._combinedTransformedClipBounds
+                ?? Parent?._combinedTransformedClipBounds
+                ?? new Rect(Root!.Size);
             
-            _combinedTransformedClipBounds = Parent?._combinedTransformedClipBounds ?? new Rect(Root!.Size);
             if (_transformedClipBounds != null)
                 _combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value);
             
             EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1);
 
-            IsVisibleInFrame = _parent?.IsVisibleInFrame != false && Visible && EffectiveOpacity > 0.04 && !_isBackface &&
-                               !_combinedTransformedClipBounds.IsDefault;
+            IsHitTestVisibleInFrame = _parent?.IsHitTestVisibleInFrame != false
+                                      && Visible
+                                      && !_isBackface
+                                      && !_combinedTransformedClipBounds.IsDefault;
+
+            IsVisibleInFrame = IsHitTestVisibleInFrame
+                               && _parent?.IsVisibleInFrame != false
+                               && EffectiveOpacity > 0.04;
 
             if (wasVisible != IsVisibleInFrame || positionChanged)
             {
@@ -187,7 +206,7 @@ namespace Avalonia.Rendering.Composition.Server
             readback.Revision = root.Revision;
             readback.Matrix = GlobalTransformMatrix;
             readback.TargetId = Root.Id;
-            readback.Visible = IsVisibleInFrame;
+            readback.Visible = IsHitTestVisibleInFrame;
         }
 
         void AddDirtyRect(Rect rc)
@@ -248,6 +267,7 @@ namespace Avalonia.Rendering.Composition.Server
         }
 
         public bool IsVisibleInFrame { get; set; }
+        public bool IsHitTestVisibleInFrame { get; set; }
         public double EffectiveOpacity { get; set; }
         public Rect TransformedOwnContentBounds { get; set; }
         public virtual Rect OwnContentBounds => new Rect(0, 0, Size.X, Size.Y);

+ 3 - 3
src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs

@@ -16,9 +16,9 @@ internal class ServerCompositionCustomVisual : ServerCompositionContainerVisual,
         _handler.Attach(this);
     }
 
-    protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt)
+    protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
     {
-        base.DeserializeChangesCore(reader, commitedAt);
+        base.DeserializeChangesCore(reader, committedAt);
         var count = reader.Read<int>();
         for (var c = 0; c < count; c++)
         {
@@ -79,4 +79,4 @@ internal class ServerCompositionCustomVisual : ServerCompositionContainerVisual,
                 ?.Log(_handler, $"Exception in {_handler.GetType().Name}.{nameof(CompositionCustomVisualHandler.OnRender)} {{0}}", e);
         }
     }
-}
+}

+ 2 - 2
src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs

@@ -14,7 +14,7 @@ namespace Avalonia.Rendering.Composition.Server
     {
         public List<T> List { get; } = new List<T>();
 
-        protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt)
+        protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
         {
             if (reader.Read<byte>() == 1)
             {
@@ -23,7 +23,7 @@ namespace Avalonia.Rendering.Composition.Server
                 for (var c = 0; c < count; c++) 
                     List.Add(reader.ReadObject<T>());
             }
-            base.DeserializeChangesCore(reader, commitedAt);
+            base.DeserializeChangesCore(reader, committedAt);
         }
 
         public override long LastChangedBy

+ 4 - 4
src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs

@@ -104,13 +104,13 @@ namespace Avalonia.Rendering.Composition.Server
         }
 
         protected void SetAnimatedValue<T>(CompositionProperty prop, ref T field,
-            TimeSpan commitedAt, IAnimationInstance animation) where T : struct
+            TimeSpan committedAt, IAnimationInstance animation) where T : struct
         {
             if (IsActive && _animations.TryGetValue(prop, out var oldAnimation))
                 oldAnimation.Deactivate();
             _animations[prop] = animation;
             
-            animation.Initialize(commitedAt, ExpressionVariant.Create(field), prop);
+            animation.Initialize(committedAt, ExpressionVariant.Create(field), prop);
             if(IsActive)
                 animation.Activate();
             
@@ -165,7 +165,7 @@ namespace Avalonia.Rendering.Composition.Server
 
         public virtual CompositionProperty? GetCompositionProperty(string fieldName) => null;
 
-        protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt)
+        protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
         {
             if (this is IDisposable disp
                 && reader.Read<byte>() == 1)
@@ -174,7 +174,7 @@ namespace Avalonia.Rendering.Composition.Server
         
         public void DeserializeChanges(BatchStreamReader reader, Batch batch)
         {
-            DeserializeChangesCore(reader, batch.CommitedAt);
+            DeserializeChangesCore(reader, batch.CommittedAt);
             ValuesInvalidated();
             ItselfLastChangedBy = batch.SequenceId;
         }

+ 1 - 1
src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs

@@ -29,7 +29,7 @@ namespace Avalonia.Rendering.Composition.Transport
 
         
         public BatchStreamData Changes { get; private set; }
-        public TimeSpan CommitedAt { get; set; }
+        public TimeSpan CommittedAt { get; set; }
         
         public void Complete()
         {

+ 51 - 2
src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs

@@ -2,6 +2,7 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
 using Avalonia.Rendering.Composition.Animations;
 using Avalonia.Rendering.Composition.Server;
 
@@ -27,6 +28,37 @@ public record struct BatchStreamSegment<TData>
     public int ElementCount { get; set; }
 }
 
+
+// Unsafe.ReadUnaligned/Unsafe.WriteUnaligned are broken on arm,
+// see https://github.com/dotnet/runtime/issues/80068
+static unsafe class UnalignedMemoryHelper
+{
+    public static T ReadUnaligned<T>(byte* src) where T : unmanaged
+    {
+#if NET6_0_OR_GREATER
+        Unsafe.SkipInit<T>(out var rv);
+#else
+        T rv;
+#endif
+        UnalignedMemcpy((byte*)&rv, src, Unsafe.SizeOf<T>());
+        return rv;
+    }
+    
+    public static void WriteUnaligned<T>(byte* dst, T value) where T : unmanaged
+    {
+        UnalignedMemcpy(dst, (byte*)&value, Unsafe.SizeOf<T>());
+    }
+
+    [MethodImpl(MethodImplOptions.NoInlining)]
+    static unsafe void UnalignedMemcpy(byte* dst, byte* src, int count)
+    {
+        for (var c = 0; c < count; c++)
+        {
+            dst[c] = src[c];
+        }
+    }
+}
+
 internal class BatchStreamWriter : IDisposable
 {
     private readonly BatchStreamData _output;
@@ -74,7 +106,15 @@ internal class BatchStreamWriter : IDisposable
         var size = Unsafe.SizeOf<T>();
         if (_currentDataSegment.Data == IntPtr.Zero || _currentDataSegment.ElementCount + size > _memoryPool.BufferSize)
             NextDataSegment();
-        Unsafe.WriteUnaligned<T>((byte*)_currentDataSegment.Data + _currentDataSegment.ElementCount, item);
+        var ptr = (byte*)_currentDataSegment.Data + _currentDataSegment.ElementCount;
+        
+        // Unsafe.ReadUnaligned/Unsafe.WriteUnaligned are broken on arm32,
+        // see https://github.com/dotnet/runtime/issues/80068
+        if (RuntimeInformation.ProcessArchitecture == Architecture.Arm)
+            UnalignedMemoryHelper.WriteUnaligned(ptr, item);
+        else
+            Unsafe.WriteUnaligned(ptr, item);
+        
         _currentDataSegment.ElementCount += size;
     }
 
@@ -125,7 +165,16 @@ internal class BatchStreamReader : IDisposable
         if (_memoryOffset + size > _currentDataSegment.ElementCount)
             throw new InvalidOperationException("Attempted to read more memory then left in the current segment");
 
-        var rv = Unsafe.ReadUnaligned<T>((byte*)_currentDataSegment.Data + _memoryOffset);
+        var ptr = (byte*)_currentDataSegment.Data + _memoryOffset;
+        T rv;
+        
+        // Unsafe.ReadUnaligned/Unsafe.WriteUnaligned are broken on arm32,
+        // see https://github.com/dotnet/runtime/issues/80068
+        if (RuntimeInformation.ProcessArchitecture == Architecture.Arm)
+            rv = UnalignedMemoryHelper.ReadUnaligned<T>(ptr);
+        else
+            rv = Unsafe.ReadUnaligned<T>(ptr);
+        
         _memoryOffset += size;
         if (_memoryOffset == _currentDataSegment.ElementCount)
         {

+ 3 - 3
src/Avalonia.Base/Rendering/ImmediateRenderer.cs

@@ -330,11 +330,11 @@ namespace Avalonia.Rendering
                     ? visual is IVisualWithRoundRectClip roundClipVisual
                         ? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius))
                         : context.PushClip(bounds) 
-                    : default(DrawingContext.PushedState))
+                    : default)
 #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 (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default)
+                using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default)
                 using (context.PushTransformContainer())
                 {
                     visual.Render(context);

+ 1 - 2
src/Avalonia.Base/Rendering/RenderLoop.cs

@@ -50,8 +50,7 @@ namespace Avalonia.Rendering
         {
             get
             {
-                return _timer ??= AvaloniaLocator.Current.GetService<IRenderTimer>() ??
-                    throw new InvalidOperationException("Cannot locate IRenderTimer.");
+                return _timer ??= AvaloniaLocator.Current.GetRequiredService<IRenderTimer>();
             }
         }
 

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است