Browse Source

Merge branch 'master' into fixes/Warnings/Unused-field

Max Katz 3 years ago
parent
commit
17b67a08a7
100 changed files with 1605 additions and 1552 deletions
  1. 3 0
      .gitignore
  2. 3 3
      build/SkiaSharp.props
  3. 0 2
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  4. 22 68
      nukebuild/Build.cs
  5. 0 4
      nukebuild/BuildParameters.cs
  6. 57 0
      nukebuild/DotNetConfigHelper.cs
  7. 34 0
      samples/ControlCatalog/Converter/HexConverter.cs
  8. 12 0
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  9. 27 0
      samples/IntegrationTestApp/MacOSIntegration.cs
  10. 13 0
      samples/IntegrationTestApp/MainWindow.axaml.cs
  11. 6 2
      samples/IntegrationTestApp/ShowWindowTest.axaml
  12. 25 3
      samples/IntegrationTestApp/ShowWindowTest.axaml.cs
  13. 35 28
      src/Avalonia.Base/Animation/Animation.cs
  14. 5 2
      src/Avalonia.Base/Animation/AnimatorKeyFrame.cs
  15. 2 2
      src/Avalonia.Base/Animation/Animators/Animator`1.cs
  16. 8 9
      src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs
  17. 1 0
      src/Avalonia.Base/Avalonia.Base.csproj
  18. 11 4
      src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
  19. 11 0
      src/Avalonia.Base/Layout/LayoutHelper.cs
  20. 0 1
      src/Avalonia.Base/Media/DashStyle.cs
  21. 31 2
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  22. 1 3
      src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
  23. 7 3
      src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
  24. 1 1
      src/Avalonia.Base/Rendering/Composition/Visual.cs
  25. 7 26
      src/Avalonia.Base/Rendering/DirtyVisuals.cs
  26. 35 0
      src/Avalonia.Controls.ColorPicker/Converters/DoNothingForNullConverter.cs
  27. 2 1
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml
  28. 4 4
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  29. 4 5
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  30. 4 4
      src/Avalonia.Controls.DataGrid/Themes/Simple.xaml
  31. 2 2
      src/Avalonia.Controls/Calendar/Calendar.cs
  32. 5 5
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  33. 18 18
      src/Avalonia.Controls/DateTimePickers/DatePicker.cs
  34. 34 34
      src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs
  35. 24 24
      src/Avalonia.Controls/DateTimePickers/TimePicker.cs
  36. 28 28
      src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs
  37. 2 13
      src/Avalonia.Controls/Documents/InlineUIContainer.cs
  38. 61 0
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  39. 26 0
      src/Avalonia.Controls/RichTextBlock.cs
  40. 6 4
      src/Avalonia.Controls/ToggleSwitch.cs
  41. 1 0
      src/Avalonia.Controls/TreeView.cs
  42. 14 5
      src/Avalonia.Controls/TreeViewItem.cs
  43. 1 1
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  44. 6 2
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  45. 4 4
      src/Avalonia.Dialogs/ManagedFileChooser.cs
  46. 3 3
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  47. 1 0
      src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml
  48. 2 2
      src/Avalonia.Themes.Fluent/Controls/Calendar.xaml
  49. 5 5
      src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
  50. 28 35
      src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml
  51. 2 2
      src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml
  52. 30 37
      src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
  53. 2 2
      src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml
  54. 2 2
      src/Avalonia.Themes.Simple/Controls/Calendar.xaml
  55. 5 5
      src/Avalonia.Themes.Simple/Controls/CalendarItem.xaml
  56. 26 26
      src/Avalonia.Themes.Simple/Controls/DatePicker.xaml
  57. 2 2
      src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml
  58. 29 29
      src/Avalonia.Themes.Simple/Controls/TimePicker.xaml
  59. 2 2
      src/Avalonia.Themes.Simple/Controls/ToggleSwitch.xaml
  60. 2 1
      src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
  61. 9 0
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  62. 17 18
      src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj
  63. 9 2
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor
  64. 24 10
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
  65. 18 0
      src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs
  66. 13 28
      src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs
  67. 11 20
      src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs
  68. 5 7
      src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs
  69. 12 20
      src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs
  70. 19 26
      src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs
  71. 15 26
      src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs
  72. 1 1
      src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs
  73. 0 41
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts
  74. 0 23
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/InputHelper.ts
  75. 0 261
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts
  76. 0 68
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.ts
  77. 0 7
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/extras.d.ts
  78. 0 326
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts
  79. 2 1
      src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs
  80. 0 14
      src/Web/Avalonia.Web.Blazor/tsconfig.json
  81. 5 0
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/Avalonia.ts
  82. 40 0
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/DpiWatcher.ts
  83. 31 0
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts
  84. 20 15
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/NativeControlHost.ts
  85. 255 0
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SKHtmlCanvas.ts
  86. 67 0
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SizeWatcher.ts
  87. 79 0
      src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/IndexedDbWrapper.ts
  88. 32 137
      src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts
  89. 16 0
      src/Web/Avalonia.Web.Blazor/webapp/package.json
  90. 18 0
      src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json
  91. 0 0
      src/Web/Avalonia.Web.Blazor/webapp/types/dotnet/index.d.ts
  92. 40 0
      src/Web/Avalonia.Web.Blazor/webapp/webpack.config.js
  93. 3 2
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  94. 7 0
      src/iOS/Avalonia.iOS/Platform.cs
  95. 64 0
      src/tools/DevAnalyzers/OnPropertyChangedOverrideAnalyzer.cs
  96. 4 3
      src/tools/DevGenerators/CompositionGenerator/CompositionRoslynGenerator.cs
  97. 14 16
      src/tools/DevGenerators/CompositionGenerator/Extensions.cs
  98. 2 2
      src/tools/DevGenerators/CompositionGenerator/Generator.Utils.cs
  99. 8 8
      src/tools/DevGenerators/CompositionGenerator/Generator.cs
  100. 1 0
      src/tools/DevGenerators/DevGenerators.csproj

+ 3 - 0
.gitignore

@@ -212,3 +212,6 @@ coc-settings.json
 *.map
 src/Web/Avalonia.Web.Blazor/wwwroot/*.js
 src/Web/Avalonia.Web.Blazor/Interop/Typescript/*.js
+node_modules
+src/Web/Avalonia.Web.Blazor/webapp/package-lock.json
+src/Web/Avalonia.Web.Blazor/wwwroot

+ 3 - 3
build/SkiaSharp.props

@@ -1,7 +1,7 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup>
-    <PackageReference Include="SkiaSharp" Version="2.88.1-preview.108" />
-    <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.88.1-preview.108" />
-    <PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.1-preview.108" />
+    <PackageReference Include="SkiaSharp" Version="2.88.1" />
+    <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.88.1" />
+    <PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.1" />
   </ItemGroup>
 </Project>

+ 0 - 2
native/Avalonia.Native/src/OSX/WindowImpl.mm

@@ -91,8 +91,6 @@ HRESULT WindowImpl::SetParent(IAvnWindow *parent) {
         if(_parent != nullptr)
         {
             _parent->_children.remove(this);
-            
-            _parent->BringToFront();
         }
 
         auto cparent = dynamic_cast<WindowImpl *>(parent);

+ 22 - 68
nukebuild/Build.cs

@@ -36,25 +36,6 @@ partial class Build : NukeBuild
 {
     [Solution("Avalonia.sln")] readonly Solution Solution;
 
-    static Lazy<string> MsBuildExe = new Lazy<string>(() =>
-    {
-        if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
-            return null;
-
-        var msBuildDirectory = VSWhere("-latest -nologo -property installationPath -format value -prerelease").FirstOrDefault().Text;
-
-        if (!string.IsNullOrWhiteSpace(msBuildDirectory))
-        {
-            string msBuildExe = Path.Combine(msBuildDirectory, @"MSBuild\Current\Bin\MSBuild.exe");
-            if (!System.IO.File.Exists(msBuildExe))
-                msBuildExe = Path.Combine(msBuildDirectory, @"MSBuild\15.0\Bin\MSBuild.exe");
-
-            return msBuildExe;
-        }
-
-        return null;
-    }, false);
-
     BuildParameters Parameters { get; set; }
     protected override void OnBuildInitialized()
     {
@@ -89,25 +70,28 @@ partial class Build : NukeBuild
         }
         ExecWait("dotnet version:", "dotnet", "--info");
         ExecWait("dotnet workloads:", "dotnet", "workload list");
+        Information("Processor count: " + Environment.ProcessorCount);
+        Information("Available RAM: " + GC.GetGCMemoryInfo().TotalAvailableMemoryBytes / 0x100000 + "MB");
     }
 
-    IReadOnlyCollection<Output> MsBuildCommon(
-        string projectFile,
-        Configure<MSBuildSettings> configurator = null)
+    DotNetConfigHelper ApplySettingCore(DotNetConfigHelper c)
     {
-        return MSBuild(c => c
-            .SetProjectFile(projectFile)
-            // This is required for VS2019 image on Azure Pipelines
-            .When(Parameters.IsRunningOnWindows &&
-                  Parameters.IsRunningOnAzure, _ => _
-                .AddProperty("JavaSdkDirectory", GetVariable<string>("JAVA_HOME_11_X64")))
-            .AddProperty("PackageVersion", Parameters.Version)
+        if (Parameters.IsRunningOnAzure)
+            c.AddProperty("JavaSdkDirectory", GetVariable<string>("JAVA_HOME_11_X64"));
+        c.AddProperty("PackageVersion", Parameters.Version)
             .AddProperty("iOSRoslynPathHackRequired", true)
-            .SetProcessToolPath(MsBuildExe.Value)
             .SetConfiguration(Parameters.Configuration)
-            .SetVerbosity(MSBuildVerbosity.Minimal)
-            .Apply(configurator));
+            .SetVerbosity(DotNetVerbosity.Minimal);
+        return c;
     }
+    DotNetBuildSettings ApplySetting(DotNetBuildSettings c, Configure<DotNetBuildSettings> configurator = null) =>
+        ApplySettingCore(c).Build.Apply(configurator);
+
+    DotNetPackSettings ApplySetting(DotNetPackSettings c, Configure<DotNetPackSettings> configurator = null) =>
+        ApplySettingCore(c).Pack.Apply(configurator);
+
+    DotNetTestSettings ApplySetting(DotNetTestSettings c, Configure<DotNetTestSettings> configurator = null) =>
+        ApplySettingCore(c).Test.Apply(configurator);
 
     Target Clean => _ => _.Executes(() =>
     {
@@ -149,20 +133,11 @@ partial class Build : NukeBuild
     Target Compile => _ => _
         .DependsOn(Clean, CompileNative)
         .DependsOn(CompileHtmlPreviewer)
-        .Executes(async () =>
+        .Executes(() =>
         {
-            if (Parameters.IsRunningOnWindows)
-                MsBuildCommon(Parameters.MSBuildSolution, c => c
-                    .SetProcessArgumentConfigurator(a => a.Add("/r"))
-                    .AddTargets("Build")
-                );
-
-            else
-                DotNetBuild(c => c
-                    .SetProjectFile(Parameters.MSBuildSolution)
-                    .AddProperty("PackageVersion", Parameters.Version)
-                    .SetConfiguration(Parameters.Configuration)
-                );
+            DotNetBuild(c => ApplySetting(c)
+                .SetProjectFile(Parameters.MSBuildSolution)
+            );
         });
 
     void RunCoreTest(string projectName)
@@ -182,9 +157,8 @@ partial class Build : NukeBuild
 
             Information($"Running for {projectName} ({fw}) ...");
 
-            DotNetTest(c => c
+            DotNetTest(c => ApplySetting(c)
                 .SetProjectFile(project)
-                .SetConfiguration(Parameters.Configuration)
                 .SetFramework(fw)
                 .EnableNoBuild()
                 .EnableNoRestore()
@@ -263,19 +237,7 @@ partial class Build : NukeBuild
         .Executes(() =>
         {
             var data = Parameters;
-            var pathToProjectSource = RootDirectory / "samples" / "ControlCatalog.NetCore";
-            var pathToPublish = pathToProjectSource / "bin" / data.Configuration / "publish";
-
-            DotNetPublish(c => c
-                .SetProject(pathToProjectSource / "ControlCatalog.NetCore.csproj")
-                .EnableNoBuild()
-                .SetConfiguration(data.Configuration)
-                .AddProperty("PackageVersion", data.Version)
-                .AddProperty("PublishDir", pathToPublish));
-
-            Zip(data.ZipCoreArtifacts, data.BinRoot);
             Zip(data.ZipNuGetArtifacts, data.NugetRoot);
-            Zip(data.ZipTargetControlCatalogNetCoreDir, pathToPublish);
         });
 
     Target CreateIntermediateNugetPackages => _ => _
@@ -283,15 +245,7 @@ partial class Build : NukeBuild
         .After(RunTests)
         .Executes(() =>
         {
-            if (Parameters.IsRunningOnWindows)
-
-                MsBuildCommon(Parameters.MSBuildSolution, c => c
-                    .AddTargets("Pack"));
-            else
-                DotNetPack(c => c
-                    .SetProject(Parameters.MSBuildSolution)
-                    .SetConfiguration(Parameters.Configuration)
-                    .AddProperty("PackageVersion", Parameters.Version));
+            DotNetPack(c => ApplySetting(c).SetProject(Parameters.MSBuildSolution));
         });
 
     Target CreateNugetPackages => _ => _

+ 0 - 4
nukebuild/BuildParameters.cs

@@ -51,14 +51,12 @@ public partial class Build
         public AbsolutePath NugetIntermediateRoot { get; }
         public AbsolutePath NugetRoot { get; }
         public AbsolutePath ZipRoot { get; }
-        public AbsolutePath BinRoot { get; }
         public AbsolutePath TestResultsRoot { get; }
         public string DirSuffix { get; }
         public List<string> BuildDirs { get; }
         public string FileZipSuffix { get; }
         public AbsolutePath ZipCoreArtifacts { get; }
         public AbsolutePath ZipNuGetArtifacts { get; }
-        public AbsolutePath ZipTargetControlCatalogNetCoreDir { get; }
 
 
         public BuildParameters(Build b)
@@ -121,14 +119,12 @@ public partial class Build
             NugetRoot = ArtifactsDir / "nuget";
             NugetIntermediateRoot = RootDirectory / "build-intermediate" / "nuget";
             ZipRoot = ArtifactsDir / "zip";
-            BinRoot = ArtifactsDir / "bin";
             TestResultsRoot = ArtifactsDir / "test-results";
             BuildDirs = GlobDirectories(RootDirectory, "**bin").Concat(GlobDirectories(RootDirectory, "**obj")).ToList();
             DirSuffix = Configuration;
             FileZipSuffix = Version + ".zip";
             ZipCoreArtifacts = ZipRoot / ("Avalonia-" + FileZipSuffix);
             ZipNuGetArtifacts = ZipRoot / ("Avalonia-NuGet-" + FileZipSuffix);
-            ZipTargetControlCatalogNetCoreDir = ZipRoot / ("ControlCatalog.NetCore-" + FileZipSuffix);
         }
 
         string GetVersion()

+ 57 - 0
nukebuild/DotNetConfigHelper.cs

@@ -0,0 +1,57 @@
+using System.Globalization;
+using JetBrains.Annotations;
+using Nuke.Common.Tools.DotNet;
+// ReSharper disable ReturnValueOfPureMethodIsNotUsed
+
+public class DotNetConfigHelper
+{
+    public DotNetBuildSettings Build;
+    public DotNetPackSettings Pack;
+    public DotNetTestSettings Test;
+
+    public DotNetConfigHelper(DotNetBuildSettings s)
+    {
+        Build = s;
+    }
+
+    public DotNetConfigHelper(DotNetPackSettings s)
+    {
+        Pack = s;
+    }
+
+    public DotNetConfigHelper(DotNetTestSettings s)
+    {
+        Test = s;
+    }
+
+    public DotNetConfigHelper AddProperty(string key, bool value) =>
+        AddProperty(key, value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
+    public DotNetConfigHelper AddProperty(string key, string value)
+    {
+        Build = Build?.AddProperty(key, value);
+        Pack = Pack?.AddProperty(key, value);
+        Test = Test?.AddProperty(key, value);
+
+        return this;
+    }
+
+    public DotNetConfigHelper SetConfiguration(string configuration)
+    {
+        Build = Build?.SetConfiguration(configuration);
+        Pack = Pack?.SetConfiguration(configuration);
+        Test = Test?.SetConfiguration(configuration);
+        return this;
+    }
+
+    public DotNetConfigHelper SetVerbosity(DotNetVerbosity verbosity)
+    {
+        Build = Build?.SetVerbosity(verbosity);
+        Pack = Pack?.SetVerbostiy(verbosity);
+        Test = Test?.SetVerbosity(verbosity);
+        return this;
+    }
+
+    public static implicit operator DotNetConfigHelper(DotNetBuildSettings s) => new DotNetConfigHelper(s);
+    public static implicit operator DotNetConfigHelper(DotNetPackSettings s) => new DotNetConfigHelper(s);
+    public static implicit operator DotNetConfigHelper(DotNetTestSettings s) => new DotNetConfigHelper(s);
+}

+ 34 - 0
samples/ControlCatalog/Converter/HexConverter.cs

@@ -0,0 +1,34 @@
+using System;
+using System.Globalization;
+using Avalonia;
+using Avalonia.Data.Converters;
+
+namespace ControlCatalog.Converter;
+
+public class HexConverter : IValueConverter
+{
+    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        var str = value?.ToString();
+        if (str == null)
+            return AvaloniaProperty.UnsetValue;
+        if (int.TryParse(str, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int x))
+            return (decimal)x;
+        return AvaloniaProperty.UnsetValue;
+
+    }
+
+    public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        try
+        {
+            if (value is decimal d)
+                return ((int)d).ToString("X8");
+            return AvaloniaProperty.UnsetValue;
+        }
+        catch
+        {
+            return AvaloniaProperty.UnsetValue;
+        }
+    }
+}

+ 12 - 0
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@@ -1,6 +1,7 @@
 <UserControl xmlns="https://github.com/avaloniaui"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:sys="clr-namespace:System;assembly=netstandard"
+             xmlns:converter="clr-namespace:ControlCatalog.Converter"
              x:Class="ControlCatalog.Pages.NumericUpDownPage">
   <StackPanel Orientation="Vertical" Spacing="4"
               MaxWidth="800">
@@ -97,6 +98,17 @@
           </DataValidationErrors.Error>
         </NumericUpDown>
       </StackPanel>
+
+      <StackPanel Orientation="Vertical" Margin="10">
+        <Label Target="HexUpDown" FontSize="14" FontWeight="Bold" VerticalAlignment="Center">NumericUpDown in HEX mode:</Label>
+        <NumericUpDown x:Name="HexUpDown" Value="0"
+                       VerticalAlignment="Center">
+          <NumericUpDown.TextConverter>
+            <converter:HexConverter></converter:HexConverter>
+          </NumericUpDown.TextConverter>
+        </NumericUpDown>
+        
+      </StackPanel>
     </WrapPanel>
 
   </StackPanel>

+ 27 - 0
samples/IntegrationTestApp/MacOSIntegration.cs

@@ -0,0 +1,27 @@
+using System;
+using System.Runtime.InteropServices;
+using Avalonia.Controls;
+
+namespace IntegrationTestApp
+{
+    public static class MacOSIntegration
+    {
+        [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "sel_registerName")]
+        private static extern IntPtr GetHandle(string name);
+        
+        [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
+        private static extern long Int64_objc_msgSend(IntPtr receiver, IntPtr selector);
+
+        private static readonly IntPtr s_orderedIndexSelector;
+
+        static MacOSIntegration()
+        {
+            s_orderedIndexSelector = GetHandle("orderedIndex");;
+        }
+        
+        public static long GetOrderedIndex(Window window)
+        {
+            return Int64_objc_msgSend(window.PlatformImpl!.Handle.Handle, s_orderedIndexSelector);
+        }
+    }
+}

+ 13 - 0
samples/IntegrationTestApp/MainWindow.axaml.cs

@@ -1,11 +1,13 @@
 using System.Collections.Generic;
 using System.Linq;
 using Avalonia;
+using Avalonia.Automation;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Interactivity;
 using Avalonia.Markup.Xaml;
 using Avalonia.VisualTree;
+using Microsoft.CodeAnalysis;
 
 namespace IntegrationTestApp
 {
@@ -63,6 +65,17 @@ namespace IntegrationTestApp
                 WindowStartupLocation = (WindowStartupLocation)locationComboBox.SelectedIndex,
             };
 
+            if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
+            {
+                // Make sure the windows have unique names and AutomationIds.
+                var existing = lifetime.Windows.OfType<ShowWindowTest>().Count();
+                if (existing > 0)
+                {
+                    AutomationProperties.SetAutomationId(window, window.Name + (existing + 1));
+                    window.Title += $" {existing + 1}";
+                }
+            }
+            
             if (size.HasValue)
             {
                 window.Width = size.Value.Width;

+ 6 - 2
samples/IntegrationTestApp/ShowWindowTest.axaml

@@ -3,7 +3,7 @@
         x:Class="IntegrationTestApp.ShowWindowTest"
         Name="SecondaryWindow"
         Title="Show Window Test">
-  <Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
+  <Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
     <Label Grid.Column="0" Grid.Row="1">Client Size</Label>
     <TextBox Name="ClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
              Text="{Binding ClientSize, Mode=OneWay}"/>
@@ -31,6 +31,10 @@
       <ComboBoxItem>Maximized</ComboBoxItem>
       <ComboBoxItem>Fullscreen</ComboBoxItem>
     </ComboBox>
-    <Button Name="HideButton" Grid.Row="8" Command="{Binding $parent[Window].Hide}">Hide</Button>
+
+    <Label Grid.Column="0" Grid.Row="8">Order (mac)</Label>
+    <TextBox Name="Order" Grid.Column="1" Grid.Row="8" IsReadOnly="True"/>
+
+    <Button Name="HideButton" Grid.Row="9" Command="{Binding $parent[Window].Hide}">Hide</Button>
   </Grid>
 </Window>

+ 25 - 3
samples/IntegrationTestApp/ShowWindowTest.axaml.cs

@@ -1,21 +1,32 @@
 using System;
+using System.Runtime.InteropServices;
 using Avalonia;
 using Avalonia.Controls;
-using Avalonia.Interactivity;
 using Avalonia.Markup.Xaml;
-using Avalonia.Rendering;
+using Avalonia.Threading;
 
 namespace IntegrationTestApp
 {
     public class ShowWindowTest : Window
     {
+        private readonly DispatcherTimer? _timer;
+        private readonly TextBox? _orderTextBox;
+        
         public ShowWindowTest()
         {
             InitializeComponent();
             DataContext = this;
             PositionChanged += (s, e) => this.GetControl<TextBox>("Position").Text = $"{Position}";
-        }
 
+            if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+            {
+                _orderTextBox = this.GetControl<TextBox>("Order");
+                _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) };
+                _timer.Tick += TimerOnTick;
+                _timer.Start();
+            }
+        }
+        
         private void InitializeComponent()
         {
             AvaloniaXamlLoader.Load(this);
@@ -36,5 +47,16 @@ namespace IntegrationTestApp
                 ownerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}";
             }
         }
+
+        protected override void OnClosed(EventArgs e)
+        {
+            base.OnClosed(e);
+            _timer?.Stop();
+        }
+
+        private void TimerOnTick(object? sender, EventArgs e)
+        {
+            _orderTextBox!.Text = MacOSIntegration.GetOrderedIndex(this).ToString();
+        }
     }
 }

+ 35 - 28
src/Avalonia.Base/Animation/Animation.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Reactive.Disposables;
 using System.Reactive.Linq;
@@ -179,14 +180,14 @@ namespace Avalonia.Animation
         public KeyFrames Children { get; } = new KeyFrames();
 
         // Store values for the Animator attached properties for IAnimationSetter objects.
-        private static readonly Dictionary<IAnimationSetter, Type> s_animators = new Dictionary<IAnimationSetter, Type>();
+        private static readonly Dictionary<IAnimationSetter, (Type Type, Func<IAnimator> Factory)> s_animators = new();
 
         /// <summary>
         /// Gets the value of the Animator attached property for a setter.
         /// </summary>
         /// <param name="setter">The animation setter.</param>
         /// <returns>The property animator type.</returns>
-        public static Type? GetAnimator(IAnimationSetter setter)
+        public static (Type Type, Func<IAnimator> Factory)? GetAnimator(IAnimationSetter setter)
         {
             if (s_animators.TryGetValue(setter, out var type))
             {
@@ -200,24 +201,28 @@ namespace Avalonia.Animation
         /// </summary>
         /// <param name="setter">The animation setter.</param>
         /// <param name="value">The property animator value.</param>
-        public static void SetAnimator(IAnimationSetter setter, Type value)
+        public static void SetAnimator(IAnimationSetter setter, 
+#if NET6_0_OR_GREATER
+            [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicMethods)]
+#endif
+            Type value)
         {
-            s_animators[setter] = value;
+            s_animators[setter] = (value, () => (IAnimator)Activator.CreateInstance(value)!);
         }
 
-        private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator)> Animators = new List<(Func<AvaloniaProperty, bool>, Type)>
+        private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator, Func<IAnimator> Factory)> Animators = new()
         {
-            ( prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator) ),
-            ( prop => typeof(byte).IsAssignableFrom(prop.PropertyType), typeof(ByteAnimator) ),
-            ( prop => typeof(Int16).IsAssignableFrom(prop.PropertyType), typeof(Int16Animator) ),
-            ( prop => typeof(Int32).IsAssignableFrom(prop.PropertyType), typeof(Int32Animator) ),
-            ( prop => typeof(Int64).IsAssignableFrom(prop.PropertyType), typeof(Int64Animator) ),
-            ( prop => typeof(UInt16).IsAssignableFrom(prop.PropertyType), typeof(UInt16Animator) ),
-            ( prop => typeof(UInt32).IsAssignableFrom(prop.PropertyType), typeof(UInt32Animator) ),
-            ( prop => typeof(UInt64).IsAssignableFrom(prop.PropertyType), typeof(UInt64Animator) ),
-            ( prop => typeof(float).IsAssignableFrom(prop.PropertyType), typeof(FloatAnimator) ),
-            ( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator) ),
-            ( prop => typeof(decimal).IsAssignableFrom(prop.PropertyType), typeof(DecimalAnimator) ),
+            ( prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator), () => new BoolAnimator() ),
+            ( prop => typeof(byte).IsAssignableFrom(prop.PropertyType), typeof(ByteAnimator), () => new ByteAnimator() ),
+            ( prop => typeof(Int16).IsAssignableFrom(prop.PropertyType), typeof(Int16Animator), () => new Int16Animator() ),
+            ( prop => typeof(Int32).IsAssignableFrom(prop.PropertyType), typeof(Int32Animator), () => new Int32Animator() ),
+            ( prop => typeof(Int64).IsAssignableFrom(prop.PropertyType), typeof(Int64Animator), () => new Int64Animator() ),
+            ( prop => typeof(UInt16).IsAssignableFrom(prop.PropertyType), typeof(UInt16Animator), () => new UInt16Animator() ),
+            ( prop => typeof(UInt32).IsAssignableFrom(prop.PropertyType), typeof(UInt32Animator), () => new UInt32Animator() ),
+            ( prop => typeof(UInt64).IsAssignableFrom(prop.PropertyType), typeof(UInt64Animator), () => new UInt64Animator() ),
+            ( prop => typeof(float).IsAssignableFrom(prop.PropertyType), typeof(FloatAnimator), () => new FloatAnimator() ),
+            ( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator), () => new DoubleAnimator() ),
+            ( prop => typeof(decimal).IsAssignableFrom(prop.PropertyType), typeof(DecimalAnimator), () => new DecimalAnimator() ),
         };
 
         /// <summary>
@@ -232,18 +237,18 @@ namespace Avalonia.Animation
         /// The type of the animator to instantiate.
         /// </typeparam>
         public static void RegisterAnimator<TAnimator>(Func<AvaloniaProperty, bool> condition)
-            where TAnimator : IAnimator
+            where TAnimator : IAnimator, new()
         {
-            Animators.Insert(0, (condition, typeof(TAnimator)));
+            Animators.Insert(0, (condition, typeof(TAnimator), () => new TAnimator()));
         }
 
-        private static Type? GetAnimatorType(AvaloniaProperty property)
+        private static (Type Type, Func<IAnimator> Factory)? GetAnimatorType(AvaloniaProperty property)
         {
-            foreach (var (condition, type) in Animators)
+            foreach (var (condition, type, factory) in Animators)
             {
                 if (condition(property))
                 {
-                    return type;
+                    return (type, factory);
                 }
             }
             return null;
@@ -251,7 +256,7 @@ namespace Avalonia.Animation
 
         private (IList<IAnimator> Animators, IList<IDisposable> subscriptions) InterpretKeyframes(Animatable control)
         {
-            var handlerList = new List<(Type type, AvaloniaProperty property)>();
+            var handlerList = new Dictionary<(Type type, AvaloniaProperty Property), Func<IAnimator>>();
             var animatorKeyFrames = new List<AnimatorKeyFrame>();
             var subscriptions = new List<IDisposable>();
 
@@ -271,8 +276,10 @@ namespace Avalonia.Animation
                         throw new InvalidOperationException($"No animator registered for the property {setter.Property}. Add an animator to the Animation.Animators collection that matches this property to animate it.");
                     }
 
-                    if (!handlerList.Contains((handler, setter.Property)))
-                        handlerList.Add((handler, setter.Property));
+                    var (type, factory) = handler.Value;
+
+                    if (!handlerList.ContainsKey((type, setter.Property)))
+                        handlerList[(type, setter.Property)] = factory;
 
                     var cue = keyframe.Cue;
 
@@ -281,7 +288,7 @@ namespace Avalonia.Animation
                         cue = new Cue(keyframe.KeyTime.TotalSeconds / Duration.TotalSeconds);
                     }
 
-                    var newKF = new AnimatorKeyFrame(handler, cue, keyframe.KeySpline);
+                    var newKF = new AnimatorKeyFrame(type, factory, cue, keyframe.KeySpline);
 
                     subscriptions.Add(newKF.BindSetter(setter, control));
 
@@ -291,10 +298,10 @@ namespace Avalonia.Animation
 
             var newAnimatorInstances = new List<IAnimator>();
 
-            foreach (var (handlerType, property) in handlerList)
+            foreach (var handler in handlerList)
             {
-                var newInstance = (IAnimator)Activator.CreateInstance(handlerType)!;
-                newInstance.Property = property;
+                var newInstance = handler.Value();
+                newInstance.Property = handler.Key.Property;
                 newAnimatorInstances.Add(newInstance);
             }
 

+ 5 - 2
src/Avalonia.Base/Animation/AnimatorKeyFrame.cs

@@ -20,22 +20,25 @@ namespace Avalonia.Animation
 
         }
 
-        public AnimatorKeyFrame(Type? animatorType, Cue cue)
+        public AnimatorKeyFrame(Type? animatorType, Func<IAnimator>? animatorFactory, Cue cue)
         {
             AnimatorType = animatorType;
+            AnimatorFactory = animatorFactory;
             Cue = cue;
             KeySpline = null;
         }
 
-        public AnimatorKeyFrame(Type? animatorType, Cue cue, KeySpline? keySpline)
+        public AnimatorKeyFrame(Type? animatorType, Func<IAnimator>? animatorFactory, Cue cue, KeySpline? keySpline)
         {
             AnimatorType = animatorType;
+            AnimatorFactory = animatorFactory;
             Cue = cue;
             KeySpline = keySpline;
         }
 
         internal bool isNeutral;
         public Type? AnimatorType { get; }
+        public Func<IAnimator>? AnimatorFactory { get; }
         public Cue Cue { get; }
         public KeySpline? KeySpline { get; }
         public AvaloniaProperty? Property { get; private set; }

+ 2 - 2
src/Avalonia.Base/Animation/Animators/Animator`1.cs

