Browse Source

Fix namspaces
Fix native asset reference
Move js interop classes

Benedikt Stebner 4 years ago
parent
commit
a212489efb
55 changed files with 640 additions and 519 deletions
  1. 1 1
      Avalonia.sln
  2. 1 1
      samples/ControlCatalog.Web/App.razor.cs
  3. 4 4
      samples/ControlCatalog.Web/ControlCatalog.Web.csproj
  4. 1 1
      samples/ControlCatalog.Web/Pages/Index.razor
  5. 1 1
      samples/ControlCatalog.Web/Program.cs
  6. BIN
      samples/ControlCatalog.Web/libHarfBuzzSharp.a
  7. 0 1
      samples/ControlCatalog.Web/wwwroot/index.html
  8. 0 20
      src/Web/Avalonia.Blazor/Interop/ActionHelper.cs
  9. 0 87
      src/Web/Avalonia.Blazor/Interop/DpiWatcherInterop.cs
  10. 0 20
      src/Web/Avalonia.Blazor/Interop/FloatFloatActionHelper.cs
  11. 0 42
      src/Web/Avalonia.Blazor/Interop/JSModuleInterop.cs
  12. 0 79
      src/Web/Avalonia.Blazor/Interop/SKHtmlCanvasInterop.cs
  13. 0 61
      src/Web/Avalonia.Blazor/Interop/SizeWatcherInterop.cs
  14. 0 63
      src/Web/Avalonia.Blazor/SKTypefaceCollection.cs
  15. 0 68
      src/Web/Avalonia.Blazor/SKTypefaceCollectionCache.cs
  16. 0 6
      src/Web/Avalonia.Blazor/package-lock.json
  17. BIN
      src/Web/Avalonia.Blazor/wwwroot/background.png
  18. 0 0
      src/Web/Avalonia.Web.Blazor/Assets/NotoMono-Regular.ttf
  19. 0 0
      src/Web/Avalonia.Web.Blazor/Assets/NotoSans-Italic.ttf
  20. 0 0
      src/Web/Avalonia.Web.Blazor/Assets/TwitterColorEmoji-SVGinOT.ttf
  21. 19 2
      src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj
  22. 2 2
      src/Web/Avalonia.Web.Blazor/AvaloniaBlazorAppBuilder.cs
  23. 0 0
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor
  24. 30 18
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
  25. 2 4
      src/Web/Avalonia.Web.Blazor/BlazorRuntimePlatform.cs
  26. 5 2
      src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs
  27. 1 2
      src/Web/Avalonia.Web.Blazor/BlazorSkiaGpu.cs
  28. 1 1
      src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderSession.cs
  29. 1 1
      src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs
  30. 2 3
      src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs
  31. 4 4
      src/Web/Avalonia.Web.Blazor/CustomFontManagerImpl.cs
  32. 1 1
      src/Web/Avalonia.Web.Blazor/ExampleJsInterop.cs
  33. 20 0
      src/Web/Avalonia.Web.Blazor/Interop/ActionHelper.cs
  34. 87 0
      src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs
  35. 20 0
      src/Web/Avalonia.Web.Blazor/Interop/FloatFloatActionHelper.cs
  36. 42 0
      src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs
  37. 79 0
      src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs
  38. 61 0
      src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs
  39. 28 0
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.js
  40. 0 0
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts
  41. 163 0
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.js
  42. 0 0
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts
  43. 42 0
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.js
  44. 0 0
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.ts
  45. 0 0
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/extras.d.ts
  46. 0 0
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts
  47. 0 0
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts
  48. 1 1
      src/Web/Avalonia.Web.Blazor/Keycodes.cs
  49. 4 4
      src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs
  50. 3 3
      src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs
  51. 5 5
      src/Web/Avalonia.Web.Blazor/WinStubs.cs
  52. 9 11
      src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs
  53. 0 0
      src/Web/Avalonia.Web.Blazor/_Imports.razor
  54. 0 0
      src/Web/Avalonia.Web.Blazor/tsconfig.json
  55. 0 0
      src/Web/Avalonia.Web.Blazor/wwwroot/.gitignore

+ 1 - 1
Avalonia.sln

@@ -232,7 +232,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniMvvm", "samples\MiniMvv
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}"
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Blazor", "src\Web\Avalonia.Blazor\Avalonia.Blazor.csproj", "{25831348-EB2A-483E-9576-E8F6528674A5}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Web.Blazor", "src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.csproj", "{25831348-EB2A-483E-9576-E8F6528674A5}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Web", "samples\ControlCatalog.Web\ControlCatalog.Web.csproj", "{C08E9894-AA92-426E-BF56-033E262CAD3E}"
 EndProject

+ 1 - 1
samples/ControlCatalog.Web/App.razor.cs

@@ -1,4 +1,4 @@
-using Avalonia.Blazor;
+using Avalonia.Web.Blazor;
 
 namespace ControlCatalog.Web;
 

+ 4 - 4
samples/ControlCatalog.Web/ControlCatalog.Web.csproj

@@ -19,19 +19,19 @@
   </PropertyGroup>
   
   <ItemGroup>
+    <PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="2.8.2-preview.171" />
+    <PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.0-preview.171" />
     <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0" />
     <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0" PrivateAssets="all" />
-    <PackageReference Include="SkiaSharp" Version="2.88.0-preview.155" />    
-    <PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.0-preview.155" />
   </ItemGroup>
 
   <ItemGroup>
-    <NativeFileReference Include="libHarfBuzzSharp.a" />
+    <NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\libHarfBuzzSharp.a" />
     <NativeFileReference Include="$(SkiaSharpStaticLibraryPath)\2.0.23\libSkiaSharp.a" />
   </ItemGroup>
 
   <ItemGroup>
-    <ProjectReference Include="..\..\src\Web\Avalonia.Blazor\Avalonia.Blazor.csproj" />
+    <ProjectReference Include="..\..\src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.csproj" />
     <ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
   </ItemGroup>
 

+ 1 - 1
samples/ControlCatalog.Web/Pages/Index.razor

@@ -1,5 +1,5 @@
 @page "/"
 
-@using Avalonia.Blazor
+@using Avalonia.Web.Blazor
 
 <AvaloniaView />

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

