Browse Source

Merge branch 'master' into fixes/compositing-renderer-hit-test

Dan Walmsley 3 years ago
parent
commit
f0befcc18d
46 changed files with 915 additions and 1156 deletions
  1. 3 0
      .gitignore
  2. 22 68
      nukebuild/Build.cs
  3. 0 4
      nukebuild/BuildParameters.cs
  4. 57 0
      nukebuild/DotNetConfigHelper.cs
  5. 0 1
      src/Avalonia.Base/Media/DashStyle.cs
  6. 21 1
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  7. 7 26
      src/Avalonia.Base/Rendering/DirtyVisuals.cs
  8. 3 3
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  9. 2 1
      src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
  10. 9 0
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  11. 16 17
      src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj
  12. 13 8
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
  13. 18 0
      src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs
  14. 13 28
      src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs
  15. 11 20
      src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs
  16. 5 7
      src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs
  17. 12 20
      src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs
  18. 19 26
      src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs
  19. 15 26
      src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs
  20. 1 1
      src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs
  21. 0 41
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts
  22. 0 23
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/InputHelper.ts
  23. 0 261
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts
  24. 0 68
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.ts
  25. 0 7
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/extras.d.ts
  26. 0 326
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts
  27. 2 1
      src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs
  28. 0 14
      src/Web/Avalonia.Web.Blazor/tsconfig.json
  29. 5 0
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/Avalonia.ts
  30. 40 0
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/DpiWatcher.ts
  31. 22 0
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts
  32. 20 15
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/NativeControlHost.ts
  33. 255 0
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SKHtmlCanvas.ts
  34. 67 0
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SizeWatcher.ts
  35. 79 0
      src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/IndexedDbWrapper.ts
  36. 36 136
      src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts
  37. 16 0
      src/Web/Avalonia.Web.Blazor/webapp/package.json
  38. 18 0
      src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json
  39. 0 0
      src/Web/Avalonia.Web.Blazor/webapp/types/dotnet/index.d.ts
  40. 40 0
      src/Web/Avalonia.Web.Blazor/webapp/webpack.config.js
  41. 3 2
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  42. 7 0
      src/iOS/Avalonia.iOS/Platform.cs
  43. 2 2
      src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs
  44. 2 2
      src/tools/DevGenerators/GetProcAddressInitialization.cs
  45. 4 1
      tests/Avalonia.Base.UnitTests/Media/PenTests.cs
  46. 50 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs

+ 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

+ 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);
+}

+ 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)
         {
         }
 

+ 21 - 1
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();
     }
@@ -116,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();
     }
@@ -200,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)
@@ -249,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)
     {

+ 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>

+ 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();

+ 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>());
         }
 
        

+ 16 - 17
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>
@@ -47,10 +32,24 @@
   <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" />
   </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>

+ 13 - 8
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,6 +29,7 @@ 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 NativeControlHostInterop? _nativeControlHost = null;
@@ -241,8 +243,10 @@ 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 = new InputHelperInterop(_avaloniaModule, _inputElement);
+                _canvasHelper = new InputHelperInterop(_avaloniaModule, _htmlCanvas);
 
                 _inputHelper.Hide();
                 _canvasHelper.SetCursor("default");
@@ -252,11 +256,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");
                 
@@ -306,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();
@@ -348,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();
             }
         }
 

+ 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 element)
         {
-            var interop = new InputHelperInterop(js, element);
-            await interop.ImportAsync();
-            return interop;
+            _module = module;
+            _inputElement = element;
         }
 
-        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);
+        }
+    }
+}

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

@@ -0,0 +1,22 @@
+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';
+    }
+}

+ 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);
+            };
+        });
+    }
+}

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

@@ -1,141 +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,
-        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
@@ -146,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
@@ -168,14 +64,18 @@ class StorageItem {
     }
 
     public async getItems(): Promise<StorageItems> {
+        if (this.handle.kind !== "directory"){
+            return new StorageItems([]);
+        }
+        
         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;
         }
@@ -190,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());
@@ -200,7 +100,7 @@ class StorageItem {
             connection.close();
         }
     }
-    
+
     public async deleteBookmark(): Promise<void> {
         if (!this.bookmarkId) {
             return;
@@ -272,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>());
         }
 
 

+ 2 - 2
src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs

@@ -32,7 +32,7 @@ public class EnumMemberDictionaryGenerator : IIncrementalGenerator
             ).Collect();
         context.RegisterSourceOutput(all, static (context, methods) =>
         {
-            foreach (var typeGroup in methods.GroupBy(f => f.ContainingType))
+            foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default))
             {
                 var classBuilder = new StringBuilder();
                 if (typeGroup.Key.ContainingNamespace != null)
@@ -91,4 +91,4 @@ public class EnumMemberDictionaryGenerator : IIncrementalGenerator
         
     }
 
-}
+}

+ 2 - 2
src/tools/DevGenerators/GetProcAddressInitialization.cs

@@ -34,7 +34,7 @@ public class GetProcAddressInitializationGenerator : IIncrementalGenerator
         var all = fieldsWithAttribute.Collect();
         context.RegisterSourceOutput(all, static (context, methods) =>
         {
-            foreach (var typeGroup in methods.GroupBy(f => f.ContainingType))
+            foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default))
             {
                 var nextContext = 0;
                 var contexts = new Dictionary<string, int>();
@@ -335,4 +335,4 @@ public class GetProcAddressInitializationGenerator : IIncrementalGenerator
         return name.ToString();
     }
 
-}
+}

+ 4 - 1
tests/Avalonia.Base.UnitTests/Media/PenTests.cs

@@ -53,7 +53,10 @@ namespace Avalonia.Base.UnitTests.Media
             var raised = false;
 
             target.Invalidated += (s, e) => raised = true;
-            dashes.Dashes.Add(0.3);
+            dashes.Dashes = new AvaloniaList<double>
+            {
+                0.3
+            };
 
             Assert.True(raised);
         }

+ 50 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs

@@ -1,3 +1,5 @@
+using System;
+using System.Linq;
 using Avalonia.Controls;
 using Avalonia.Controls.Presenters;
 using Avalonia.Data;
@@ -5,12 +7,49 @@ using Avalonia.Diagnostics;
 using Avalonia.Markup.Xaml.Templates;
 using Avalonia.Media;
 using Avalonia.UnitTests;
+using Avalonia.VisualTree;
 using Xunit;
 
 namespace Avalonia.Markup.Xaml.UnitTests.Xaml
 {
     public class ControlTemplateTests : XamlTestBase
     {
+        [Fact]
+        public void StyledProperties_Should_Be_Set_In_The_ControlTemplate()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        xmlns:controls=""using:Avalonia.Markup.Xaml.UnitTests.Xaml"">
+    <Button>
+        <Button.Template>
+            <ControlTemplate>
+                <controls:ListBoxHierachyLine>
+                    <controls:ListBoxHierachyLine.LineDashStyle>
+                        <DashStyle Dashes=""2,2"" Offset=""1"" />
+                    </controls:ListBoxHierachyLine.LineDashStyle>
+                </controls:ListBoxHierachyLine>
+            </ControlTemplate>
+        </Button.Template>
+    </Button>
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var button = (Button)window.Content;
+
+                window.ApplyTemplate();
+                button.ApplyTemplate();
+                var listBoxHierarchyLine = button.GetVisualChildren().ElementAt(0) as ListBoxHierachyLine;
+                Assert.Equal(1, listBoxHierarchyLine.LineDashStyle.Offset);
+                Assert.Equal(2, listBoxHierarchyLine.LineDashStyle.Dashes.Count);
+                Assert.Equal(2, listBoxHierarchyLine.LineDashStyle.Dashes[0]);
+                Assert.Equal(2, listBoxHierarchyLine.LineDashStyle.Dashes[1]);
+            }
+            
+        }
+
         [Fact]
         public void Inline_ControlTemplate_Styled_Values_Are_Set_With_Style_Priority()
         {
@@ -270,4 +309,15 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
             Assert.Equal("Bar", bar.Name);
         }
     }
+    public class ListBoxHierachyLine : Panel
+    {
+        public static readonly StyledProperty<DashStyle> LineDashStyleProperty =
+        AvaloniaProperty.Register<ListBoxHierachyLine, DashStyle>(nameof(LineDashStyle));
+
+        public DashStyle LineDashStyle
+        {
+            get => GetValue(LineDashStyleProperty);
+            set => SetValue(LineDashStyleProperty, value);
+        }
+    }
 }