@@ -171,12 +171,12 @@ namespace Avalonia.Animation.Animators
         {
             if (!hasStartKey)
             {
-                _convertedKeyframes.Insert(0, new AnimatorKeyFrame(null, new Cue(0.0d)) { Value = default(T), isNeutral = true });
+                _convertedKeyframes.Insert(0, new AnimatorKeyFrame(null, null, new Cue(0.0d)) { Value = default(T), isNeutral = true });
             }
 
             if (!hasEndKey)
             {
-                _convertedKeyframes.Add(new AnimatorKeyFrame(null, new Cue(1.0d)) { Value = default(T), isNeutral = true });
+                _convertedKeyframes.Add(new AnimatorKeyFrame(null, null, new Cue(1.0d)) { Value = default(T), isNeutral = true });
             }
         }
     }

+ 8 - 9
src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs

@@ -17,8 +17,7 @@ namespace Avalonia.Animation.Animators
     /// </summary>
     public class BaseBrushAnimator : Animator<IBrush?>
     {
-        private static readonly List<(Func<Type, bool> Match, Type AnimatorType)> _brushAnimators =
-            new List<(Func<Type, bool> Match, Type AnimatorType)>();
+        private static readonly List<(Func<Type, bool> Match, Type AnimatorType, Func<IAnimator> AnimatorFactory)> _brushAnimators = new();
 
         /// <summary>
         /// Register an <see cref="Animator{T}"/> that handles a specific
@@ -34,7 +33,7 @@ namespace Avalonia.Animation.Animators
         public static void RegisterBrushAnimator<TAnimator>(Func<Type, bool> condition)
             where TAnimator : IAnimator, new()
         {
-            _brushAnimators.Insert(0, (condition, typeof(TAnimator)));
+            _brushAnimators.Insert(0, (condition, typeof(TAnimator), () => new TAnimator()));
         }
 
         /// <inheritdoc/>
@@ -85,14 +84,14 @@ namespace Avalonia.Animation.Animators
             {
                 if (keyframe.Value is ISolidColorBrush solidColorBrush)
                 {
-                    gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), keyframe.Cue, keyframe.KeySpline)
+                    gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), () => new GradientBrushAnimator(), keyframe.Cue, keyframe.KeySpline)
                     {
                         Value = GradientBrushAnimator.ConvertSolidColorBrushToGradient(firstGradient, solidColorBrush)
                     });
                 }
                 else if (keyframe.Value is IGradientBrush)
                 {
-                    gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), keyframe.Cue, keyframe.KeySpline)
+                    gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), () => new GradientBrushAnimator(), keyframe.Cue, keyframe.KeySpline)
                     {
                         Value = keyframe.Value
                     });