@@ -15,7 +15,7 @@ public class Program
     public static WebAssemblyHostBuilder CreateHostBuilder(string[] args)
     {
         var builder = WebAssemblyHostBuilder.CreateDefault(args);
-            
+        
         builder.RootComponents.Add<App>("#app");
 
         builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

BIN
samples/ControlCatalog.Web/libHarfBuzzSharp.a


+ 0 - 1
samples/ControlCatalog.Web/wwwroot/index.html

@@ -6,7 +6,6 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
     <title>Avalonia Sample</title>
     <base href="/" />
-    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
     <link href="css/app.css" rel="stylesheet" />
 </head>
 

+ 0 - 20
src/Web/Avalonia.Blazor/Interop/ActionHelper.cs

@@ -1,20 +0,0 @@
-using System;
-using System.ComponentModel;
-using Microsoft.JSInterop;
-
-namespace Avalonia.Blazor.Interop
-{
-	[EditorBrowsable(EditorBrowsableState.Never)]
-	public class ActionHelper
-	{
-		private readonly Action action;
-
-		public ActionHelper(Action action)
-		{
-			this.action = action;
-		}
-
-		[JSInvokable]
-		public void Invoke() => action?.Invoke();
-	}
-}

+ 0 - 87
src/Web/Avalonia.Blazor/Interop/DpiWatcherInterop.cs

@@ -1,87 +0,0 @@
-using System;
-using System.Threading.Tasks;
-using Microsoft.JSInterop;
-
-namespace Avalonia.Blazor.Interop
-{
-	internal class DpiWatcherInterop : JSModuleInterop
-	{
-		private const string JsFilename = "./_content/Avalonia.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 DotNetObjectReference<FloatFloatActionHelper>? callbackReference;
-
-		public static async Task<DpiWatcherInterop> ImportAsync(IJSRuntime js, Action<double>? callback = null)
-		{
-			var interop = Get(js);
-			await interop.ImportAsync();
-			if (callback != null)
-				interop.Subscribe(callback);
-			return interop;
-		}
-
-		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));
-		}
-
-		protected override void OnDisposingModule() =>
-			Stop();
-
-		public void Subscribe(Action<double> callback)
-		{
-			var shouldStart = callbacksEvent == null;
-
-			callbacksEvent += callback;
-
-			var dpi = shouldStart
-				? Start()
-				: GetDpi();
-
-			callback(dpi);
-		}
-
-		public void Unsubscribe(Action<double> callback)
-		{
-			callbacksEvent -= callback;
-
-			if (callbacksEvent == null)
-				Stop();
-		}
-
-		private double Start()
-		{
-			if (callbackReference != null)
-				return GetDpi();
-
-			callbackReference = DotNetObjectReference.Create(callbackHelper);
-
-			return Invoke<double>(StartSymbol, callbackReference);
-		}
-
-		private void Stop()
-		{
-			if (callbackReference == null)
-				return;
-
-			Invoke(StopSymbol);
-
-			callbackReference?.Dispose();
-			callbackReference = null;
-		}
-
-		public double GetDpi() =>
-			Invoke<double>(GetDpiSymbol);
-	}
-}

+ 0 - 20
src/Web/Avalonia.Blazor/Interop/FloatFloatActionHelper.cs

@@ -1,20 +0,0 @@
-using System;
-using System.ComponentModel;
-using Microsoft.JSInterop;
-
-namespace Avalonia.Blazor.Interop
-{
-	[EditorBrowsable(EditorBrowsableState.Never)]
-	public class FloatFloatActionHelper
-	{
-		private readonly Action<float, float> action;
-
-		public FloatFloatActionHelper(Action<float, float> action)
-		{
-			this.action = action;
-		}
-
-		[JSInvokable]
-		public void Invoke(float width, float height) => action?.Invoke(width, height);
-	}
-}

+ 0 - 42
src/Web/Avalonia.Blazor/Interop/JSModuleInterop.cs

@@ -1,42 +0,0 @@
-using System;
-using System.Threading.Tasks;
-using Microsoft.JSInterop;
-
-namespace Avalonia.Blazor.Interop
-{
-	internal class JSModuleInterop : IDisposable
-	{
-		private readonly Task<IJSUnmarshalledObjectReference> moduleTask;
-		private IJSUnmarshalledObjectReference? module;
-
-		public JSModuleInterop(IJSRuntime js, string filename)
-		{
-			if (js is not IJSInProcessRuntime)
-				throw new NotSupportedException("SkiaSharp currently only works on Web Assembly.");
-
-			moduleTask = js.InvokeAsync<IJSUnmarshalledObjectReference>("import", filename).AsTask();
-		}
-
-		public async Task ImportAsync()
-		{
-			module = await moduleTask;
-		}
-
-		public void Dispose()
-		{
-			OnDisposingModule();
-			Module.Dispose();
-		}
-
-		protected IJSUnmarshalledObjectReference Module =>
-			module ?? throw new InvalidOperationException("Make sure to run ImportAsync() first.");
-
-		protected void Invoke(string identifier, params object?[]? args) =>
-			Module.InvokeVoid(identifier, args);
-
-		protected TValue Invoke<TValue>(string identifier, params object?[]? args) =>
-			Module.Invoke<TValue>(identifier, args);
-
-		protected virtual void OnDisposingModule() { }
-	}
-}

+ 0 - 79
src/Web/Avalonia.Blazor/Interop/SKHtmlCanvasInterop.cs

