55 changed files with 640 additions and 519 deletions
@ -1,5 +1,5 @@ |
|||||
@page "/" |
@page "/" |
||||
|
|
||||
@using Avalonia.Blazor |
@using Avalonia.Web.Blazor |
||||
|
|
||||
<AvaloniaView /> |
<AvaloniaView /> |
||||
|
|||||
Binary file not shown.
@ -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(); |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
@ -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() { } |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,6 +0,0 @@ |
|||||
{ |
|
||||
"name": "Avalonia.Blazor", |
|
||||
"lockfileVersion": 2, |
|
||||
"requires": true, |
|
||||
"packages": {} |
|
||||
} |
|
||||
|
Before Width: | Height: | Size: 378 B |
@ -1,8 +1,7 @@ |
|||||
using System.Collections.Generic; |
using System.Collections.Generic; |
||||
using Avalonia; |
|
||||
using Avalonia.Skia; |
using Avalonia.Skia; |
||||
|
|
||||
namespace Avalonia.Blazor |
namespace Avalonia.Web.Blazor |
||||
{ |
{ |
||||
public class BlazorSkiaGpu : ISkiaGpu |
public class BlazorSkiaGpu : ISkiaGpu |
||||
{ |
{ |
||||
@ -1,7 +1,7 @@ |
|||||
using Avalonia.Skia; |
using Avalonia.Skia; |
||||
using SkiaSharp; |
using SkiaSharp; |
||||
|
|
||||
namespace Avalonia.Blazor |
namespace Avalonia.Web.Blazor |
||||
{ |
{ |
||||
internal class BlazorSkiaGpuRenderSession : ISkiaGpuRenderSession |
internal class BlazorSkiaGpuRenderSession : ISkiaGpuRenderSession |
||||
{ |
{ |
||||
@ -1,7 +1,7 @@ |
|||||
using Avalonia.Skia; |
using Avalonia.Skia; |
||||
using SkiaSharp; |
using SkiaSharp; |
||||
|
|
||||
namespace Avalonia.Blazor |
namespace Avalonia.Web.Blazor |
||||
{ |
{ |
||||
internal class BlazorSkiaGpuRenderTarget : ISkiaGpuRenderTarget |
internal class BlazorSkiaGpuRenderTarget : ISkiaGpuRenderTarget |
||||
{ |
{ |
||||
@ -1,8 +1,7 @@ |
|||||
using Avalonia; |
using Avalonia.Web.Blazor.Interop; |
||||
using Avalonia.Blazor.Interop; |
|
||||
using SkiaSharp; |
using SkiaSharp; |
||||
|
|
||||
namespace Avalonia.Blazor |
namespace Avalonia.Web.Blazor |
||||
{ |
{ |
||||
internal class BlazorSkiaSurface |
internal class BlazorSkiaSurface |
||||
{ |
{ |
||||
@ -1,6 +1,6 @@ |
|||||
using Microsoft.JSInterop; |
using Microsoft.JSInterop; |
||||
|
|
||||
namespace Avalonia.Blazor |
namespace Avalonia.Web.Blazor |
||||
{ |
{ |
||||
// This class provides an example of how JavaScript functionality can be wrapped
|
// This class provides an example of how JavaScript functionality can be wrapped
|
||||
// in a .NET class for easy consumption. The associated JavaScript module is
|
// in a .NET class for easy consumption. The associated JavaScript module is
|
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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() { } |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 +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 +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
|
||||
@ -1,7 +1,7 @@ |
|||||
using System.Collections.Generic; |
using System.Collections.Generic; |
||||
using Avalonia.Input; |
using Avalonia.Input; |
||||
|
|
||||
namespace Avalonia.Blazor |
namespace Avalonia.Web.Blazor |
||||
{ |
{ |
||||
internal static class Keycodes |
internal static class Keycodes |
||||
{ |
{ |
||||
Loading…
Reference in new issue