@@ -117,7 +116,7 @@ namespace Avalonia.Animation.Animators
             {
                 if (keyframe.Value is ISolidColorBrush)
                 {
-                    solidColorBrushAnimator.Add(new AnimatorKeyFrame(typeof(ISolidColorBrushAnimator), keyframe.Cue, keyframe.KeySpline)
+                    solidColorBrushAnimator.Add(new AnimatorKeyFrame(typeof(ISolidColorBrushAnimator), () => new ISolidColorBrushAnimator(), keyframe.Cue, keyframe.KeySpline)
                     {
                         Value = keyframe.Value
                     });
@@ -137,18 +136,18 @@ namespace Avalonia.Animation.Animators
         {
             if (_brushAnimators.Count > 0 && this[0].Value?.GetType() is Type firstKeyType)
             {
-                foreach (var (match, animatorType) in _brushAnimators)
+                foreach (var (match, animatorType, animatorFactory) in _brushAnimators)
                 {
                     if (!match(firstKeyType))
                         continue;
 
-                    animator = (IAnimator?)Activator.CreateInstance(animatorType);
+                    animator = animatorFactory();
                     if (animator != null)
                     {
                         animator.Property = Property;
                         foreach (var keyframe in this)
                         {
-                            animator.Add(new AnimatorKeyFrame(animatorType, keyframe.Cue, keyframe.KeySpline)
+                            animator.Add(new AnimatorKeyFrame(animatorType, animatorFactory, keyframe.Cue, keyframe.KeySpline)
                             {
                                 Value = keyframe.Value
                             });

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

@@ -6,6 +6,7 @@
     <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
     <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
     <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
+    <IsTrimmable>true</IsTrimmable>
   </PropertyGroup>
   <ItemGroup>
     <EmbeddedResource Include="Assets\*.trie" />

+ 11 - 4
src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs

@@ -55,13 +55,20 @@ namespace Avalonia.Data.Core.Plugins
 
             var methods = type.GetMethods(bindingFlags);
 
-            foreach (MethodInfo methodInfo in methods)
+            foreach (var methodInfo in methods)
             {
                 if (methodInfo.Name == methodName)
                 {
-                    found = methodInfo;
-
-                    break;
+                    var parameters = methodInfo.GetParameters();
+                    if (parameters.Length == 1 && parameters[0].ParameterType == typeof(object))
+                    {
+                        found = methodInfo;
+                        break;
+                    }
+                    else if (parameters.Length == 0)
+                    {
+                        found = methodInfo;
+                    }
                 }
             }
 

+ 11 - 0
src/Avalonia.Base/Layout/LayoutHelper.cs

@@ -251,6 +251,17 @@ namespace Avalonia.Layout
         {
             double newValue;
 
+            // Round the value to avoid FP errors. This is needed because if `value` has a floating
+            // point precision error (e.g. 79.333333333333343) then when it's multiplied by
+            // `dpiScale` and rounded up, it will be rounded up to a value one greater than it
+            // should be.
+#if NET6_0_OR_GREATER
+            value = Math.Round(value, 8, MidpointRounding.ToZero);
+#else
+            // MidpointRounding.ToZero isn't available in netstandard2.0.
+            value = Math.Truncate(value * 1e8) / 1e8;
+#endif
+
             // If DPI == 1, don't use DPI-aware rounding.
             if (!MathUtilities.IsOne(dpiScale))
             {

+ 0 - 1
src/Avalonia.Base/Media/DashStyle.cs

@@ -35,7 +35,6 @@ namespace Avalonia.Media
         /// Initializes a new instance of the <see cref="DashStyle"/> class.
         /// </summary>
         public DashStyle()
-            : this(null, 0)
         {
         }
 

+ 31 - 2
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@@ -29,6 +29,7 @@ public class CompositingRenderer : IRendererWithCompositor
     private bool _queuedUpdate;
     private Action _update;
     private Action _invalidateScene;
+    private bool _updating;
 
     internal CompositionTarget CompositionTarget;
     
@@ -77,6 +78,8 @@ public class CompositingRenderer : IRendererWithCompositor
     /// <inheritdoc/>
     public void AddDirty(IVisual visual)
     {
+        if (_updating)
+            throw new InvalidOperationException("Visual was invalidated during the render pass");
         _dirty.Add((Visual)visual);
         QueueUpdate();
     }
@@ -84,7 +87,16 @@ public class CompositingRenderer : IRendererWithCompositor
     /// <inheritdoc/>
     public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool>? filter)
     {
-        var res = CompositionTarget.TryHitTest(p, filter);
+        Func<CompositionVisual, bool>? f = null;
+        if (filter != null)
+            f = v =>
+            {
+                if (v is CompositionDrawListVisual dlv)
+                    return filter(dlv.Visual);
+                return true;
+            };
+        
+        var res = CompositionTarget.TryHitTest(p, f);
         if(res == null)
             yield break;
         foreach(var v in res)
@@ -107,6 +119,8 @@ public class CompositingRenderer : IRendererWithCompositor
     /// <inheritdoc/>
     public void RecalculateChildren(IVisual visual)
     {
+        if (_updating)
+            throw new InvalidOperationException("Visual was invalidated during the render pass");
         _recalculateChildren.Add((Visual)visual);
         QueueUpdate();
     }
@@ -191,7 +205,7 @@ public class CompositingRenderer : IRendererWithCompositor
     private void InvalidateScene() =>
         SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize)));
 
-    private void Update()
+    private void UpdateCore()
     {
         _queuedUpdate = false;
         foreach (var visual in _dirty)
@@ -240,6 +254,21 @@ public class CompositingRenderer : IRendererWithCompositor
         CompositionTarget.Scaling = _root.RenderScaling;
         Compositor.InvokeOnNextCommit(_invalidateScene);
     }
+
+    private void Update()
+    {
+        if(_updating)
+            return;
+        _updating = true;
+        try
+        {
+            UpdateCore();
+        }
+        finally
+        {
+            _updating = false;
+        }
+    }
     
     public void Resized(Size size)
     {

+ 1 - 3
src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs

@@ -54,13 +54,11 @@ internal class CompositionDrawListVisual : CompositionContainerVisual
         Visual = visual;
     }
 
-    internal override bool HitTest(Point pt, Func<IVisual, bool>? filter)
+    internal override bool HitTest(Point pt)
     {
         var custom = Visual as ICustomHitTest;
         if (DrawList == null && custom == null)
             return false;
-        if (filter != null && !filter(Visual))
-            return false;
         if (custom != null)
         {
             // Simulate the old behavior

+ 7 - 3
src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs

@@ -31,7 +31,7 @@ namespace Avalonia.Rendering.Composition
         /// <param name="point"></param>
         /// <param name="filter"></param>
         /// <returns></returns>
-        public PooledList<CompositionVisual>? TryHitTest(Point point, Func<IVisual, bool>? filter)
+        public PooledList<CompositionVisual>? TryHitTest(Point point, Func<CompositionVisual, bool>? filter)
         {
             Server.Readback.NextRead();
             if (Root == null)
@@ -88,10 +88,14 @@ namespace Avalonia.Rendering.Composition
         }
         
         void HitTestCore(CompositionVisual visual, Point globalPoint, PooledList<CompositionVisual> result,
-            Func<IVisual, bool>? filter)
+            Func<CompositionVisual, bool>? filter)
         {
             if (visual.Visible == false)
                 return;
+            
+            if (filter != null && !filter(visual))
+                return;
+            
             if (!TryTransformTo(visual, globalPoint, out var point))
                 return;
 
@@ -111,7 +115,7 @@ namespace Avalonia.Rendering.Composition
                 }
             
             // Hit-test the current node
-            if (visual.HitTest(point, filter)) 
+            if (visual.HitTest(point)) 
                 result.Add(visual);
         }
 

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

@@ -53,6 +53,6 @@ namespace Avalonia.Rendering.Composition
         
         internal object? Tag { get; set; }
 
-        internal virtual bool HitTest(Point point, Func<IVisual, bool>? filter) => true;
+        internal virtual bool HitTest(Point point) => true;
     }
 }

+ 7 - 26
src/Avalonia.Base/Rendering/DirtyVisuals.cs

@@ -17,8 +17,7 @@ namespace Avalonia.Rendering
     {
         private SortedDictionary<int, List<IVisual>> _inner = new SortedDictionary<int, List<IVisual>>();
         private Dictionary<IVisual, int> _index = new Dictionary<IVisual, int>();
-        private List<IVisual> _deferredChanges = new List<IVisual>();
-        private int _deferring;
+        private int _enumerating;
 
         /// <summary>
         /// Gets the number of dirty visuals.
@@ -31,10 +30,9 @@ namespace Avalonia.Rendering
         /// <param name="visual">The dirty visual.</param>
         public void Add(IVisual visual)
         {
-            if (_deferring > 0)
+            if (_enumerating > 0)
             {
-                _deferredChanges.Add(visual);
-                return;
+                throw new InvalidOperationException("Visual was invalidated during a render pass");
             }
 
             var distance = visual.CalculateDistanceFromAncestor(visual.VisualRoot);
@@ -65,7 +63,7 @@ namespace Avalonia.Rendering
         /// </summary>
         public void Clear()
         {
-            if (_deferring > 0)
+            if (_enumerating > 0)
             {
                 throw new InvalidOperationException("Cannot clear while enumerating");
             }
@@ -80,7 +78,7 @@ namespace Avalonia.Rendering
         /// <returns>A collection of visuals.</returns>
         public IEnumerator<IVisual> GetEnumerator()
         {
-            BeginDefer();
+            _enumerating++;
             try
             {
                 foreach (var i in _inner)
@@ -93,27 +91,10 @@ namespace Avalonia.Rendering
             }
             finally
             {
-                EndDefer();
+                _enumerating--;
             }
         }
-
-        private void BeginDefer()
-        {
-            ++_deferring;
-        }
-
-        private void EndDefer()
-        {
-            if (--_deferring > 0) return;
-
-            foreach (var visual in _deferredChanges)
-            {
-                Add(visual);
-            }
-
-            _deferredChanges.Clear();
-        }
-
+        
         /// <summary>
         /// Gets the dirty visuals, in ascending order of distance to their root.
         /// </summary>

+ 35 - 0
src/Avalonia.Controls.ColorPicker/Converters/DoNothingForNullConverter.cs

@@ -0,0 +1,35 @@
+using System;
+using System.Globalization;
+using Avalonia.Data;
+using Avalonia.Data.Converters;
+
+namespace Avalonia.Controls.Converters
+{
+    /// <summary>
+    /// Converter that will do nothing (not update bound values) when a null value is encountered.
+    /// This converter enables binding nullable with non-nullable properties in some scenarios.
+    /// </summary>
+    public class DoNothingForNullConverter : IValueConverter
+    {
+        /// <inheritdoc/>
+        public object? Convert(
+            object? value,
+            Type targetType,
+            object? parameter,
+            CultureInfo culture)
+        {
+            return value ?? BindingOperations.DoNothing;
+        }
+
+        /// <inheritdoc/>
+        public object? ConvertBack(
+            object? value,
+            Type targetType,
+            object? parameter,
+            CultureInfo culture)
+        {
+            return value ?? BindingOperations.DoNothing;
+        }
+    }
+}
+

+ 2 - 1
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml

@@ -11,6 +11,7 @@
   <pc:ThirdComponentConverter x:Key="ThirdComponentConverter" />
   <converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
   <converters:ColorToHexConverter x:Key="ColorToHexConverter" />
+  <converters:DoNothingForNullConverter x:Key="DoNothingForNullConverter" />
   <globalization:NumberFormatInfo x:Key="ColorViewComponentNumberFormat" NumberDecimalDigits="0" />
   <x:Double x:Key="ColorViewTabStripHeight">48</x:Double>
   <x:Double x:Key="ColorViewComponentLabelWidth">30</x:Double>
@@ -205,7 +206,7 @@
                 </Border>
               </TabItem.Header>
               <ListBox Items="{TemplateBinding PaletteColors}"
-                       SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
+                       SelectedItem="{Binding Color, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource DoNothingForNullConverter}, Mode=TwoWay}"
                        UseLayoutRounding="False"
                        Margin="12">
                 <ListBox.Styles>

+ 4 - 4
src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs

@@ -23,10 +23,10 @@ namespace Avalonia.Controls
     [PseudoClasses(":pressed", ":current", ":expanded")]
     public class DataGridRowGroupHeader : TemplatedControl
     {
-        private const string DATAGRIDROWGROUPHEADER_expanderButton = "ExpanderButton";
-        private const string DATAGRIDROWGROUPHEADER_indentSpacer = "IndentSpacer";
-        private const string DATAGRIDROWGROUPHEADER_itemCountElement = "ItemCountElement";
-        private const string DATAGRIDROWGROUPHEADER_propertyNameElement = "PropertyNameElement";
+        private const string DATAGRIDROWGROUPHEADER_expanderButton = "PART_ExpanderButton";
+        private const string DATAGRIDROWGROUPHEADER_indentSpacer = "PART_IndentSpacer";
+        private const string DATAGRIDROWGROUPHEADER_itemCountElement = "PART_ItemCountElement";
+        private const string DATAGRIDROWGROUPHEADER_propertyNameElement = "PART_PropertyNameElement";
 
         private bool _areIsCheckedHandlersSuspended;
         private ToggleButton _expanderButton;

+ 4 - 5
src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml

@@ -53,7 +53,6 @@
       <Setter Property="VerticalAlignment" Value="Center" />
     </ControlTheme>
     <ControlTheme x:Key="DataGridCellTextBoxTheme" TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
-      <Setter Property="Margin" Value="12,0,12,0" />
       <Setter Property="VerticalAlignment" Value="Stretch" />
       <Setter Property="Background" Value="Transparent" />
       <Style Selector="^ /template/ DataValidationErrors">
@@ -410,9 +409,9 @@
                               ColumnDefinitions="Auto,Auto,Auto,Auto,*"
                               RowDefinitions="*,Auto">
 
-            <Rectangle Name="IndentSpacer"
+            <Rectangle Name="PART_IndentSpacer"
                        Grid.Column="1" />
-            <ToggleButton Name="ExpanderButton"
+            <ToggleButton Name="PART_ExpanderButton"
                           Grid.Column="2"
                           Width="12"
                           Height="12"
@@ -429,14 +428,14 @@
                         Orientation="Horizontal"
                         VerticalAlignment="Center"
                         Margin="12,0,0,0">
-              <TextBlock Name="PropertyNameElement"
+              <TextBlock Name="PART_PropertyNameElement"
                          Margin="4,0,0,0"
                          IsVisible="{TemplateBinding IsPropertyNameVisible}"
                          Foreground="{TemplateBinding Foreground}" />
               <TextBlock Margin="4,0,0,0"
                          Text="{ReflectionBinding Key}"
                          Foreground="{TemplateBinding Foreground}" />
-              <TextBlock Name="ItemCountElement"
+              <TextBlock Name="PART_ItemCountElement"
                          Margin="4,0,0,0"
                          IsVisible="{TemplateBinding IsItemCountVisible}"
                          Foreground="{TemplateBinding Foreground}" />

+ 4 - 4
src/Avalonia.Controls.DataGrid/Themes/Simple.xaml

@@ -258,10 +258,10 @@
                               ColumnDefinitions="Auto,Auto,Auto,Auto"
                               RowDefinitions="Auto,*,Auto">
 
-            <Rectangle Name="IndentSpacer"
+            <Rectangle Name="PART_IndentSpacer"
                        Grid.Row="1"
                        Grid.Column="1" />
-            <ToggleButton Name="ExpanderButton"
+            <ToggleButton Name="PART_ExpanderButton"
                           Grid.Row="1"
                           Grid.Column="2"
                           Margin="2,0,0,0"
@@ -277,12 +277,12 @@
                         Margin="0,1,0,1"
                         VerticalAlignment="Center"
                         Orientation="Horizontal">
-              <TextBlock Name="PropertyNameElement"
+              <TextBlock Name="PART_PropertyNameElement"
                          Margin="4,0,0,0"
                          IsVisible="{TemplateBinding IsPropertyNameVisible}" />
               <TextBlock Margin="4,0,0,0"
                          Text="{Binding Key}" />
-              <TextBlock Name="ItemCountElement"
+              <TextBlock Name="PART_ItemCountElement"
                          Margin="4,0,0,0"
                          IsVisible="{TemplateBinding IsItemCountVisible}" />
             </StackPanel>

+ 2 - 2
src/Avalonia.Controls/Calendar/Calendar.cs

@@ -2109,8 +2109,8 @@ namespace Avalonia.Controls
             RemovedItems = new Collection<DateTime>();
         }
 
-        private const string PART_ElementRoot = "Root";
-        private const string PART_ElementMonth = "CalendarItem";
+        private const string PART_ElementRoot = "PART_Root";
+        private const string PART_ElementMonth = "PART_CalendarItem";
 
         /// <summary>
         /// Builds the visual tree for the

+ 5 - 5
src/Avalonia.Controls/Calendar/CalendarItem.cs

@@ -32,11 +32,11 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         private const int NumberOfDaysPerWeek = 7;
 
-        private const string PART_ElementHeaderButton = "HeaderButton";
-        private const string PART_ElementPreviousButton = "PreviousButton";
-        private const string PART_ElementNextButton = "NextButton";
-        private const string PART_ElementMonthView = "MonthView";
-        private const string PART_ElementYearView = "YearView";
+        private const string PART_ElementHeaderButton = "PART_HeaderButton";
+        private const string PART_ElementPreviousButton = "PART_PreviousButton";
+        private const string PART_ElementNextButton = "PART_NextButton";
+        private const string PART_ElementMonthView = "PART_MonthView";
+        private const string PART_ElementYearView = "PART_YearView";
 
         private Button? _headerButton;
         private Button? _nextButton;

+ 18 - 18
src/Avalonia.Controls/DateTimePickers/DatePicker.cs

@@ -14,15 +14,15 @@ namespace Avalonia.Controls
     /// <summary>
     /// A control to allow the user to select a date
     /// </summary>
-    [TemplatePart("ButtonContentGrid", typeof(Grid))]
-    [TemplatePart("DayText",           typeof(TextBlock))]
-    [TemplatePart("FirstSpacer",       typeof(Rectangle))]
-    [TemplatePart("FlyoutButton",      typeof(Button))]
-    [TemplatePart("MonthText",         typeof(TextBlock))]
-    [TemplatePart("PickerPresenter",   typeof(DatePickerPresenter))]
-    [TemplatePart("Popup",             typeof(Popup))]
-    [TemplatePart("SecondSpacer",      typeof(Rectangle))]
-    [TemplatePart("YearText",          typeof(TextBlock))]
+    [TemplatePart("PART_ButtonContentGrid", typeof(Grid))]
+    [TemplatePart("PART_DayTextBlock",      typeof(TextBlock))]
+    [TemplatePart("PART_FirstSpacer",       typeof(Rectangle))]
+    [TemplatePart("PART_FlyoutButton",      typeof(Button))]
+    [TemplatePart("PART_MonthTextBlock",    typeof(TextBlock))]
+    [TemplatePart("PART_PickerPresenter",   typeof(DatePickerPresenter))]
+    [TemplatePart("PART_Popup",             typeof(Popup))]
+    [TemplatePart("PART_SecondSpacer",      typeof(Rectangle))]
+    [TemplatePart("PART_YearTextBlock",     typeof(TextBlock))]
     [PseudoClasses(":hasnodate")]
     public class DatePicker : TemplatedControl
     {
@@ -280,15 +280,15 @@ namespace Avalonia.Controls
             }
 
             base.OnApplyTemplate(e);
-            _flyoutButton = e.NameScope.Find<Button>("FlyoutButton");
-            _dayText = e.NameScope.Find<TextBlock>("DayText");
-            _monthText = e.NameScope.Find<TextBlock>("MonthText");
-            _yearText = e.NameScope.Find<TextBlock>("YearText");
-            _container = e.NameScope.Find<Grid>("ButtonContentGrid");
-            _spacer1 = e.NameScope.Find<Rectangle>("FirstSpacer");
-            _spacer2 = e.NameScope.Find<Rectangle>("SecondSpacer");
-            _popup = e.NameScope.Find<Popup>("Popup");
-            _presenter = e.NameScope.Find<DatePickerPresenter>("PickerPresenter");
+            _flyoutButton = e.NameScope.Find<Button>("PART_FlyoutButton");
+            _dayText = e.NameScope.Find<TextBlock>("PART_DayTextBlock");
+            _monthText = e.NameScope.Find<TextBlock>("PART_MonthTextBlock");
+            _yearText = e.NameScope.Find<TextBlock>("PART_YearTextBlock");
+            _container = e.NameScope.Find<Grid>("PART_ButtonContentGrid");
+            _spacer1 = e.NameScope.Find<Rectangle>("PART_FirstSpacer");
+            _spacer2 = e.NameScope.Find<Rectangle>("PART_SecondSpacer");
+            _popup = e.NameScope.Find<Popup>("PART_Popup");
+            _presenter = e.NameScope.Find<DatePickerPresenter>("PART_PickerPresenter");
 
             _areControlsAvailable = true;
 

+ 34 - 34
src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs

@@ -13,23 +13,23 @@ namespace Avalonia.Controls
     /// Defines the presenter used for selecting a date for a 
     /// <see cref="DatePicker"/>
     /// </summary>
-    [TemplatePart("AcceptButton",    typeof(Button))]
-    [TemplatePart("DayDownButton",   typeof(RepeatButton))]
-    [TemplatePart("DayHost",         typeof(Panel))]
-    [TemplatePart("DaySelector",     typeof(DateTimePickerPanel))]
-    [TemplatePart("DayUpButton",     typeof(RepeatButton))]
-    [TemplatePart("DismissButton",   typeof(Button))]
-    [TemplatePart("FirstSpacer",     typeof(Rectangle))]
-    [TemplatePart("MonthDownButton", typeof(RepeatButton))]
-    [TemplatePart("MonthHost",       typeof(Panel))]
-    [TemplatePart("MonthSelector",   typeof(DateTimePickerPanel))]
-    [TemplatePart("MonthUpButton",   typeof(RepeatButton))]
-    [TemplatePart("PickerContainer", typeof(Grid))]
-    [TemplatePart("SecondSpacer",    typeof(Rectangle))]
-    [TemplatePart("YearDownButton",  typeof(RepeatButton))]
-    [TemplatePart("YearHost",        typeof(Panel))]
-    [TemplatePart("YearSelector",    typeof(DateTimePickerPanel))]
-    [TemplatePart("YearUpButton",    typeof(RepeatButton))]
+    [TemplatePart("PART_AcceptButton",    typeof(Button))]
+    [TemplatePart("PART_DayDownButton",   typeof(RepeatButton))]
+    [TemplatePart("PART_DayHost",         typeof(Panel))]
+    [TemplatePart("PART_DaySelector",     typeof(DateTimePickerPanel))]
+    [TemplatePart("PART_DayUpButton",     typeof(RepeatButton))]
+    [TemplatePart("PART_DismissButton",   typeof(Button))]
+    [TemplatePart("PART_FirstSpacer",     typeof(Rectangle))]
+    [TemplatePart("PART_MonthDownButton", typeof(RepeatButton))]
+    [TemplatePart("PART_MonthHost",       typeof(Panel))]
+    [TemplatePart("PART_MonthSelector",   typeof(DateTimePickerPanel))]
+    [TemplatePart("PART_MonthUpButton",   typeof(RepeatButton))]
+    [TemplatePart("PART_PickerContainer", typeof(Grid))]
+    [TemplatePart("PART_SecondSpacer",    typeof(Rectangle))]
+    [TemplatePart("PART_YearDownButton",  typeof(RepeatButton))]
+    [TemplatePart("PART_YearHost",        typeof(Panel))]
+    [TemplatePart("PART_YearSelector",    typeof(DateTimePickerPanel))]
+    [TemplatePart("PART_YearUpButton",    typeof(RepeatButton))]
     public class DatePickerPresenter : PickerPresenterBase
     {
         /// <summary>
@@ -253,58 +253,58 @@ namespace Avalonia.Controls
         {
             base.OnApplyTemplate(e);
             // These are requirements, so throw if not found
-            _pickerContainer = e.NameScope.Get<Grid>("PickerContainer");
-            _monthHost = e.NameScope.Get<Panel>("MonthHost");
-            _dayHost = e.NameScope.Get<Panel>("DayHost");
-            _yearHost = e.NameScope.Get<Panel>("YearHost");
+            _pickerContainer = e.NameScope.Get<Grid>("PART_PickerContainer");
+            _monthHost = e.NameScope.Get<Panel>("PART_MonthHost");
+            _dayHost = e.NameScope.Get<Panel>("PART_DayHost");
+            _yearHost = e.NameScope.Get<Panel>("PART_YearHost");
 
-            _monthSelector = e.NameScope.Get<DateTimePickerPanel>("MonthSelector");
+            _monthSelector = e.NameScope.Get<DateTimePickerPanel>("PART_MonthSelector");
             _monthSelector.SelectionChanged += OnMonthChanged;
 
-            _daySelector = e.NameScope.Get<DateTimePickerPanel>("DaySelector");
+            _daySelector = e.NameScope.Get<DateTimePickerPanel>("PART_DaySelector");
             _daySelector.SelectionChanged += OnDayChanged;
 
-            _yearSelector = e.NameScope.Get<DateTimePickerPanel>("YearSelector");
+            _yearSelector = e.NameScope.Get<DateTimePickerPanel>("PART_YearSelector");
             _yearSelector.SelectionChanged += OnYearChanged;
 
-            _acceptButton = e.NameScope.Get<Button>("AcceptButton");
+            _acceptButton = e.NameScope.Get<Button>("PART_AcceptButton");
 
-            _monthUpButton = e.NameScope.Find<RepeatButton>("MonthUpButton");
+            _monthUpButton = e.NameScope.Find<RepeatButton>("PART_MonthUpButton");
             if (_monthUpButton != null)
             {
                 _monthUpButton.Click += OnSelectorButtonClick;
             }
-            _monthDownButton = e.NameScope.Find<RepeatButton>("MonthDownButton");
+            _monthDownButton = e.NameScope.Find<RepeatButton>("PART_MonthDownButton");
             if (_monthDownButton != null)
             {
                 _monthDownButton.Click += OnSelectorButtonClick;
             }
 
-            _dayUpButton = e.NameScope.Find<RepeatButton>("DayUpButton");
+            _dayUpButton = e.NameScope.Find<RepeatButton>("PART_DayUpButton");
             if (_dayUpButton != null)
             {
                 _dayUpButton.Click += OnSelectorButtonClick;
             }
-            _dayDownButton = e.NameScope.Find<RepeatButton>("DayDownButton");
+            _dayDownButton = e.NameScope.Find<RepeatButton>("PART_DayDownButton");
             if (_dayDownButton != null)
             {
                 _dayDownButton.Click += OnSelectorButtonClick;
             }
 
-            _yearUpButton = e.NameScope.Find<RepeatButton>("YearUpButton");
+            _yearUpButton = e.NameScope.Find<RepeatButton>("PART_YearUpButton");
             if (_yearUpButton != null)
             {
                 _yearUpButton.Click += OnSelectorButtonClick;
             }
-            _yearDownButton = e.NameScope.Find<RepeatButton>("YearDownButton");
+            _yearDownButton = e.NameScope.Find<RepeatButton>("PART_YearDownButton");
             if (_yearDownButton != null)
             {
                 _yearDownButton.Click += OnSelectorButtonClick;
             }
 
-            _dismissButton = e.NameScope.Find<Button>("DismissButton");
-            _spacer1 = e.NameScope.Find<Rectangle>("FirstSpacer");
-            _spacer2 = e.NameScope.Find<Rectangle>("SecondSpacer");
+            _dismissButton = e.NameScope.Find<Button>("PART_DismissButton");
+            _spacer1 = e.NameScope.Find<Rectangle>("PART_FirstSpacer");
+            _spacer2 = e.NameScope.Find<Rectangle>("PART_SecondSpacer");
 
             if (_acceptButton != null)
             {

+ 24 - 24
src/Avalonia.Controls/DateTimePickers/TimePicker.cs

@@ -12,18 +12,18 @@ namespace Avalonia.Controls
     /// <summary>
     /// A control to allow the user to select a time.
     /// </summary>
-    [TemplatePart("FirstColumnDivider",      typeof(Rectangle))]
-    [TemplatePart("FirstPickerHost",         typeof(Border))]
-    [TemplatePart("FlyoutButton",            typeof(Button))]
-    [TemplatePart("FlyoutButtonContentGrid", typeof(Grid))]
-    [TemplatePart("HourTextBlock",           typeof(TextBlock))]
-    [TemplatePart("MinuteTextBlock",         typeof(TextBlock))]
-    [TemplatePart("PeriodTextBlock",         typeof(TextBlock))]
-    [TemplatePart("PickerPresenter",         typeof(TimePickerPresenter))]
-    [TemplatePart("Popup",                   typeof(Popup))]
-    [TemplatePart("SecondColumnDivider",     typeof(Rectangle))]
-    [TemplatePart("SecondPickerHost",        typeof(Border))]
-    [TemplatePart("ThirdPickerHost",         typeof(Border))]
+    [TemplatePart("PART_FirstColumnDivider",      typeof(Rectangle))]
+    [TemplatePart("PART_FirstPickerHost",         typeof(Border))]
+    [TemplatePart("PART_FlyoutButton",            typeof(Button))]
+    [TemplatePart("PART_FlyoutButtonContentGrid", typeof(Grid))]
+    [TemplatePart("PART_HourTextBlock",           typeof(TextBlock))]
+    [TemplatePart("PART_MinuteTextBlock",         typeof(TextBlock))]
+    [TemplatePart("PART_PeriodTextBlock",         typeof(TextBlock))]
+    [TemplatePart("PART_PickerPresenter",         typeof(TimePickerPresenter))]
+    [TemplatePart("PART_Popup",                   typeof(Popup))]
+    [TemplatePart("PART_SecondColumnDivider",     typeof(Rectangle))]
+    [TemplatePart("PART_SecondPickerHost",        typeof(Border))]
+    [TemplatePart("PART_ThirdPickerHost",         typeof(Border))]
     [PseudoClasses(":hasnotime")]
     public class TimePicker : TemplatedControl
     {
@@ -169,23 +169,23 @@ namespace Avalonia.Controls
             }
             base.OnApplyTemplate(e);
 
-            _flyoutButton = e.NameScope.Find<Button>("FlyoutButton");
+            _flyoutButton = e.NameScope.Find<Button>("PART_FlyoutButton");
 
-            _firstPickerHost = e.NameScope.Find<Border>("FirstPickerHost");
-            _secondPickerHost = e.NameScope.Find<Border>("SecondPickerHost");
-            _thirdPickerHost = e.NameScope.Find<Border>("ThirdPickerHost");
+            _firstPickerHost = e.NameScope.Find<Border>("PART_FirstPickerHost");
+            _secondPickerHost = e.NameScope.Find<Border>("PART_SecondPickerHost");
+            _thirdPickerHost = e.NameScope.Find<Border>("PART_ThirdPickerHost");
 
-            _hourText = e.NameScope.Find<TextBlock>("HourTextBlock");
-            _minuteText = e.NameScope.Find<TextBlock>("MinuteTextBlock");
-            _periodText = e.NameScope.Find<TextBlock>("PeriodTextBlock");
+            _hourText = e.NameScope.Find<TextBlock>("PART_HourTextBlock");
+            _minuteText = e.NameScope.Find<TextBlock>("PART_MinuteTextBlock");
+            _periodText = e.NameScope.Find<TextBlock>("PART_PeriodTextBlock");
 
-            _firstSplitter = e.NameScope.Find<Rectangle>("FirstColumnDivider");
-            _secondSplitter = e.NameScope.Find<Rectangle>("SecondColumnDivider");
+            _firstSplitter = e.NameScope.Find<Rectangle>("PART_FirstColumnDivider");
+            _secondSplitter = e.NameScope.Find<Rectangle>("PART_SecondColumnDivider");
 
-            _contentGrid = e.NameScope.Find<Grid>("FlyoutButtonContentGrid");
+            _contentGrid = e.NameScope.Find<Grid>("PART_FlyoutButtonContentGrid");
 
-            _popup = e.NameScope.Find<Popup>("Popup");
-            _presenter = e.NameScope.Find<TimePickerPresenter>("PickerPresenter");
+            _popup = e.NameScope.Find<Popup>("PART_Popup");
+            _presenter = e.NameScope.Find<TimePickerPresenter>("PART_PickerPresenter");
 
             if (_flyoutButton != null)
                 _flyoutButton.Click += OnFlyoutButtonClicked;

+ 28 - 28
src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs

@@ -11,20 +11,20 @@ namespace Avalonia.Controls
     /// Defines the presenter used for selecting a time. Intended for use with
     /// <see cref="TimePicker"/> but can be used independently
     /// </summary>
-    [TemplatePart("AcceptButton",     typeof(Button))]
-    [TemplatePart("DismissButton",    typeof(Button))]
-    [TemplatePart("HourDownButton",   typeof(RepeatButton))]
-    [TemplatePart("HourSelector",     typeof(DateTimePickerPanel))]
-    [TemplatePart("HourUpButton",     typeof(RepeatButton))]
-    [TemplatePart("MinuteDownButton", typeof(RepeatButton))]
-    [TemplatePart("MinuteSelector",   typeof(DateTimePickerPanel))]
-    [TemplatePart("MinuteUpButton",   typeof(RepeatButton))]
-    [TemplatePart("PeriodDownButton", typeof(RepeatButton))]
-    [TemplatePart("PeriodHost",       typeof(Panel))]
-    [TemplatePart("PeriodSelector",   typeof(DateTimePickerPanel))]
-    [TemplatePart("PeriodUpButton",   typeof(RepeatButton))]
-    [TemplatePart("PickerContainer",  typeof(Grid))]
-    [TemplatePart("SecondSpacer",     typeof(Rectangle))]
+    [TemplatePart("PART_AcceptButton",     typeof(Button))]
+    [TemplatePart("PART_DismissButton",    typeof(Button))]
+    [TemplatePart("PART_HourDownButton",   typeof(RepeatButton))]
+    [TemplatePart("PART_HourSelector",     typeof(DateTimePickerPanel))]
+    [TemplatePart("PART_HourUpButton",     typeof(RepeatButton))]
+    [TemplatePart("PART_MinuteDownButton", typeof(RepeatButton))]
+    [TemplatePart("PART_MinuteSelector",   typeof(DateTimePickerPanel))]
+    [TemplatePart("PART_MinuteUpButton",   typeof(RepeatButton))]
+    [TemplatePart("PART_PeriodDownButton", typeof(RepeatButton))]
+    [TemplatePart("PART_PeriodHost",       typeof(Panel))]
+    [TemplatePart("PART_PeriodSelector",   typeof(DateTimePickerPanel))]
+    [TemplatePart("PART_PeriodUpButton",   typeof(RepeatButton))]
+    [TemplatePart("PART_PickerContainer",  typeof(Grid))]
+    [TemplatePart("PART_SecondSpacer",     typeof(Rectangle))]
     public class TimePickerPresenter : PickerPresenterBase
     {
         /// <summary>
@@ -122,40 +122,40 @@ namespace Avalonia.Controls
         {
             base.OnApplyTemplate(e);
 
-            _pickerContainer = e.NameScope.Get<Grid>("PickerContainer");
-            _periodHost = e.NameScope.Get<Panel>("PeriodHost");
+            _pickerContainer = e.NameScope.Get<Grid>("PART_PickerContainer");
+            _periodHost = e.NameScope.Get<Panel>("PART_PeriodHost");
 
-            _hourSelector = e.NameScope.Get<DateTimePickerPanel>("HourSelector");
-            _minuteSelector = e.NameScope.Get<DateTimePickerPanel>("MinuteSelector");
-            _periodSelector = e.NameScope.Get<DateTimePickerPanel>("PeriodSelector");
+            _hourSelector = e.NameScope.Get<DateTimePickerPanel>("PART_HourSelector");
+            _minuteSelector = e.NameScope.Get<DateTimePickerPanel>("PART_MinuteSelector");
+            _periodSelector = e.NameScope.Get<DateTimePickerPanel>("PART_PeriodSelector");
 
-            _spacer2 = e.NameScope.Get<Rectangle>("SecondSpacer");
+            _spacer2 = e.NameScope.Get<Rectangle>("PART_SecondSpacer");
 
-            _acceptButton = e.NameScope.Get<Button>("AcceptButton");
+            _acceptButton = e.NameScope.Get<Button>("PART_AcceptButton");
             _acceptButton.Click += OnAcceptButtonClicked;
 
-            _hourUpButton = e.NameScope.Find<RepeatButton>("HourUpButton");
+            _hourUpButton = e.NameScope.Find<RepeatButton>("PART_HourUpButton");
             if (_hourUpButton != null)
                 _hourUpButton.Click += OnSelectorButtonClick;
-            _hourDownButton = e.NameScope.Find<RepeatButton>("HourDownButton");
+            _hourDownButton = e.NameScope.Find<RepeatButton>("PART_HourDownButton");
             if (_hourDownButton != null)
                 _hourDownButton.Click += OnSelectorButtonClick;
 
-            _minuteUpButton = e.NameScope.Find<RepeatButton>("MinuteUpButton");
+            _minuteUpButton = e.NameScope.Find<RepeatButton>("PART_MinuteUpButton");
             if (_minuteUpButton != null)
                 _minuteUpButton.Click += OnSelectorButtonClick;
-            _minuteDownButton = e.NameScope.Find<RepeatButton>("MinuteDownButton");
+            _minuteDownButton = e.NameScope.Find<RepeatButton>("PART_MinuteDownButton");
             if (_minuteDownButton != null)
                 _minuteDownButton.Click += OnSelectorButtonClick;
 
-            _periodUpButton = e.NameScope.Find<RepeatButton>("PeriodUpButton");
+            _periodUpButton = e.NameScope.Find<RepeatButton>("PART_PeriodUpButton");
             if (_periodUpButton != null)
                 _periodUpButton.Click += OnSelectorButtonClick;
-            _periodDownButton = e.NameScope.Find<RepeatButton>("PeriodDownButton");
+            _periodDownButton = e.NameScope.Find<RepeatButton>("PART_PeriodDownButton");
             if (_periodDownButton != null)
                 _periodDownButton.Click += OnSelectorButtonClick;
 
-            _dismissButton = e.NameScope.Find<Button>("DismissButton");
+            _dismissButton = e.NameScope.Find<Button>("PART_DismissButton");
             if (_dismissButton != null)
                 _dismissButton.Click += OnDismissButtonClicked;
 

+ 2 - 13
src/Avalonia.Controls/Documents/InlineUIContainer.cs

@@ -87,18 +87,7 @@ namespace Avalonia.Controls.Documents
 
             public override TextRunProperties? Properties { get; }
 
-            public override Size Size
-            {
-                get
-                {
-                    if (!Control.IsMeasureValid)
-                    {
-                        Control.Measure(Size.Infinity);
-                    }
-
-                    return Control.DesiredSize;
-                }
-            }
+            public override Size Size => Control.DesiredSize;
 
             public override double Baseline
             {
@@ -118,7 +107,7 @@ namespace Avalonia.Controls.Documents
 
             public override void Draw(DrawingContext drawingContext, Point origin)
             {             
-                Control.Arrange(new Rect(origin, Size));                
+                //noop            
             }
         }
     }

+ 61 - 0
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@@ -5,6 +5,7 @@ using System.Linq;
 using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
 using Avalonia.Data;
+using Avalonia.Data.Converters;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.Layout;
@@ -96,6 +97,13 @@ namespace Avalonia.Controls
             AvaloniaProperty.RegisterDirect<NumericUpDown, string?>(nameof(Text), o => o.Text, (o, v) => o.Text = v,
                 defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
 
+        /// <summary>
+        /// Defines the <see cref="TextConverter"/> property.
+        /// </summary>
+        public static readonly DirectProperty<NumericUpDown, IValueConverter?> TextConverterProperty =
+            AvaloniaProperty.RegisterDirect<NumericUpDown, IValueConverter?>(nameof(TextConverter),
+                updown => updown.TextConverter, (o, v) => o.TextConverter = v, null, BindingMode.OneWay, false);
+
         /// <summary>
         /// Defines the <see cref="Value"/> property.
         /// </summary>
@@ -125,6 +133,7 @@ namespace Avalonia.Controls
 
         private decimal? _value;
         private string? _text;
+        private IValueConverter? _textConverter;
         private bool _internalValueSet;
         private bool _clipValueToMinMax;
         private bool _isSyncingTextAndValueProperties;
@@ -234,6 +243,8 @@ namespace Avalonia.Controls
 
         /// <summary>
         /// Gets or sets the parsing style (AllowLeadingWhite, Float, AllowHexSpecifier, ...). By default, Any.
+        /// Note that Hex style does not work with decimal. 
+        /// For hexadecimal display, use <see cref="TextConverter"/>.
         /// </summary>
         public NumberStyles ParsingNumberStyle
         {
@@ -250,6 +261,17 @@ namespace Avalonia.Controls
             set { SetAndRaise(TextProperty, ref _text, value); }
         }
 
+        /// <summary>
+        /// Gets or sets the custom bidirectional Text-Value converter.
+        /// Non-null converter overrides <see cref="ParsingNumberStyle"/>, providing finer control over 
+        /// string representation of the underlying value.
+        /// </summary>
+        public IValueConverter? TextConverter
+        {
+            get { return _textConverter; }
+            set { SetAndRaise(TextConverterProperty, ref _textConverter, value); }
+        }
+
         /// <summary>
         /// Gets or sets the value.
         /// </summary>
@@ -318,6 +340,7 @@ namespace Avalonia.Controls
             MaximumProperty.Changed.Subscribe(OnMaximumChanged);
             MinimumProperty.Changed.Subscribe(OnMinimumChanged);
             TextProperty.Changed.Subscribe(OnTextChanged);
+            TextConverterProperty.Changed.Subscribe(OnTextConverterChanged);
             ValueProperty.Changed.Subscribe(OnValueChanged);
         }
 
@@ -484,6 +507,19 @@ namespace Avalonia.Controls
                 SyncTextAndValueProperties(true, Text);
             }
         }
+        
+        /// <summary>
+        /// Called when the <see cref="Text"/> property value changed.
+        /// </summary>
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnTextConverterChanged(IValueConverter? oldValue, IValueConverter? newValue)
+        {
+            if (IsInitialized)
+            {
+                SyncTextAndValueProperties(false, null);
+            }
+        }
 
         /// <summary>
         /// Called when the <see cref="Value"/> property value changed.
@@ -611,6 +647,10 @@ namespace Avalonia.Controls
         /// <returns></returns>
         private string? ConvertValueToText()
         {
+            if (TextConverter != null)
+            {
+                return TextConverter.ConvertBack(Value, typeof(string), null, CultureInfo.CurrentCulture)?.ToString();
+            }
             //Manage FormatString of type "{}{0:N2} °" (in xaml) or "{0:N2} °" in code-behind.
             if (FormatString.Contains("{0"))
             {
@@ -787,6 +827,21 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Called when the <see cref="TextConverter"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void OnTextConverterChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is NumericUpDown upDown)
+            {
+                var oldValue = (IValueConverter?)e.OldValue;
+                var newValue = (IValueConverter?)e.NewValue;
+                upDown.OnTextConverterChanged(oldValue, newValue);
+            }
+        }
+
+
         /// <summary>
         /// Called when the <see cref="Value"/> property value changed.
         /// </summary>
@@ -1011,6 +1066,12 @@ namespace Avalonia.Controls
                 return null;
             }
             
+            if (TextConverter != null)
+            {
+                var valueFromText = TextConverter.Convert(text, typeof(decimal?), null, CultureInfo.CurrentCulture);
+                return (decimal?)valueFromText;
+            }
+
             if (IsPercent(FormatString))
             {
                 result = ParsePercent(text, NumberFormat);

+ 26 - 0
src/Avalonia.Controls/RichTextBlock.cs

@@ -544,6 +544,32 @@ namespace Avalonia.Controls
             }
         }
 
+        protected override Size MeasureOverride(Size availableSize)
+        {
+            foreach (var child in VisualChildren)
+            {
+                if (child is Control control)
+                {
+                    control.Measure(Size.Infinity);
+                }
+            }
+            
+            return base.MeasureOverride(availableSize);
+        }
+
+        protected override Size ArrangeOverride(Size finalSize)
+        {
+            foreach (var child in VisualChildren)
+            {
+                if (child is Control control)
+                {
+                    control.Arrange(new Rect(control.DesiredSize));
+                }
+            }
+            
+            return base.ArrangeOverride(finalSize);
+        }
+
         private string GetSelection()
         {
             if (!IsTextSelectionEnabled)

+ 6 - 4
src/Avalonia.Controls/ToggleSwitch.cs

@@ -9,8 +9,10 @@ namespace Avalonia.Controls
     /// <summary>
     /// A Toggle Switch control.
     /// </summary>
-    [TemplatePart("MovingKnobs", typeof(Panel))]
-    [TemplatePart("SwitchKnob",  typeof(Panel))]
+    [TemplatePart("PART_MovingKnobs",         typeof(Panel))]
+    [TemplatePart("PART_OffContentPresenter", typeof(ContentPresenter))]
+    [TemplatePart("PART_OnContentPresenter",  typeof(ContentPresenter))]
+    [TemplatePart("PART_SwitchKnob",          typeof(Panel))]
     [PseudoClasses(":dragging")]
     public class ToggleSwitch : ToggleButton
     {
@@ -163,8 +165,8 @@ namespace Avalonia.Controls
         {
             base.OnApplyTemplate(e);
 
-            _switchKnob = e.NameScope.Find<Panel>("SwitchKnob");
-            _knobsPanel = e.NameScope.Get<Panel>("MovingKnobs");
+            _switchKnob = e.NameScope.Find<Panel>("PART_SwitchKnob");
+            _knobsPanel = e.NameScope.Get<Panel>("PART_MovingKnobs");
             
             _knobsPanel.PointerPressed += KnobsPanel_PointerPressed;
             _knobsPanel.PointerReleased += KnobsPanel_PointerReleased;

+ 1 - 0
src/Avalonia.Controls/TreeView.cs

@@ -495,6 +495,7 @@ namespace Avalonia.Controls
                     break;
 
                 case NavigationDirection.Down:
+                case NavigationDirection.Right:
                     if (from?.IsExpanded == true && intoChildren && from.ItemCount > 0)
                     {
                         result = (TreeViewItem)from.ItemContainerGenerator.ContainerFromIndex(0)!;

+ 14 - 5
src/Avalonia.Controls/TreeViewItem.cs

@@ -157,17 +157,26 @@ namespace Avalonia.Controls
                 switch (e.Key)
                 {
                     case Key.Right:
-                        if (Items != null && Items.Cast<object>().Any())
+                        if (Items != null && Items.Cast<object>().Any() && !IsExpanded)
                         {
                             IsExpanded = true;
+                            e.Handled = true;
                         }
-
-                        e.Handled = true;
                         break;
 
                     case Key.Left:
-                        IsExpanded = false;
-                        e.Handled = true;
+                        if (Items is not null && Items.Cast<object>().Any() && IsExpanded)
+                        {
+                            if (IsFocused)
+                            {
+                                IsExpanded = false;
+                            }
+                            else
+                            {
+                                FocusManager.Instance?.Focus(this, NavigationMethod.Directional);
+                            }
+                            e.Handled = true;
+                        }
                         break;
                 }
             }

+ 1 - 1
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@@ -61,7 +61,7 @@ namespace Avalonia.Controls.Utils
                     _backgroundGeometryCache = null;
                 }
 
-                if (boundRect.Width != 0 && innerRect.Height != 0)
+                if (boundRect.Width != 0 && boundRect.Height != 0)
                 {
                     var borderGeometryKeypoints =
                         new BorderGeometryKeypoints(boundRect, borderThickness, cornerRadius, false);

+ 6 - 2
src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs

@@ -168,8 +168,12 @@ namespace Avalonia.DesignerSupport.Remote
             var entryPoint = asm.EntryPoint;
             if (entryPoint == null)
                 throw Die($"Assembly {args.AppPath} doesn't have an entry point");
-            var builderMethod = entryPoint.DeclaringType.GetMethod(BuilderMethodName,
-                BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, Array.Empty<Type>(), null);
+            var builderMethod = entryPoint.DeclaringType.GetMethod(
+                BuilderMethodName,
+                BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy,
+                null,
+                Array.Empty<Type>(),
+                null);
             if (builderMethod == null)
                 throw Die($"{entryPoint.DeclaringType.FullName} doesn't have a method named {BuilderMethodName}");
             Design.IsDesignMode = true;

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

@@ -10,8 +10,8 @@ using Avalonia.LogicalTree;
 
 namespace Avalonia.Dialogs
 {
-    [TemplatePart("QuickLinks", typeof(Control))]
-    [TemplatePart("Files",      typeof(ListBox))]
+    [TemplatePart("PART_QuickLinks", typeof(Control))]
+    [TemplatePart("PART_Files",      typeof(ListBox))]
     public class ManagedFileChooser : TemplatedControl
     {
         private Control _quickLinksRoot;
@@ -90,8 +90,8 @@ namespace Avalonia.Dialogs
 
         protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
         {
-            _quickLinksRoot = e.NameScope.Get<Control>("QuickLinks");
-            _filesView = e.NameScope.Get<ListBox>("Files");
+            _quickLinksRoot = e.NameScope.Get<Control>("PART_QuickLinks");
+            _filesView = e.NameScope.Get<ListBox>("PART_Files");
         }
     }
 }

+ 3 - 3
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@@ -72,7 +72,7 @@ namespace Avalonia.FreeDesktop
             using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException);
             var uris = await tsc.Task ?? Array.Empty<string>();
 
-            return uris.Select(path => new BclStorageFile(new FileInfo(new Uri(path).AbsolutePath))).ToList();
+            return uris.Select(path => new BclStorageFile(new FileInfo(new Uri(path).LocalPath))).ToList();
         }
 
         public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
@@ -96,7 +96,7 @@ namespace Avalonia.FreeDesktop
             var tsc = new TaskCompletionSource<string[]?>();
             using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException);
             var uris = await tsc.Task;
-            var path = uris?.FirstOrDefault() is { } filePath ? new Uri(filePath).AbsolutePath : null;
+            var path = uris?.FirstOrDefault() is { } filePath ? new Uri(filePath).LocalPath : null;
 
             if (path is null)
             {
@@ -126,7 +126,7 @@ namespace Avalonia.FreeDesktop
             var uris = await tsc.Task ?? Array.Empty<string>();
 
             return uris
-                .Select(path => new Uri(path).AbsolutePath)
+                .Select(path => new Uri(path).LocalPath)
                 // WSL2 freedesktop allows to select files as well in directory picker, filter it out.
                 .Where(Directory.Exists)
                 .Select(path => new BclStorageFolder(new DirectoryInfo(path))).ToList();

+ 1 - 0
src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml

@@ -65,6 +65,7 @@
     </Style>
 
     <Style Selector="^:disabled /template/ ContentPresenter#PART_ContentPresenter">
+      <Setter Property="Background" Value="{DynamicResource RepeatButtonBackgroundDisabled}" />
       <Setter Property="Foreground" Value="{DynamicResource RepeatButtonForegroundDisabled}" />
     </Style>
   </ControlTheme>

+ 2 - 2
src/Avalonia.Themes.Fluent/Controls/Calendar.xaml

@@ -23,11 +23,11 @@
     <Setter Property="Template">
       <ControlTemplate>
         <StackPanel
-            Name="Root"
+            Name="PART_Root"
             HorizontalAlignment="Center"
             ClipToBounds="True">
           <CalendarItem
-              Name="CalendarItem"
+              Name="PART_CalendarItem"
               Background="{TemplateBinding Background}"
               BorderBrush="{TemplateBinding BorderBrush}"
               BorderThickness="{TemplateBinding BorderThickness}"

+ 5 - 5
src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml

@@ -79,12 +79,12 @@
                  -->
           <Grid VerticalAlignment="Stretch" HorizontalAlignment="Stretch" RowDefinitions="40,*" MinWidth="294">
             <Grid ColumnDefinitions="5*,*,*">
-              <Button Name="HeaderButton"
+              <Button Name="PART_HeaderButton"
                       Theme="{StaticResource FluentCalendarButton}"
                       Foreground="{TemplateBinding Foreground}"
                       Padding="12,0,0,0"
                       HorizontalContentAlignment="Left" />
-              <Button Name="PreviousButton"
+              <Button Name="PART_PreviousButton"
                       Grid.Column="1"
                       Theme="{StaticResource FluentCalendarButton}"
                       Foreground="{TemplateBinding Foreground}"
@@ -97,7 +97,7 @@
                       HorizontalAlignment="Center" 
                       VerticalAlignment="Center" />
               </Button>
-              <Button Name="NextButton"
+              <Button Name="PART_NextButton"
                       Grid.Column="2"
                       Theme="{StaticResource FluentCalendarButton}"
                       Foreground="{TemplateBinding Foreground}"
@@ -113,7 +113,7 @@
             </Grid>
             <!--Border below is used only for MonthView but it can't be moved inside of Grid because CalendarItem expects it to be empty and it will cause side-effects-->
             <Border Name="BackgroundLayer" Background="{TemplateBinding BorderBrush}" Margin="0,38,0,0" IsVisible="{ReflectionBinding #MonthView.IsVisible}" Grid.Row="1" />
-            <Grid Name="MonthView" Grid.Row="1" IsVisible="False" MinHeight="290">
+            <Grid Name="PART_MonthView" Grid.Row="1" IsVisible="False" MinHeight="290">
               <Grid.RowDefinitions>
                 <!--This should always be the week day names??-->
                 <RowDefinition Height="38" />
@@ -134,7 +134,7 @@
                 <ColumnDefinition Width="Auto" />
               </Grid.ColumnDefinitions>
             </Grid>
-            <Grid Name="YearView"
+            <Grid Name="PART_YearView"
                   Background="{TemplateBinding BorderBrush}"
                   MinHeight="290"
                   Grid.Row="1"

+ 28 - 35
src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml

@@ -56,18 +56,11 @@
       </ControlTemplate>
     </Setter>
     
-    <Style Selector="^:pointerover /template/ ContentPresenter#PART_ContentPresenter">
-      <Setter Property="BorderBrush" Value="{DynamicResource DatePickerButtonBorderBrushPointerOver}"/>
-      <Setter Property="Background" Value="{DynamicResource DatePickerButtonBackgroundPointerOver}"/>
-      <Setter Property="Foreground" Value="{DynamicResource DatePickerButtonForegroundPointerOver}"/>
-    </Style>
-    
     <Style Selector="^:pressed">
       <Setter Property="RenderTransform" Value="scale(0.98)" />
     </Style>
     
     <Style Selector="^:pressed /template/ ContentPresenter#PART_ContentPresenter">
-      <Setter Property="BorderBrush" Value="{DynamicResource DatePickerButtonBorderBrushPressed}"/>
       <Setter Property="Background" Value="{DynamicResource DatePickerButtonBackgroundPressed}"/>
       <Setter Property="Foreground" Value="{DynamicResource DatePickerButtonForegroundPressed}"/>
     </Style>
@@ -101,7 +94,7 @@
                               HorizontalAlignment="Stretch"
                               VerticalAlignment="Top"/>
 
-            <Button Name="FlyoutButton"
+            <Button Name="PART_FlyoutButton"
                     Grid.Row="1"
                     Theme="{StaticResource FluentDatePickerFlyoutButton}"
                     Foreground="{TemplateBinding Foreground}"
@@ -115,28 +108,28 @@
                     HorizontalAlignment="Stretch"
                     VerticalAlignment="Stretch"
                     TemplatedControl.IsTemplateFocusTarget="True">
-              <Grid Name="ButtonContentGrid" ColumnDefinitions="78*,Auto,132*,Auto,78*">
-                <TextBlock Name="DayText" Text="day" HorizontalAlignment="Center"
+              <Grid Name="PART_ButtonContentGrid" ColumnDefinitions="78*,Auto,132*,Auto,78*">
+                <TextBlock Name="PART_DayTextBlock" Text="day" HorizontalAlignment="Center"
                            Padding="{DynamicResource DatePickerHostPadding}"
                            FontFamily="{TemplateBinding FontFamily}"
                            FontWeight="{TemplateBinding FontWeight}"
                            FontSize="{TemplateBinding FontSize}"/>
-                <TextBlock Name="MonthText" Text="month" TextAlignment="Left"
+                <TextBlock Name="PART_MonthTextBlock" Text="month" TextAlignment="Left"
                            Padding="{DynamicResource DatePickerHostMonthPadding}"
                            FontFamily="{TemplateBinding FontFamily}"
                            FontWeight="{TemplateBinding FontWeight}"
                            FontSize="{TemplateBinding FontSize}"/>
-                <TextBlock Name="YearText" Text="year" HorizontalAlignment="Center"
+                <TextBlock Name="PART_YearTextBlock" Text="year" HorizontalAlignment="Center"
                            Padding="{DynamicResource DatePickerHostPadding}"
                            FontFamily="{TemplateBinding FontFamily}"
                            FontWeight="{TemplateBinding FontWeight}"
                            FontSize="{TemplateBinding FontSize}"/>
-                <Rectangle x:Name="FirstSpacer"
+                <Rectangle x:Name="PART_FirstSpacer"
                       Fill="{DynamicResource DatePickerSpacerFill}"
                       HorizontalAlignment="Center"
                       Width="1"
                       Grid.Column="1" />
-                <Rectangle x:Name="SecondSpacer"
+                <Rectangle x:Name="PART_SecondSpacer"
                       Fill="{DynamicResource DatePickerSpacerFill}"
                       HorizontalAlignment="Center"
                       Width="1"
@@ -144,10 +137,10 @@
               </Grid>
             </Button>
 
-            <Popup Name="Popup" WindowManagerAddShadowHint="False"
+            <Popup Name="PART_Popup" WindowManagerAddShadowHint="False"
                    IsLightDismissEnabled="True" PlacementTarget="{TemplateBinding}"
                    PlacementMode="Bottom">
-              <DatePickerPresenter Name="PickerPresenter" />
+              <DatePickerPresenter Name="PART_PickerPresenter" />
             </Popup>
 
           </Grid>
@@ -188,62 +181,62 @@
                 Padding="{DynamicResource DateTimeFlyoutBorderPadding}"
                 MaxHeight="398" CornerRadius="{TemplateBinding CornerRadius}">
           <Grid Name="ContentRoot" RowDefinitions="*,Auto">
-            <Grid Name="PickerContainer">
+            <Grid Name="PART_PickerContainer">
               <Grid.Styles>
                 <Style Selector="DateTimePickerPanel > ListBoxItem">
                   <Setter Property="Theme" Value="{StaticResource FluentDateTimePickerItem}" />
                 </Style>
               </Grid.Styles>
               <!--Column Definitions set in code, ignore here-->
-              <Panel Name="MonthHost">
+              <Panel Name="PART_MonthHost">
                 <ScrollViewer HorizontalScrollBarVisibility="Disabled"
                               VerticalScrollBarVisibility="Hidden">
-                  <DateTimePickerPanel Name="MonthSelector"
+                  <DateTimePickerPanel Name="PART_MonthSelector"
                                        PanelType="Month"
                                        ItemHeight="{DynamicResource DatePickerFlyoutPresenterItemHeight}"
                                        ShouldLoop="True" />
                 </ScrollViewer>
-                <RepeatButton Name="MonthUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
-                <RepeatButton Name="MonthDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
+                <RepeatButton Name="PART_MonthUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
+                <RepeatButton Name="PART_MonthDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
               </Panel>
-              <Panel Name="DayHost">
+              <Panel Name="PART_DayHost">
                 <ScrollViewer HorizontalScrollBarVisibility="Disabled"
                               VerticalScrollBarVisibility="Hidden">
-                  <DateTimePickerPanel Name="DaySelector"
+                  <DateTimePickerPanel Name="PART_DaySelector"
                                        PanelType="Day"
                                        ItemHeight="{DynamicResource DatePickerFlyoutPresenterItemHeight}"
                                        ShouldLoop="True" />
                 </ScrollViewer>
-                <RepeatButton Name="DayUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
-                <RepeatButton Name="DayDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
+                <RepeatButton Name="PART_DayUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
+                <RepeatButton Name="PART_DayDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
               </Panel>
-              <Panel Name="YearHost">
+              <Panel Name="PART_YearHost">
                 <ScrollViewer HorizontalScrollBarVisibility="Disabled"
                               VerticalScrollBarVisibility="Hidden">
-                  <DateTimePickerPanel Name="YearSelector"
+                  <DateTimePickerPanel Name="PART_YearSelector"
                                        PanelType="Year"
                                        ItemHeight="{DynamicResource DatePickerFlyoutPresenterItemHeight}"
                                        ShouldLoop="False" />
                 </ScrollViewer>
-                <RepeatButton Name="YearUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
-                <RepeatButton Name="YearDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
+                <RepeatButton Name="PART_YearUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
+                <RepeatButton Name="PART_YearDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
               </Panel>
               <Rectangle Name="HighlightRect" IsHitTestVisible="False" ZIndex="-1"
                          Fill="{DynamicResource DatePickerFlyoutPresenterHighlightFill}"
                          Grid.Column="0" Grid.ColumnSpan="5" VerticalAlignment="Center"
                          Height="{DynamicResource DatePickerFlyoutPresenterHighlightHeight}" />
-              <Rectangle Name="FirstSpacer"
+              <Rectangle Name="PART_FirstSpacer"
                          Fill="{DynamicResource DatePickerFlyoutPresenterSpacerFill}"
                          HorizontalAlignment="Center"
                          Width="{DynamicResource DatePickerSpacerThemeWidth}"
                          Grid.Column="1" />
-              <Rectangle Name="SecondSpacer"
+              <Rectangle Name="PART_SecondSpacer"
                          Fill="{DynamicResource DatePickerFlyoutPresenterSpacerFill}"
                          HorizontalAlignment="Center"
                          Width="{DynamicResource DatePickerSpacerThemeWidth}"
                          Grid.Column="3" />
             </Grid>
-            
+
             <Grid Name="AcceptDismissGrid"
                   Grid.Row="1"
                   ColumnDefinitions="*,*">
@@ -251,7 +244,7 @@
                          VerticalAlignment="Top"
                          Fill="{DynamicResource DatePickerFlyoutPresenterSpacerFill}"
                          Grid.ColumnSpan="2"/>
-              <Button Name="AcceptButton"
+              <Button Name="PART_AcceptButton"
                       Grid.Column="0"
                       Theme="{StaticResource FluentDateTimePickerButton}"
                       Height="{DynamicResource DatePickerFlyoutPresenterAcceptDismissHostGridHeight}"
@@ -262,8 +255,8 @@
                       StrokeThickness="0.75"
                       Data="M0.5,8.5 5,13.5 15.5,3" />
               </Button>
-              <Button Name="DismissButton" 
-                      Grid.Column="1" 
+              <Button Name="PART_DismissButton"
+                      Grid.Column="1"
                       Theme="{StaticResource FluentDateTimePickerButton}"
                       Height="{DynamicResource DatePickerFlyoutPresenterAcceptDismissHostGridHeight}"
                       HorizontalAlignment="Stretch" 

+ 2 - 2
src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml

@@ -134,7 +134,7 @@
     <Setter Property="Template">
       <ControlTemplate>
         <DockPanel>
-          <ListBox x:Name="QuickLinks" DockPanel.Dock="Left" Items="{Binding QuickLinks}" SelectedIndex="{Binding QuickLinksSelectedIndex}" Focusable="False">
+          <ListBox x:Name="PART_QuickLinks" DockPanel.Dock="Left" Items="{Binding QuickLinks}" SelectedIndex="{Binding QuickLinksSelectedIndex}" Focusable="False">
             <ListBox.ItemTemplate>
               <DataTemplate>
                 <StackPanel Spacing="4" Orientation="Horizontal" Background="Transparent">
@@ -213,7 +213,7 @@
               <GridSplitter Grid.Column="7" />
               <TextBlock Grid.Column="8" Text="Size" />
             </Grid>
-            <ListBox x:Name="Files"
+            <ListBox x:Name="PART_Files"
                 VirtualizationMode="Simple"
                 Items="{Binding Items}"
                 Margin="0 5"

+ 30 - 37
src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml

@@ -55,18 +55,11 @@
       </ControlTemplate>
     </Setter>
 
-    <Style Selector="^:pointerover /template/ ContentPresenter#PART_ContentPresenter">
-      <Setter Property="BorderBrush" Value="{DynamicResource TimePickerButtonBorderBrushPointerOver}"/>
-      <Setter Property="Background" Value="{DynamicResource TimePickerButtonBackgroundPointerOver}"/>
-      <Setter Property="Foreground" Value="{DynamicResource TimePickerButtonForegroundPointerOver}"/>
-    </Style>
-
     <Style Selector="^:pressed">
       <Setter Property="RenderTransform" Value="scale(0.98)" />
     </Style>
 
     <Style Selector="^:pressed /template/ ContentPresenter#PART_ContentPresenter">
-      <Setter Property="BorderBrush" Value="{DynamicResource TimePickerButtonBorderBrushPressed}"/>
       <Setter Property="Background" Value="{DynamicResource TimePickerButtonBackgroundPressed}"/>
       <Setter Property="Foreground" Value="{DynamicResource TimePickerButtonForegroundPressed}"/>
     </Style>
@@ -101,7 +94,7 @@
                               HorizontalAlignment="Stretch"
                               VerticalAlignment="Top" />
 
-            <Button x:Name="FlyoutButton"
+            <Button x:Name="PART_FlyoutButton"
                     Grid.Row="1"
                     Theme="{StaticResource FluentTimePickerFlyoutButton}"
                     Foreground="{TemplateBinding Foreground}"
@@ -115,13 +108,13 @@
                     HorizontalAlignment="Stretch"
                     VerticalAlignment="Top">
 
-              <Grid Name="FlyoutButtonContentGrid">
+              <Grid Name="PART_FlyoutButtonContentGrid">
                 <!--Ignore col defs here, set in code-->
-                <Border x:Name="FirstPickerHost"
+                <Border x:Name="PART_FirstPickerHost"
                         Grid.Column="0"
                         HorizontalAlignment="Stretch"
                         VerticalAlignment="Stretch">
-                  <TextBlock x:Name="HourTextBlock"
+                  <TextBlock x:Name="PART_HourTextBlock"
                       HorizontalAlignment="Center"
                       Padding="{DynamicResource TimePickerHostPadding}"
                       FontFamily="{TemplateBinding FontFamily}"
@@ -129,17 +122,17 @@
                       FontSize="{TemplateBinding FontSize}" />
                 </Border>
 
-                <Rectangle Name="FirstColumnDivider"
+                <Rectangle Name="PART_FirstColumnDivider"
                            Fill="{DynamicResource TimePickerSpacerFill}"
                            HorizontalAlignment="Center"
                            Width="{DynamicResource TimePickerSpacerThemeWidth}"
                            Grid.Column="1" />
 
-                <Border x:Name="SecondPickerHost"
+                <Border x:Name="PART_SecondPickerHost"
                         Grid.Column="2"
                         HorizontalAlignment="Stretch"
                         VerticalAlignment="Stretch">
-                  <TextBlock x:Name="MinuteTextBlock"
+                  <TextBlock x:Name="PART_MinuteTextBlock"
                              HorizontalAlignment="Center"
                              Padding="{DynamicResource TimePickerHostPadding}"
                              FontFamily="{TemplateBinding FontFamily}"
@@ -147,17 +140,17 @@
                              FontSize="{TemplateBinding FontSize}"/>
                 </Border>
 
-                <Rectangle Name="SecondColumnDivider"
+                <Rectangle Name="PART_SecondColumnDivider"
                            Fill="{DynamicResource TimePickerSpacerFill}"
                            HorizontalAlignment="Center"
                            Width="{DynamicResource TimePickerSpacerThemeWidth}"
                            Grid.Column="3" />
 
-                <Border x:Name="ThirdPickerHost"
+                <Border x:Name="PART_ThirdPickerHost"
                         Grid.Column="4"
                         HorizontalAlignment="Stretch"
                         VerticalAlignment="Stretch">
-                  <TextBlock x:Name="PeriodTextBlock"
+                  <TextBlock x:Name="PART_PeriodTextBlock"
                              HorizontalAlignment="Center"
                              Padding="{DynamicResource TimePickerHostPadding}"
                              FontFamily="{TemplateBinding FontFamily}"
@@ -167,12 +160,12 @@
               </Grid>
             </Button>
 
-            <Popup Name="Popup"
+            <Popup Name="PART_Popup"
                    WindowManagerAddShadowHint="False"
                    IsLightDismissEnabled="True"
                    PlacementTarget="{TemplateBinding}"
                    PlacementMode="Bottom">
-              <TimePickerPresenter Name="PickerPresenter" />
+              <TimePickerPresenter Name="PART_PickerPresenter" />
             </Popup>
 
           </Grid>
@@ -216,42 +209,42 @@
                 Padding="{DynamicResource DateTimeFlyoutBorderPadding}"
                 MaxHeight="398">
           <Grid Name="ContentPanel" RowDefinitions="*,Auto">
-            <Grid Name="PickerContainer">
+            <Grid Name="PART_PickerContainer">
               <!--Ignore col defs here, set in code-->
-              <Panel Name="HourHost" Grid.Column="0">
+              <Panel Name="PART_HourHost" Grid.Column="0">
                 <ScrollViewer HorizontalScrollBarVisibility="Disabled"
                               VerticalScrollBarVisibility="Hidden">
-                  <DateTimePickerPanel Name="HourSelector"
+                  <DateTimePickerPanel Name="PART_HourSelector"
                                        PanelType="Hour"
                                        ShouldLoop="True"
                                        ItemHeight="{DynamicResource TimePickerFlyoutPresenterItemHeight}"/>
                 </ScrollViewer>
-                <RepeatButton Name="HourUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
-                <RepeatButton Name="HourDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
+                <RepeatButton Name="PART_HourUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
+                <RepeatButton Name="PART_HourDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
               </Panel>
 
-              <Panel Name="MinuteHost" Grid.Column="2">
+              <Panel Name="PART_MinuteHost" Grid.Column="2">
                 <ScrollViewer HorizontalScrollBarVisibility="Disabled"
                               VerticalScrollBarVisibility="Hidden">
-                  <DateTimePickerPanel Name="MinuteSelector"
+                  <DateTimePickerPanel Name="PART_MinuteSelector"
                                        PanelType="Minute"
                                        ShouldLoop="True"
                                        ItemHeight="{DynamicResource TimePickerFlyoutPresenterItemHeight}"/>
                 </ScrollViewer>
-                <RepeatButton Name="MinuteUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
-                <RepeatButton Name="MinuteDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
+                <RepeatButton Name="PART_MinuteUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
+                <RepeatButton Name="PART_MinuteDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
               </Panel>
 
-              <Panel Name="PeriodHost" Grid.Column="4">
+              <Panel Name="PART_PeriodHost" Grid.Column="4">
                 <ScrollViewer HorizontalScrollBarVisibility="Disabled"
                               VerticalScrollBarVisibility="Hidden">
-                  <DateTimePickerPanel Name="PeriodSelector"
+                  <DateTimePickerPanel Name="PART_PeriodSelector"
                                        PanelType="TimePeriod"
                                        ShouldLoop="False"
                                        ItemHeight="{DynamicResource TimePickerFlyoutPresenterItemHeight}"/>
                 </ScrollViewer>
-                <RepeatButton Name="PeriodUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
-                <RepeatButton Name="PeriodDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
+                <RepeatButton Name="PART_PeriodUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
+                <RepeatButton Name="PART_PeriodDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
               </Panel>
 
               <Rectangle x:Name="HighlightRect" ZIndex="-1"
@@ -260,12 +253,12 @@
                          Grid.ColumnSpan="5"
                          VerticalAlignment="Center"
                          Height="{DynamicResource TimePickerFlyoutPresenterHighlightHeight}" />
-              <Rectangle Name="FirstSpacer"
+              <Rectangle Name="PART_FirstSpacer"
                          Fill="{DynamicResource TimePickerFlyoutPresenterSpacerFill}"
                          HorizontalAlignment="Center"
                          Width="{DynamicResource TimePickerSpacerThemeWidth}"
                          Grid.Column="1" />
-              <Rectangle Name="SecondSpacer"
+              <Rectangle Name="PART_SecondSpacer"
                          Fill="{DynamicResource TimePickerFlyoutPresenterSpacerFill}"
                          HorizontalAlignment="Center"
                          Width="{DynamicResource TimePickerSpacerThemeWidth}"
@@ -279,7 +272,7 @@
                          VerticalAlignment="Top"
                          Fill="{DynamicResource TimePickerFlyoutPresenterSpacerFill}"
                          Grid.ColumnSpan="2"/>
-              <Button Name="AcceptButton"
+              <Button Name="PART_AcceptButton"
                       Grid.Column="0"
                       Theme="{StaticResource FluentDateTimePickerButton}"
                       Height="{DynamicResource TimePickerFlyoutPresenterAcceptDismissHostGridHeight}"
@@ -290,8 +283,8 @@
                       StrokeThickness="0.75"
                       Data="M0.5,8.5 5,13.5 15.5,3" />
               </Button>
-              <Button Name="DismissButton" 
-                      Grid.Column="1" 
+              <Button Name="PART_DismissButton"
+                      Grid.Column="1"
                       Theme="{StaticResource FluentDateTimePickerButton}"
                       Height="{DynamicResource TimePickerFlyoutPresenterAcceptDismissHostGridHeight}"
                       FontSize="16" 

+ 2 - 2
src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml

@@ -106,13 +106,13 @@
                 CornerRadius="10" />
 
             <Canvas
-                x:Name="SwitchKnob"
+                x:Name="PART_SwitchKnob"
                 Grid.Row="1"
                 Width="20"
                 Height="20"
                 HorizontalAlignment="Left">
 
-              <Grid x:Name="MovingKnobs" Width="20" Height="20">
+              <Grid x:Name="PART_MovingKnobs" Width="20" Height="20">
                 <Ellipse
                   x:Name="SwitchKnobOn"
                   Fill="{DynamicResource ToggleSwitchKnobFillOn}"

+ 2 - 2
src/Avalonia.Themes.Simple/Controls/Calendar.xaml

@@ -16,10 +16,10 @@
     <Setter Property="HeaderBackground" Value="{DynamicResource ThemeAccentBrush2}" />
     <Setter Property="Template">
       <ControlTemplate>
-        <StackPanel Name="Root"
+        <StackPanel Name="PART_Root"
                     HorizontalAlignment="Center"
                     ClipToBounds="True">
-          <CalendarItem Name="CalendarItem"
+          <CalendarItem Name="PART_CalendarItem"
                         Background="{TemplateBinding Background}"
                         BorderBrush="{TemplateBinding BorderBrush}"
                         BorderThickness="{TemplateBinding BorderThickness}"

+ 5 - 5
src/Avalonia.Themes.Simple/Controls/CalendarItem.xaml

@@ -65,7 +65,7 @@
                            Fill="{TemplateBinding HeaderBackground}"
                            Stretch="Fill" />
 
-                <Button Name="PreviousButton"
+                <Button Name="PART_PreviousButton"
                         HorizontalAlignment="Left"
                         Classes="CalendarHeader CalendarNavigation"
                         IsVisible="False">
@@ -80,7 +80,7 @@
 
                 </Button>
 
-                <Button Name="HeaderButton"
+                <Button Name="PART_HeaderButton"
                         Grid.Column="1"
                         Padding="1,5,1,9"
                         HorizontalAlignment="Center"
@@ -89,7 +89,7 @@
                         FontSize="10.5"
                         FontWeight="Bold" />
 
-                <Button Name="NextButton"
+                <Button Name="PART_NextButton"
                         Grid.Column="2"
                         HorizontalAlignment="Right"
                         Classes="CalendarHeader CalendarNavigation"
@@ -105,7 +105,7 @@
 
                 </Button>
 
-                <Grid Name="MonthView"
+                <Grid Name="PART_MonthView"
                       Grid.Row="1"
                       Grid.ColumnSpan="3"
                       Margin="6,-1,6,6"
@@ -130,7 +130,7 @@
                   </Grid.ColumnDefinitions>
                 </Grid>
 
-                <Grid Name="YearView"
+                <Grid Name="PART_YearView"
                       Grid.Row="1"
                       Grid.ColumnSpan="3"
                       Margin="6,-3,7,6"

+ 26 - 26
src/Avalonia.Themes.Simple/Controls/DatePicker.xaml

@@ -102,7 +102,7 @@
                               Content="{TemplateBinding Header}"
                               ContentTemplate="{TemplateBinding HeaderTemplate}" />
 
-            <Button Name="FlyoutButton"
+            <Button Name="PART_FlyoutButton"
                     Grid.Row="1"
                     MinWidth="{DynamicResource DatePickerThemeMinWidth}"
                     MaxWidth="{DynamicResource DatePickerThemeMaxWidth}"
@@ -116,35 +116,35 @@
                     IsEnabled="{TemplateBinding IsEnabled}"
                     TemplatedControl.IsTemplateFocusTarget="True"
                     Theme="{StaticResource SimpleDatePickerFlyoutButton}">
-              <Grid Name="ButtonContentGrid"
+              <Grid Name="PART_ButtonContentGrid"
                     ColumnDefinitions="78*,Auto,132*,Auto,78*">
-                <TextBlock Name="DayText"
+                <TextBlock Name="PART_DayTextBlock"
                            Padding="{DynamicResource DatePickerHostPadding}"
                            HorizontalAlignment="Center"
                            FontFamily="{TemplateBinding FontFamily}"
                            FontSize="{TemplateBinding FontSize}"
                            FontWeight="{TemplateBinding FontWeight}"
                            Text="day" />
-                <TextBlock Name="MonthText"
+                <TextBlock Name="PART_MonthTextBlock"
                            Padding="{DynamicResource DatePickerHostMonthPadding}"
                            FontFamily="{TemplateBinding FontFamily}"
                            FontSize="{TemplateBinding FontSize}"
                            FontWeight="{TemplateBinding FontWeight}"
                            Text="month"
                            TextAlignment="Left" />
-                <TextBlock Name="YearText"
+                <TextBlock Name="PART_YearTextBlock"
                            Padding="{DynamicResource DatePickerHostPadding}"
                            HorizontalAlignment="Center"
                            FontFamily="{TemplateBinding FontFamily}"
                            FontSize="{TemplateBinding FontSize}"
                            FontWeight="{TemplateBinding FontWeight}"
                            Text="year" />
-                <Rectangle x:Name="FirstSpacer"
+                <Rectangle x:Name="PART_FirstSpacer"
                            Grid.Column="1"
                            Width="1"
                            HorizontalAlignment="Center"
                            Fill="{DynamicResource DatePickerSpacerFill}" />
-                <Rectangle x:Name="SecondSpacer"
+                <Rectangle x:Name="PART_SecondSpacer"
                            Grid.Column="3"
                            Width="1"
                            HorizontalAlignment="Center"
@@ -152,12 +152,12 @@
               </Grid>
             </Button>
 
-            <Popup Name="Popup"
+            <Popup Name="PART_Popup"
                    IsLightDismissEnabled="True"
                    PlacementMode="Bottom"
                    PlacementTarget="{TemplateBinding}"
                    WindowManagerAddShadowHint="False">
-              <DatePickerPresenter Name="PickerPresenter" />
+              <DatePickerPresenter Name="PART_PickerPresenter" />
             </Popup>
 
           </Grid>
@@ -197,50 +197,50 @@
                 CornerRadius="{TemplateBinding CornerRadius}">
           <Grid Name="ContentRoot"
                 RowDefinitions="*,Auto">
-            <Grid Name="PickerContainer">
+            <Grid Name="PART_PickerContainer">
               <Grid.Styles>
                 <Style Selector="DateTimePickerPanel > ListBoxItem">
                   <Setter Property="Theme" Value="{StaticResource SimpleDateTimePickerItem}" />
                 </Style>
               </Grid.Styles>
               <!--  Column Definitions set in code, ignore here  -->
-              <Panel Name="MonthHost">
+              <Panel Name="PART_MonthHost">
                 <ScrollViewer HorizontalScrollBarVisibility="Disabled"
                               VerticalScrollBarVisibility="Hidden">
-                  <DateTimePickerPanel Name="MonthSelector"
+                  <DateTimePickerPanel Name="PART_MonthSelector"
                                        ItemHeight="{DynamicResource DatePickerFlyoutPresenterItemHeight}"
                                        PanelType="Month"
                                        ShouldLoop="True" />
                 </ScrollViewer>
-                <RepeatButton Name="MonthUpButton"
+                <RepeatButton Name="PART_MonthUpButton"
                               Theme="{StaticResource SimpleDateTimePickerUpButton}" />
-                <RepeatButton Name="MonthDownButton"
+                <RepeatButton Name="PART_MonthDownButton"
                               Theme="{StaticResource SimpleDateTimePickerDownButton}" />
               </Panel>
-              <Panel Name="DayHost">
+              <Panel Name="PART_DayHost">
                 <ScrollViewer HorizontalScrollBarVisibility="Disabled"
                               VerticalScrollBarVisibility="Hidden">
-                  <DateTimePickerPanel Name="DaySelector"
+                  <DateTimePickerPanel Name="PART_DaySelector"
                                        ItemHeight="{DynamicResource DatePickerFlyoutPresenterItemHeight}"
                                        PanelType="Day"
                                        ShouldLoop="True" />
                 </ScrollViewer>
-                <RepeatButton Name="DayUpButton"
+                <RepeatButton Name="PART_DayUpButton"
                               Theme="{StaticResource SimpleDateTimePickerUpButton}" />
-                <RepeatButton Name="DayDownButton"
+                <RepeatButton Name="PART_DayDownButton"
                               Theme="{StaticResource SimpleDateTimePickerDownButton}" />
               </Panel>
-              <Panel Name="YearHost">
+              <Panel Name="PART_YearHost">
                 <ScrollViewer HorizontalScrollBarVisibility="Disabled"
                               VerticalScrollBarVisibility="Hidden">
-                  <DateTimePickerPanel Name="YearSelector"
+                  <DateTimePickerPanel Name="PART_YearSelector"
                                        ItemHeight="{DynamicResource DatePickerFlyoutPresenterItemHeight}"
                                        PanelType="Year"
                                        ShouldLoop="False" />
                 </ScrollViewer>
-                <RepeatButton Name="YearUpButton"
+                <RepeatButton Name="PART_YearUpButton"
                               Theme="{StaticResource SimpleDateTimePickerUpButton}" />
-                <RepeatButton Name="YearDownButton"
+                <RepeatButton Name="PART_YearDownButton"
                               Theme="{StaticResource SimpleDateTimePickerDownButton}" />
               </Panel>
               <Rectangle Name="HighlightRect"
@@ -255,12 +255,12 @@
                                    Color="{DynamicResource ThemeAccentColor}" />
                 </Rectangle.Fill>
               </Rectangle>
-              <Rectangle Name="FirstSpacer"
+              <Rectangle Name="PART_FirstSpacer"
                          Grid.Column="1"
                          Width="{DynamicResource DatePickerSpacerThemeWidth}"
                          HorizontalAlignment="Center"
                          Fill="{DynamicResource ThemeControlMidHighBrush}" />
-              <Rectangle Name="SecondSpacer"
+              <Rectangle Name="PART_SecondSpacer"
                          Grid.Column="3"
                          Width="{DynamicResource DatePickerSpacerThemeWidth}"
                          HorizontalAlignment="Center"
@@ -274,7 +274,7 @@
                          Height="{DynamicResource DatePickerSpacerThemeWidth}"
                          VerticalAlignment="Top"
                          Fill="{DynamicResource ThemeControlMidHighBrush}" />
-              <Button Name="AcceptButton"
+              <Button Name="PART_AcceptButton"
                       Grid.Column="0"
                       Height="{DynamicResource DatePickerFlyoutPresenterAcceptDismissHostGridHeight}"
                       HorizontalAlignment="Stretch"
@@ -285,7 +285,7 @@
                       StrokeLineCap="Round"
                       StrokeThickness="0.75" />
               </Button>
-              <Button Name="DismissButton"
+              <Button Name="PART_DismissButton"
                       Grid.Column="1"
                       Height="{DynamicResource DatePickerFlyoutPresenterAcceptDismissHostGridHeight}"
                       HorizontalAlignment="Stretch"

+ 2 - 2
src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml

@@ -94,7 +94,7 @@
                    Text="{Binding FileName}"
                    Watermark="File name" />
 
-          <ListBox x:Name="QuickLinks"
+          <ListBox x:Name="PART_QuickLinks"
                    Margin="0,0,5,5"
                    BorderBrush="Transparent"
                    DockPanel.Dock="Left"
@@ -143,7 +143,7 @@
               <TextBlock Grid.Column="8"
                          Text="Size" />
             </Grid>
-            <ListBox x:Name="Files"
+            <ListBox x:Name="PART_Files"
                      Margin="0,5"
                      Items="{Binding Items}"
                      ScrollViewer.HorizontalScrollBarVisibility="Disabled"

+ 29 - 29
src/Avalonia.Themes.Simple/Controls/TimePicker.xaml

@@ -102,7 +102,7 @@
                               ContentTemplate="{TemplateBinding HeaderTemplate}"
                               Foreground="{DynamicResource ThemeForegroundColor}" />
 
-            <Button x:Name="FlyoutButton"
+            <Button x:Name="PART_FlyoutButton"
                     Grid.Row="1"
                     MinWidth="{DynamicResource TimePickerThemeMinWidth}"
                     MaxWidth="{DynamicResource TimePickerThemeMaxWidth}"
@@ -116,13 +116,13 @@
                     IsEnabled="{TemplateBinding IsEnabled}"
                     Theme="{StaticResource SimpleTimePickerFlyoutButton}">
 
-              <Grid Name="FlyoutButtonContentGrid">
+              <Grid Name="PART_FlyoutButtonContentGrid">
                 <!--  Ignore col defs here, set in code  -->
-                <Border x:Name="FirstPickerHost"
+                <Border x:Name="PART_FirstPickerHost"
                         Grid.Column="0"
                         HorizontalAlignment="Stretch"
                         VerticalAlignment="Stretch">
-                  <TextBlock x:Name="HourTextBlock"
+                  <TextBlock x:Name="PART_HourTextBlock"
                              Padding="{DynamicResource TimePickerHostPadding}"
                              HorizontalAlignment="Center"
                              FontFamily="{TemplateBinding FontFamily}"
@@ -130,17 +130,17 @@
                              FontWeight="{TemplateBinding FontWeight}" />
                 </Border>
 
-                <Rectangle Name="FirstColumnDivider"
+                <Rectangle Name="PART_FirstColumnDivider"
                            Grid.Column="1"
                            Width="{DynamicResource TimePickerSpacerThemeWidth}"
                            HorizontalAlignment="Center"
                            Fill="{DynamicResource TimePickerSpacerFill}" />
 
-                <Border x:Name="SecondPickerHost"
+                <Border x:Name="PART_SecondPickerHost"
                         Grid.Column="2"
                         HorizontalAlignment="Stretch"
                         VerticalAlignment="Stretch">
-                  <TextBlock x:Name="MinuteTextBlock"
+                  <TextBlock x:Name="PART_MinuteTextBlock"
                              Padding="{DynamicResource TimePickerHostPadding}"
                              HorizontalAlignment="Center"
                              FontFamily="{TemplateBinding FontFamily}"
@@ -148,17 +148,17 @@
                              FontWeight="{TemplateBinding FontWeight}" />
                 </Border>
 
-                <Rectangle Name="SecondColumnDivider"
+                <Rectangle Name="PART_SecondColumnDivider"
                            Grid.Column="3"
                            Width="{DynamicResource TimePickerSpacerThemeWidth}"
                            HorizontalAlignment="Center"
                            Fill="{DynamicResource TimePickerSpacerFill}" />
 
-                <Border x:Name="ThirdPickerHost"
+                <Border x:Name="PART_ThirdPickerHost"
                         Grid.Column="4"
                         HorizontalAlignment="Stretch"
                         VerticalAlignment="Stretch">
-                  <TextBlock x:Name="PeriodTextBlock"
+                  <TextBlock x:Name="PART_PeriodTextBlock"
                              Padding="{DynamicResource TimePickerHostPadding}"
                              HorizontalAlignment="Center"
                              FontFamily="{TemplateBinding FontFamily}"
@@ -168,12 +168,12 @@
               </Grid>
             </Button>
 
-            <Popup Name="Popup"
+            <Popup Name="PART_Popup"
                    IsLightDismissEnabled="True"
                    PlacementMode="Bottom"
                    PlacementTarget="{TemplateBinding}"
                    WindowManagerAddShadowHint="False">
-              <TimePickerPresenter Name="PickerPresenter" />
+              <TimePickerPresenter Name="PART_PickerPresenter" />
             </Popup>
 
           </Grid>
@@ -214,50 +214,50 @@
                 CornerRadius="{TemplateBinding CornerRadius}">
           <Grid Name="ContentPanel"
                 RowDefinitions="*,Auto">
-            <Grid Name="PickerContainer">
+            <Grid Name="PART_PickerContainer">
               <!--  Ignore col defs here, set in code  -->
-              <Panel Name="HourHost"
+              <Panel Name="PART_HourHost"
                      Grid.Column="0">
                 <ScrollViewer HorizontalScrollBarVisibility="Disabled"
                               VerticalScrollBarVisibility="Hidden">
-                  <DateTimePickerPanel Name="HourSelector"
+                  <DateTimePickerPanel Name="PART_HourSelector"
                                        ItemHeight="{DynamicResource TimePickerFlyoutPresenterItemHeight}"
                                        PanelType="Hour"
                                        ShouldLoop="True" />
                 </ScrollViewer>
-                <RepeatButton Name="HourUpButton"
+                <RepeatButton Name="PART_HourUpButton"
                               Theme="{StaticResource SimpleDateTimePickerUpButton}" />
-                <RepeatButton Name="HourDownButton"
+                <RepeatButton Name="PART_HourDownButton"
                               Theme="{StaticResource SimpleDateTimePickerDownButton}" />
               </Panel>
 
-              <Panel Name="MinuteHost"
+              <Panel Name="PART_MinuteHost"
                      Grid.Column="2">
                 <ScrollViewer HorizontalScrollBarVisibility="Disabled"
                               VerticalScrollBarVisibility="Hidden">
-                  <DateTimePickerPanel Name="MinuteSelector"
+                  <DateTimePickerPanel Name="PART_MinuteSelector"
                                        ItemHeight="{DynamicResource TimePickerFlyoutPresenterItemHeight}"
                                        PanelType="Minute"
                                        ShouldLoop="True" />
                 </ScrollViewer>
-                <RepeatButton Name="MinuteUpButton"
+                <RepeatButton Name="PART_MinuteUpButton"
                               Theme="{StaticResource SimpleDateTimePickerUpButton}" />
-                <RepeatButton Name="MinuteDownButton"
+                <RepeatButton Name="PART_MinuteDownButton"
                               Theme="{StaticResource SimpleDateTimePickerDownButton}" />
               </Panel>
 
-              <Panel Name="PeriodHost"
+              <Panel Name="PART_PeriodHost"
                      Grid.Column="4">
                 <ScrollViewer HorizontalScrollBarVisibility="Disabled"
                               VerticalScrollBarVisibility="Hidden">
-                  <DateTimePickerPanel Name="PeriodSelector"
+                  <DateTimePickerPanel Name="PART_PeriodSelector"
                                        ItemHeight="{DynamicResource TimePickerFlyoutPresenterItemHeight}"
                                        PanelType="TimePeriod"
                                        ShouldLoop="False" />
                 </ScrollViewer>
-                <RepeatButton Name="PeriodUpButton"
+                <RepeatButton Name="PART_PeriodUpButton"
                               Theme="{StaticResource SimpleDateTimePickerUpButton}" />
-                <RepeatButton Name="PeriodDownButton"
+                <RepeatButton Name="PART_PeriodDownButton"
                               Theme="{StaticResource SimpleDateTimePickerDownButton}" />
               </Panel>
 
@@ -272,12 +272,12 @@
                                    Color="{DynamicResource ThemeAccentColor}" />
                 </Rectangle.Fill>
               </Rectangle>
-              <Rectangle Name="FirstSpacer"
+              <Rectangle Name="PART_FirstSpacer"
                          Grid.Column="1"
                          Width="{DynamicResource TimePickerSpacerThemeWidth}"
                          HorizontalAlignment="Center"
                          Fill="{DynamicResource ThemeControlMidHighBrush}" />
-              <Rectangle Name="SecondSpacer"
+              <Rectangle Name="PART_SecondSpacer"
                          Grid.Column="3"
                          Width="{DynamicResource TimePickerSpacerThemeWidth}"
                          HorizontalAlignment="Center"
@@ -291,7 +291,7 @@
                          Height="{DynamicResource TimePickerSpacerThemeWidth}"
                          VerticalAlignment="Top"
                          Fill="{DynamicResource ThemeControlMidHighBrush}" />
-              <Button Name="AcceptButton"
+              <Button Name="PART_AcceptButton"
                       Grid.Column="0"
                       Height="{DynamicResource TimePickerFlyoutPresenterAcceptDismissHostGridHeight}"
                       HorizontalAlignment="Stretch"
@@ -302,7 +302,7 @@
                       StrokeLineCap="Round"
                       StrokeThickness="0.75" />
               </Button>
-              <Button Name="DismissButton"
+              <Button Name="PART_DismissButton"
                       Grid.Column="1"
                       Height="{DynamicResource TimePickerFlyoutPresenterAcceptDismissHostGridHeight}"
                       HorizontalAlignment="Stretch"

+ 2 - 2
src/Avalonia.Themes.Simple/Controls/ToggleSwitch.xaml

@@ -149,11 +149,11 @@
                     BorderBrush="{DynamicResource ToggleSwitchStrokeOn}"
                     BorderThickness="{DynamicResource ToggleSwitchOnStrokeThickness}" />
 
-            <Canvas x:Name="SwitchKnob" Grid.Row="1"
+            <Canvas x:Name="PART_SwitchKnob" Grid.Row="1"
                     HorizontalAlignment="Left"
                     Width="20" Height="20">
 
-              <Grid x:Name="MovingKnobs"
+              <Grid x:Name="PART_MovingKnobs"
                     Width="20" Height="20">
                 <Grid.Transitions>
                   <Transitions>

+ 2 - 1
src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs

@@ -7,6 +7,7 @@ using Avalonia.LinuxFramebuffer.Input;
 using Avalonia.LinuxFramebuffer.Output;
 using Avalonia.Platform;
 using Avalonia.Rendering;
+using Avalonia.Rendering.Composition;
 
 namespace Avalonia.LinuxFramebuffer
 {
@@ -32,7 +33,7 @@ namespace Avalonia.LinuxFramebuffer
         {
             var factory = AvaloniaLocator.Current.GetService<IRendererFactory>();
             var renderLoop = AvaloniaLocator.Current.GetService<IRenderLoop>();
-            return factory?.Create(root, renderLoop) ?? new DeferredRenderer(root, renderLoop);
+            return factory?.Create(root, renderLoop) ?? new CompositingRenderer(root, LinuxFramebufferPlatform.Compositor);
         }
 
         public void Dispose()

+ 9 - 0
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@@ -15,6 +15,7 @@ using Avalonia.LinuxFramebuffer.Output;
 using Avalonia.OpenGL;
 using Avalonia.Platform;
 using Avalonia.Rendering;
+using Avalonia.Rendering.Composition;
 using Avalonia.Threading;
 using JetBrains.Annotations;
 
@@ -26,6 +27,10 @@ namespace Avalonia.LinuxFramebuffer
         private static readonly Stopwatch St = Stopwatch.StartNew();
         internal static uint Timestamp => (uint)St.ElapsedTicks;
         public static InternalPlatformThreadingInterface Threading;
+        
+        internal static Compositor Compositor { get; private set; }
+        
+        
         LinuxFramebufferPlatform(IOutputBackend backend)
         {
             _fb = backend;
@@ -48,6 +53,10 @@ namespace Avalonia.LinuxFramebuffer
                 .Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice())
                 .Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
                 .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
+            
+            Compositor = new Compositor(
+                AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(),
+                AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>());
         }
 
        

+ 17 - 18
src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj

@@ -8,31 +8,16 @@
     <MSBuildEnableWorkloadResolver>false</MSBuildEnableWorkloadResolver>
     <StaticWebAssetsDisableProjectBuildPropsFileGeneration>true</StaticWebAssetsDisableProjectBuildPropsFileGeneration>
   </PropertyGroup>
-  
+
   <ItemGroup>
     <SupportedPlatform Include="browser" />
   </ItemGroup>
 
-  <PropertyGroup>
-    <TypescriptOutDir>wwwroot</TypescriptOutDir>
-    <TypeScriptNoEmitOnError>true</TypeScriptNoEmitOnError>
-    <TypeScriptNoImplicitReturns>true</TypeScriptNoImplicitReturns>
-  </PropertyGroup>
-
-  <PropertyGroup Condition="'$(Configuration)' == 'Debug'">
-    <TypeScriptRemoveComments>false</TypeScriptRemoveComments>
-    <TypeScriptSourceMap>true</TypeScriptSourceMap>
-  </PropertyGroup>
-  <PropertyGroup Condition="'$(Configuration)' == 'Release'">
-    <TypeScriptRemoveComments>true</TypeScriptRemoveComments>
-    <TypeScriptSourceMap>false</TypeScriptSourceMap>
-  </PropertyGroup>
-
   <Import Project="..\..\..\build\BuildTargets.targets" />
   <Import Project="..\..\..\build\SkiaSharp.props" />
   <Import Project="..\..\..\build\HarfBuzzSharp.props" />
   <Import Project="..\..\..\build\NullableEnable.props" />
-  
+
   <ItemGroup>
     <Content Include="*.props">
       <Pack>true</Pack>
@@ -45,12 +30,26 @@
   </ItemGroup>
 
   <ItemGroup>
+    <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.8" />
+    <PackageReference Include="Microsoft.TypeScript.MSBuild" Version="4.7.4" PrivateAssets="all" />
     <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.0" />
-    <PackageReference Include="Microsoft.TypeScript.MSBuild" Version="4.5.2" PrivateAssets="all" />
   </ItemGroup>
 
   <ItemGroup>
     <ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />
     <ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
   </ItemGroup>
+
+  <ItemGroup>
+    <Folder Include="wwwroot\" />
+  </ItemGroup>
+
+  <Target Name="NpmInstall" Inputs="webapp/package.json" Outputs="webapp/node_modules/.install-stamp">
+    <Exec Command="npm install" WorkingDirectory="webapp" />
+    <!-- Write the stamp file, so incremental builds work -->
+    <Touch Files="webapp/node_modules/.install-stamp" AlwaysCreate="true" />
+  </Target>
+  <Target Name="NpmRunBuild" DependsOnTargets="NpmInstall" BeforeTargets="BeforeBuild">
+    <Exec Command="npm run build" WorkingDirectory="webapp" />
+  </Target>
 </Project>

+ 9 - 2
src/Web/Avalonia.Web.Blazor/AvaloniaView.razor

@@ -1,7 +1,11 @@
-<div id="container" class="avalonia-container" tabindex="0" oncontextmenu="return false;"
+<div id="container" @ref="_containerElement"  
+     class="avalonia-container" 
+     tabindex="0" oncontextmenu="return false;"
      @onwheel="OnWheel"
      @onkeydown="OnKeyDown"
+     @onkeydown:preventDefault="true"
      @onkeyup="OnKeyUp"
+     @onkeyup:preventDefault="true"
      @onpointerdown="OnPointerDown"
      @onpointerup="OnPointerUp"
      @onpointermove="OnPointerMove"
@@ -15,7 +19,10 @@
     <input id="inputElement" @ref="_inputElement" type="text" @oninput="OnInput" 
         onpaste="return false;"
         oncopy="return false;" 
-        oncut="return false;"/>
+        oncut="return false;"
+        @onkeydown:preventDefault="true"
+        @onkeyup:preventDefault="true"
+        autocapitalize="none"/>
 </div>
 
 <style>

+ 24 - 10
src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs

@@ -6,6 +6,7 @@ using Avalonia.Input.Raw;
 using Avalonia.Input.TextInput;
 using Avalonia.Platform.Storage;
 using Avalonia.Rendering;
+using Avalonia.Rendering.Composition;
 using Avalonia.Web.Blazor.Interop;
 using Avalonia.Web.Blazor.Interop.Storage;
 
@@ -28,12 +29,15 @@ namespace Avalonia.Web.Blazor
         private SizeWatcherInterop? _sizeWatcher = null;
         private DpiWatcherInterop? _dpiWatcher = null;
         private SKHtmlCanvasInterop.GLInfo? _jsGlInfo = null;
+        private AvaloniaModule? _avaloniaModule = null;
         private InputHelperInterop? _inputHelper = null;
         private InputHelperInterop? _canvasHelper = null;
+        private InputHelperInterop? _containerHelper = null;
         private NativeControlHostInterop? _nativeControlHost = null;
         private StorageProviderInterop? _storageProvider = null;
         private ElementReference _htmlCanvas;
         private ElementReference _inputElement;
+        private ElementReference _containerElement;
         private ElementReference _nativeControlsContainer;
         private double _dpi = 1;
         private SKSize _canvasSize = new (100, 100);
@@ -240,10 +244,13 @@ namespace Avalonia.Web.Blazor
             {
                 AvaloniaLocator.CurrentMutable.Bind<IJSInProcessRuntime>().ToConstant((IJSInProcessRuntime)Js);
 
-                _inputHelper = await InputHelperInterop.ImportAsync(Js, _inputElement);
-                _canvasHelper = await InputHelperInterop.ImportAsync(Js, _htmlCanvas);
+                _avaloniaModule = await AvaloniaModule.ImportAsync(Js);
 
-                _inputHelper.Hide();
+                _inputHelper = new InputHelperInterop(_avaloniaModule, _inputElement);
+                _canvasHelper = new InputHelperInterop(_avaloniaModule, _htmlCanvas);
+                _containerHelper = new InputHelperInterop(_avaloniaModule, _containerElement);
+
+                HideIme();
                 _canvasHelper.SetCursor("default");
                 _topLevelImpl.SetCssCursor = x =>
                 {
@@ -251,11 +258,11 @@ namespace Avalonia.Web.Blazor
                     _canvasHelper.SetCursor(x); //windows
                 };
 
-                _nativeControlHost = await NativeControlHostInterop.ImportAsync(Js, _nativeControlsContainer);
+                _nativeControlHost = new NativeControlHostInterop(_avaloniaModule, _nativeControlsContainer);
                 _storageProvider = await StorageProviderInterop.ImportAsync(Js);
                 
                 Console.WriteLine("starting html canvas setup");
-                _interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame);
+                _interop = new SKHtmlCanvasInterop(_avaloniaModule, _htmlCanvas, OnRenderFrame);
 
                 Console.WriteLine("Interop created");
                 
@@ -303,9 +310,10 @@ namespace Avalonia.Web.Blazor
                 {
                     _interop.RequestAnimationFrame(true);
                     
-                    _sizeWatcher = await SizeWatcherInterop.ImportAsync(Js, _htmlCanvas, OnSizeChanged);
-                    _dpiWatcher = await DpiWatcherInterop.ImportAsync(Js, OnDpiChanged);
+                    _sizeWatcher = new SizeWatcherInterop(_avaloniaModule, _htmlCanvas, OnSizeChanged);
+                    _dpiWatcher = new DpiWatcherInterop(_avaloniaModule, OnDpiChanged);
                     
+                    _sizeWatcher.Start();
                     _topLevel.Prepare();
 
                     _topLevel.Renderer.Start();
@@ -345,9 +353,9 @@ namespace Avalonia.Web.Blazor
             // We also don't want to have it as a meaningful public API.
             // Therefore we have InternalsVisibleTo hack here.
 
-            if (_topLevel.Renderer is DeferredRenderer dr)
+            if (_topLevel.Renderer is CompositingRenderer dr)
             {
-                dr.Render(true);
+                dr.CompositionTarget.ImmediateUIThreadRender();
             }
         }
 
@@ -379,6 +387,12 @@ namespace Avalonia.Web.Blazor
             }
         }
 
+        private void HideIme()
+        {
+            _inputHelper?.Hide();
+            _containerHelper?.Focus();
+        }
+
         public void SetClient(ITextInputMethodClient? client)
         {
             if (_inputHelper is null)
@@ -399,7 +413,7 @@ namespace Avalonia.Web.Blazor
             else
             {
                 _inputElementFocused = false;
-                _inputHelper.Hide();
+                HideIme();
             }
         }
 

+ 18 - 0
src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs

@@ -0,0 +1,18 @@
+using Microsoft.JSInterop;
+
+namespace Avalonia.Web.Blazor.Interop
+{
+    internal class AvaloniaModule : JSModuleInterop
+    {
+        private AvaloniaModule(IJSRuntime js) : base(js, "./_content/Avalonia.Web.Blazor/avalonia.js")
+        {
+        }
+
+        public static async Task<AvaloniaModule> ImportAsync(IJSRuntime js)
+        {
+            var interop = new AvaloniaModule(js);
+            await interop.ImportAsync();
+            return interop;
+        }
+    }
+}

+ 13 - 28
src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs

@@ -1,43 +1,29 @@
-using System;
-using System.Threading.Tasks;
 using Microsoft.JSInterop;
 
 namespace Avalonia.Web.Blazor.Interop
 {
-    internal class DpiWatcherInterop : JSModuleInterop
+    internal class DpiWatcherInterop : IDisposable
     {
-        private const string JsFilename = "./_content/Avalonia.Web.Blazor/DpiWatcher.js";
         private const string StartSymbol = "DpiWatcher.start";
         private const string StopSymbol = "DpiWatcher.stop";
         private const string GetDpiSymbol = "DpiWatcher.getDpi";
 
-        private static DpiWatcherInterop? instance;
-
         private event Action<double>? callbacksEvent;
-        private readonly FloatFloatActionHelper callbackHelper;
+        private readonly FloatFloatActionHelper _callbackHelper;
+        private readonly AvaloniaModule _module;
 
         private DotNetObjectReference<FloatFloatActionHelper>? callbackReference;
 
-        public static async Task<DpiWatcherInterop> ImportAsync(IJSRuntime js, Action<double>? callback = null)
+        public DpiWatcherInterop(AvaloniaModule module, Action<double>? callback = null)
         {
-            var interop = Get(js);
-            await interop.ImportAsync();
-            if (callback != null)
-                interop.Subscribe(callback);
-            return interop;
-        }
+            _module = module;
+            _callbackHelper = new FloatFloatActionHelper((o, n) => callbacksEvent?.Invoke(n));
 
-        public static DpiWatcherInterop Get(IJSRuntime js) =>
-            instance ??= new DpiWatcherInterop(js);
-
-        private DpiWatcherInterop(IJSRuntime js)
-            : base(js, JsFilename)
-        {
-            callbackHelper = new FloatFloatActionHelper((o, n) => callbacksEvent?.Invoke(n));
+            if (callback != null)
+                Subscribe(callback);
         }
 
-        protected override void OnDisposingModule() =>
-            Stop();
+        public void Dispose() => Stop();
 
         public void Subscribe(Action<double> callback)
         {
@@ -65,9 +51,9 @@ namespace Avalonia.Web.Blazor.Interop
             if (callbackReference != null)
                 return GetDpi();
 
-            callbackReference = DotNetObjectReference.Create(callbackHelper);
+            callbackReference = DotNetObjectReference.Create(_callbackHelper);
 
-            return Invoke<double>(StartSymbol, callbackReference);
+            return _module.Invoke<double>(StartSymbol, callbackReference);
         }
 
         private void Stop()
@@ -75,13 +61,12 @@ namespace Avalonia.Web.Blazor.Interop
             if (callbackReference == null)
                 return;
 
-            Invoke(StopSymbol);
+            _module.Invoke(StopSymbol);
 
             callbackReference?.Dispose();
             callbackReference = null;
         }
 
-        public double GetDpi() =>
-            Invoke<double>(GetDpiSymbol);
+        public double GetDpi() => _module.Invoke<double>(GetDpiSymbol);
     }
 }

+ 11 - 20
src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs

@@ -1,41 +1,32 @@
 using Microsoft.AspNetCore.Components;
-using Microsoft.JSInterop;
-using SkiaSharp;
 
 namespace Avalonia.Web.Blazor.Interop
 {
-    internal class InputHelperInterop : JSModuleInterop
+    internal class InputHelperInterop
     {
-        private const string JsFilename = "./_content/Avalonia.Web.Blazor/InputHelper.js";
         private const string ClearSymbol = "InputHelper.clear";
         private const string FocusSymbol = "InputHelper.focus";
         private const string SetCursorSymbol = "InputHelper.setCursor";
         private const string HideSymbol = "InputHelper.hide";
         private const string ShowSymbol = "InputHelper.show";
 
-        private readonly ElementReference inputElement;
+        private readonly AvaloniaModule _module;
+        private readonly ElementReference _inputElement;
 
-        public static async Task<InputHelperInterop> ImportAsync(IJSRuntime js, ElementReference element)
+        public InputHelperInterop(AvaloniaModule module, ElementReference inputElement)
         {
-            var interop = new InputHelperInterop(js, element);
-            await interop.ImportAsync();
-            return interop;
+            _module = module;
+            _inputElement = inputElement;
         }
 
-        public InputHelperInterop(IJSRuntime js, ElementReference element)
-            : base(js, JsFilename)
-        {
-            inputElement = element;
-        }
-
-        public void Clear() => Invoke(ClearSymbol, inputElement);
+        public void Clear() => _module.Invoke(ClearSymbol, _inputElement);
 
-        public void Focus() => Invoke(FocusSymbol, inputElement);
+        public void Focus() => _module.Invoke(FocusSymbol, _inputElement);
 
-        public void SetCursor(string kind) => Invoke(SetCursorSymbol, inputElement, kind);
+        public void SetCursor(string kind) => _module.Invoke(SetCursorSymbol, _inputElement, kind);
 
-        public void Hide() => Invoke(HideSymbol, inputElement);
+        public void Hide() => _module.Invoke(HideSymbol, _inputElement);
 
-        public void Show() => Invoke(ShowSymbol, inputElement);
+        public void Show() => _module.Invoke(ShowSymbol, _inputElement);
     }
 }

+ 5 - 7
src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs

@@ -1,6 +1,4 @@
-using System;
-using System.Threading.Tasks;
-using Microsoft.JSInterop;
+using Microsoft.JSInterop;
 
 namespace Avalonia.Web.Blazor.Interop
 {
@@ -31,16 +29,16 @@ namespace Avalonia.Web.Blazor.Interop
         protected IJSUnmarshalledObjectReference Module =>
             module ?? throw new InvalidOperationException("Make sure to run ImportAsync() first.");
 
-        protected void Invoke(string identifier, params object?[]? args) =>
+        internal void Invoke(string identifier, params object?[]? args) =>
             Module.InvokeVoid(identifier, args);
 
-        protected TValue Invoke<TValue>(string identifier, params object?[]? args) =>
+        internal TValue Invoke<TValue>(string identifier, params object?[]? args) =>
             Module.Invoke<TValue>(identifier, args);
 
-        protected ValueTask InvokeAsync(string identifier, params object?[]? args) =>
+        internal ValueTask InvokeAsync(string identifier, params object?[]? args) =>
             Module.InvokeVoidAsync(identifier, args);
 
-        protected ValueTask<TValue> InvokeAsync<TValue>(string identifier, params object?[]? args) =>
+        internal ValueTask<TValue> InvokeAsync<TValue>(string identifier, params object?[]? args) =>
             Module.InvokeAsync<TValue>(identifier, args);
 
         protected virtual void OnDisposingModule() { }

+ 12 - 20
src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs

@@ -1,5 +1,4 @@
-#nullable enable
-using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.CodeAnalysis;
 
 using Avalonia.Controls.Platform;
 using Avalonia.Platform;
@@ -10,31 +9,24 @@ using Microsoft.JSInterop;
 namespace Avalonia.Web.Blazor.Interop
 {
 
-    internal class NativeControlHostInterop : JSModuleInterop, INativeControlHostImpl
+    internal class NativeControlHostInterop : INativeControlHostImpl
     {
-        private const string JsFilename = "./_content/Avalonia.Web.Blazor/NativeControlHost.js";
         private const string CreateDefaultChildSymbol = "NativeControlHost.CreateDefaultChild";
         private const string CreateAttachmentSymbol = "NativeControlHost.CreateAttachment";
         private const string GetReferenceSymbol = "NativeControlHost.GetReference";
 
-        private readonly ElementReference hostElement;
+        private readonly AvaloniaModule _module;
+        private readonly ElementReference _hostElement;
 
-        public static async Task<NativeControlHostInterop> ImportAsync(IJSRuntime js, ElementReference element)
+        public NativeControlHostInterop(AvaloniaModule module, ElementReference element)
         {
-            var interop = new NativeControlHostInterop(js, element);
-            await interop.ImportAsync();
-            return interop;
-        }
-
-        public NativeControlHostInterop(IJSRuntime js, ElementReference element)
-            : base(js, JsFilename)
-        {
-            hostElement = element;
+            _module = module;
+            _hostElement = element;
         }
 
         public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent)
         {
-            var element = Invoke<IJSInProcessObjectReference>(CreateDefaultChildSymbol);
+            var element = _module.Invoke<IJSInProcessObjectReference>(CreateDefaultChildSymbol);
             return new JSObjectControlHandle(element);
         }
 
@@ -43,9 +35,9 @@ namespace Avalonia.Web.Blazor.Interop
             Attachment? a = null;
             try
             {
-                using var hostElementJsReference = Invoke<IJSInProcessObjectReference>(GetReferenceSymbol, hostElement);                
+                using var hostElementJsReference = _module.Invoke<IJSInProcessObjectReference>(GetReferenceSymbol, _hostElement);                
                 var child = create(new JSObjectControlHandle(hostElementJsReference));
-                var attachmenetReference = Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol);
+                var attachmenetReference = _module.Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol);
                 // It has to be assigned to the variable before property setter is called so we dispose it on exception
 #pragma warning disable IDE0017 // Simplify object initialization
                 a = new Attachment(attachmenetReference, child);
@@ -62,7 +54,7 @@ namespace Avalonia.Web.Blazor.Interop
 
         public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle)
         {
-            var attachmenetReference = Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol);
+            var attachmenetReference = _module.Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol);
             var a = new Attachment(attachmenetReference, handle);
             a.AttachedTo = this;
             return a;
@@ -111,7 +103,7 @@ namespace Avalonia.Web.Blazor.Interop
                     }
                     else
                     {
-                        _native.InvokeVoid(AttachToSymbol, host.hostElement);
+                        _native.InvokeVoid(AttachToSymbol, host._hostElement);
                     }
                     _attachedTo = host;
                 }

+ 19 - 26
src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs

@@ -4,7 +4,7 @@ using SkiaSharp;
 
 namespace Avalonia.Web.Blazor.Interop
 {
-    internal class SKHtmlCanvasInterop : JSModuleInterop
+    internal class SKHtmlCanvasInterop : IDisposable
     {
         private const string JsFilename = "./_content/Avalonia.Web.Blazor/SKHtmlCanvas.js";
         private const string InitGLSymbol = "SKHtmlCanvas.initGL";
@@ -14,39 +14,32 @@ namespace Avalonia.Web.Blazor.Interop
         private const string SetCanvasSizeSymbol = "SKHtmlCanvas.setCanvasSize";
         private const string PutImageDataSymbol = "SKHtmlCanvas.putImageData";
 
-        private readonly ElementReference htmlCanvas;
-        private readonly string htmlElementId;
-        private readonly ActionHelper callbackHelper;
+        private readonly AvaloniaModule _module;
+        private readonly ElementReference _htmlCanvas;
+        private readonly string _htmlElementId;
+        private readonly ActionHelper _callbackHelper;
 
         private DotNetObjectReference<ActionHelper>? callbackReference;
 
-        public static async Task<SKHtmlCanvasInterop> ImportAsync(IJSRuntime js, ElementReference element, Action callback)
+        public SKHtmlCanvasInterop(AvaloniaModule module, ElementReference element, Action renderFrameCallback)
         {
-            var interop = new SKHtmlCanvasInterop(js, element, callback);
-            await interop.ImportAsync();
-            return interop;
-        }
-
-        public SKHtmlCanvasInterop(IJSRuntime js, ElementReference element, Action renderFrameCallback)
-            : base(js, JsFilename)
-        {
-            htmlCanvas = element;
-            htmlElementId = element.Id;
+            _module = module;
+            _htmlCanvas = element;
+            _htmlElementId = element.Id;
 
-            callbackHelper = new ActionHelper(renderFrameCallback);
+            _callbackHelper = new ActionHelper(renderFrameCallback);
         }
 
-        protected override void OnDisposingModule() =>
-            Deinit();
+        public void Dispose() => Deinit();
 
         public GLInfo InitGL()
         {
             if (callbackReference != null)
                 throw new InvalidOperationException("Unable to initialize the same canvas more than once.");
 
-            callbackReference = DotNetObjectReference.Create(callbackHelper);
+            callbackReference = DotNetObjectReference.Create(_callbackHelper);
 
-            return Invoke<GLInfo>(InitGLSymbol, htmlCanvas, htmlElementId, callbackReference);
+            return _module.Invoke<GLInfo>(InitGLSymbol, _htmlCanvas, _htmlElementId, callbackReference);
         }
 
         public bool InitRaster()
@@ -54,9 +47,9 @@ namespace Avalonia.Web.Blazor.Interop
             if (callbackReference != null)
                 throw new InvalidOperationException("Unable to initialize the same canvas more than once.");
 
-            callbackReference = DotNetObjectReference.Create(callbackHelper);
+            callbackReference = DotNetObjectReference.Create(_callbackHelper);
 
-            return Invoke<bool>(InitRasterSymbol, htmlCanvas, htmlElementId, callbackReference);
+            return _module.Invoke<bool>(InitRasterSymbol, _htmlCanvas, _htmlElementId, callbackReference);
         }
 
         public void Deinit()
@@ -64,19 +57,19 @@ namespace Avalonia.Web.Blazor.Interop
             if (callbackReference == null)
                 return;
 
-            Invoke(DeinitSymbol, htmlElementId);
+            _module.Invoke(DeinitSymbol, _htmlElementId);
 
             callbackReference?.Dispose();
         }
 
         public void RequestAnimationFrame(bool enableRenderLoop) =>
-            Invoke(RequestAnimationFrameSymbol, htmlCanvas, enableRenderLoop);
+            _module.Invoke(RequestAnimationFrameSymbol, _htmlCanvas, enableRenderLoop);
 
         public void SetCanvasSize(int rawWidth, int rawHeight) =>
-            Invoke(SetCanvasSizeSymbol, htmlCanvas, rawWidth, rawHeight);
+            _module.Invoke(SetCanvasSizeSymbol, _htmlCanvas, rawWidth, rawHeight);
 
         public void PutImageData(IntPtr intPtr, SKSizeI rawSize) =>
-            Invoke(PutImageDataSymbol, htmlCanvas, intPtr.ToInt64(), rawSize.Width, rawSize.Height);
+            _module.Invoke(PutImageDataSymbol, _htmlCanvas, intPtr.ToInt64(), rawSize.Width, rawSize.Height);
 
         public record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth);
     }

+ 15 - 26
src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs

@@ -1,50 +1,39 @@
-using System;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components;
 using Microsoft.JSInterop;
 using SkiaSharp;
 
 namespace Avalonia.Web.Blazor.Interop
 {
-    internal class SizeWatcherInterop : JSModuleInterop
+    internal class SizeWatcherInterop : IDisposable
     {
-        private const string JsFilename = "./_content/Avalonia.Web.Blazor/SizeWatcher.js";
         private const string ObserveSymbol = "SizeWatcher.observe";
         private const string UnobserveSymbol = "SizeWatcher.unobserve";
 
-        private readonly ElementReference htmlElement;
-        private readonly string htmlElementId;
-        private readonly FloatFloatActionHelper callbackHelper;
+        private readonly AvaloniaModule _module;
+        private readonly ElementReference _htmlElement;
+        private readonly string _htmlElementId;
+        private readonly FloatFloatActionHelper _callbackHelper;
 
         private DotNetObjectReference<FloatFloatActionHelper>? callbackReference;
 
-        public static async Task<SizeWatcherInterop> ImportAsync(IJSRuntime js, ElementReference element, Action<SKSize> callback)
+        public SizeWatcherInterop(AvaloniaModule module, ElementReference element, Action<SKSize> callback)
         {
-            var interop = new SizeWatcherInterop(js, element, callback);
-            await interop.ImportAsync();
-            interop.Start();
-            return interop;
+            _module = module;
+            _htmlElement = element;
+            _htmlElementId = element.Id;
+            _callbackHelper = new FloatFloatActionHelper((x, y) => callback(new SKSize(x, y)));
         }
 
-        public SizeWatcherInterop(IJSRuntime js, ElementReference element, Action<SKSize> callback)
-            : base(js, JsFilename)
-        {
-            htmlElement = element;
-            htmlElementId = element.Id;
-            callbackHelper = new FloatFloatActionHelper((x, y) => callback(new SKSize(x, y)));
-        }
-
-        protected override void OnDisposingModule() =>
-            Stop();
+        public void Dispose() => Stop();
 
         public void Start()
         {
             if (callbackReference != null)
                 return;
 
-            callbackReference = DotNetObjectReference.Create(callbackHelper);
+            callbackReference = DotNetObjectReference.Create(_callbackHelper);
 
-            Invoke(ObserveSymbol, htmlElement, htmlElementId, callbackReference);
+            _module.Invoke(ObserveSymbol, _htmlElement, _htmlElementId, callbackReference);
         }
 
         public void Stop()
@@ -52,7 +41,7 @@ namespace Avalonia.Web.Blazor.Interop
             if (callbackReference == null)
                 return;
 
-            Invoke(UnobserveSymbol, htmlElementId);
+            _module.Invoke(UnobserveSymbol, _htmlElementId);
 
             callbackReference?.Dispose();
             callbackReference = null;

+ 1 - 1
src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs

@@ -12,7 +12,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage
 
     internal class StorageProviderInterop : JSModuleInterop, IStorageProvider
     {
-        private const string JsFilename = "./_content/Avalonia.Web.Blazor/StorageProvider.js";
+        private const string JsFilename = "./_content/Avalonia.Web.Blazor/avaloniaStorage.js";
         private const string PickerCancelMessage = "The user aborted a request";
 
         public static async Task<StorageProviderInterop> ImportAsync(IJSRuntime js)

+ 0 - 41
src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts

@@ -1,41 +0,0 @@
-
-export class DpiWatcher {
-	static lastDpi: number;
-	static timerId: number;
-	static callback: DotNet.DotNetObjectReference;
-
-	public static getDpi() {
-		return window.devicePixelRatio;
-	}
-
-	public static start(callback: DotNet.DotNetObjectReference): number {
-		//console.info(`Starting DPI watcher with callback ${callback._id}...`);
-
-		DpiWatcher.lastDpi = window.devicePixelRatio;
-		DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000);
-		DpiWatcher.callback = callback;
-
-		return DpiWatcher.lastDpi;
-	}
-
-	public static stop() {
-		//console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`);
-
-		window.clearInterval(DpiWatcher.timerId);
-
-		DpiWatcher.callback = undefined;
-	}
-
-	static update() {
-		if (!DpiWatcher.callback)
-			return;
-
-		const currentDpi = window.devicePixelRatio;
-		const lastDpi = DpiWatcher.lastDpi;
-		DpiWatcher.lastDpi = currentDpi;
-
-		if (Math.abs(lastDpi - currentDpi) > 0.001) {
-			DpiWatcher.callback.invokeMethod('Invoke', lastDpi, currentDpi);
-		}
-	}
-}

+ 0 - 23
src/Web/Avalonia.Web.Blazor/Interop/Typescript/InputHelper.ts

@@ -1,23 +0,0 @@
-
-export class InputHelper {
-    public static clear (inputElement: HTMLInputElement){
-        inputElement.value = "";
-    }
-    
-    public static focus (inputElement: HTMLInputElement){
-        inputElement.focus();
-        inputElement.setSelectionRange(0, 0);
-    }
-    
-    public static setCursor (inputElement: HTMLInputElement, kind: string) {
-        inputElement.style.cursor = kind;
-    }
-
-    public static hide (inputElement: HTMLInputElement){
-        inputElement.style.display = 'none';
-    }
-
-    public static show (inputElement: HTMLInputElement){
-        inputElement.style.display = 'block';
-    }
-}

+ 0 - 261
src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts

@@ -1,261 +0,0 @@
-// aliases for emscripten
-declare let GL: any;
-declare let GLctx: WebGLRenderingContext;
-declare let Module: EmscriptenModule;
-
-// container for gl info
-type SKGLViewInfo = {
-	context: WebGLRenderingContext | WebGL2RenderingContext | undefined;
-	fboId: number;
-	stencil: number;
-	sample: number;
-	depth: number;
-}
-
-// alias for a potential skia html canvas
-type SKHtmlCanvasElement = {
-	SKHtmlCanvas: SKHtmlCanvas
-} & HTMLCanvasElement
-
-export class SKHtmlCanvas {
-	static elements: Map<string, HTMLCanvasElement>;
-
-	htmlCanvas: HTMLCanvasElement;
-	glInfo: SKGLViewInfo;
-	renderFrameCallback: DotNet.DotNetObjectReference;
-	renderLoopEnabled: boolean = false;
-	renderLoopRequest: number = 0;
-    newWidth: number;
-    newHeight: number;
-
-	public static initGL(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKGLViewInfo {
-		var view = SKHtmlCanvas.init(true, element, elementId, callback);
-		if (!view || !view.glInfo)
-			return null;
-
-		return view.glInfo;
-	}
-
-	public static initRaster(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): boolean {
-		var view = SKHtmlCanvas.init(false, element, elementId, callback);
-		if (!view)
-			return false;
-
-		return true;
-	}
-
-	static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKHtmlCanvas {
-		var htmlCanvas = element as SKHtmlCanvasElement;
-		if (!htmlCanvas) {
-			console.error(`No canvas element was provided.`);
-			return null;
-		}
-
-		if (!SKHtmlCanvas.elements)
-			SKHtmlCanvas.elements = new Map<string, HTMLCanvasElement>();
-		SKHtmlCanvas.elements[elementId] = element;
-
-		const view = new SKHtmlCanvas(useGL, element, callback);
-
-		htmlCanvas.SKHtmlCanvas = view;
-
-		return view;
-	}
-
-	public static deinit(elementId: string) {
-		if (!elementId)
-			return;
-
-		const element = SKHtmlCanvas.elements[elementId];
-		SKHtmlCanvas.elements.delete(elementId);
-
-		const htmlCanvas = element as SKHtmlCanvasElement;
-		if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
-			return;
-
-		htmlCanvas.SKHtmlCanvas.deinit();
-		htmlCanvas.SKHtmlCanvas = undefined;
-	}
-
-	public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean) {
-		const htmlCanvas = element as SKHtmlCanvasElement;
-		if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
-			return;
-
-		htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop);
-	}
-    
-    public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number)
-    {
-        const htmlCanvas = element as SKHtmlCanvasElement;
-        if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
-            return;
-        
-        htmlCanvas.SKHtmlCanvas.setCanvasSize(width, height);
-    }
-
-	public static setEnableRenderLoop(element: HTMLCanvasElement, enable: boolean) {
-		const htmlCanvas = element as SKHtmlCanvasElement;
-		if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
-			return;
-
-		htmlCanvas.SKHtmlCanvas.setEnableRenderLoop(enable);
-	}
-
-	public static putImageData(element: HTMLCanvasElement, pData: number, width: number, height: number) {
-		const htmlCanvas = element as SKHtmlCanvasElement;
-		if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
-			return;
-
-		htmlCanvas.SKHtmlCanvas.putImageData(pData, width, height);
-	}
-
-	public constructor(useGL: boolean, element: HTMLCanvasElement, callback: DotNet.DotNetObjectReference) {
-		this.htmlCanvas = element;
-		this.renderFrameCallback = callback;
-
-		if (useGL) {
-			const ctx = SKHtmlCanvas.createWebGLContext(this.htmlCanvas);
-			if (!ctx) {
-				console.error(`Failed to create WebGL context: err ${ctx}`);
-				return null;
-			}
-
-			// make current
-			GL.makeContextCurrent(ctx);
-
-			// read values
-			const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING);
-			this.glInfo = {
-				context: ctx,
-				fboId: fbo ? fbo.id : 0,
-				stencil: GLctx.getParameter(GLctx.STENCIL_BITS),
-				sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES)
-				depth: GLctx.getParameter(GLctx.DEPTH_BITS),
-			};
-		}
-	}
-
-	public deinit() {
-		this.setEnableRenderLoop(false);
-	}
-    
-    public setCanvasSize(width: number, height: number)
-    {
-        this.newWidth = width;
-        this.newHeight = height;
-
-        if(this.htmlCanvas.width != this.newWidth)
-        {
-            this.htmlCanvas.width = this.newWidth;
-        }
-
-        if(this.htmlCanvas.height != this.newHeight)
-        {
-            this.htmlCanvas.height = this.newHeight;
-        }
-
-        if (this.glInfo) {
-            // make current
-            GL.makeContextCurrent(this.glInfo.context);
-        }
-    }
-
-	public requestAnimationFrame(renderLoop?: boolean) {
-		// optionally update the render loop
-		if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop)
-			this.setEnableRenderLoop(renderLoop);
-
-		// skip because we have a render loop
-		if (this.renderLoopRequest !== 0)
-			return;
-
-		// add the draw to the next frame
-		this.renderLoopRequest = window.requestAnimationFrame(() => {
-            if (this.glInfo) {
-                // make current
-                GL.makeContextCurrent(this.glInfo.context);
-            }
-            
-            if(this.htmlCanvas.width != this.newWidth)
-            {
-                this.htmlCanvas.width = this.newWidth;
-            }
-
-            if(this.htmlCanvas.height != this.newHeight)
-            {
-                this.htmlCanvas.height = this.newHeight;
-            }
-
-			this.renderFrameCallback.invokeMethod('Invoke');
-			this.renderLoopRequest = 0;
-
-			// we may want to draw the next frame
-			if (this.renderLoopEnabled)
-				this.requestAnimationFrame();
-		});
-	}
-
-	public setEnableRenderLoop(enable: boolean) {
-		this.renderLoopEnabled = enable;
-
-		// either start the new frame or cancel the existing one
-		if (enable) {
-			//console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`);
-			this.requestAnimationFrame();
-		} else if (this.renderLoopRequest !== 0) {
-			window.cancelAnimationFrame(this.renderLoopRequest);
-			this.renderLoopRequest = 0;
-		}
-	}
-
-	public putImageData(pData: number, width: number, height: number): boolean {
-		if (this.glInfo || !pData || width <= 0 || width <= 0)
-			return false;
-
-		var ctx = this.htmlCanvas.getContext('2d');
-		if (!ctx) {
-			console.error(`Failed to obtain 2D canvas context.`);
-			return false;
-		}
-
-		// make sure the canvas is scaled correctly for the drawing
-		this.htmlCanvas.width = width;
-		this.htmlCanvas.height = height;
-
-		// set the canvas to be the bytes
-		var buffer = new Uint8ClampedArray(Module.HEAPU8.buffer, pData, width * height * 4);
-		var imageData = new ImageData(buffer, width, height);
-		ctx.putImageData(imageData, 0, 0);
-
-		return true;
-	}
-
-	static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext {
-		const contextAttributes = {
-			alpha: 1,
-			depth: 1,
-			stencil: 8,
-			antialias: 0,
-			premultipliedAlpha: 1,
-			preserveDrawingBuffer: 0,
-			preferLowPowerToHighPerformance: 0,
-			failIfMajorPerformanceCaveat: 0,
-			majorVersion: 2,
-			minorVersion: 0,
-			enableExtensionsByDefault: 1,
-			explicitSwapControl: 0,
-			renderViaOffscreenBackBuffer: 1,
-		};
-
-		let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes);
-		if (!ctx && contextAttributes.majorVersion > 1) {
-			console.warn('Falling back to WebGL 1.0');
-			contextAttributes.majorVersion = 1;
-			contextAttributes.minorVersion = 0;
-			ctx = GL.createContext(htmlCanvas, contextAttributes);
-		}
-
-		return ctx;
-	}
-}