@@ -1,79 +0,0 @@
-using Microsoft.AspNetCore.Components;
-using Microsoft.JSInterop;
-using SkiaSharp;
-
-namespace Avalonia.Blazor.Interop
-{
-    internal class SKHtmlCanvasInterop : JSModuleInterop
-	{
-		private const string JsFilename = "./_content/Avalonia.Blazor/SKHtmlCanvas.js";
-		private const string InitGLSymbol = "SKHtmlCanvas.initGL";
-		private const string InitRasterSymbol = "SKHtmlCanvas.initRaster";
-		private const string DeinitSymbol = "SKHtmlCanvas.deinit";
-		private const string RequestAnimationFrameSymbol = "SKHtmlCanvas.requestAnimationFrame";
-		private const string PutImageDataSymbol = "SKHtmlCanvas.putImageData";
-
-		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)
-		{
-			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;
-
-			callbackHelper = new ActionHelper(renderFrameCallback);
-		}
-
-		protected override void OnDisposingModule() =>
-			Deinit();
-
-		public GLInfo InitGL()
-		{
-			if (callbackReference != null)
-				throw new InvalidOperationException("Unable to initialize the same canvas more than once.");
-
-			callbackReference = DotNetObjectReference.Create(callbackHelper);
-
-			return Invoke<GLInfo>(InitGLSymbol, htmlCanvas, htmlElementId, callbackReference);
-		}
-
-		public bool InitRaster()
-		{
-			if (callbackReference != null)
-				throw new InvalidOperationException("Unable to initialize the same canvas more than once.");
-
-			callbackReference = DotNetObjectReference.Create(callbackHelper);
-
-			return Invoke<bool>(InitRasterSymbol, htmlCanvas, htmlElementId, callbackReference);
-		}
-
-		public void Deinit()
-		{
-			if (callbackReference == null)
-				return;
-
-			Invoke(DeinitSymbol, htmlElementId);
-
-			callbackReference?.Dispose();
-		}
-
-		public void RequestAnimationFrame(bool enableRenderLoop, int rawWidth, int rawHeight) =>
-			Invoke(RequestAnimationFrameSymbol, htmlCanvas, enableRenderLoop, rawWidth, rawHeight);
-
-		public void PutImageData(IntPtr intPtr, SKSizeI rawSize) =>
-			Invoke(PutImageDataSymbol, htmlCanvas, intPtr.ToInt64(), rawSize.Width, rawSize.Height);
-
-		public record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth);
-	}
-}

+ 0 - 61
src/Web/Avalonia.Blazor/Interop/SizeWatcherInterop.cs

@@ -1,61 +0,0 @@
-using System;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Components;
-using Microsoft.JSInterop;
-using SkiaSharp;
-
-namespace Avalonia.Blazor.Interop
-{
-	internal class SizeWatcherInterop : JSModuleInterop
-	{
-		private const string JsFilename = "./_content/Avalonia.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 DotNetObjectReference<FloatFloatActionHelper>? callbackReference;
-
-		public static async Task<SizeWatcherInterop> ImportAsync(IJSRuntime js, ElementReference element, Action<SKSize> callback)
-		{
-			var interop = new SizeWatcherInterop(js, element, callback);
-			await interop.ImportAsync();
-			interop.Start();
-			return interop;
-		}
-
-		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 Start()
-		{
-			if (callbackReference != null)
-				return;
-
-			callbackReference = DotNetObjectReference.Create(callbackHelper);
-
-			Invoke(ObserveSymbol, htmlElement, htmlElementId, callbackReference);
-		}
-
-		public void Stop()
-		{
-			if (callbackReference == null)
-				return;
-
-			Invoke(UnobserveSymbol, htmlElementId);
-
-			callbackReference?.Dispose();
-			callbackReference = null;
-		}
-	}
-}

+ 0 - 63
src/Web/Avalonia.Blazor/SKTypefaceCollection.cs

@@ -1,63 +0,0 @@
-using System.Collections.Concurrent;
-using Avalonia.Media;
-using SkiaSharp;
-
-namespace Avalonia.Skia
-{
-    internal class SKTypefaceCollection
-    {
-        private readonly ConcurrentDictionary<Typeface, SKTypeface> _typefaces =
-            new ConcurrentDictionary<Typeface, SKTypeface>();
-
-        public void AddTypeface(Typeface key, SKTypeface typeface)
-        {
-            _typefaces.TryAdd(key, typeface);
-        }
-
-        public SKTypeface Get(Typeface typeface)
-        {
-            return GetNearestMatch(_typefaces, typeface);
-        }
-
-        private static SKTypeface GetNearestMatch(IDictionary<Typeface, SKTypeface> typefaces, Typeface key)
-        {
-            if (typefaces.TryGetValue(key, out var typeface))
-            {
-                return typeface;
-            }
-            
-            var initialWeight = (int)key.Weight;
-
-            var weight = (int)key.Weight;
-
-            weight -= weight % 50; // make sure we start at a full weight
-
-            for (var i = 0; i < 2; i++)
-            {
-                for (var j = 0; j < initialWeight; j += 50)
-                {
-                    if (weight - j >= 100)
-                    {
-                        if (typefaces.TryGetValue(new Typeface(key.FontFamily, (FontStyle)i, (FontWeight)(weight - j)), out typeface))
-                        {
-                            return typeface;
-                        }
-                    }
-
-                    if (weight + j > 900)
-                    {
-                        continue;
-                    }
-
-                    if (typefaces.TryGetValue(new Typeface(key.FontFamily, (FontStyle)i, (FontWeight)(weight + j)), out typeface))
-                    {
-                        return typeface;
-                    }
-                }
-            }
-
-            //Nothing was found so we try to get a regular typeface.
-            return typefaces.TryGetValue(new Typeface(key.FontFamily), out typeface) ? typeface : null;
-        }
-    }
-}

+ 0 - 68
src/Web/Avalonia.Blazor/SKTypefaceCollectionCache.cs

@@ -1,68 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using Avalonia.Media;
-using Avalonia.Media.Fonts;
-using Avalonia.Platform;
-using SkiaSharp;
-
-namespace Avalonia.Skia
-{
-    internal static class SKTypefaceCollectionCache
-    {
-        private static readonly ConcurrentDictionary<FontFamily, SKTypefaceCollection> s_cachedCollections;
-
-        static SKTypefaceCollectionCache()
-        {
-            s_cachedCollections = new ConcurrentDictionary<FontFamily, SKTypefaceCollection>();
-        }
-
-        /// <summary>
-        /// Gets the or add typeface collection.
-        /// </summary>
-        /// <param name="fontFamily">The font family.</param>
-        /// <returns></returns>
-        public static SKTypefaceCollection GetOrAddTypefaceCollection(FontFamily fontFamily)
-        {
-            return s_cachedCollections.GetOrAdd(fontFamily, x => CreateCustomFontCollection(fontFamily));
-        }
-
-        /// <summary>
-        /// Creates the custom font collection.
-        /// </summary>
-        /// <param name="fontFamily">The font family.</param>
-        /// <returns></returns>
-        private static SKTypefaceCollection CreateCustomFontCollection(FontFamily fontFamily)
-        {
-            var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key);
-
-            var typeFaceCollection = new SKTypefaceCollection();
-
-            var assetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>();
-
-            foreach (var asset in fontAssets)
-            {
-                var assetStream = assetLoader.Open(asset);
-
-                if (assetStream == null)
-                    throw new InvalidOperationException("Asset could not be loaded.");
-
-                var typeface = SKTypeface.FromStream(assetStream);
-
-                if (typeface == null)
-                    throw new InvalidOperationException("Typeface could not be loaded.");
-
-                if (typeface.FamilyName != fontFamily.Name)
-                {
-                    continue;
-                }
-
-                var key = new Typeface(fontFamily, typeface.FontSlant.ToAvalonia(),
-                    (FontWeight)typeface.FontWeight);
-
-                typeFaceCollection.AddTypeface(key, typeface);
-            }
-
-            return typeFaceCollection;
-        }
-    }
-}

