55 changed files with 640 additions and 519 deletions
@ -1,5 +1,5 @@ |
|||
@page "/" |
|||
|
|||
@using Avalonia.Blazor |
|||
@using Avalonia.Web.Blazor |
|||
|
|||
<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 Avalonia; |
|||
using Avalonia.Skia; |
|||
|
|||
namespace Avalonia.Blazor |
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
public class BlazorSkiaGpu : ISkiaGpu |
|||
{ |
|||
@ -1,7 +1,7 @@ |
|||
using Avalonia.Skia; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Blazor |
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
internal class BlazorSkiaGpuRenderSession : ISkiaGpuRenderSession |
|||
{ |
|||
@ -1,7 +1,7 @@ |
|||
using Avalonia.Skia; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Blazor |
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
internal class BlazorSkiaGpuRenderTarget : ISkiaGpuRenderTarget |
|||
{ |
|||
@ -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 |
|||
{ |
|||
@ -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
|
|||
@ -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 Avalonia.Input; |
|||
|
|||
namespace Avalonia.Blazor |
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
internal static class Keycodes |
|||
{ |
|||
Loading…
Reference in new issue