+ 0 - 68
src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.ts

@@ -1,68 +0,0 @@
-
-type SizeWatcherElement = {
-	SizeWatcher: SizeWatcherInstance;
-} & HTMLElement
-
-type SizeWatcherInstance = {
-	callback: DotNet.DotNetObjectReference;
-}
-
-export class SizeWatcher {
-	static observer: ResizeObserver;
-	static elements: Map<string, HTMLElement>;
-
-	public static observe(element: HTMLElement, elementId: string, callback: DotNet.DotNetObjectReference) {
-		if (!element || !callback)
-			return;
-
-		//console.info(`Adding size watcher observation with callback ${callback._id}...`);
-
-		SizeWatcher.init();
-
-		const watcherElement = element as SizeWatcherElement;
-		watcherElement.SizeWatcher = {
-			callback: callback
-		};
-
-		SizeWatcher.elements[elementId] = element;
-		SizeWatcher.observer.observe(element);
-
-		SizeWatcher.invoke(element);
-	}
-
-	public static unobserve(elementId: string) {
-		if (!elementId || !SizeWatcher.observer)
-			return;
-
-		//console.info('Removing size watcher observation...');
-
-		const element = SizeWatcher.elements[elementId];
-
-		SizeWatcher.elements.delete(elementId);
-		SizeWatcher.observer.unobserve(element);
-	}
-
-	static init() {
-		if (SizeWatcher.observer)
-			return;
-
-		//console.info('Starting size watcher...');
-
-		SizeWatcher.elements = new Map<string, HTMLElement>();
-		SizeWatcher.observer = new ResizeObserver((entries) => {
-			for (let entry of entries) {
-				SizeWatcher.invoke(entry.target);
-			}
-		});
-	}
-
-	static invoke(element: Element) {
-		const watcherElement = element as SizeWatcherElement;
-		const instance = watcherElement.SizeWatcher;
-
-		if (!instance || !instance.callback)
-			return;
-
-		return instance.callback.invokeMethod('Invoke', element.clientWidth, element.clientHeight);
-	}
-}