+ 0 - 6
src/Web/Avalonia.Blazor/package-lock.json

@@ -1,6 +0,0 @@
-{
-  "name": "Avalonia.Blazor",
-  "lockfileVersion": 2,
-  "requires": true,
-  "packages": {}
-}

BIN
src/Web/Avalonia.Blazor/wwwroot/background.png


+ 0 - 0
src/Web/Avalonia.Blazor/Assets/NotoMono-Regular.ttf → src/Web/Avalonia.Web.Blazor/Assets/NotoMono-Regular.ttf


+ 0 - 0
src/Web/Avalonia.Blazor/Assets/NotoSans-Italic.ttf → src/Web/Avalonia.Web.Blazor/Assets/NotoSans-Italic.ttf


+ 0 - 0
src/Web/Avalonia.Blazor/Assets/TwitterColorEmoji-SVGinOT.ttf → src/Web/Avalonia.Web.Blazor/Assets/TwitterColorEmoji-SVGinOT.ttf


+ 19 - 2
src/Web/Avalonia.Blazor/Avalonia.Blazor.csproj → src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj

@@ -7,13 +7,30 @@
     <PackageId>Avalonia.Web.Blazor</PackageId>
     <LangVersion>preview</LangVersion>
   </PropertyGroup>
-
-
+  
   <ItemGroup>
     <SupportedPlatform Include="browser" />
     <Compile Include="..\..\Shared\PlatformSupport\AssetLoader.cs" />
   </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\SkiaSharp.props" />
+  <Import Project="..\..\..\build\HarfBuzzSharp.props" />
+
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.0" />
     <PackageReference Include="Microsoft.TypeScript.MSBuild" Version="4.5.2" PrivateAssets="all" />

+ 2 - 2
src/Web/Avalonia.Blazor/AvaloniaBlazorAppBuilder.cs → src/Web/Avalonia.Web.Blazor/AvaloniaBlazorAppBuilder.cs

@@ -2,11 +2,11 @@ using System;
 using Avalonia.Controls;
 using Avalonia.Platform;
 