+ 0 - 7
src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/extras.d.ts

@@ -1,7 +0,0 @@
-
-declare namespace DotNet {
-    interface DotNetObjectReference extends DotNet.DotNetObject {
-        _id: number;
-        dispose();
-    }
-}

+ 0 - 326
src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts

@@ -1,326 +0,0 @@
-// Type definitions for Emscripten 1.39.16
-// Project: https://emscripten.org
-// Definitions by: Kensuke Matsuzaki <https://github.com/zakki>
-//                 Periklis Tsirakidis <https://github.com/periklis>
-//                 Bumsik Kim <https://github.com/kbumsik>
-//                 Louis DeScioli <https://github.com/lourd>
-// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
-// TypeScript Version: 2.2
-
-/** Other WebAssembly declarations, for compatibility with older versions of Typescript */
-declare namespace WebAssembly {
-    interface Module {}
-}
-
-declare namespace Emscripten {
-    interface FileSystemType {}
-    type EnvironmentType = 'WEB' | 'NODE' | 'SHELL' | 'WORKER';
-
-    type JSType = 'number' | 'string' | 'array' | 'boolean';
-    type TypeCompatibleWithC = number | string | any[] | boolean;
-
-    type CIntType = 'i8' | 'i16' | 'i32' | 'i64';
-    type CFloatType = 'float' | 'double';
-    type CPointerType = 'i8*' | 'i16*' | 'i32*' | 'i64*' | 'float*' | 'double*' | '*';
-    type CType = CIntType | CFloatType | CPointerType;
-
-    type WebAssemblyImports = Array<{
-        name: string;
-        kind: string;
-    }>;
-
-    type WebAssemblyExports = Array<{
-        module: string;
-        name: string;
-        kind: string;
-    }>;
-
-    interface CCallOpts {
-        async?: boolean | undefined;
-    }
-}
-
-interface EmscriptenModule {
-    print(str: string): void;
-    printErr(str: string): void;
-    arguments: string[];
-    environment: Emscripten.EnvironmentType;
-    preInit: Array<{ (): void }>;
-    preRun: Array<{ (): void }>;
-    postRun: Array<{ (): void }>;
-    onAbort: { (what: any): void };
-    onRuntimeInitialized: { (): void };
-    preinitializedWebGLContext: WebGLRenderingContext;
-    noInitialRun: boolean;
-    noExitRuntime: boolean;
-    logReadFiles: boolean;
-    filePackagePrefixURL: string;
-    wasmBinary: ArrayBuffer;
-
-    destroy(object: object): void;
-    getPreloadedPackage(remotePackageName: string, remotePackageSize: number): ArrayBuffer;
-    instantiateWasm(
-        imports: Emscripten.WebAssemblyImports,
-        successCallback: (module: WebAssembly.Module) => void,
-    ): Emscripten.WebAssemblyExports;
-    locateFile(url: string, scriptDirectory: string): string;
-    onCustomMessage(event: MessageEvent): void;
-
-    // USE_TYPED_ARRAYS == 1
-    HEAP: Int32Array;
-    IHEAP: Int32Array;
-    FHEAP: Float64Array;
-
-    // USE_TYPED_ARRAYS == 2
-    HEAP8: Int8Array;
-    HEAP16: Int16Array;
-    HEAP32: Int32Array;
-    HEAPU8: Uint8Array;
-    HEAPU16: Uint16Array;
-    HEAPU32: Uint32Array;
-    HEAPF32: Float32Array;
-    HEAPF64: Float64Array;
-
-    TOTAL_STACK: number;
-    TOTAL_MEMORY: number;
-    FAST_MEMORY: number;
-
-    addOnPreRun(cb: () => any): void;
-    addOnInit(cb: () => any): void;
-    addOnPreMain(cb: () => any): void;
-    addOnExit(cb: () => any): void;
-    addOnPostRun(cb: () => any): void;
-
-    preloadedImages: any;
-    preloadedAudios: any;
-
-    _malloc(size: number): number;
-    _free(ptr: number): void;
-}
-
-/**
- * A factory function is generated when setting the `MODULARIZE` build option
- * to `1` in your Emscripten build. It return a Promise that resolves to an
- * initialized, ready-to-call `EmscriptenModule` instance.
- *
- * By default, the factory function will be named `Module`. It's recommended to
- * use the `EXPORT_ES6` option, in which the factory function will be the
- * default export. If used without `EXPORT_ES6`, the factory function will be a
- * global variable. You can rename the variable using the `EXPORT_NAME` build
- * option. It's left to you to declare any global variables as needed in your
- * application's types.
- * @param moduleOverrides Default properties for the initialized module.
- */
-type EmscriptenModuleFactory<T extends EmscriptenModule = EmscriptenModule> = (
-    moduleOverrides?: Partial<T>,
-) => Promise<T>;
-
-declare namespace FS {
-    interface Lookup {
-        path: string;
-        node: FSNode;
-    }
-
-    interface FSStream {}
-    interface FSNode {}
-    interface ErrnoError {}
-
-    let ignorePermissions: boolean;
-    let trackingDelegate: any;
-    let tracking: any;
-    let genericErrors: any;
-
-    //
-    // paths
-    //
-    function lookupPath(path: string, opts: any): Lookup;
-    function getPath(node: FSNode): string;
-
-    //
-    // nodes
-    //
-    function isFile(mode: number): boolean;
-    function isDir(mode: number): boolean;
-    function isLink(mode: number): boolean;
-    function isChrdev(mode: number): boolean;
-    function isBlkdev(mode: number): boolean;
-    function isFIFO(mode: number): boolean;
-    function isSocket(mode: number): boolean;
-
-    //
-    // devices
-    //
-    function major(dev: number): number;
-    function minor(dev: number): number;
-    function makedev(ma: number, mi: number): number;
-    function registerDevice(dev: number, ops: any): void;
-
-    //
-    // core
-    //
-    function syncfs(populate: boolean, callback: (e: any) => any): void;
-    function syncfs(callback: (e: any) => any, populate?: boolean): void;
-    function mount(type: Emscripten.FileSystemType, opts: any, mountpoint: string): any;
-    function unmount(mountpoint: string): void;
-
-    function mkdir(path: string, mode?: number): any;
-    function mkdev(path: string, mode?: number, dev?: number): any;
-    function symlink(oldpath: string, newpath: string): any;
-    function rename(old_path: string, new_path: string): void;
-    function rmdir(path: string): void;
-    function readdir(path: string): any;
-    function unlink(path: string): void;
-    function readlink(path: string): string;
-    function stat(path: string, dontFollow?: boolean): any;
-    function lstat(path: string): any;
-    function chmod(path: string, mode: number, dontFollow?: boolean): void;
-    function lchmod(path: string, mode: number): void;
-    function fchmod(fd: number, mode: number): void;
-    function chown(path: string, uid: number, gid: number, dontFollow?: boolean): void;
-    function lchown(path: string, uid: number, gid: number): void;
-    function fchown(fd: number, uid: number, gid: number): void;
-    function truncate(path: string, len: number): void;
-    function ftruncate(fd: number, len: number): void;
-    function utime(path: string, atime: number, mtime: number): void;
-    function open(path: string, flags: string, mode?: number, fd_start?: number, fd_end?: number): FSStream;
-    function close(stream: FSStream): void;
-    function llseek(stream: FSStream, offset: number, whence: number): any;
-    function read(stream: FSStream, buffer: ArrayBufferView, offset: number, length: number, position?: number): number;
-    function write(
-        stream: FSStream,
-        buffer: ArrayBufferView,
-        offset: number,
-        length: number,
-        position?: number,
-        canOwn?: boolean,
-    ): number;
-    function allocate(stream: FSStream, offset: number, length: number): void;
-    function mmap(
-        stream: FSStream,
-        buffer: ArrayBufferView,
-        offset: number,
-        length: number,
-        position: number,
-        prot: number,
-        flags: number,
-    ): any;
-    function ioctl(stream: FSStream, cmd: any, arg: any): any;
-    function readFile(path: string, opts: { encoding: 'binary'; flags?: string | undefined }): Uint8Array;
-    function readFile(path: string, opts: { encoding: 'utf8'; flags?: string | undefined }): string;
-    function readFile(path: string, opts?: { flags?: string | undefined }): Uint8Array;
-    function writeFile(path: string, data: string | ArrayBufferView, opts?: { flags?: string | undefined }): void;
-
-    //
-    // module-level FS code
-    //
-    function cwd(): string;
-    function chdir(path: string): void;
-    function init(
-        input: null | (() => number | null),
-        output: null | ((c: number) => any),
-        error: null | ((c: number) => any),
-    ): void;
-
-    function createLazyFile(
-        parent: string | FSNode,
-        name: string,
-        url: string,
-        canRead: boolean,
-        canWrite: boolean,
-    ): FSNode;
-    function createPreloadedFile(
-        parent: string | FSNode,
-        name: string,
-        url: string,
-        canRead: boolean,
-        canWrite: boolean,
-        onload?: () => void,
-        onerror?: () => void,
-        dontCreateFile?: boolean,
-        canOwn?: boolean,
-    ): void;
-    function createDataFile(
-        parent: string | FSNode,
-        name: string,
-        data: ArrayBufferView,
-        canRead: boolean,
-        canWrite: boolean,
-        canOwn: boolean,
-    ): FSNode;
-}
-
-declare var MEMFS: Emscripten.FileSystemType;
-declare var NODEFS: Emscripten.FileSystemType;
-declare var IDBFS: Emscripten.FileSystemType;
-
-// Below runtime function/variable declarations are exportable by
-// -s EXTRA_EXPORTED_RUNTIME_METHODS. You can extend or merge
-// EmscriptenModule interface to add runtime functions.
-//
-// For example, by using -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']"
-// You can access ccall() via Module["ccall"]. In this case, you should
-// extend EmscriptenModule to pass the compiler check like the following:
-//
-// interface YourOwnEmscriptenModule extends EmscriptenModule {
-//     ccall: typeof ccall;
-// }
-//
-// See: https://emscripten.org/docs/getting_started/FAQ.html#why-do-i-get-typeerror-module-something-is-not-a-function
-
-declare function ccall(
-    ident: string,
-    returnType: Emscripten.JSType | null,
-    argTypes: Emscripten.JSType[],
-    args: Emscripten.TypeCompatibleWithC[],
-    opts?: Emscripten.CCallOpts,
-): any;
-declare function cwrap(
-    ident: string,
-    returnType: Emscripten.JSType | null,
-    argTypes: Emscripten.JSType[],
-    opts?: Emscripten.CCallOpts,
-): (...args: any[]) => any;
-
-declare function setValue(ptr: number, value: any, type: Emscripten.CType, noSafe?: boolean): void;
-declare function getValue(ptr: number, type: Emscripten.CType, noSafe?: boolean): number;
-
-declare function allocate(
-    slab: number[] | ArrayBufferView | number,
-    types: Emscripten.CType | Emscripten.CType[],
-    allocator: number,
-    ptr?: number,
-): number;
-
-declare function stackAlloc(size: number): number;
-declare function stackSave(): number;
-declare function stackRestore(ptr: number): void;
-
-declare function UTF8ToString(ptr: number, maxBytesToRead?: number): string;
-declare function stringToUTF8(str: string, outPtr: number, maxBytesToRead?: number): void;
-declare function lengthBytesUTF8(str: string): number;
-declare function allocateUTF8(str: string): number;
-declare function allocateUTF8OnStack(str: string): number;
-declare function UTF16ToString(ptr: number): string;
-declare function stringToUTF16(str: string, outPtr: number, maxBytesToRead?: number): void;
-declare function lengthBytesUTF16(str: string): number;
-declare function UTF32ToString(ptr: number): string;
-declare function stringToUTF32(str: string, outPtr: number, maxBytesToRead?: number): void;
-declare function lengthBytesUTF32(str: string): number;
-
-declare function intArrayFromString(stringy: string, dontAddNull?: boolean, length?: number): number[];
-declare function intArrayToString(array: number[]): string;
-declare function writeStringToMemory(str: string, buffer: number, dontAddNull: boolean): void;
-declare function writeArrayToMemory(array: number[], buffer: number): void;
-declare function writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void;
-
-declare function addRunDependency(id: any): void;
-declare function removeRunDependency(id: any): void;
-
-declare function addFunction(func: (...args: any[]) => any, signature?: string): number;
-declare function removeFunction(funcPtr: number): void;
-
-declare var ALLOC_NORMAL: number;
-declare var ALLOC_STACK: number;
-declare var ALLOC_STATIC: number;
-declare var ALLOC_DYNAMIC: number;
-declare var ALLOC_NONE: number;

+ 2 - 1
src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs

@@ -7,6 +7,7 @@ using Avalonia.Input.TextInput;
 using Avalonia.Platform;
 using Avalonia.Platform.Storage;
 using Avalonia.Rendering;
+using Avalonia.Rendering.Composition;
 using Avalonia.Web.Blazor.Interop;
 using SkiaSharp;
 
@@ -146,7 +147,7 @@ namespace Avalonia.Web.Blazor
         public IRenderer CreateRenderer(IRenderRoot root)
         {
             var loop = AvaloniaLocator.Current.GetRequiredService<IRenderLoop>();
-            return new DeferredRenderer(root, loop);
+            return new CompositingRenderer(root, new Compositor(loop, null));
         }
 
         public void Invalidate(Rect rect)

+ 0 - 14
src/Web/Avalonia.Web.Blazor/tsconfig.json

@@ -1,14 +0,0 @@
-{
-  "compilerOptions": {
-    "noImplicitAny": false,
-    "noEmitOnError": true,
-    "removeComments": false,
-    "sourceMap": true,
-    "target": "ES2020",
-    "module": "ES2020",
-    "outDir": "wwwroot"
-  },
-  "exclude": [
-    "node_modules"
-  ]
-}

+ 5 - 0
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/Avalonia.ts