-namespace Avalonia.Blazor
+namespace Avalonia.Web.Blazor
 {
     public class AvaloniaBlazorAppBuilder : AppBuilderBase<AvaloniaBlazorAppBuilder>
     {
-        public AvaloniaBlazorAppBuilder(IRuntimePlatform platform, Action<AvaloniaBlazorAppBuilder> platformServices) 
+        public AvaloniaBlazorAppBuilder(IRuntimePlatform platform, Action<AvaloniaBlazorAppBuilder> platformServices)
             : base(platform, platformServices)
         {
         }

+ 0 - 0
src/Web/Avalonia.Blazor/AvaloniaView.razor → src/Web/Avalonia.Web.Blazor/AvaloniaView.razor


+ 30 - 18
src/Web/Avalonia.Blazor/AvaloniaView.razor.cs → src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs

@@ -1,15 +1,15 @@
-using Avalonia.Blazor.Interop;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Controls.Embedding;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
 using Avalonia.Input.TextInput;
+using Avalonia.Web.Blazor.Interop;
 using Microsoft.AspNetCore.Components;
 using Microsoft.AspNetCore.Components.Web;
 using Microsoft.JSInterop;
 using SkiaSharp;
 
-namespace Avalonia.Blazor
+namespace Avalonia.Web.Blazor
 {
     public partial class AvaloniaView : ITextInputMethodImpl
     {
@@ -103,36 +103,48 @@ namespace Avalonia.Blazor
 
         static RawInputModifiers GetModifiers(WheelEventArgs e)
         {
-            RawInputModifiers modifiers = RawInputModifiers.None;
+            var modifiers = RawInputModifiers.None;
 
-            if (e.CtrlKey) modifiers |= RawInputModifiers.Control;
-            if (e.AltKey) modifiers |= RawInputModifiers.Alt;
-            if (e.ShiftKey) modifiers |= RawInputModifiers.Shift;
-            if (e.MetaKey) modifiers |= RawInputModifiers.Meta;
+            if (e.CtrlKey)
+                modifiers |= RawInputModifiers.Control;
+            if (e.AltKey)
+                modifiers |= RawInputModifiers.Alt;
+            if (e.ShiftKey)
+                modifiers |= RawInputModifiers.Shift;
+            if (e.MetaKey)
+                modifiers |= RawInputModifiers.Meta;
 
             return modifiers;
         }
 
         static RawInputModifiers GetModifiers(MouseEventArgs e)
         {
-            RawInputModifiers modifiers = RawInputModifiers.None;
+            var modifiers = RawInputModifiers.None;
 
-            if (e.CtrlKey) modifiers |= RawInputModifiers.Control;
-            if (e.AltKey) modifiers |= RawInputModifiers.Alt;
-            if (e.ShiftKey) modifiers |= RawInputModifiers.Shift;
-            if (e.MetaKey) modifiers |= RawInputModifiers.Meta;
+            if (e.CtrlKey)
+                modifiers |= RawInputModifiers.Control;
+            if (e.AltKey)
+                modifiers |= RawInputModifiers.Alt;
+            if (e.ShiftKey)
+                modifiers |= RawInputModifiers.Shift;
+            if (e.MetaKey)
+                modifiers |= RawInputModifiers.Meta;
 
             return modifiers;
         }
 
         static RawInputModifiers GetModifiers(KeyboardEventArgs e)
         {
-            RawInputModifiers modifiers = RawInputModifiers.None;
+            var modifiers = RawInputModifiers.None;
 
-            if (e.CtrlKey) modifiers |= RawInputModifiers.Control;
-            if (e.AltKey) modifiers |= RawInputModifiers.Alt;
-            if (e.ShiftKey) modifiers |= RawInputModifiers.Shift;
-            if (e.MetaKey) modifiers |= RawInputModifiers.Meta;
+            if (e.CtrlKey)
+                modifiers |= RawInputModifiers.Control;
+            if (e.AltKey)
+                modifiers |= RawInputModifiers.Alt;
+            if (e.ShiftKey)
+                modifiers |= RawInputModifiers.Shift;
+            if (e.MetaKey)
+                modifiers |= RawInputModifiers.Meta;
 
             return modifiers;
         }
@@ -163,7 +175,7 @@ namespace Avalonia.Blazor
         {
             if (firstRender)
             {
-                Avalonia.Threading.Dispatcher.UIThread.Post(async () =>
+                Threading.Dispatcher.UIThread.Post(async () =>
                 {
                     await Js.InvokeVoidAsync("hideInput");
                     await Js.InvokeVoidAsync("setCursor", "default");

+ 2 - 4
src/Web/Avalonia.Blazor/BlazorRuntimePlatform.cs → src/Web/Avalonia.Web.Blazor/BlazorRuntimePlatform.cs

@@ -1,10 +1,8 @@
-using System;
 using System.Runtime.InteropServices;
-using System.Threading;
 using Avalonia.Platform;
 using Avalonia.Shared.PlatformSupport;
 
-namespace Avalonia.Blazor
+namespace Avalonia.Web.Blazor
 {
     class BlazorRuntimePlatform : IRuntimePlatform
     {
@@ -46,7 +44,7 @@ namespace Avalonia.Blazor
             public int Size { get; }
             public bool IsDisposed => _data == IntPtr.Zero;
         }
-        
+
         public IUnmanagedBlob AllocBlob(int size)
         {
             return new BasicBlob(size);

+ 5 - 2
src/Web/Avalonia.Blazor/BlazorSingleViewLifetime.cs → src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs

@@ -1,7 +1,8 @@
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Platform;
 
-namespace Avalonia.Blazor
+namespace Avalonia.Web.Blazor
 {
     public class BlazorSingleViewLifetime : ISingleViewApplicationLifetime
     {
@@ -10,7 +11,7 @@ namespace Avalonia.Blazor
 
     public static class BlazorSingleViewLifetimeExtensions
     {
-       
+
 
         public static AvaloniaBlazorAppBuilder SetupWithBlazorSingleViewLifetime<TApp>()
             where TApp : Application, new()
@@ -20,6 +21,8 @@ namespace Avalonia.Blazor
             .With(new SkiaOptions() { CustomGpuFactory = () => new BlazorSkiaGpu() })
             .SetupWithLifetime(new BlazorSingleViewLifetime());
 
+            AvaloniaLocator.CurrentMutable.Bind<IFontManagerImpl>().ToConstant(new CustomFontManagerImpl());
+
             return builder;
         }
     }

+ 1 - 2
src/Web/Avalonia.Blazor/BlazorSkiaGpu.cs → src/Web/Avalonia.Web.Blazor/BlazorSkiaGpu.cs

@@ -1,8 +1,7 @@
 using System.Collections.Generic;
-using Avalonia;
 using Avalonia.Skia;
 
-namespace Avalonia.Blazor
+namespace Avalonia.Web.Blazor
 {
     public class BlazorSkiaGpu : ISkiaGpu
     {

+ 1 - 1
src/Web/Avalonia.Blazor/BlazorSkiaGpuRenderSession.cs → src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderSession.cs

@@ -1,7 +1,7 @@
 using Avalonia.Skia;
 using SkiaSharp;
 
-namespace Avalonia.Blazor
+namespace Avalonia.Web.Blazor
 {
     internal class BlazorSkiaGpuRenderSession : ISkiaGpuRenderSession
     {

+ 1 - 1
src/Web/Avalonia.Blazor/BlazorSkiaGpuRenderTarget.cs → src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs

@@ -1,7 +1,7 @@
 using Avalonia.Skia;
 using SkiaSharp;
 
-namespace Avalonia.Blazor
+namespace Avalonia.Web.Blazor
 {
     internal class BlazorSkiaGpuRenderTarget : ISkiaGpuRenderTarget
     {

+ 2 - 3
src/Web/Avalonia.Blazor/BlazorSkiaSurface.cs → src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs

@@ -1,8 +1,7 @@
-using Avalonia;
-using Avalonia.Blazor.Interop;
+using Avalonia.Web.Blazor.Interop;
 using SkiaSharp;
 
-namespace Avalonia.Blazor
+namespace Avalonia.Web.Blazor
 {
     internal class BlazorSkiaSurface
     {

+ 4 - 4
src/Web/Avalonia.Blazor/CustomFontManagerImpl.cs → src/Web/Avalonia.Web.Blazor/CustomFontManagerImpl.cs

@@ -6,7 +6,7 @@ using Avalonia.Platform;
 using Avalonia.Skia;
 using SkiaSharp;
 
-namespace Avalonia.Blazor
+namespace Avalonia.Web.Blazor
 {
     public class CustomFontManagerImpl : IFontManagerImpl
     {
@@ -14,11 +14,11 @@ namespace Avalonia.Blazor
         private readonly string _defaultFamilyName;
 
         private readonly Typeface _defaultTypeface =
-            new Typeface("avares://Avalonia.Blazor/Assets#Noto Mono");
+            new Typeface("avares://Avalonia.Web.Blazor/Assets#Noto Mono");
         private readonly Typeface _italicTypeface =
-            new Typeface("avares://Avalonia.Blazor/Assets#Noto Sans");
+            new Typeface("avares://Avalonia.Web.Blazor/Assets#Noto Sans");
         private readonly Typeface _emojiTypeface =
-            new Typeface("avares://Avalonia.Blazor/Assets#Twitter Color Emoji");
+            new Typeface("avares://Avalonia.Web.Blazor/Assets#Twitter Color Emoji");
 
         public CustomFontManagerImpl()
         {

+ 1 - 1
src/Web/Avalonia.Blazor/ExampleJsInterop.cs → src/Web/Avalonia.Web.Blazor/ExampleJsInterop.cs

@@ -1,6 +1,6 @@
 using Microsoft.JSInterop;
 
-namespace Avalonia.Blazor 
+namespace Avalonia.Web.Blazor
 {
     // This class provides an example of how JavaScript functionality can be wrapped
     // in a .NET class for easy consumption. The associated JavaScript module is

+ 20 - 0
src/Web/Avalonia.Web.Blazor/Interop/ActionHelper.cs

@@ -0,0 +1,20 @@
+using System;
+using System.ComponentModel;
+using Microsoft.JSInterop;
+
+namespace Avalonia.Web.Blazor.Interop
+{
+    [EditorBrowsable(EditorBrowsableState.Never)]
+    public class ActionHelper
+    {
+        private readonly Action action;
+
+        public ActionHelper(Action action)
+        {
+            this.action = action;
+        }
+
+        [JSInvokable]
+        public void Invoke() => action?.Invoke();
+    }
+}

+ 87 - 0
src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs

@@ -0,0 +1,87 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.JSInterop;
+
+namespace Avalonia.Web.Blazor.Interop
+{
+    internal class DpiWatcherInterop : JSModuleInterop
+    {
+        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 DotNetObjectReference<FloatFloatActionHelper>? callbackReference;
+
+        public static async Task<DpiWatcherInterop> ImportAsync(IJSRuntime js, Action<double>? callback = null)
+        {
+            var interop = Get(js);
+            await interop.ImportAsync();
+            if (callback != null)
+                interop.Subscribe(callback);
+            return interop;
+        }
+
+        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));
+        }
+
+        protected override void OnDisposingModule() =>
+            Stop();
+
+        public void Subscribe(Action<double> callback)
+        {
+            var shouldStart = callbacksEvent == null;
+
+            callbacksEvent += callback;
+
+            var dpi = shouldStart
+                ? Start()
+                : GetDpi();
+
+            callback(dpi);
+        }
+
+        public void Unsubscribe(Action<double> callback)
+        {
+            callbacksEvent -= callback;
+
+            if (callbacksEvent == null)
+                Stop();
+        }
+
+        private double Start()
+        {
+            if (callbackReference != null)
+                return GetDpi();
+
+            callbackReference = DotNetObjectReference.Create(callbackHelper);
+
+            return Invoke<double>(StartSymbol, callbackReference);
+        }
+
+        private void Stop()
+        {
+            if (callbackReference == null)
+                return;
+
+            Invoke(StopSymbol);
+
+            callbackReference?.Dispose();
+            callbackReference = null;
+        }
+
+        public double GetDpi() =>
+            Invoke<double>(GetDpiSymbol);
+    }
+}

+ 20 - 0
src/Web/Avalonia.Web.Blazor/Interop/FloatFloatActionHelper.cs

@@ -0,0 +1,20 @@
+using System;
+using System.ComponentModel;
+using Microsoft.JSInterop;
+
+namespace Avalonia.Web.Blazor.Interop
+{
+    [EditorBrowsable(EditorBrowsableState.Never)]
+    public class FloatFloatActionHelper
+    {
+        private readonly Action<float, float> action;
+
+        public FloatFloatActionHelper(Action<float, float> action)
+        {
+            this.action = action;
+        }
+
+        [JSInvokable]
+        public void Invoke(float width, float height) => action?.Invoke(width, height);
+    }
+}

+ 42 - 0
src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs

@@ -0,0 +1,42 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.JSInterop;
+
+namespace Avalonia.Web.Blazor.Interop
+{
+    internal class JSModuleInterop : IDisposable
+    {
+        private readonly Task<IJSUnmarshalledObjectReference> moduleTask;
+        private IJSUnmarshalledObjectReference? module;
+
+        public JSModuleInterop(IJSRuntime js, string filename)
+        {
+            if (js is not IJSInProcessRuntime)
+                throw new NotSupportedException("SkiaSharp currently only works on Web Assembly.");
+
+            moduleTask = js.InvokeAsync<IJSUnmarshalledObjectReference>("import", filename).AsTask();
+        }
+
+        public async Task ImportAsync()
+        {
+            module = await moduleTask;
+        }
+
+        public void Dispose()
+        {
+            OnDisposingModule();
+            Module.Dispose();
+        }
+
+        protected IJSUnmarshalledObjectReference Module =>
+            module ?? throw new InvalidOperationException("Make sure to run ImportAsync() first.");
+
+        protected void Invoke(string identifier, params object?[]? args) =>
+            Module.InvokeVoid(identifier, args);
+
+        protected TValue Invoke<TValue>(string identifier, params object?[]? args) =>
+            Module.Invoke<TValue>(identifier, args);
+
+        protected virtual void OnDisposingModule() { }
+    }
+}

+ 79 - 0
src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs

@@ -0,0 +1,79 @@
+using Microsoft.AspNetCore.Components;
+using Microsoft.JSInterop;
+using SkiaSharp;
+
+namespace Avalonia.Web.Blazor.Interop
+{
+    internal class SKHtmlCanvasInterop : JSModuleInterop
+    {
+        private const string JsFilename = "./_content/Avalonia.Web.Blazor/SKHtmlCanvas.js";
+        private const string InitGLSymbol = "SKHtmlCanvas.initGL";
+        private const string InitRasterSymbol = "SKHtmlCanvas.initRaster";
+        private const string DeinitSymbol = "SKHtmlCanvas.deinit";
+        private const string RequestAnimationFrameSymbol = "SKHtmlCanvas.requestAnimationFrame";
+        private const string PutImageDataSymbol = "SKHtmlCanvas.putImageData";
+
+        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)
+        {
+            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;
+
+            callbackHelper = new ActionHelper(renderFrameCallback);
+        }
+
+        protected override void OnDisposingModule() =>
+            Deinit();
+
+        public GLInfo InitGL()
+        {
+            if (callbackReference != null)
+                throw new InvalidOperationException("Unable to initialize the same canvas more than once.");
+
+            callbackReference = DotNetObjectReference.Create(callbackHelper);
+
+            return Invoke<GLInfo>(InitGLSymbol, htmlCanvas, htmlElementId, callbackReference);
+        }
+
+        public bool InitRaster()
+        {
+            if (callbackReference != null)
+                throw new InvalidOperationException("Unable to initialize the same canvas more than once.");
+
+            callbackReference = DotNetObjectReference.Create(callbackHelper);
+
+            return Invoke<bool>(InitRasterSymbol, htmlCanvas, htmlElementId, callbackReference);
+        }
+
+        public void Deinit()
+        {
+            if (callbackReference == null)
+                return;
+
+            Invoke(DeinitSymbol, htmlElementId);
+
+            callbackReference?.Dispose();
+        }
+
+        public void RequestAnimationFrame(bool enableRenderLoop, int rawWidth, int rawHeight) =>
+            Invoke(RequestAnimationFrameSymbol, htmlCanvas, enableRenderLoop, rawWidth, rawHeight);
+
+        public void PutImageData(IntPtr intPtr, SKSizeI rawSize) =>
+            Invoke(PutImageDataSymbol, htmlCanvas, intPtr.ToInt64(), rawSize.Width, rawSize.Height);
+
+        public record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth);
+    }
+}

+ 61 - 0
src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs

@@ -0,0 +1,61 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components;
+using Microsoft.JSInterop;
+using SkiaSharp;
+
+namespace Avalonia.Web.Blazor.Interop
+{
+    internal class SizeWatcherInterop : JSModuleInterop
+    {
+        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 DotNetObjectReference<FloatFloatActionHelper>? callbackReference;
+
+        public static async Task<SizeWatcherInterop> ImportAsync(IJSRuntime js, ElementReference element, Action<SKSize> callback)
+        {
+            var interop = new SizeWatcherInterop(js, element, callback);
+            await interop.ImportAsync();
+            interop.Start();
+            return interop;
+        }
+
+        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 Start()
+        {
+            if (callbackReference != null)
+                return;
+
+            callbackReference = DotNetObjectReference.Create(callbackHelper);
+
+            Invoke(ObserveSymbol, htmlElement, htmlElementId, callbackReference);
+        }
+
+        public void Stop()
+        {
+            if (callbackReference == null)
+                return;
+
+            Invoke(UnobserveSymbol, htmlElementId);
+
+            callbackReference?.Dispose();
+            callbackReference = null;
+        }
+    }
+}

+ 28 - 0
src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.js

@@ -0,0 +1,28 @@
+export class DpiWatcher {
+    static getDpi() {
+        return window.devicePixelRatio;
+    }
+    static start(callback) {
+        //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;
+    }
+    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);
+        }
+    }
+}
+//# sourceMappingURL=DpiWatcher.js.map

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


+ 163 - 0
src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.js

@@ -0,0 +1,163 @@
+export class SKHtmlCanvas {
+    constructor(useGL, element, callback) {
+        this.renderLoopEnabled = false;
+        this.renderLoopRequest = 0;
+        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,
+                depth: GLctx.getParameter(GLctx.DEPTH_BITS),
+            };
+        }
+    }
+    static initGL(element, elementId, callback) {
+        var view = SKHtmlCanvas.init(true, element, elementId, callback);
+        if (!view || !view.glInfo)
+            return null;
+        return view.glInfo;
+    }
+    static initRaster(element, elementId, callback) {
+        var view = SKHtmlCanvas.init(false, element, elementId, callback);
+        if (!view)
+            return false;
+        return true;
+    }
+    static init(useGL, element, elementId, callback) {
+        var htmlCanvas = element;
+        if (!htmlCanvas) {
+            console.error(`No canvas element was provided.`);
+            return null;
+        }
+        if (!SKHtmlCanvas.elements)
+            SKHtmlCanvas.elements = new Map();
+        SKHtmlCanvas.elements[elementId] = element;
+        const view = new SKHtmlCanvas(useGL, element, callback);
+        htmlCanvas.SKHtmlCanvas = view;
+        return view;
+    }
+    static deinit(elementId) {
+        if (!elementId)
+            return;
+        const element = SKHtmlCanvas.elements[elementId];
+        SKHtmlCanvas.elements.delete(elementId);
+        const htmlCanvas = element;
+        if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
+            return;
+        htmlCanvas.SKHtmlCanvas.deinit();
+        htmlCanvas.SKHtmlCanvas = undefined;
+    }
+    static requestAnimationFrame(element, renderLoop, width, height) {
+        const htmlCanvas = element;
+        if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
+            return;
+        htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop, width, height);
+    }
+    static setEnableRenderLoop(element, enable) {
+        const htmlCanvas = element;
+        if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
+            return;
+        htmlCanvas.SKHtmlCanvas.setEnableRenderLoop(enable);
+    }
+    static putImageData(element, pData, width, height) {
+        const htmlCanvas = element;
+        if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
+            return;
+        htmlCanvas.SKHtmlCanvas.putImageData(pData, width, height);
+    }
+    deinit() {
+        this.setEnableRenderLoop(false);
+    }
+    requestAnimationFrame(renderLoop, width, height) {
+        // optionally update the render loop
+        if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop)
+            this.setEnableRenderLoop(renderLoop);
+        // make sure the canvas is scaled correctly for the drawing
+        if (width && height) {
+            this.htmlCanvas.width = width;
+            this.htmlCanvas.height = height;
+        }
+        // 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);
+            }
+            this.renderFrameCallback.invokeMethod('Invoke');
+            this.renderLoopRequest = 0;
+            // we may want to draw the next frame
+            if (this.renderLoopEnabled)
+                this.requestAnimationFrame();
+        });
+    }
+    setEnableRenderLoop(enable) {
+        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;
+        }
+    }
+    putImageData(pData, width, height) {
+        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) {
+        const contextAttributes = {
+            alpha: 1,
+            depth: 1,
+            stencil: 8,
+            antialias: 1,
+            premultipliedAlpha: 1,
+            preserveDrawingBuffer: 0,
+            preferLowPowerToHighPerformance: 0,
+            failIfMajorPerformanceCaveat: 0,
+            majorVersion: 2,
+            minorVersion: 0,
+            enableExtensionsByDefault: 1,
+            explicitSwapControl: 0,
+            renderViaOffscreenBackBuffer: 0,
+        };
+        let ctx = 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;
+    }
+}
+//# sourceMappingURL=SKHtmlCanvas.js.map

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


+ 42 - 0
src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.js

@@ -0,0 +1,42 @@
+export class SizeWatcher {
+    static observe(element, elementId, callback) {
+        if (!element || !callback)
+            return;
+        //console.info(`Adding size watcher observation with callback ${callback._id}...`);
+        SizeWatcher.init();
+        const watcherElement = element;
+        watcherElement.SizeWatcher = {
+            callback: callback
+        };
+        SizeWatcher.elements[elementId] = element;
+        SizeWatcher.observer.observe(element);
+        SizeWatcher.invoke(element);
+    }
+    static unobserve(elementId) {
+        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();
+        SizeWatcher.observer = new ResizeObserver((entries) => {
+            for (let entry of entries) {
+                SizeWatcher.invoke(entry.target);
+            }
+        });
+    }
+    static invoke(element) {
+        const watcherElement = element;
+        const instance = watcherElement.SizeWatcher;
+        if (!instance || !instance.callback)
+            return;
+        return instance.callback.invokeMethod('Invoke', element.clientWidth, element.clientHeight);
+    }
+}
+//# sourceMappingURL=SizeWatcher.js.map

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


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


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


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


+ 1 - 1
src/Web/Avalonia.Blazor/Keycodes.cs → src/Web/Avalonia.Web.Blazor/Keycodes.cs

@@ -1,7 +1,7 @@
 using System.Collections.Generic;
 using Avalonia.Input;
 
-namespace Avalonia.Blazor
+namespace Avalonia.Web.Blazor
 {
     internal static class Keycodes
     {

+ 4 - 4
src/Web/Avalonia.Blazor/ManualTriggerRenderTimer.cs → src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs

@@ -2,16 +2,16 @@ using System;
 using System.Diagnostics;
 using Avalonia.Rendering;
 
-namespace Avalonia.Blazor
+namespace Avalonia.Web.Blazor
 {
     public class ManualTriggerRenderTimer : IRenderTimer
     {
         private static readonly Stopwatch _sw = Stopwatch.StartNew();
-        
+
         public static ManualTriggerRenderTimer Instance { get; } = new ManualTriggerRenderTimer();
-        
+
         public void RaiseTick() => Tick?.Invoke(_sw.Elapsed);
-        
+
         public event Action<TimeSpan> Tick;
     }
 }

+ 3 - 3
src/Web/Avalonia.Blazor/RazorViewTopLevelImpl.cs → src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs

@@ -1,5 +1,4 @@
 using System.Diagnostics;
-using Avalonia.Blazor.Interop;
 using Avalonia.Controls;
 using Avalonia.Controls.Platform;
 using Avalonia.Input;
@@ -7,9 +6,10 @@ using Avalonia.Input.Raw;
 using Avalonia.Input.TextInput;
 using Avalonia.Platform;
 using Avalonia.Rendering;
+using Avalonia.Web.Blazor.Interop;
 using SkiaSharp;
 
-namespace Avalonia.Blazor
+namespace Avalonia.Web.Blazor
 {
     internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod
     {
@@ -50,7 +50,7 @@ namespace Avalonia.Blazor
 
                 if (_currentSurface is { })
                 {
-                    _currentSurface.Size = new PixelSize((int)(size.Width), (int)(size.Height));
+                    _currentSurface.Size = new PixelSize((int)size.Width, (int)size.Height);
                 }
 
                 Resized?.Invoke(newSize, PlatformResizeReason.User);

+ 5 - 5
src/Web/Avalonia.Blazor/WinStubs.cs → src/Web/Avalonia.Web.Blazor/WinStubs.cs

@@ -9,7 +9,7 @@ using Avalonia.Input;
 using Avalonia.Input.Platform;
 using Avalonia.Platform;
 
-namespace Avalonia.Blazor
+namespace Avalonia.Web.Blazor
 {
     class ClipboardStub : IClipboard
     {
@@ -30,7 +30,7 @@ namespace Avalonia.Blazor
     {
         public void Dispose()
         {
-            
+
         }
     }
 
@@ -53,7 +53,7 @@ namespace Avalonia.Blazor
         {
             public void Save(Stream outputStream)
             {
-                
+
             }
         }
 
@@ -67,10 +67,10 @@ namespace Avalonia.Blazor
     class SystemDialogsStub : ISystemDialogImpl
     {
         public Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent) =>
-            Task.FromResult((string[]) null);
+            Task.FromResult((string[])null);
 
         public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) =>
-            Task.FromResult((string) null);
+            Task.FromResult((string)null);
     }
 
     class ScreenStub : IScreenImpl

+ 9 - 11
src/Web/Avalonia.Blazor/WindowingPlatform.cs → src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs

@@ -1,5 +1,3 @@
-using System;
-using System.Threading;
 using Avalonia.Controls.Platform;
 using Avalonia.Input;
 using Avalonia.Input.Platform;
@@ -7,7 +5,7 @@ using Avalonia.Platform;
 using Avalonia.Rendering;
 using Avalonia.Threading;
 
-namespace Avalonia.Blazor
+namespace Avalonia.Web.Blazor
 {
     public class BlazorWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface
     {
@@ -17,7 +15,7 @@ namespace Avalonia.Blazor
         private static int _uiThreadId = -1;
         private static int _lockNesting = 0;
 
-        public IWindowImpl CreateWindow() => throw new System.NotSupportedException();
+        public IWindowImpl CreateWindow() => throw new NotSupportedException();
 
         IWindowImpl IWindowingPlatform.CreateEmbeddableWindow()
         {
@@ -28,9 +26,9 @@ namespace Avalonia.Blazor
         {
             return null;
         }
-        
+
         public static KeyboardDevice Keyboard { get; private set; }
-        
+
         public static void Register()
         {
             var instance = new BlazorWindowingPlatform();
@@ -75,7 +73,7 @@ namespace Avalonia.Blazor
         {
             lock (_syncRootLock)
             {
-                if(_signaled)
+                if (_signaled)
                     return;
                 _signaled = true;
                 IDisposable disp = null;
@@ -88,7 +86,7 @@ namespace Avalonia.Blazor
                                 _signaled = false;
                                 disp.Dispose();
                             }
-                            
+
                             using (Lock())
                                 Signaled?.Invoke(null);
                         });
@@ -102,7 +100,7 @@ namespace Avalonia.Blazor
                 return true; // Blazor is single threaded.
             }
         }
-        
+
         public event Action<DispatcherPriority?> Signaled;
 
         class LockDisposable : IDisposable
@@ -119,7 +117,7 @@ namespace Avalonia.Blazor
                 Monitor.Exit(_uiLock);
             }
         }
-        
+
         public static IDisposable Lock()
         {
             Monitor.Enter(_uiLock);
@@ -132,4 +130,4 @@ namespace Avalonia.Blazor
             return new LockDisposable();
         }
     }
-}
+}

+ 0 - 0
src/Web/Avalonia.Blazor/_Imports.razor → src/Web/Avalonia.Web.Blazor/_Imports.razor


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


+ 0 - 0
src/Web/Avalonia.Blazor/wwwroot/.gitignore → src/Web/Avalonia.Web.Blazor/wwwroot/.gitignore