@@ -0,0 +1,5 @@
+export { DpiWatcher } from "./DpiWatcher"
+export { InputHelper } from "./InputHelper"
+export { NativeControlHost } from "./NativeControlHost"
+export { SizeWatcher } from "./SizeWatcher"
+export { SKHtmlCanvas } from "./SKHtmlCanvas"

+ 40 - 0
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/DpiWatcher.ts

@@ -0,0 +1,40 @@
+export class DpiWatcher {
+    static lastDpi: number;
+    static timerId: number;
+    static callback?: DotNet.DotNetObject;
+
+    public static getDpi() {
+        return window.devicePixelRatio;
+    }
+
+    public static start(callback: DotNet.DotNetObject): number {
+        //console.info(`Starting DPI watcher with callback ${callback._id}...`);
+
+        DpiWatcher.lastDpi = window.devicePixelRatio;
+        DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000);
+        DpiWatcher.callback = callback;
+
+        return DpiWatcher.lastDpi;
+    }
+
+    public static stop() {
+        //console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`);
+
+        window.clearInterval(DpiWatcher.timerId);
+
+        DpiWatcher.callback = undefined;
+    }
+
+    static update() {
+        if (!DpiWatcher.callback)
+            return;
+
+        const currentDpi = window.devicePixelRatio;
+        const lastDpi = DpiWatcher.lastDpi;
+        DpiWatcher.lastDpi = currentDpi;
+
+        if (Math.abs(lastDpi - currentDpi) > 0.001) {
+            DpiWatcher.callback.invokeMethod('Invoke', lastDpi, currentDpi);
+        }
+    }
+}

+ 31 - 0
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts

@@ -0,0 +1,31 @@
+export class InputHelper {
+    public static clear(inputElement: HTMLInputElement) {
+        inputElement.value = "";
+    }
+
+    
+    public static isInputElement( element : HTMLInputElement | HTMLElement ) : element is HTMLInputElement {
+        return ( element as HTMLInputElement).setSelectionRange !== undefined;
+    }
+
+    public static focus(inputElement: HTMLElement) {
+        inputElement.focus();
+        
+        if(this.isInputElement(inputElement))
+        {
+            (inputElement as HTMLInputElement).setSelectionRange(0,0);
+        }
+    }
+
+    public static setCursor(inputElement: HTMLInputElement, kind: string) {
+        inputElement.style.cursor = kind;
+    }
+
+    public static hide(inputElement: HTMLInputElement) {
+        inputElement.style.display = 'none';
+    }
+
+    public static show(inputElement: HTMLInputElement) {
+        inputElement.style.display = 'block';
+    }
+}

+ 20 - 15
src/Web/Avalonia.Web.Blazor/Interop/Typescript/NativeControlHost.ts → src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/NativeControlHost.ts

@@ -14,10 +14,9 @@
     }
 }
 
-class NativeControlHostTopLevelAttachment
-{
-    _child: HTMLElement;
-    _host: HTMLElement;
+class NativeControlHostTopLevelAttachment {
+    _child?: HTMLElement;
+    _host?: HTMLElement;
 
     InitializeWithChildHandle(child: HTMLElement) {
         this._child = child;
@@ -25,32 +24,38 @@ class NativeControlHostTopLevelAttachment
     }
 
     AttachTo(host: HTMLElement): void {
-        if (this._host) {
+        if (this._host && this._child) {
             this._host.removeChild(this._child);
         }
 
         this._host = host;
 
-        if (this._host) {
+        if (this._host && this._child) {
             this._host.appendChild(this._child);
         }
     }
 
     ShowInBounds(x: number, y: number, width: number, height: number): void {
-        this._child.style.top = y + "px";
-        this._child.style.left = x + "px";
-        this._child.style.width = width + "px";
-        this._child.style.height = height + "px";
-        this._child.style.display = "block";
+        if (this._child) {
+            this._child.style.top = y + "px";
+            this._child.style.left = x + "px";
+            this._child.style.width = width + "px";
+            this._child.style.height = height + "px";
+            this._child.style.display = "block";
+        }
     }
 
     HideWithSize(width: number, height: number): void {
-        this._child.style.width = width + "px";
-        this._child.style.height = height + "px";
-        this._child.style.display = "none";
+        if (this._child) {
+            this._child.style.width = width + "px";
+            this._child.style.height = height + "px";
+            this._child.style.display = "none";
+        }
     }
 
     ReleaseChild(): void {
-        this._child = null;
+        if (this._child) {
+            this._child = undefined;
+        }
     }
 }

+ 255 - 0
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SKHtmlCanvas.ts

@@ -0,0 +1,255 @@
+// aliases for emscripten
+declare let GL: any;
+declare let GLctx: WebGLRenderingContext;
+declare let Module: EmscriptenModule;
+
+// container for gl info
+type SKGLViewInfo = {
+    context: WebGLRenderingContext | WebGL2RenderingContext | undefined;
+    fboId: number;
+    stencil: number;
+    sample: number;
+    depth: number;
+}
+
+// alias for a potential skia html canvas
+type SKHtmlCanvasElement = {
+    SKHtmlCanvas: SKHtmlCanvas | undefined
+} & HTMLCanvasElement
+
+export class SKHtmlCanvas {
+    static elements: Map<string, HTMLCanvasElement>;
+
+    htmlCanvas: HTMLCanvasElement;
+    glInfo?: SKGLViewInfo;
+    renderFrameCallback: DotNet.DotNetObject;
+    renderLoopEnabled: boolean = false;
+    renderLoopRequest: number = 0;
+    newWidth?: number;
+    newHeight?: number;
+
+    public static initGL(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): SKGLViewInfo | null {
+        var view = SKHtmlCanvas.init(true, element, elementId, callback);
+        if (!view || !view.glInfo)
+            return null;
+
+        return view.glInfo;
+    }
+
+    public static initRaster(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): boolean {
+        var view = SKHtmlCanvas.init(false, element, elementId, callback);
+        if (!view)
+            return false;
+
+        return true;
+    }
+
+    static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): SKHtmlCanvas | null {
+        var htmlCanvas = element as SKHtmlCanvasElement;
+        if (!htmlCanvas) {
+            console.error(`No canvas element was provided.`);
+            return null;
+        }
+
+        if (!SKHtmlCanvas.elements)
+            SKHtmlCanvas.elements = new Map<string, HTMLCanvasElement>();
+        SKHtmlCanvas.elements.set(elementId, element);
+
+        const view = new SKHtmlCanvas(useGL, element, callback);
+
+        htmlCanvas.SKHtmlCanvas = view;
+
+        return view;
+    }
+
+    public static deinit(elementId: string) {
+        if (!elementId)
+            return;
+
+        const element = SKHtmlCanvas.elements.get(elementId);
+        SKHtmlCanvas.elements.delete(elementId);
+
+        const htmlCanvas = element as SKHtmlCanvasElement;
+        if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
+            return;
+
+        htmlCanvas.SKHtmlCanvas.deinit();
+        htmlCanvas.SKHtmlCanvas = undefined;
+    }
+
+    public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean) {
+        const htmlCanvas = element as SKHtmlCanvasElement;
+        if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
+            return;
+
+        htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop);
+    }
+
+    public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number) {
+        const htmlCanvas = element as SKHtmlCanvasElement;
+        if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
+            return;
+
+        htmlCanvas.SKHtmlCanvas.setCanvasSize(width, height);
+    }
+
+    public static setEnableRenderLoop(element: HTMLCanvasElement, enable: boolean) {
+        const htmlCanvas = element as SKHtmlCanvasElement;
+        if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
+            return;
+
+        htmlCanvas.SKHtmlCanvas.setEnableRenderLoop(enable);
+    }
+
+    public static putImageData(element: HTMLCanvasElement, pData: number, width: number, height: number) {
+        const htmlCanvas = element as SKHtmlCanvasElement;
+        if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
+            return;
+
+        htmlCanvas.SKHtmlCanvas.putImageData(pData, width, height);
+    }
+
+    public constructor(useGL: boolean, element: HTMLCanvasElement, callback: DotNet.DotNetObject) {
+        this.htmlCanvas = element;
+        this.renderFrameCallback = callback;
+
+        if (useGL) {
+            const ctx = SKHtmlCanvas.createWebGLContext(this.htmlCanvas);
+            if (!ctx) {
+                console.error(`Failed to create WebGL context: err ${ctx}`);
+                return;
+            }
+
+            // make current
+            GL.makeContextCurrent(ctx);
+
+            // read values
+            const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING);
+            this.glInfo = {
+                context: ctx,
+                fboId: fbo ? fbo.id : 0,
+                stencil: GLctx.getParameter(GLctx.STENCIL_BITS),
+                sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES)
+                depth: GLctx.getParameter(GLctx.DEPTH_BITS),
+            };
+        }
+    }
+
+    public deinit() {
+        this.setEnableRenderLoop(false);
+    }
+
+    public setCanvasSize(width: number, height: number) {
+        this.newWidth = width;
+        this.newHeight = height;
+
+        if (this.htmlCanvas.width != this.newWidth) {
+            this.htmlCanvas.width = this.newWidth;
+        }
+
+        if (this.htmlCanvas.height != this.newHeight) {
+            this.htmlCanvas.height = this.newHeight;
+        }
+
+        if (this.glInfo) {
+            // make current
+            GL.makeContextCurrent(this.glInfo.context);
+        }
+    }
+
+    public requestAnimationFrame(renderLoop?: boolean) {
+        // optionally update the render loop
+        if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop)
+            this.setEnableRenderLoop(renderLoop);
+
+        // skip because we have a render loop
+        if (this.renderLoopRequest !== 0)
+            return;
+
+        // add the draw to the next frame
+        this.renderLoopRequest = window.requestAnimationFrame(() => {
+            if (this.glInfo) {
+                // make current
+                GL.makeContextCurrent(this.glInfo.context);
+            }
+
+            if (this.htmlCanvas.width != this.newWidth) {
+                this.htmlCanvas.width = this.newWidth || 0;
+            }
+
+            if (this.htmlCanvas.height != this.newHeight) {
+                this.htmlCanvas.height = this.newHeight || 0;
+            }
+
+            this.renderFrameCallback.invokeMethod('Invoke');
+            this.renderLoopRequest = 0;
+
+            // we may want to draw the next frame
+            if (this.renderLoopEnabled)
+                this.requestAnimationFrame();
+        });
+    }
+
+    public setEnableRenderLoop(enable: boolean) {
+        this.renderLoopEnabled = enable;
+
+        // either start the new frame or cancel the existing one
+        if (enable) {
+            //console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`);
+            this.requestAnimationFrame();
+        } else if (this.renderLoopRequest !== 0) {
+            window.cancelAnimationFrame(this.renderLoopRequest);
+            this.renderLoopRequest = 0;
+        }
+    }
+
+    public putImageData(pData: number, width: number, height: number): boolean {
+        if (this.glInfo || !pData || width <= 0 || width <= 0)
+            return false;
+
+        var ctx = this.htmlCanvas.getContext('2d');
+        if (!ctx) {
+            console.error(`Failed to obtain 2D canvas context.`);
+            return false;
+        }
+
+        // make sure the canvas is scaled correctly for the drawing
+        this.htmlCanvas.width = width;
+        this.htmlCanvas.height = height;
+
+        // set the canvas to be the bytes
+        var buffer = new Uint8ClampedArray(Module.HEAPU8.buffer, pData, width * height * 4);
+        var imageData = new ImageData(buffer, width, height);
+        ctx.putImageData(imageData, 0, 0);
+
+        return true;
+    }
+
+    static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext {
+        const contextAttributes = {
+            alpha: 1,
+            depth: 1,
+            stencil: 8,
+            antialias: 0,
+            premultipliedAlpha: 1,
+            preserveDrawingBuffer: 0,
+            preferLowPowerToHighPerformance: 0,
+            failIfMajorPerformanceCaveat: 0,
+            majorVersion: 2,
+            minorVersion: 0,
+            enableExtensionsByDefault: 1,
+            explicitSwapControl: 0,
+            renderViaOffscreenBackBuffer: 1,
+        };
+
+        let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes);
+        if (!ctx && contextAttributes.majorVersion > 1) {
+            console.warn('Falling back to WebGL 1.0');
+            contextAttributes.majorVersion = 1;
+            contextAttributes.minorVersion = 0;
+            ctx = GL.createContext(htmlCanvas, contextAttributes);
+        }
+
+        return ctx;
+    }
+}

+ 67 - 0
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SizeWatcher.ts

@@ -0,0 +1,67 @@
+type SizeWatcherElement = {
+    SizeWatcher: SizeWatcherInstance;
+} & HTMLElement
+
+type SizeWatcherInstance = {
+    callback: DotNet.DotNetObject;
+}
+
+export class SizeWatcher {
+    static observer: ResizeObserver;
+    static elements: Map<string, HTMLElement>;
+
+    public static observe(element: HTMLElement, elementId: string, callback: DotNet.DotNetObject) {
+        if (!element || !callback)
+            return;
+
+        //console.info(`Adding size watcher observation with callback ${callback._id}...`);
+
+        SizeWatcher.init();
+
+        const watcherElement = element as SizeWatcherElement;
+        watcherElement.SizeWatcher = {
+            callback: callback
+        };
+
+        SizeWatcher.elements.set(elementId, element);
+        SizeWatcher.observer.observe(element);
+
+        SizeWatcher.invoke(element);
+    }
+
+    public static unobserve(elementId: string) {
+        if (!elementId || !SizeWatcher.observer)
+            return;
+
+        //console.info('Removing size watcher observation...');
+
+        const element = SizeWatcher.elements.get(elementId)!;
+
+        SizeWatcher.elements.delete(elementId);
+        SizeWatcher.observer.unobserve(element);
+    }
+
+    static init() {
+        if (SizeWatcher.observer)
+            return;
+
+        //console.info('Starting size watcher...');
+
+        SizeWatcher.elements = new Map<string, HTMLElement>();
+        SizeWatcher.observer = new ResizeObserver((entries) => {
+            for (let entry of entries) {
+                SizeWatcher.invoke(entry.target);
+            }
+        });
+    }
+
+    static invoke(element: Element) {
+        const watcherElement = element as SizeWatcherElement;
+        const instance = watcherElement.SizeWatcher;
+
+        if (!instance || !instance.callback)
+            return;
+
+        return instance.callback.invokeMethod('Invoke', element.clientWidth, element.clientHeight);
+    }
+}

+ 79 - 0
src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/IndexedDbWrapper.ts

@@ -0,0 +1,79 @@
+class InnerDbConnection {
+    constructor(private database: IDBDatabase) { }
+
+    private openStore(store: string, mode: IDBTransactionMode): IDBObjectStore {
+        const tx = this.database.transaction(store, mode);
+        return tx.objectStore(store);
+    }
+
+    public put(store: string, obj: any, key?: IDBValidKey): Promise<IDBValidKey> {
+        const os = this.openStore(store, "readwrite");
+
+        return new Promise((resolve, reject) => {
+            const response = os.put(obj, key);
+            response.onsuccess = () => {
+                resolve(response.result);
+            };
+            response.onerror = () => {
+                reject(response.error);
+            };
+        });
+    }
+
+    public get(store: string, key: IDBValidKey): any {
+        const os = this.openStore(store, "readonly");
+
+        return new Promise((resolve, reject) => {
+            const response = os.get(key);
+            response.onsuccess = () => {
+                resolve(response.result);
+            };
+            response.onerror = () => {
+                reject(response.error);
+            };
+        });
+    }
+
+    public delete(store: string, key: IDBValidKey): Promise<void> {
+        const os = this.openStore(store, "readwrite");
+
+        return new Promise((resolve, reject) => {
+            const response = os.delete(key);
+            response.onsuccess = () => {
+                resolve();
+            };
+            response.onerror = () => {
+                reject(response.error);
+            };
+        });
+    }
+
+    public close() {
+        this.database.close();
+    }
+}
+
+export class IndexedDbWrapper {
+    constructor(private databaseName: string, private objectStores: [string]) {
+    }
+
+    public connect(): Promise<InnerDbConnection> {
+        const conn = window.indexedDB.open(this.databaseName, 1);
+
+        conn.onupgradeneeded = event => {
+            const db = (<IDBRequest<IDBDatabase>>event.target).result;
+            this.objectStores.forEach(store => {
+                db.createObjectStore(store);
+            });
+        };
+
+        return new Promise((resolve, reject) => {
+            conn.onsuccess = event => {
+                resolve(new InnerDbConnection((<IDBRequest<IDBDatabase>>event.target).result));
+            };
+            conn.onerror = event => {
+                reject((<IDBRequest<IDBDatabase>>event.target).error);
+            };
+        });
+    }
+}

+ 32 - 137
src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts → src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts

@@ -1,142 +1,23 @@
-// As we don't have proper package managing for Avalonia.Web project, declare types manually
-declare global {
-    interface FileSystemWritableFileStream {
-        write(position: number, data: BufferSource | Blob | string): Promise<void>;
-        truncate(size: number): Promise<void>;
-        close(): Promise<void>;
-    }
-    type PermissionsMode = "read" | "readwrite";
-    interface FileSystemFileHandle {
-        name: string,
-        kind: "file" | "directory",
-        getFile(): Promise<File>;
-        createWritable(options?: { keepExistingData?: boolean }): Promise<FileSystemWritableFileStream>;
-
-        queryPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">;
-        requestPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">;
+import { IndexedDbWrapper } from "./IndexedDbWrapper";
 
-        entries(): AsyncIterableIterator<[string, FileSystemFileHandle]>;
-    }
-    type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; 
-    type StartInDirectory =  WellKnownDirectory | FileSystemFileHandle;
-    interface FilePickerAcceptType {
-        description: string,
-        // mime -> ext[] array
-        accept: { [mime: string]: string | string[] }
-    }
-    interface FilePickerOptions {
-        types?: FilePickerAcceptType[],
-        excludeAcceptAllOption: boolean,
-        id?: string,
+declare global {
+    type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
+    type StartInDirectory = WellKnownDirectory | FileSystemHandle;
+    interface OpenFilePickerOptions {
         startIn?: StartInDirectory
     }
-    interface OpenFilePickerOptions extends FilePickerOptions {
-        multiple: boolean
-    }
-    interface SaveFilePickerOptions extends FilePickerOptions {
-        suggestedName?: string
-    }
-    interface DirectoryPickerOptions {
-        id?: string,
+    interface SaveFilePickerOptions {
         startIn?: StartInDirectory
     }
-    
-    interface Window {
-        showOpenFilePicker: (options: OpenFilePickerOptions) => Promise<FileSystemFileHandle[]>;
-        showSaveFilePicker: (options: SaveFilePickerOptions) => Promise<FileSystemFileHandle>;
-        showDirectoryPicker: (options: DirectoryPickerOptions) => Promise<FileSystemFileHandle>;
-    }
-}
-
-// TODO move to another file and use import
-class IndexedDbWrapper {
-    constructor(private databaseName: string, private objectStores: [ string ]) {
-
-    }
-
-    public connect(): Promise<InnerDbConnection> {
-        const conn = window.indexedDB.open(this.databaseName, 1);
-
-        conn.onupgradeneeded = event => {
-            const db = (<IDBRequest<IDBDatabase>>event.target).result;
-            this.objectStores.forEach(store => {
-                db.createObjectStore(store);
-            });
-        }
-
-        return new Promise((resolve, reject) => {
-            conn.onsuccess = event => {
-                resolve(new InnerDbConnection((<IDBRequest<IDBDatabase>>event.target).result));
-            }
-            conn.onerror = event => {
-                reject((<IDBRequest<IDBDatabase>>event.target).error);
-            }
-        });
-    }
-}
-
-class InnerDbConnection {
-    constructor(private database: IDBDatabase) { }
-
-    private openStore(store: string, mode: IDBTransactionMode): IDBObjectStore {
-        const tx = this.database.transaction(store, mode);
-        return tx.objectStore(store);
-    }
-
-    public put(store: string, obj: any, key?: IDBValidKey): Promise<IDBValidKey> {
-        const os = this.openStore(store, "readwrite");
-
-        return new Promise((resolve, reject) => {
-            const response = os.put(obj, key);
-            response.onsuccess = () => {
-                resolve(response.result);
-            };
-            response.onerror = () => {
-                reject(response.error);
-            };
-        });
-    }
-
-    public get(store: string, key: IDBValidKey): any {
-        const os = this.openStore(store, "readonly");
-
-        return new Promise((resolve, reject) => {
-            const response = os.get(key);
-            response.onsuccess = () => {
-                resolve(response.result);
-            };
-            response.onerror = () => {
-                reject(response.error);
-            };
-        });
-    }
-
-    public delete(store: string, key: IDBValidKey): Promise<void> {
-        const os = this.openStore(store, "readwrite");
-
-        return new Promise((resolve, reject) => {
-            const response = os.delete(key);
-            response.onsuccess = () => {
-                resolve();
-            };
-            response.onerror = () => {
-                reject(response.error);
-            };
-        });
-    }
-
-    public close() {
-        this.database.close();
-    }
 }
 
 const fileBookmarksStore: string = "fileBookmarks";
 const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [
     fileBookmarksStore
-])
+]);
 
 class StorageItem {
-    constructor(public handle: FileSystemFileHandle, private bookmarkId?: string) { }
+    constructor(public handle: FileSystemHandle, private bookmarkId?: string) { }
 
     public getName(): string {
         return this.handle.name
@@ -147,21 +28,35 @@ class StorageItem {
     }
 
     public async openRead(): Promise<Blob> {
+        if (!(this.handle instanceof FileSystemFileHandle)) {
+            throw new Error("StorageItem is not a file");
+        }
+
         await this.verityPermissions('read');
 
-        return await this.handle.getFile();
+        const file = await this.handle.getFile();
+        return file;
     }
 
     public async openWrite(): Promise<FileSystemWritableFileStream> {
+        if (!(this.handle instanceof FileSystemFileHandle)) {
+            throw new Error("StorageItem is not a file");
+        }
+
         await this.verityPermissions('readwrite');
 
         return await this.handle.createWritable({ keepExistingData: true });
     }
 
-    public async getProperties(): Promise<{ Size: number, LastModified: number, Type: string }> {
-        const file = this.handle.getFile && await this.handle.getFile();
-        
-        return file && {
+    public async getProperties(): Promise<{ Size: number, LastModified: number, Type: string } | null> {
+        const file = this.handle instanceof FileSystemFileHandle
+            && await this.handle.getFile();
+
+        if (!file) {
+            return null;
+        }
+
+        return {
             Size: file.size,
             LastModified: file.lastModified,
             Type: file.type
@@ -174,13 +69,13 @@ class StorageItem {
         }
         
         const items: StorageItem[] = [];
-        for await (const [key, value] of this.handle.entries()) {
+        for await (const [key, value] of (this.handle as any).entries()) {
             items.push(new StorageItem(value));
         }
         return new StorageItems(items);
     }
     
-    private async verityPermissions(mode: PermissionsMode): Promise<void | never> {
+    private async verityPermissions(mode: FileSystemPermissionMode): Promise<void | never> {
         if (await this.handle.queryPermission({ mode }) === 'granted') {
             return;
         }
@@ -195,7 +90,7 @@ class StorageItem {
         if (this.bookmarkId) {
             return this.bookmarkId;
         }
-
+        
         const connection = await avaloniaDb.connect();
         try {
             const key = await connection.put(fileBookmarksStore, this.handle, this.generateBookmarkId());
@@ -205,7 +100,7 @@ class StorageItem {
             connection.close();
         }
     }
-    
+
     public async deleteBookmark(): Promise<void> {
         if (!this.bookmarkId) {
             return;
@@ -277,7 +172,7 @@ export class StorageProvider {
         };
 
         const handles = await window.showOpenFilePicker(options);
-        return new StorageItems(handles.map(handle => new StorageItem(handle)));
+        return new StorageItems(handles.map((handle: FileSystemHandle) => new StorageItem(handle)));
     }
 
     public static async saveFileDialog(

+ 16 - 0
src/Web/Avalonia.Web.Blazor/webapp/package.json

@@ -0,0 +1,16 @@
+{
+  "name": "avalonia.web",
+  "scripts": {
+    "dist": "cross-env NODE_ENV=production webpack",
+    "build": "cross-env NODE_ENV=development webpack"
+  },
+  "devDependencies": {
+    "@types/emscripten": "^1.39.6",
+    "@types/wicg-file-system-access": "^2020.9.5",
+    "cross-env": "^7.0.3",
+    "ts-loader": "^9.3.1",
+    "typescript": "^4.7.4",
+    "webpack": "^5.73.0",
+    "webpack-cli": "^4.10.0"
+  }
+}

+ 18 - 0
src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json

@@ -0,0 +1,18 @@
+{
+  "compilerOptions": {
+    "target": "ES6",
+    "strict": true,
+    "sourceMap": true,
+    "outDir": "../wwwroot",
+    "removeComments": true,
+    "noEmitOnError": true,
+    "lib": [
+      "dom",
+      "ES6",
+      "esnext.asynciterable"
+    ]
+  },
+  "exclude": [
+    "node_modules"
+  ]
+}

+ 0 - 0
src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts → src/Web/Avalonia.Web.Blazor/webapp/types/dotnet/index.d.ts


+ 40 - 0
src/Web/Avalonia.Web.Blazor/webapp/webpack.config.js

@@ -0,0 +1,40 @@
+const path = require('path');
+const prod = process.env.NODE_ENV == 'production';
+
+module.exports = {
+    mode: prod ? "production" : "development",
+    devtool: 'source-map',
+    target: ["web", "es2020"],
+    entry: {
+        avalonia: './modules/Avalonia/Avalonia.ts',
+        avaloniaStorage: {
+            import: './modules/Storage/StorageProvider.ts',
+            dependOn: 'avalonia',
+        }
+    },
+    output: {
+        filename: '[name].js',
+        path: path.resolve(__dirname, '../wwwroot'),
+        library: {
+            type: 'module',
+        },
+    },
+    module: {
+        rules: [
+            {
+                test: /\.tsx?$/,
+                use: 'ts-loader',
+                exclude: /node_modules/,
+            },
+        ],
+    },
+    resolve: {
+        extensions: ['.ts', '.js'],
+    },
+    optimization: {
+        minimize: false
+    },
+    experiments: {
+        outputModule: true,
+    }
+};

+ 3 - 2
src/iOS/Avalonia.iOS/AvaloniaView.cs

@@ -10,6 +10,7 @@ using Avalonia.iOS.Storage;
 using Avalonia.Platform;
 using Avalonia.Platform.Storage;
 using Avalonia.Rendering;
+using Avalonia.Rendering.Composition;
 using CoreAnimation;
 using Foundation;
 using ObjCRuntime;
@@ -63,8 +64,8 @@ namespace Avalonia.iOS
                 // No-op
             }
 
-            public IRenderer CreateRenderer(IRenderRoot root) => new DeferredRenderer(root,
-                AvaloniaLocator.Current.GetService<IRenderLoop>());
+            public IRenderer CreateRenderer(IRenderRoot root) => new CompositingRenderer(root, Platform.Compositor);
+                    
 
             public void Invalidate(Rect rect)
             {

+ 7 - 0
src/iOS/Avalonia.iOS/Platform.cs

@@ -6,6 +6,7 @@ using Avalonia.Input.Platform;
 using Avalonia.OpenGL;
 using Avalonia.Platform;
 using Avalonia.Rendering;
+using Avalonia.Rendering.Composition;
 
 namespace Avalonia
 {
@@ -26,6 +27,8 @@ namespace Avalonia.iOS
     {
         public static EaglFeature GlFeature;
         public static DisplayLinkTimer Timer;
+        internal static Compositor Compositor { get; private set; }
+        
         class PlatformSettings : IPlatformSettings
         {
             /// <inheritdoc cref="IPlatformSettings.TouchDoubleClickSize"/>
@@ -57,6 +60,10 @@ namespace Avalonia.iOS
                 .Bind<IRenderTimer>().ToConstant(Timer)
                 .Bind<IPlatformThreadingInterface>().ToConstant(new PlatformThreadingInterface())
                 .Bind<IKeyboardDevice>().ToConstant(keyboard);
+
+                Compositor = new Compositor(
+                    AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(),
+                    AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>());
         }
 
 

+ 64 - 0
src/tools/DevAnalyzers/OnPropertyChangedOverrideAnalyzer.cs

@@ -0,0 +1,64 @@
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace DevAnalyzers
+{
+    [DiagnosticAnalyzer(LanguageNames.CSharp)]
+    public class OnPropertyChangedOverrideAnalyzer : DiagnosticAnalyzer
+    {
+        public const string DiagnosticId = "AVADEV2001";
+
+        private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
+            DiagnosticId,
+            "Missing invoke base.OnPropertyChanged",
+            "Method '{0}' do not invoke base.{0}",
+            "Potential issue",
+            DiagnosticSeverity.Warning,
+            isEnabledByDefault: true,
+            description: "The OnPropertyChanged of the base class was not invoked in the override method declaration, which could lead to unwanted behavior.");
+
+        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+        public override void Initialize(AnalysisContext context)
+        {
+            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+            context.EnableConcurrentExecution();
+            context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method);
+        }
+
+        private static void AnalyzeMethod(SymbolAnalysisContext context)
+        {
+            if (context.Symbol is IMethodSymbol currentMethod
+                && currentMethod.Name == "OnPropertyChanged"
+                && currentMethod.OverriddenMethod is IMethodSymbol originalMethod)
+            {
+                var declaration = currentMethod.DeclaringSyntaxReferences.FirstOrDefault()
+                    ?.GetSyntax(context.CancellationToken);
+                if (declaration is not null && context.Compilation.GetSemanticModel(declaration!.SyntaxTree) is { } semanticModel)
+                {
+                    if (declaration.SyntaxTree.TryGetRoot(out var root))
+                    {
+                        var baseInvocations = root.DescendantNodes().OfType<BaseExpressionSyntax>();
+                        if (baseInvocations.Any())
+                        {
+                            foreach (var baseInvocation in baseInvocations)
+                            {
+                                var parent = baseInvocation.Parent;
+                                var targetSymbol = semanticModel.GetSymbolInfo(parent, context.CancellationToken);
+                                if (SymbolEqualityComparer.Default.Equals(targetSymbol.Symbol, originalMethod))
+                                {
+                                    return;
+                                }
+                            }
+                        }
+                        context.ReportDiagnostic(Diagnostic.Create(Rule, currentMethod.Locations[0], currentMethod.Name));
+                    }
+                }
+            }
+        }
+
+    }
+}

+ 4 - 3
src/tools/DevGenerators/CompositionGenerator/CompositionRoslynGenerator.cs

@@ -11,8 +11,9 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
         {
             var schema =
                 context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith("composition-schema.xml"));
-            var configs = schema.Select((t, _) =>
-                (GConfig)new XmlSerializer(typeof(GConfig)).Deserialize(new StringReader(t.GetText().ToString())));
+            var configs = schema.Select((t, _) => t.GetText())
+                .Where(source => source is not null)
+                .Select((source, _) => (GConfig)new XmlSerializer(typeof(GConfig)).Deserialize(new StringReader(source!.ToString())));
             context.RegisterSourceOutput(configs, (spc, config) =>
             {
                 var generator = new Generator(new RoslynCompositionGeneratorSink(spc), config);
@@ -20,4 +21,4 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
             });
         }
     }
-}
+}

+ 14 - 16
src/tools/DevGenerators/CompositionGenerator/Extensions.cs

@@ -7,49 +7,47 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
 {
     public static class Extensions
     {
-        public static ClassDeclarationSyntax AddModifiers(this ClassDeclarationSyntax cl, params SyntaxKind[] modifiers)
+        public static ClassDeclarationSyntax AddModifiers(this ClassDeclarationSyntax cl, params SyntaxKind[]? modifiers)
         {
-            if (modifiers == null)
+            if (modifiers is null)
                 return cl;
             return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray());
         }
         
-        public static MethodDeclarationSyntax AddModifiers(this MethodDeclarationSyntax cl, params SyntaxKind[] modifiers)
+        public static MethodDeclarationSyntax AddModifiers(this MethodDeclarationSyntax cl, params SyntaxKind[]? modifiers)
         {
-            if (modifiers == null)
+            if (modifiers is null)
                 return cl;
             return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray());
         }
         
-        public static PropertyDeclarationSyntax AddModifiers(this PropertyDeclarationSyntax cl, params SyntaxKind[] modifiers)
+        public static PropertyDeclarationSyntax AddModifiers(this PropertyDeclarationSyntax cl, params SyntaxKind[]? modifiers)
         {
-            if (modifiers == null)
+            if (modifiers is null)
                 return cl;
             return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray());
         }
 
-        public static ConstructorDeclarationSyntax AddModifiers(this ConstructorDeclarationSyntax cl, params SyntaxKind[] modifiers)
+        public static ConstructorDeclarationSyntax AddModifiers(this ConstructorDeclarationSyntax cl, params SyntaxKind[]? modifiers)
         {
-            if (modifiers == null)
+            if (modifiers is null)
                 return cl;
             return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray());
         }
         
-        public static AccessorDeclarationSyntax AddModifiers(this AccessorDeclarationSyntax cl, params SyntaxKind[] modifiers)
+        public static AccessorDeclarationSyntax AddModifiers(this AccessorDeclarationSyntax cl, params SyntaxKind[]? modifiers)
         {
-            if (modifiers == null)
+            if (modifiers is null)
                 return cl;
             return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray());
         }
         
-        public static EnumDeclarationSyntax AddModifiers(this EnumDeclarationSyntax cl, params SyntaxKind[] modifiers)
+        public static EnumDeclarationSyntax AddModifiers(this EnumDeclarationSyntax cl, params SyntaxKind[]? modifiers)
         {
-            if (modifiers == null)
+            if (modifiers is null)
                 return cl;
             return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray());
         }
-        
-        
 
         public static string WithLowerFirst(this string s)
         {
@@ -58,7 +56,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
             return char.ToLowerInvariant(s[0]) + s.Substring(1);
         }
 
-        public static ExpressionSyntax MemberAccess(params string[] identifiers)
+        public static ExpressionSyntax MemberAccess(params string[]? identifiers)
         {
             if (identifiers == null || identifiers.Length == 0)
                 throw new ArgumentException();
@@ -96,4 +94,4 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
                 ? s.Substring(prefix.Length)
                 : s;
     }
-}
+}

+ 2 - 2
src/tools/DevGenerators/CompositionGenerator/Generator.Utils.cs

@@ -54,7 +54,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
         FieldDeclarationSyntax DeclareField(string type, string name, params SyntaxKind[] modifiers) =>
             DeclareField(type, name, null, modifiers);
 
-        FieldDeclarationSyntax DeclareField(string type, string name, EqualsValueClauseSyntax initializer,
+        FieldDeclarationSyntax DeclareField(string type, string name, EqualsValueClauseSyntax? initializer,
             params SyntaxKind[] modifiers) =>
             FieldDeclaration(
                     VariableDeclaration(ParseTypeName(type),
@@ -63,4 +63,4 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
                 .WithSemicolonToken(Semicolon())
                 .WithModifiers(TokenList(modifiers.Select(x => Token(x))));
     }
-}
+}

+ 8 - 8
src/tools/DevGenerators/CompositionGenerator/Generator.cs

@@ -37,8 +37,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
 
 
 
-        string ServerName(string c) => c != null ? ("Server" + c) : "ServerObject";
-        string ChangesName(string c) => c != null ? (c + "Changes") : "ChangeSet";
+        string ServerName(string? c) => c != null ? ("Server" + c) : "ServerObject";
+        string ChangesName(string? c) => c != null ? (c + "Changes") : "ChangeSet";
         string ChangedFieldsTypeName(GClass c) => c.Name + "ChangedFields";
         string ChangedFieldsFieldName(GClass c) =>  "_changedFieldsOf" + c.Name;
         string PropertyBackingFieldName(GProperty prop) => "_" + prop.Name.WithLowerFirst();
@@ -298,10 +298,10 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
             if(cl.Implements.Count > 0)
                 foreach (var impl in cl.Implements)
                 {
-                    client = client.WithBaseList(client.BaseList.AddTypes(SimpleBaseType(ParseTypeName(impl.Name))));
+                    client = client.WithBaseList(client.BaseList?.AddTypes(SimpleBaseType(ParseTypeName(impl.Name))));
                     if (impl.ServerName != null)
                         server = server.WithBaseList(
-                            server.BaseList.AddTypes(SimpleBaseType(ParseTypeName(impl.ServerName))));
+                            server.BaseList?.AddTypes(SimpleBaseType(ParseTypeName(impl.ServerName))));
 
                     client = client.AddMembers(
                         ParseMemberDeclaration($"{impl.ServerName} {impl.Name}.Server => Server;"));
@@ -527,7 +527,7 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>();
             body = body.AddStatements(
                 ParseStatement("return base.GetPropertyForAnimation(name);"));
             var method = ((MethodDeclarationSyntax) ParseMemberDeclaration(
-                    $"public override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}"))
+                    $"public override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}")!)
                 .WithBody(body);
 
             return cl.AddMembers(method);
@@ -540,7 +540,7 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>();
             body = body.AddStatements(
                 ParseStatement("return base.GetCompositionProperty(name);"));
             var method = ((MethodDeclarationSyntax)ParseMemberDeclaration(
-                    $"public override CompositionProperty? GetCompositionProperty(string name){{}}"))
+                    $"public override CompositionProperty? GetCompositionProperty(string name){{}}")!)
                 .WithBody(body);
 
             return cl.AddMembers(method);
@@ -559,11 +559,11 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>();
             );
             return cl.AddMembers(
                 ((MethodDeclarationSyntax) ParseMemberDeclaration(
-                    "internal override void StartAnimation(string propertyName, CompositionAnimation animation, Avalonia.Rendering.Composition.Expressions.ExpressionVariant? finalValue){}"))
+                    "internal override void StartAnimation(string propertyName, CompositionAnimation animation, Avalonia.Rendering.Composition.Expressions.ExpressionVariant? finalValue){}")!)
                 .WithBody(body));
 
 
         }
         
     }
-}
+}

+ 1 - 0
src/tools/DevGenerators/DevGenerators.csproj

@@ -4,6 +4,7 @@
     <TargetFramework>netstandard2.0</TargetFramework>
     <Nullable>enable</Nullable>
     <IsPackable>false</IsPackable>
+    <LangVersion>10</LangVersion>
   </PropertyGroup>
 
   <ItemGroup>

Some files were not shown because too many files changed in this diff