committed by
GitHub
58 changed files with 2820 additions and 40 deletions
@ -1,6 +1,7 @@ |
|||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
<ItemGroup> |
|||
<PackageReference Include="HarfBuzzSharp" Version="2.8.2-preview.155" /> |
|||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2-preview.155" /> |
|||
<PackageReference Include="HarfBuzzSharp" Version="2.8.2-preview.171" /> |
|||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2-preview.171" /> |
|||
<PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="2.8.2-preview.171"/> |
|||
</ItemGroup> |
|||
</Project> |
|||
|
|||
@ -1,6 +1,7 @@ |
|||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
<ItemGroup> |
|||
<PackageReference Include="SkiaSharp" Version="2.88.0-preview.155" /> |
|||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.88.0-preview.155" /> |
|||
<PackageReference Include="SkiaSharp" Version="2.88.0-preview.171" /> |
|||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.88.0-preview.171" /> |
|||
<PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.0-preview.171"/> |
|||
</ItemGroup> |
|||
</Project> |
|||
|
|||
@ -0,0 +1,10 @@ |
|||
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> |
|||
<Found Context="routeData"> |
|||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> |
|||
</Found> |
|||
<NotFound> |
|||
<LayoutView Layout="@typeof(MainLayout)"> |
|||
<p>Sorry, there's nothing at this address.</p> |
|||
</LayoutView> |
|||
</NotFound> |
|||
</Router> |
|||
@ -0,0 +1,14 @@ |
|||
using Avalonia.Web.Blazor; |
|||
|
|||
namespace ControlCatalog.Web; |
|||
|
|||
public partial class App |
|||
{ |
|||
protected override void OnParametersSet() |
|||
{ |
|||
WebAppBuilder.Configure<ControlCatalog.App>() |
|||
.SetupWithSingleViewLifetime(); |
|||
|
|||
base.OnParametersSet(); |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> |
|||
<PropertyGroup> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<Nullable>enable</Nullable> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<BlazorLinkerDescriptor Include="LinkerConfig.xml" /> |
|||
</ItemGroup> |
|||
|
|||
<!-- In debug, make builds faster by reducing optimizations --> |
|||
<PropertyGroup Condition="'$(Configuration)' == 'Debug'"> |
|||
<WasmNativeStrip>false</WasmNativeStrip> |
|||
<EmccCompileOptimizationFlag>-O1</EmccCompileOptimizationFlag> |
|||
<RunAOTCompilation>false</RunAOTCompilation> |
|||
</PropertyGroup> |
|||
|
|||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> |
|||
<Optimize>true</Optimize> |
|||
<WasmNativeStrip>true</WasmNativeStrip> |
|||
<EmccCompileOptimizationFlag>-O3</EmccCompileOptimizationFlag> |
|||
<EmccLinkOptimizationFlag>-O3</EmccLinkOptimizationFlag> |
|||
<RunAOTCompilation>false</RunAOTCompilation> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0"/> |
|||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0" PrivateAssets="all"/> |
|||
</ItemGroup> |
|||
|
|||
<Import Project="..\..\src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.targets" /> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.csproj"/> |
|||
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj"/> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,28 @@ |
|||
<?xml version="1.0" encoding="UTF-8" ?> |
|||
<!-- |
|||
This file specifies which parts of the BCL or Blazor packages must not be |
|||
stripped by the IL Linker even if they aren't referenced by user code. |
|||
--> |
|||
<linker> |
|||
<assembly fullname="mscorlib"> |
|||
<!-- |
|||
Preserve the methods in WasmRuntime because its methods are called by |
|||
JavaScript client-side code to implement timers. |
|||
Fixes: https://github.com/dotnet/blazor/issues/239 |
|||
--> |
|||
<type fullname="System.Threading.WasmRuntime"/> |
|||
</assembly> |
|||
|
|||
<assembly fullname="System.Core"> |
|||
<!-- |
|||
System.Linq.Expressions* is required by Json.NET and any |
|||
expression.Compile caller. The assembly isn't stripped. |
|||
--> |
|||
<type fullname="System.Linq.Expressions*"/> |
|||
</assembly> |
|||
<!-- |
|||
In this example, the app's entry point assembly is listed. The assembly |
|||
isn't stripped by the IL Linker. |
|||
--> |
|||
<assembly fullname="ControlCatalog" preserve="All" /> |
|||
</linker> |
|||
@ -0,0 +1,5 @@ |
|||
@page "/" |
|||
|
|||
@using Avalonia.Web.Blazor |
|||
|
|||
<AvaloniaView /> |
|||
@ -0,0 +1,29 @@ |
|||
using System; |
|||
using System.Net.Http; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using ControlCatalog.Web; |
|||
|
|||
public class Program |
|||
{ |
|||
public static async Task Main(string[] args) |
|||
{ |
|||
await CreateHostBuilder(args).Build().RunAsync(); |
|||
} |
|||
|
|||
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) }); |
|||
|
|||
return builder; |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
@ -0,0 +1,30 @@ |
|||
{ |
|||
"iisSettings": { |
|||
"windowsAuthentication": false, |
|||
"anonymousAuthentication": true, |
|||
"iisExpress": { |
|||
"applicationUrl": "http://localhost:13961", |
|||
"sslPort": 44319 |
|||
} |
|||
}, |
|||
"profiles": { |
|||
"ControlCatalog.Web - IIS Express": { |
|||
"commandName": "IISExpress", |
|||
"launchBrowser": true, |
|||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", |
|||
"environmentVariables": { |
|||
"ASPNETCORE_ENVIRONMENT": "Development" |
|||
} |
|||
}, |
|||
"ControlCatalog.Web": { |
|||
"commandName": "Project", |
|||
"dotnetRunMessages": "true", |
|||
"launchBrowser": true, |
|||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", |
|||
"applicationUrl": "https://localhost:5001;http://localhost:5000", |
|||
"environmentVariables": { |
|||
"ASPNETCORE_ENVIRONMENT": "Development" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
@inherits LayoutComponentBase |
|||
|
|||
<div class="page"> |
|||
<div class="main"> |
|||
@Body |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,70 @@ |
|||
.page { |
|||
position: relative; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.main { |
|||
flex: 1; |
|||
} |
|||
|
|||
.sidebar { |
|||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); |
|||
} |
|||
|
|||
.top-row { |
|||
background-color: #f7f7f7; |
|||
border-bottom: 1px solid #d6d5d5; |
|||
justify-content: flex-end; |
|||
height: 3.5rem; |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.top-row ::deep a, .top-row .btn-link { |
|||
white-space: nowrap; |
|||
margin-left: 1.5rem; |
|||
} |
|||
|
|||
.top-row a:first-child { |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
|
|||
@media (max-width: 640.98px) { |
|||
.top-row:not(.auth) { |
|||
display: none; |
|||
} |
|||
|
|||
.top-row.auth { |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.top-row a, .top-row .btn-link { |
|||
margin-left: 0; |
|||
} |
|||
} |
|||
|
|||
@media (min-width: 641px) { |
|||
.page { |
|||
flex-direction: row; |
|||
} |
|||
|
|||
.sidebar { |
|||
width: 250px; |
|||
height: 100vh; |
|||
position: sticky; |
|||
top: 0; |
|||
} |
|||
|
|||
.top-row { |
|||
position: sticky; |
|||
top: 0; |
|||
z-index: 1; |
|||
} |
|||
|
|||
.main > div { |
|||
padding-left: 2rem !important; |
|||
padding-right: 1.5rem !important; |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
@using System.Net.Http |
|||
@using System.Net.Http.Json |
|||
@using Microsoft.AspNetCore.Components.Forms |
|||
@using Microsoft.AspNetCore.Components.Routing |
|||
@using Microsoft.AspNetCore.Components.Web |
|||
@using Microsoft.AspNetCore.Components.Web.Virtualization |
|||
@using Microsoft.AspNetCore.Components.WebAssembly.Http |
|||
@using Microsoft.JSInterop |
|||
@using ControlCatalog.Web |
|||
@using ControlCatalog.Web.Shared |
|||
@using SkiaSharp |
|||
@ -0,0 +1,90 @@ |
|||
html, body { |
|||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; |
|||
margin: 0; |
|||
height: 100vh; |
|||
overflow: hidden; |
|||
touch-action: none; |
|||
} |
|||
|
|||
a, .btn-link { |
|||
color: #0366d6; |
|||
} |
|||
|
|||
.btn-primary { |
|||
color: #fff; |
|||
background-color: #1b6ec2; |
|||
border-color: #1861ac; |
|||
} |
|||
|
|||
.content { |
|||
padding-top: 1.1rem; |
|||
} |
|||
|
|||
.valid.modified:not([type=checkbox]) { |
|||
outline: 1px solid #26b050; |
|||
} |
|||
|
|||
.invalid { |
|||
outline: 1px solid red; |
|||
} |
|||
|
|||
.validation-message { |
|||
color: red; |
|||
} |
|||
|
|||
#blazor-error-ui { |
|||
background: lightyellow; |
|||
bottom: 0; |
|||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); |
|||
display: none; |
|||
left: 0; |
|||
padding: 0.6rem 1.25rem 0.7rem 1.25rem; |
|||
position: fixed; |
|||
width: 100%; |
|||
z-index: 1000; |
|||
} |
|||
|
|||
#blazor-error-ui .dismiss { |
|||
cursor: pointer; |
|||
position: absolute; |
|||
right: 0.75rem; |
|||
top: 0.5rem; |
|||
} |
|||
|
|||
.canvas-container { |
|||
opacity:1; |
|||
background-color:#ccc; |
|||
position:fixed; |
|||
width:100%; |
|||
height:100%; |
|||
top:0px; |
|||
left:0px; |
|||
z-index:500; |
|||
} |
|||
|
|||
canvas |
|||
{ |
|||
opacity:1; |
|||
background-color:#ccc; |
|||
position:fixed; |
|||
width:100%; |
|||
height:100%; |
|||
top:0px; |
|||
left:0px; |
|||
z-index:500; |
|||
} |
|||
|
|||
#app, .page { |
|||
height: 100%; |
|||
} |
|||
|
|||
.overlay{ |
|||
opacity:0.0; |
|||
background-color:#ccc; |
|||
position:fixed; |
|||
width:100vw; |
|||
height:100vh; |
|||
top:0px; |
|||
left:0px; |
|||
z-index:1000; |
|||
} |
|||
|
After Width: | Height: | Size: 172 KiB |
@ -0,0 +1,23 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
|
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
<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/app.css" rel="stylesheet" /> |
|||
</head> |
|||
|
|||
<body> |
|||
<div id="app">Powered by Avalonia</div> |
|||
|
|||
<div id="blazor-error-ui"> |
|||
An unhandled error has occurred. |
|||
<a href="" class="reload">Reload</a> |
|||
<a class="dismiss">🗙</a> |
|||
</div> |
|||
<script src="js/app.js"></script> |
|||
<script src="_framework/blazor.webassembly.js"></script> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1 @@ |
|||
|
|||
Binary file not shown.
Binary file not shown.
@ -0,0 +1,57 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk.Razor"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<Nullable>enable</Nullable> |
|||
<ImplicitUsings>enable</ImplicitUsings> |
|||
<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\BuildTargets.targets" /> |
|||
<Import Project="..\..\..\build\SkiaSharp.props" /> |
|||
<Import Project="..\..\..\build\HarfBuzzSharp.props" /> |
|||
|
|||
<ItemGroup> |
|||
<AvaloniaResource Include="Assets\*" /> |
|||
<Content Include="*.props"> |
|||
<Pack>true</Pack> |
|||
<PackagePath>build\;buildTransitive\</PackagePath> |
|||
</Content> |
|||
<Content Include="*.targets"> |
|||
<Pack>true</Pack> |
|||
<PackagePath>build\;buildTransitive\</PackagePath> |
|||
</Content> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.0" /> |
|||
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="4.5.2" PrivateAssets="all" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" /> |
|||
<ProjectReference Include="..\..\Avalonia.Controls\Avalonia.Controls.csproj" /> |
|||
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -0,0 +1,6 @@ |
|||
<Project> |
|||
<ItemGroup> |
|||
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\libHarfBuzzSharp.a" /> |
|||
<NativeFileReference Include="$(SkiaSharpStaticLibraryPath)\2.0.23\libSkiaSharp.a" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -0,0 +1,18 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
public class AvaloniaBlazorAppBuilder : AppBuilderBase<AvaloniaBlazorAppBuilder> |
|||
{ |
|||
public AvaloniaBlazorAppBuilder(IRuntimePlatform platform, Action<AvaloniaBlazorAppBuilder> platformServices) |
|||
: base(platform, platformServices) |
|||
{ |
|||
} |
|||
|
|||
public AvaloniaBlazorAppBuilder() : base(BlazorRuntimePlatform.Instance, BlazorRuntimePlatform.RegisterServices) |
|||
{ |
|||
UseWindowingSubsystem(BlazorWindowingPlatform.Register); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
<div id="container" class="avalonia-container" tabindex="0" oncontextmenu="return false;" |
|||
ontouchstart="@OnTouchStart" |
|||
ontouchend="@OnTouchEnd" |
|||
ontouchcancel="@OnTouchCancel" |
|||
ontouchmove="@OnTouchMove" |
|||
onmousemove="@OnMouseMove" |
|||
onmousedown="@OnMouseDown" |
|||
onmouseup="@OnMouseUp" |
|||
onwheel="@OnWheel" |
|||
onkeydown="@OnKeyDown" |
|||
onkeyup="@OnKeyUp"> |
|||
|
|||
<canvas @ref="_htmlCanvas" @attributes="AdditionalAttributes"/> |
|||
|
|||
<input @ref="_inputElement" |
|||
class="overlay" |
|||
type="text" |
|||
oninput="@OnInput"/> |
|||
</div> |
|||
@ -0,0 +1,375 @@ |
|||
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.Web.Blazor |
|||
{ |
|||
public partial class AvaloniaView : ITextInputMethodImpl |
|||
{ |
|||
private readonly RazorViewTopLevelImpl _topLevelImpl; |
|||
private EmbeddableControlRoot _topLevel; |
|||
|
|||
// Interop
|
|||
private SKHtmlCanvasInterop _interop = null!; |
|||
private SizeWatcherInterop _sizeWatcher = null!; |
|||
private DpiWatcherInterop _dpiWatcher = null!; |
|||
private SKHtmlCanvasInterop.GLInfo? _jsGlInfo = null!; |
|||
private InputHelperInterop _inputHelper = null!; |
|||
private ElementReference _htmlCanvas; |
|||
private ElementReference _inputElement; |
|||
private double _dpi; |
|||
private SKSize _canvasSize; |
|||
|
|||
private GRContext? _context; |
|||
private GRGlInterface? _glInterface; |
|||
private const SKColorType ColorType = SKColorType.Rgba8888; |
|||
|
|||
private bool _initialised; |
|||
|
|||
[Inject] private IJSRuntime Js { get; set; } = null!; |
|||
|
|||
public AvaloniaView() |
|||
{ |
|||
_topLevelImpl = new RazorViewTopLevelImpl(this); |
|||
|
|||
_topLevel = new EmbeddableControlRoot(_topLevelImpl); |
|||
|
|||
if (Application.Current.ApplicationLifetime is ISingleViewApplicationLifetime lifetime) |
|||
{ |
|||
_topLevel.Content = lifetime.MainView; |
|||
} |
|||
} |
|||
|
|||
private void OnTouchStart(TouchEventArgs e) |
|||
{ |
|||
foreach (var touch in e.ChangedTouches) |
|||
{ |
|||
_topLevelImpl.RawTouchEvent(RawPointerEventType.TouchBegin, new Point(touch.ClientX, touch.ClientY), |
|||
GetModifiers(e), touch.Identifier); |
|||
} |
|||
} |
|||
|
|||
private void OnTouchEnd(TouchEventArgs e) |
|||
{ |
|||
foreach (var touch in e.ChangedTouches) |
|||
{ |
|||
_topLevelImpl.RawTouchEvent(RawPointerEventType.TouchEnd, new Point(touch.ClientX, touch.ClientY), |
|||
GetModifiers(e), touch.Identifier); |
|||
} |
|||
} |
|||
|
|||
private void OnTouchCancel(TouchEventArgs e) |
|||
{ |
|||
foreach (var touch in e.ChangedTouches) |
|||
{ |
|||
_topLevelImpl.RawTouchEvent(RawPointerEventType.TouchCancel, new Point(touch.ClientX, touch.ClientY), |
|||
GetModifiers(e), touch.Identifier); |
|||
} |
|||
} |
|||
|
|||
private void OnTouchMove(TouchEventArgs e) |
|||
{ |
|||
foreach (var touch in e.ChangedTouches) |
|||
{ |
|||
_topLevelImpl.RawTouchEvent(RawPointerEventType.TouchUpdate, new Point(touch.ClientX, touch.ClientY), |
|||
GetModifiers(e), touch.Identifier); |
|||
} |
|||
} |
|||
|
|||
private void OnMouseMove(MouseEventArgs e) |
|||
{ |
|||
_topLevelImpl.RawMouseEvent(RawPointerEventType.Move, new Point(e.ClientX, e.ClientY), GetModifiers(e)); |
|||
} |
|||
|
|||
private void OnMouseUp(MouseEventArgs e) |
|||
{ |
|||
RawPointerEventType type = default; |
|||
|
|||
switch (e.Button) |
|||
{ |
|||
case 0: |
|||
type = RawPointerEventType.LeftButtonUp; |
|||
break; |
|||
|
|||
case 1: |
|||
type = RawPointerEventType.MiddleButtonUp; |
|||
break; |
|||
|
|||
case 2: |
|||
type = RawPointerEventType.RightButtonUp; |
|||
break; |
|||
} |
|||
|
|||
_topLevelImpl.RawMouseEvent(type, new Point(e.ClientX, e.ClientY), GetModifiers(e)); |
|||
} |
|||
|
|||
private void OnMouseDown(MouseEventArgs e) |
|||
{ |
|||
RawPointerEventType type = default; |
|||
|
|||
switch (e.Button) |
|||
{ |
|||
case 0: |
|||
type = RawPointerEventType.LeftButtonDown; |
|||
break; |
|||
|
|||
case 1: |
|||
type = RawPointerEventType.MiddleButtonDown; |
|||
break; |
|||
|
|||
case 2: |
|||
type = RawPointerEventType.RightButtonDown; |
|||
break; |
|||
} |
|||
|
|||
_topLevelImpl.RawMouseEvent(type, new Point(e.ClientX, e.ClientY), GetModifiers(e)); |
|||
} |
|||
|
|||
private void OnWheel(WheelEventArgs e) |
|||
{ |
|||
_topLevelImpl.RawMouseWheelEvent(new Point(e.ClientX, e.ClientY), |
|||
new Vector(-(e.DeltaX / 50), -(e.DeltaY / 50)), GetModifiers(e)); |
|||
} |
|||
|
|||
private static RawInputModifiers GetModifiers(WheelEventArgs e) |
|||
{ |
|||
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.Buttons & 1L) == 1) |
|||
modifiers |= RawInputModifiers.LeftMouseButton; |
|||
|
|||
if ((e.Buttons & 2L) == 2) |
|||
modifiers |= RawInputModifiers.RightMouseButton; |
|||
|
|||
if ((e.Buttons & 4L) == 4) |
|||
modifiers |= RawInputModifiers.MiddleMouseButton; |
|||
|
|||
return modifiers; |
|||
} |
|||
|
|||
private static RawInputModifiers GetModifiers(TouchEventArgs e) |
|||
{ |
|||
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; |
|||
|
|||
return modifiers; |
|||
} |
|||
|
|||
private static RawInputModifiers GetModifiers(MouseEventArgs e) |
|||
{ |
|||
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.Buttons & 1L) == 1) |
|||
modifiers |= RawInputModifiers.LeftMouseButton; |
|||
|
|||
if ((e.Buttons & 2L) == 2) |
|||
modifiers |= RawInputModifiers.RightMouseButton; |
|||
|
|||
if ((e.Buttons & 4L) == 4) |
|||
modifiers |= RawInputModifiers.MiddleMouseButton; |
|||
|
|||
return modifiers; |
|||
} |
|||
|
|||
private static RawInputModifiers GetModifiers(KeyboardEventArgs e) |
|||
{ |
|||
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; |
|||
|
|||
return modifiers; |
|||
} |
|||
|
|||
private void OnKeyDown(KeyboardEventArgs e) |
|||
{ |
|||
_topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, e.Key, GetModifiers(e)); |
|||
} |
|||
|
|||
private void OnKeyUp(KeyboardEventArgs e) |
|||
{ |
|||
_topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, e.Code, GetModifiers(e)); |
|||
} |
|||
|
|||
private void OnInput(ChangeEventArgs e) |
|||
{ |
|||
if (e.Value != null) |
|||
{ |
|||
var inputData = e.Value.ToString(); |
|||
if (inputData != null) |
|||
{ |
|||
_topLevelImpl.RawTextEvent(inputData); |
|||
} |
|||
} |
|||
|
|||
_inputHelper.Clear(); |
|||
} |
|||
|
|||
[Parameter(CaptureUnmatchedValues = true)] |
|||
public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; } |
|||
|
|||
protected override void OnAfterRender(bool firstRender) |
|||
{ |
|||
if (firstRender) |
|||
{ |
|||
Threading.Dispatcher.UIThread.Post(async () => |
|||
{ |
|||
_inputHelper = await InputHelperInterop.ImportAsync(Js, _inputElement); |
|||
|
|||
_inputHelper.Hide(); |
|||
_inputHelper.SetCursor("default"); |
|||
|
|||
Console.WriteLine("starting html canvas setup"); |
|||
_interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame); |
|||
|
|||
Console.WriteLine("Interop created"); |
|||
_jsGlInfo = _interop.InitGL(); |
|||
|
|||
Console.WriteLine("jsglinfo created - init gl"); |
|||
|
|||
_sizeWatcher = await SizeWatcherInterop.ImportAsync(Js, _htmlCanvas, OnSizeChanged); |
|||
_dpiWatcher = await DpiWatcherInterop.ImportAsync(Js, OnDpiChanged); |
|||
|
|||
Console.WriteLine("watchers created."); |
|||
|
|||
// create the SkiaSharp context
|
|||
if (_context == null) |
|||
{ |
|||
Console.WriteLine("create glcontext"); |
|||
_glInterface = GRGlInterface.Create(); |
|||
_context = GRContext.CreateGl(_glInterface); |
|||
|
|||
|
|||
// bump the default resource cache limit
|
|||
_context.SetResourceCacheLimit(256 * 1024 * 1024); |
|||
Console.WriteLine("glcontext created and resource limit set"); |
|||
} |
|||
|
|||
_topLevelImpl.SetSurface(_context, _jsGlInfo, ColorType, |
|||
new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi); |
|||
|
|||
_initialised = true; |
|||
|
|||
_topLevel.Prepare(); |
|||
|
|||
_topLevel.Renderer.Start(); |
|||
Invalidate(); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
private void OnRenderFrame() |
|||
{ |
|||
if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0 || _jsGlInfo == null) |
|||
{ |
|||
Console.WriteLine("nothing to render"); |
|||
return; |
|||
} |
|||
|
|||
ManualTriggerRenderTimer.Instance.RaiseTick(); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_dpiWatcher.Unsubscribe(OnDpiChanged); |
|||
_sizeWatcher.Dispose(); |
|||
_interop.Dispose(); |
|||
} |
|||
|
|||
private void OnDpiChanged(double newDpi) |
|||
{ |
|||
_dpi = newDpi; |
|||
|
|||
_topLevelImpl.SetClientSize(_canvasSize, _dpi); |
|||
|
|||
Invalidate(); |
|||
} |
|||
|
|||
private void OnSizeChanged(SKSize newSize) |
|||
{ |
|||
_canvasSize = newSize; |
|||
|
|||
_topLevelImpl.SetClientSize(_canvasSize, _dpi); |
|||
|
|||
Invalidate(); |
|||
} |
|||
|
|||
public void Invalidate() |
|||
{ |
|||
if (!_initialised || _canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0 || _jsGlInfo == null) |
|||
{ |
|||
Console.WriteLine("invalidate ignored"); |
|||
return; |
|||
} |
|||
|
|||
_interop.RequestAnimationFrame(true, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); |
|||
} |
|||
|
|||
public void SetActive(bool active) |
|||
{ |
|||
_inputHelper.Clear(); |
|||
|
|||
if (active) |
|||
{ |
|||
_inputHelper.Show(); |
|||
_inputHelper.Focus(); |
|||
} |
|||
else |
|||
{ |
|||
_inputHelper.Hide(); |
|||
} |
|||
} |
|||
|
|||
public void SetCursorRect(Rect rect) |
|||
{ |
|||
} |
|||
|
|||
public void SetOptions(TextInputOptionsQueryEventArgs options) |
|||
{ |
|||
} |
|||
|
|||
public void Reset() |
|||
{ |
|||
_inputHelper.Clear(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Shared.PlatformSupport; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
internal class BlazorRuntimePlatform : IRuntimePlatform |
|||
{ |
|||
public static readonly IRuntimePlatform Instance = new BlazorRuntimePlatform(); |
|||
|
|||
public IDisposable StartSystemTimer(TimeSpan interval, Action tick) |
|||
{ |
|||
return new Timer(_ => tick(), null, interval, interval); |
|||
} |
|||
|
|||
public RuntimePlatformInfo GetRuntimeInfo() |
|||
{ |
|||
return new RuntimePlatformInfo |
|||
{ |
|||
IsDesktop = false, |
|||
IsMobile = false, |
|||
IsMono = true, |
|||
IsUnix = false, |
|||
IsCoreClr = false, |
|||
IsDotNetFramework = false |
|||
}; |
|||
} |
|||
|
|||
private class BasicBlob : IUnmanagedBlob |
|||
{ |
|||
public BasicBlob(int size) |
|||
{ |
|||
Address = Marshal.AllocHGlobal(size); |
|||
Size = size; |
|||
} |
|||
public void Dispose() |
|||
{ |
|||
if (Address != IntPtr.Zero) |
|||
Marshal.FreeHGlobal(Address); |
|||
Address = IntPtr.Zero; |
|||
} |
|||
|
|||
public IntPtr Address { get; private set; } |
|||
|
|||
public int Size { get; } |
|||
public bool IsDisposed => Address == IntPtr.Zero; |
|||
} |
|||
|
|||
public IUnmanagedBlob AllocBlob(int size) |
|||
{ |
|||
return new BasicBlob(size); |
|||
} |
|||
|
|||
public static void RegisterServices(AvaloniaBlazorAppBuilder builder) |
|||
{ |
|||
AssetLoader.RegisterResUriParsers(); |
|||
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>().ToConstant(Instance); |
|||
AvaloniaLocator.CurrentMutable.Bind<IAssetLoader>().ToConstant(new AssetLoader()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
public class BlazorSingleViewLifetime : ISingleViewApplicationLifetime |
|||
{ |
|||
public Control? MainView { get; set; } |
|||
} |
|||
|
|||
public static class WebAppBuilder |
|||
{ |
|||
public static T SetupWithSingleViewLifetime<T>( |
|||
this T builder) |
|||
where T : AppBuilderBase<T>, new() |
|||
{ |
|||
return builder.SetupWithLifetime(new BlazorSingleViewLifetime()); |
|||
} |
|||
|
|||
public static AvaloniaBlazorAppBuilder Configure<TApp>() |
|||
where TApp : Application, new() |
|||
{ |
|||
var builder = AvaloniaBlazorAppBuilder.Configure<TApp>() |
|||
.UseSkia() |
|||
.With(new SkiaOptions { CustomGpuFactory = () => new BlazorSkiaGpu() }); |
|||
|
|||
AvaloniaLocator.CurrentMutable.Bind<FontManager>().ToConstant(new FontManager(new CustomFontManagerImpl())); |
|||
|
|||
return builder; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
using Avalonia.Skia; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
public class BlazorSkiaGpu : ISkiaGpu |
|||
{ |
|||
public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable<object> surfaces) |
|||
{ |
|||
foreach (var surface in surfaces) |
|||
{ |
|||
if (surface is BlazorSkiaSurface blazorSkiaSurface) |
|||
{ |
|||
return new BlazorSkiaGpuRenderTarget(blazorSkiaSurface); |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public ISkiaSurface? TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session) |
|||
{ |
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
using Avalonia.Skia; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
internal class BlazorSkiaGpuRenderSession : ISkiaGpuRenderSession |
|||
{ |
|||
private readonly SKSurface _surface; |
|||
|
|||
|
|||
public BlazorSkiaGpuRenderSession(BlazorSkiaSurface blazorSkiaSurface, GRBackendRenderTarget renderTarget) |
|||
{ |
|||
_surface = SKSurface.Create(blazorSkiaSurface.Context, renderTarget, blazorSkiaSurface.Origin, blazorSkiaSurface.ColorType); |
|||
|
|||
GrContext = blazorSkiaSurface.Context; |
|||
|
|||
ScaleFactor = blazorSkiaSurface.Scaling; |
|||
|
|||
SurfaceOrigin = blazorSkiaSurface.Origin; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_surface.Flush(); |
|||
|
|||
_surface.Dispose(); |
|||
} |
|||
|
|||
public GRContext GrContext { get; } |
|||
|
|||
public SKSurface SkSurface => _surface; |
|||
|
|||
public double ScaleFactor { get; } |
|||
|
|||
public GRSurfaceOrigin SurfaceOrigin { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
using Avalonia.Skia; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
internal class BlazorSkiaGpuRenderTarget : ISkiaGpuRenderTarget |
|||
{ |
|||
private readonly GRBackendRenderTarget _renderTarget; |
|||
private readonly BlazorSkiaSurface _blazorSkiaSurface; |
|||
private readonly PixelSize _size; |
|||
|
|||
public BlazorSkiaGpuRenderTarget(BlazorSkiaSurface blazorSkiaSurface) |
|||
{ |
|||
_size = blazorSkiaSurface.Size; |
|||
|
|||
var glFbInfo = new GRGlFramebufferInfo(blazorSkiaSurface.GlInfo.FboId, blazorSkiaSurface.ColorType.ToGlSizedFormat()); |
|||
{ |
|||
_blazorSkiaSurface = blazorSkiaSurface; |
|||
_renderTarget = new GRBackendRenderTarget( |
|||
(int)(blazorSkiaSurface.Size.Width * blazorSkiaSurface.Scaling), |
|||
(int)(blazorSkiaSurface.Size.Height * blazorSkiaSurface.Scaling), |
|||
blazorSkiaSurface.GlInfo.Samples, |
|||
blazorSkiaSurface.GlInfo.Stencils, glFbInfo); |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_renderTarget.Dispose(); |
|||
} |
|||
|
|||
public ISkiaGpuRenderSession BeginRenderingSession() |
|||
{ |
|||
return new BlazorSkiaGpuRenderSession(_blazorSkiaSurface, _renderTarget); |
|||
} |
|||
|
|||
public bool IsCorrupted |
|||
{ |
|||
get |
|||
{ |
|||
var result = _size.Width != _renderTarget.Width || _size.Height != _renderTarget.Height; |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
using Avalonia.Web.Blazor.Interop; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
internal class BlazorSkiaSurface |
|||
{ |
|||
public BlazorSkiaSurface(GRContext context, SKHtmlCanvasInterop.GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling, GRSurfaceOrigin origin) |
|||
{ |
|||
Context = context; |
|||
GlInfo = glInfo; |
|||
ColorType = colorType; |
|||
Size = size; |
|||
Scaling = scaling; |
|||
Origin = origin; |
|||
} |
|||
|
|||
public SKColorType ColorType { get; set; } |
|||
|
|||
public PixelSize Size { get; set; } |
|||
|
|||
public GRContext Context { get; set; } |
|||
|
|||
public GRSurfaceOrigin Origin { get; set; } |
|||
|
|||
public double Scaling { get; set; } |
|||
|
|||
public SKHtmlCanvasInterop.GLInfo GlInfo { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
using System.Globalization; |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Skia; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
public class CustomFontManagerImpl : IFontManagerImpl |
|||
{ |
|||
private readonly Typeface[] _customTypefaces; |
|||
private readonly string _defaultFamilyName; |
|||
|
|||
private readonly Typeface _defaultTypeface = |
|||
new Typeface("avares://Avalonia.Web.Blazor/Assets#Noto Mono"); |
|||
private readonly Typeface _italicTypeface = |
|||
new Typeface("avares://Avalonia.Web.Blazor/Assets#Noto Sans"); |
|||
|
|||
public CustomFontManagerImpl() |
|||
{ |
|||
_customTypefaces = new[] { _italicTypeface, _defaultTypeface }; |
|||
_defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName; |
|||
} |
|||
|
|||
public string GetDefaultFontFamilyName() |
|||
{ |
|||
return _defaultFamilyName; |
|||
} |
|||
|
|||
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) |
|||
{ |
|||
return _customTypefaces.Select(x => x.FontFamily.Name); |
|||
} |
|||
|
|||
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, |
|||
CultureInfo culture, out Typeface typeface) |
|||
{ |
|||
foreach (var customTypeface in _customTypefaces) |
|||
{ |
|||
if (customTypeface.GlyphTypeface.GetGlyph((uint)codepoint) == 0) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
typeface = new Typeface(customTypeface.FontFamily, fontStyle, fontWeight); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
typeface = _defaultTypeface; |
|||
|
|||
return true; |
|||
} |
|||
|
|||
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) |
|||
{ |
|||
SKTypeface skTypeface; |
|||
|
|||
switch (typeface.FontFamily.Name) |
|||
{ |
|||
case "Noto Sans": |
|||
{ |
|||
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily); |
|||
skTypeface = typefaceCollection.Get(typeface); |
|||
break; |
|||
} |
|||
default: |
|||
{ |
|||
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily); |
|||
skTypeface = typefaceCollection.Get(_defaultTypeface); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return new GlyphTypefaceImpl(skTypeface); |
|||
} |
|||
} |
|||
} |
|||
@ -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,41 @@ |
|||
using Microsoft.AspNetCore.Components; |
|||
using Microsoft.JSInterop; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Web.Blazor.Interop |
|||
{ |
|||
internal class InputHelperInterop : JSModuleInterop |
|||
{ |
|||
private const string JsFilename = "./_content/Avalonia.Web.Blazor/InputHelper.js"; |
|||
private const string ClearSymbol = "InputHelper.clear"; |
|||
private const string FocusSymbol = "InputHelper.focus"; |
|||
private const string SetCursorSymbol = "InputHelper.setCursor"; |
|||
private const string HideSymbol = "InputHelper.hide"; |
|||
private const string ShowSymbol = "InputHelper.show"; |
|||
|
|||
private readonly ElementReference inputElement; |
|||
|
|||
public static async Task<InputHelperInterop> ImportAsync(IJSRuntime js, ElementReference element) |
|||
{ |
|||
var interop = new InputHelperInterop(js, element); |
|||
await interop.ImportAsync(); |
|||
return interop; |
|||
} |
|||
|
|||
public InputHelperInterop(IJSRuntime js, ElementReference element) |
|||
: base(js, JsFilename) |
|||
{ |
|||
inputElement = element; |
|||
} |
|||
|
|||
public void Clear() => Invoke(ClearSymbol, inputElement); |
|||
|
|||
public void Focus() => Invoke(FocusSymbol, inputElement); |
|||
|
|||
public void SetCursor(string kind) => Invoke(SetCursorSymbol, inputElement, kind); |
|||
|
|||
public void Hide() => Invoke(HideSymbol, inputElement); |
|||
|
|||
public void Show() => Invoke(ShowSymbol, inputElement); |
|||
} |
|||
} |
|||
@ -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,41 @@ |
|||
|
|||
export class DpiWatcher { |
|||
static lastDpi: number; |
|||
static timerId: number; |
|||
static callback: DotNet.DotNetObjectReference; |
|||
|
|||
public static getDpi() { |
|||
return window.devicePixelRatio; |
|||
} |
|||
|
|||
public static start(callback: DotNet.DotNetObjectReference): number { |
|||
//console.info(`Starting DPI watcher with callback ${callback._id}...`);
|
|||
|
|||
DpiWatcher.lastDpi = window.devicePixelRatio; |
|||
DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000); |
|||
DpiWatcher.callback = callback; |
|||
|
|||
return DpiWatcher.lastDpi; |
|||
} |
|||
|
|||
public static stop() { |
|||
//console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`);
|
|||
|
|||
window.clearInterval(DpiWatcher.timerId); |
|||
|
|||
DpiWatcher.callback = undefined; |
|||
} |
|||
|
|||
static update() { |
|||
if (!DpiWatcher.callback) |
|||
return; |
|||
|
|||
const currentDpi = window.devicePixelRatio; |
|||
const lastDpi = DpiWatcher.lastDpi; |
|||
DpiWatcher.lastDpi = currentDpi; |
|||
|
|||
if (Math.abs(lastDpi - currentDpi) > 0.001) { |
|||
DpiWatcher.callback.invokeMethod('Invoke', lastDpi, currentDpi); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
|
|||
export class InputHelper { |
|||
public static clear (inputElement: HTMLInputElement){ |
|||
inputElement.value = ""; |
|||
} |
|||
|
|||
public static focus (inputElement: HTMLInputElement){ |
|||
inputElement.focus(); |
|||
inputElement.setSelectionRange(0, 0); |
|||
} |
|||
|
|||
public static setCursor (inputElement: HTMLInputElement, kind: string) { |
|||
inputElement.style.cursor = kind; |
|||
} |
|||
|
|||
public static hide (inputElement: HTMLInputElement){ |
|||
inputElement.style.display = 'none'; |
|||
} |
|||
|
|||
public static show (inputElement: HTMLInputElement){ |
|||
inputElement.style.display = 'block'; |
|||
} |
|||
} |
|||
@ -0,0 +1,225 @@ |
|||
// aliases for emscripten
|
|||
declare let GL: any; |
|||
declare let GLctx: WebGLRenderingContext; |
|||
declare let Module: EmscriptenModule; |
|||
|
|||
// container for gl info
|
|||
type SKGLViewInfo = { |
|||
context: WebGLRenderingContext | WebGL2RenderingContext | undefined; |
|||
fboId: number; |
|||
stencil: number; |
|||
sample: number; |
|||
depth: number; |
|||
} |
|||
|
|||
// alias for a potential skia html canvas
|
|||
type SKHtmlCanvasElement = { |
|||
SKHtmlCanvas: SKHtmlCanvas |
|||
} & HTMLCanvasElement |
|||
|
|||
export class SKHtmlCanvas { |
|||
static elements: Map<string, HTMLCanvasElement>; |
|||
|
|||
htmlCanvas: HTMLCanvasElement; |
|||
glInfo: SKGLViewInfo; |
|||
renderFrameCallback: DotNet.DotNetObjectReference; |
|||
renderLoopEnabled: boolean = false; |
|||
renderLoopRequest: number = 0; |
|||
|
|||
public static initGL(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKGLViewInfo { |
|||
var view = SKHtmlCanvas.init(true, element, elementId, callback); |
|||
if (!view || !view.glInfo) |
|||
return null; |
|||
|
|||
return view.glInfo; |
|||
} |
|||
|
|||
public static initRaster(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): boolean { |
|||
var view = SKHtmlCanvas.init(false, element, elementId, callback); |
|||
if (!view) |
|||
return false; |
|||
|
|||
return true; |
|||
} |
|||
|
|||
static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKHtmlCanvas { |
|||
var htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas) { |
|||
console.error(`No canvas element was provided.`); |
|||
return null; |
|||
} |
|||
|
|||
if (!SKHtmlCanvas.elements) |
|||
SKHtmlCanvas.elements = new Map<string, HTMLCanvasElement>(); |
|||
SKHtmlCanvas.elements[elementId] = element; |
|||
|
|||
const view = new SKHtmlCanvas(useGL, element, callback); |
|||
|
|||
htmlCanvas.SKHtmlCanvas = view; |
|||
|
|||
return view; |
|||
} |
|||
|
|||
public static deinit(elementId: string) { |
|||
if (!elementId) |
|||
return; |
|||
|
|||
const element = SKHtmlCanvas.elements[elementId]; |
|||
SKHtmlCanvas.elements.delete(elementId); |
|||
|
|||
const htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) |
|||
return; |
|||
|
|||
htmlCanvas.SKHtmlCanvas.deinit(); |
|||
htmlCanvas.SKHtmlCanvas = undefined; |
|||
} |
|||
|
|||
public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean, width?: number, height?: number) { |
|||
const htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) |
|||
return; |
|||
|
|||
htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop, width, height); |
|||
} |
|||
|
|||
public static setEnableRenderLoop(element: HTMLCanvasElement, enable: boolean) { |
|||
const htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) |
|||
return; |
|||
|
|||
htmlCanvas.SKHtmlCanvas.setEnableRenderLoop(enable); |
|||
} |
|||
|
|||
public static putImageData(element: HTMLCanvasElement, pData: number, width: number, height: number) { |
|||
const htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) |
|||
return; |
|||
|
|||
htmlCanvas.SKHtmlCanvas.putImageData(pData, width, height); |
|||
} |
|||
|
|||
public constructor(useGL: boolean, element: HTMLCanvasElement, callback: DotNet.DotNetObjectReference) { |
|||
this.htmlCanvas = element; |
|||
this.renderFrameCallback = callback; |
|||
|
|||
if (useGL) { |
|||
const ctx = SKHtmlCanvas.createWebGLContext(this.htmlCanvas); |
|||
if (!ctx) { |
|||
console.error(`Failed to create WebGL context: err ${ctx}`); |
|||
return null; |
|||
} |
|||
|
|||
// make current
|
|||
GL.makeContextCurrent(ctx); |
|||
|
|||
// read values
|
|||
const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); |
|||
this.glInfo = { |
|||
context: ctx, |
|||
fboId: fbo ? fbo.id : 0, |
|||
stencil: GLctx.getParameter(GLctx.STENCIL_BITS), |
|||
sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES)
|
|||
depth: GLctx.getParameter(GLctx.DEPTH_BITS), |
|||
}; |
|||
} |
|||
} |
|||
|
|||
public deinit() { |
|||
this.setEnableRenderLoop(false); |
|||
} |
|||
|
|||
public requestAnimationFrame(renderLoop?: boolean, width?: number, height?: number) { |
|||
// 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(); |
|||
}); |
|||
} |
|||
|
|||
public setEnableRenderLoop(enable: boolean) { |
|||
this.renderLoopEnabled = enable; |
|||
|
|||
// either start the new frame or cancel the existing one
|
|||
if (enable) { |
|||
//console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`);
|
|||
this.requestAnimationFrame(); |
|||
} else if (this.renderLoopRequest !== 0) { |
|||
window.cancelAnimationFrame(this.renderLoopRequest); |
|||
this.renderLoopRequest = 0; |
|||
} |
|||
} |
|||
|
|||
public putImageData(pData: number, width: number, height: number): boolean { |
|||
if (this.glInfo || !pData || width <= 0 || width <= 0) |
|||
return false; |
|||
|
|||
var ctx = this.htmlCanvas.getContext('2d'); |
|||
if (!ctx) { |
|||
console.error(`Failed to obtain 2D canvas context.`); |
|||
return false; |
|||
} |
|||
|
|||
// make sure the canvas is scaled correctly for the drawing
|
|||
this.htmlCanvas.width = width; |
|||
this.htmlCanvas.height = height; |
|||
|
|||
// set the canvas to be the bytes
|
|||
var buffer = new Uint8ClampedArray(Module.HEAPU8.buffer, pData, width * height * 4); |
|||
var imageData = new ImageData(buffer, width, height); |
|||
ctx.putImageData(imageData, 0, 0); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext { |
|||
const contextAttributes = { |
|||
alpha: 1, |
|||
depth: 1, |
|||
stencil: 8, |
|||
antialias: 0, |
|||
premultipliedAlpha: 1, |
|||
preserveDrawingBuffer: 0, |
|||
preferLowPowerToHighPerformance: 0, |
|||
failIfMajorPerformanceCaveat: 0, |
|||
majorVersion: 2, |
|||
minorVersion: 0, |
|||
enableExtensionsByDefault: 1, |
|||
explicitSwapControl: 0, |
|||
renderViaOffscreenBackBuffer: 1, |
|||
}; |
|||
|
|||
let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes); |
|||
if (!ctx && contextAttributes.majorVersion > 1) { |
|||
console.warn('Falling back to WebGL 1.0'); |
|||
contextAttributes.majorVersion = 1; |
|||
contextAttributes.minorVersion = 0; |
|||
ctx = GL.createContext(htmlCanvas, contextAttributes); |
|||
} |
|||
|
|||
return ctx; |
|||
} |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
|
|||
type SizeWatcherElement = { |
|||
SizeWatcher: SizeWatcherInstance; |
|||
} & HTMLElement |
|||
|
|||
type SizeWatcherInstance = { |
|||
callback: DotNet.DotNetObjectReference; |
|||
} |
|||
|
|||
export class SizeWatcher { |
|||
static observer: ResizeObserver; |
|||
static elements: Map<string, HTMLElement>; |
|||
|
|||
public static observe(element: HTMLElement, elementId: string, callback: DotNet.DotNetObjectReference) { |
|||
if (!element || !callback) |
|||
return; |
|||
|
|||
//console.info(`Adding size watcher observation with callback ${callback._id}...`);
|
|||
|
|||
SizeWatcher.init(); |
|||
|
|||
const watcherElement = element as SizeWatcherElement; |
|||
watcherElement.SizeWatcher = { |
|||
callback: callback |
|||
}; |
|||
|
|||
SizeWatcher.elements[elementId] = element; |
|||
SizeWatcher.observer.observe(element); |
|||
|
|||
SizeWatcher.invoke(element); |
|||
} |
|||
|
|||
public static unobserve(elementId: string) { |
|||
if (!elementId || !SizeWatcher.observer) |
|||
return; |
|||
|
|||
//console.info('Removing size watcher observation...');
|
|||
|
|||
const element = SizeWatcher.elements[elementId]; |
|||
|
|||
SizeWatcher.elements.delete(elementId); |
|||
SizeWatcher.observer.unobserve(element); |
|||
} |
|||
|
|||
static init() { |
|||
if (SizeWatcher.observer) |
|||
return; |
|||
|
|||
//console.info('Starting size watcher...');
|
|||
|
|||
SizeWatcher.elements = new Map<string, HTMLElement>(); |
|||
SizeWatcher.observer = new ResizeObserver((entries) => { |
|||
for (let entry of entries) { |
|||
SizeWatcher.invoke(entry.target); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
static invoke(element: Element) { |
|||
const watcherElement = element as SizeWatcherElement; |
|||
const instance = watcherElement.SizeWatcher; |
|||
|
|||
if (!instance || !instance.callback) |
|||
return; |
|||
|
|||
return instance.callback.invokeMethod('Invoke', element.clientWidth, element.clientHeight); |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
|
|||
declare namespace DotNet { |
|||
interface DotNetObjectReference extends DotNet.DotNetObject { |
|||
_id: number; |
|||
dispose(); |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
// Type definitions for non-npm package @blazor/javascript-interop 3.1
|
|||
// Project: https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interop?view=aspnetcore-3.1
|
|||
// Definitions by: Piotr Błażejewicz (Peter Blazejewicz) <https://github.com/peterblazejewicz>
|
|||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
|||
// Minimum TypeScript Version: 3.0
|
|||
|
|||
// Here be dragons!
|
|||
// This is community-maintained definition file intended to ease the process of developing
|
|||
// high quality JavaScript interop code to be used in Blazor application from your C# .Net code.
|
|||
// Could be removed without a notice in case official definition types ships with Blazor itself.
|
|||
|
|||
// tslint:disable:no-unnecessary-generics
|
|||
|
|||
declare namespace DotNet { |
|||
/** |
|||
* Invokes the specified .NET public method synchronously. Not all hosting scenarios support |
|||
* synchronous invocation, so if possible use invokeMethodAsync instead. |
|||
* |
|||
* @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method. |
|||
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. |
|||
* @param args Arguments to pass to the method, each of which must be JSON-serializable. |
|||
* @returns The result of the operation. |
|||
*/ |
|||
function invokeMethod<T>(assemblyName: string, methodIdentifier: string, ...args: any[]): T; |
|||
/** |
|||
* Invokes the specified .NET public method asynchronously. |
|||
* |
|||
* @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method. |
|||
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. |
|||
* @param args Arguments to pass to the method, each of which must be JSON-serializable. |
|||
* @returns A promise representing the result of the operation. |
|||
*/ |
|||
function invokeMethodAsync<T>(assemblyName: string, methodIdentifier: string, ...args: any[]): Promise<T>; |
|||
/** |
|||
* Represents the .NET instance passed by reference to JavaScript. |
|||
*/ |
|||
interface DotNetObject { |
|||
/** |
|||
* Invokes the specified .NET instance public method synchronously. Not all hosting scenarios support |
|||
* synchronous invocation, so if possible use invokeMethodAsync instead. |
|||
* |
|||
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. |
|||
* @param args Arguments to pass to the method, each of which must be JSON-serializable. |
|||
* @returns The result of the operation. |
|||
*/ |
|||
invokeMethod<T>(methodIdentifier: string, ...args: any[]): T; |
|||
/** |
|||
* Invokes the specified .NET instance public method asynchronously. |
|||
* |
|||
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. |
|||
* @param args Arguments to pass to the method, each of which must be JSON-serializable. |
|||
* @returns A promise representing the result of the operation. |
|||
*/ |
|||
invokeMethodAsync<T>(methodIdentifier: string, ...args: any[]): Promise<T>; |
|||
} |
|||
} |
|||
@ -0,0 +1,326 @@ |
|||
// Type definitions for Emscripten 1.39.16
|
|||
// Project: https://emscripten.org
|
|||
// Definitions by: Kensuke Matsuzaki <https://github.com/zakki>
|
|||
// Periklis Tsirakidis <https://github.com/periklis>
|
|||
// Bumsik Kim <https://github.com/kbumsik>
|
|||
// Louis DeScioli <https://github.com/lourd>
|
|||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
|||
// TypeScript Version: 2.2
|
|||
|
|||
/** Other WebAssembly declarations, for compatibility with older versions of Typescript */ |
|||
declare namespace WebAssembly { |
|||
interface Module {} |
|||
} |
|||
|
|||
declare namespace Emscripten { |
|||
interface FileSystemType {} |
|||
type EnvironmentType = 'WEB' | 'NODE' | 'SHELL' | 'WORKER'; |
|||
|
|||
type JSType = 'number' | 'string' | 'array' | 'boolean'; |
|||
type TypeCompatibleWithC = number | string | any[] | boolean; |
|||
|
|||
type CIntType = 'i8' | 'i16' | 'i32' | 'i64'; |
|||
type CFloatType = 'float' | 'double'; |
|||
type CPointerType = 'i8*' | 'i16*' | 'i32*' | 'i64*' | 'float*' | 'double*' | '*'; |
|||
type CType = CIntType | CFloatType | CPointerType; |
|||
|
|||
type WebAssemblyImports = Array<{ |
|||
name: string; |
|||
kind: string; |
|||
}>; |
|||
|
|||
type WebAssemblyExports = Array<{ |
|||
module: string; |
|||
name: string; |
|||
kind: string; |
|||
}>; |
|||
|
|||
interface CCallOpts { |
|||
async?: boolean | undefined; |
|||
} |
|||
} |
|||
|
|||
interface EmscriptenModule { |
|||
print(str: string): void; |
|||
printErr(str: string): void; |
|||
arguments: string[]; |
|||
environment: Emscripten.EnvironmentType; |
|||
preInit: Array<{ (): void }>; |
|||
preRun: Array<{ (): void }>; |
|||
postRun: Array<{ (): void }>; |
|||
onAbort: { (what: any): void }; |
|||
onRuntimeInitialized: { (): void }; |
|||
preinitializedWebGLContext: WebGLRenderingContext; |
|||
noInitialRun: boolean; |
|||
noExitRuntime: boolean; |
|||
logReadFiles: boolean; |
|||
filePackagePrefixURL: string; |
|||
wasmBinary: ArrayBuffer; |
|||
|
|||
destroy(object: object): void; |
|||
getPreloadedPackage(remotePackageName: string, remotePackageSize: number): ArrayBuffer; |
|||
instantiateWasm( |
|||
imports: Emscripten.WebAssemblyImports, |
|||
successCallback: (module: WebAssembly.Module) => void, |
|||
): Emscripten.WebAssemblyExports; |
|||
locateFile(url: string, scriptDirectory: string): string; |
|||
onCustomMessage(event: MessageEvent): void; |
|||
|
|||
// USE_TYPED_ARRAYS == 1
|
|||
HEAP: Int32Array; |
|||
IHEAP: Int32Array; |
|||
FHEAP: Float64Array; |
|||
|
|||
// USE_TYPED_ARRAYS == 2
|
|||
HEAP8: Int8Array; |
|||
HEAP16: Int16Array; |
|||
HEAP32: Int32Array; |
|||
HEAPU8: Uint8Array; |
|||
HEAPU16: Uint16Array; |
|||
HEAPU32: Uint32Array; |
|||
HEAPF32: Float32Array; |
|||
HEAPF64: Float64Array; |
|||
|
|||
TOTAL_STACK: number; |
|||
TOTAL_MEMORY: number; |
|||
FAST_MEMORY: number; |
|||
|
|||
addOnPreRun(cb: () => any): void; |
|||
addOnInit(cb: () => any): void; |
|||
addOnPreMain(cb: () => any): void; |
|||
addOnExit(cb: () => any): void; |
|||
addOnPostRun(cb: () => any): void; |
|||
|
|||
preloadedImages: any; |
|||
preloadedAudios: any; |
|||
|
|||
_malloc(size: number): number; |
|||
_free(ptr: number): void; |
|||
} |
|||
|
|||
/** |
|||
* A factory function is generated when setting the `MODULARIZE` build option |
|||
* to `1` in your Emscripten build. It return a Promise that resolves to an |
|||
* initialized, ready-to-call `EmscriptenModule` instance. |
|||
* |
|||
* By default, the factory function will be named `Module`. It's recommended to |
|||
* use the `EXPORT_ES6` option, in which the factory function will be the |
|||
* default export. If used without `EXPORT_ES6`, the factory function will be a |
|||
* global variable. You can rename the variable using the `EXPORT_NAME` build |
|||
* option. It's left to you to declare any global variables as needed in your |
|||
* application's types. |
|||
* @param moduleOverrides Default properties for the initialized module. |
|||
*/ |
|||
type EmscriptenModuleFactory<T extends EmscriptenModule = EmscriptenModule> = ( |
|||
moduleOverrides?: Partial<T>, |
|||
) => Promise<T>; |
|||
|
|||
declare namespace FS { |
|||
interface Lookup { |
|||
path: string; |
|||
node: FSNode; |
|||
} |
|||
|
|||
interface FSStream {} |
|||
interface FSNode {} |
|||
interface ErrnoError {} |
|||
|
|||
let ignorePermissions: boolean; |
|||
let trackingDelegate: any; |
|||
let tracking: any; |
|||
let genericErrors: any; |
|||
|
|||
//
|
|||
// paths
|
|||
//
|
|||
function lookupPath(path: string, opts: any): Lookup; |
|||
function getPath(node: FSNode): string; |
|||
|
|||
//
|
|||
// nodes
|
|||
//
|
|||
function isFile(mode: number): boolean; |
|||
function isDir(mode: number): boolean; |
|||
function isLink(mode: number): boolean; |
|||
function isChrdev(mode: number): boolean; |
|||
function isBlkdev(mode: number): boolean; |
|||
function isFIFO(mode: number): boolean; |
|||
function isSocket(mode: number): boolean; |
|||
|
|||
//
|
|||
// devices
|
|||
//
|
|||
function major(dev: number): number; |
|||
function minor(dev: number): number; |
|||
function makedev(ma: number, mi: number): number; |
|||
function registerDevice(dev: number, ops: any): void; |
|||
|
|||
//
|
|||
// core
|
|||
//
|
|||
function syncfs(populate: boolean, callback: (e: any) => any): void; |
|||
function syncfs(callback: (e: any) => any, populate?: boolean): void; |
|||
function mount(type: Emscripten.FileSystemType, opts: any, mountpoint: string): any; |
|||
function unmount(mountpoint: string): void; |
|||
|
|||
function mkdir(path: string, mode?: number): any; |
|||
function mkdev(path: string, mode?: number, dev?: number): any; |
|||
function symlink(oldpath: string, newpath: string): any; |
|||
function rename(old_path: string, new_path: string): void; |
|||
function rmdir(path: string): void; |
|||
function readdir(path: string): any; |
|||
function unlink(path: string): void; |
|||
function readlink(path: string): string; |
|||
function stat(path: string, dontFollow?: boolean): any; |
|||
function lstat(path: string): any; |
|||
function chmod(path: string, mode: number, dontFollow?: boolean): void; |
|||
function lchmod(path: string, mode: number): void; |
|||
function fchmod(fd: number, mode: number): void; |
|||
function chown(path: string, uid: number, gid: number, dontFollow?: boolean): void; |
|||
function lchown(path: string, uid: number, gid: number): void; |
|||
function fchown(fd: number, uid: number, gid: number): void; |
|||
function truncate(path: string, len: number): void; |
|||
function ftruncate(fd: number, len: number): void; |
|||
function utime(path: string, atime: number, mtime: number): void; |
|||
function open(path: string, flags: string, mode?: number, fd_start?: number, fd_end?: number): FSStream; |
|||
function close(stream: FSStream): void; |
|||
function llseek(stream: FSStream, offset: number, whence: number): any; |
|||
function read(stream: FSStream, buffer: ArrayBufferView, offset: number, length: number, position?: number): number; |
|||
function write( |
|||
stream: FSStream, |
|||
buffer: ArrayBufferView, |
|||
offset: number, |
|||
length: number, |
|||
position?: number, |
|||
canOwn?: boolean, |
|||
): number; |
|||
function allocate(stream: FSStream, offset: number, length: number): void; |
|||
function mmap( |
|||
stream: FSStream, |
|||
buffer: ArrayBufferView, |
|||
offset: number, |
|||
length: number, |
|||
position: number, |
|||
prot: number, |
|||
flags: number, |
|||
): any; |
|||
function ioctl(stream: FSStream, cmd: any, arg: any): any; |
|||
function readFile(path: string, opts: { encoding: 'binary'; flags?: string | undefined }): Uint8Array; |
|||
function readFile(path: string, opts: { encoding: 'utf8'; flags?: string | undefined }): string; |
|||
function readFile(path: string, opts?: { flags?: string | undefined }): Uint8Array; |
|||
function writeFile(path: string, data: string | ArrayBufferView, opts?: { flags?: string | undefined }): void; |
|||
|
|||
//
|
|||
// module-level FS code
|
|||
//
|
|||
function cwd(): string; |
|||
function chdir(path: string): void; |
|||
function init( |
|||
input: null | (() => number | null), |
|||
output: null | ((c: number) => any), |
|||
error: null | ((c: number) => any), |
|||
): void; |
|||
|
|||
function createLazyFile( |
|||
parent: string | FSNode, |
|||
name: string, |
|||
url: string, |
|||
canRead: boolean, |
|||
canWrite: boolean, |
|||
): FSNode; |
|||
function createPreloadedFile( |
|||
parent: string | FSNode, |
|||
name: string, |
|||
url: string, |
|||
canRead: boolean, |
|||
canWrite: boolean, |
|||
onload?: () => void, |
|||
onerror?: () => void, |
|||
dontCreateFile?: boolean, |
|||
canOwn?: boolean, |
|||
): void; |
|||
function createDataFile( |
|||
parent: string | FSNode, |
|||
name: string, |
|||
data: ArrayBufferView, |
|||
canRead: boolean, |
|||
canWrite: boolean, |
|||
canOwn: boolean, |
|||
): FSNode; |
|||
} |
|||
|
|||
declare var MEMFS: Emscripten.FileSystemType; |
|||
declare var NODEFS: Emscripten.FileSystemType; |
|||
declare var IDBFS: Emscripten.FileSystemType; |
|||
|
|||
// Below runtime function/variable declarations are exportable by
|
|||
// -s EXTRA_EXPORTED_RUNTIME_METHODS. You can extend or merge
|
|||
// EmscriptenModule interface to add runtime functions.
|
|||
//
|
|||
// For example, by using -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']"
|
|||
// You can access ccall() via Module["ccall"]. In this case, you should
|
|||
// extend EmscriptenModule to pass the compiler check like the following:
|
|||
//
|
|||
// interface YourOwnEmscriptenModule extends EmscriptenModule {
|
|||
// ccall: typeof ccall;
|
|||
// }
|
|||
//
|
|||
// See: https://emscripten.org/docs/getting_started/FAQ.html#why-do-i-get-typeerror-module-something-is-not-a-function
|
|||
|
|||
declare function ccall( |
|||
ident: string, |
|||
returnType: Emscripten.JSType | null, |
|||
argTypes: Emscripten.JSType[], |
|||
args: Emscripten.TypeCompatibleWithC[], |
|||
opts?: Emscripten.CCallOpts, |
|||
): any; |
|||
declare function cwrap( |
|||
ident: string, |
|||
returnType: Emscripten.JSType | null, |
|||
argTypes: Emscripten.JSType[], |
|||
opts?: Emscripten.CCallOpts, |
|||
): (...args: any[]) => any; |
|||
|
|||
declare function setValue(ptr: number, value: any, type: Emscripten.CType, noSafe?: boolean): void; |
|||
declare function getValue(ptr: number, type: Emscripten.CType, noSafe?: boolean): number; |
|||
|
|||
declare function allocate( |
|||
slab: number[] | ArrayBufferView | number, |
|||
types: Emscripten.CType | Emscripten.CType[], |
|||
allocator: number, |
|||
ptr?: number, |
|||
): number; |
|||
|
|||
declare function stackAlloc(size: number): number; |
|||
declare function stackSave(): number; |
|||
declare function stackRestore(ptr: number): void; |
|||
|
|||
declare function UTF8ToString(ptr: number, maxBytesToRead?: number): string; |
|||
declare function stringToUTF8(str: string, outPtr: number, maxBytesToRead?: number): void; |
|||
declare function lengthBytesUTF8(str: string): number; |
|||
declare function allocateUTF8(str: string): number; |
|||
declare function allocateUTF8OnStack(str: string): number; |
|||
declare function UTF16ToString(ptr: number): string; |
|||
declare function stringToUTF16(str: string, outPtr: number, maxBytesToRead?: number): void; |
|||
declare function lengthBytesUTF16(str: string): number; |
|||
declare function UTF32ToString(ptr: number): string; |
|||
declare function stringToUTF32(str: string, outPtr: number, maxBytesToRead?: number): void; |
|||
declare function lengthBytesUTF32(str: string): number; |
|||
|
|||
declare function intArrayFromString(stringy: string, dontAddNull?: boolean, length?: number): number[]; |
|||
declare function intArrayToString(array: number[]): string; |
|||
declare function writeStringToMemory(str: string, buffer: number, dontAddNull: boolean): void; |
|||
declare function writeArrayToMemory(array: number[], buffer: number): void; |
|||
declare function writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void; |
|||
|
|||
declare function addRunDependency(id: any): void; |
|||
declare function removeRunDependency(id: any): void; |
|||
|
|||
declare function addFunction(func: (...args: any[]) => any, signature?: string): number; |
|||
declare function removeFunction(funcPtr: number): void; |
|||
|
|||
declare var ALLOC_NORMAL: number; |
|||
declare var ALLOC_STACK: number; |
|||
declare var ALLOC_STATIC: number; |
|||
declare var ALLOC_DYNAMIC: number; |
|||
declare var ALLOC_NONE: number; |
|||
@ -0,0 +1,127 @@ |
|||
using Avalonia.Input; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
internal static class Keycodes |
|||
{ |
|||
public static Dictionary<string, Key> KeyCodes = new() |
|||
{ |
|||
{ "Escape", Key.Escape }, |
|||
{ "Digit1", Key.D1 }, |
|||
{ "Digit2", Key.D2 }, |
|||
{ "Digit3", Key.D3 }, |
|||
{ "Digit4", Key.D4 }, |
|||
{ "Digit5", Key.D5 }, |
|||
{ "Digit6", Key.D6 }, |
|||
{ "Digit7", Key.D7 }, |
|||
{ "Digit8", Key.D8 }, |
|||
{ "Digit9", Key.D9 }, |
|||
{ "Digit0", Key.D0 }, |
|||
{ "Minus", Key.OemMinus }, |
|||
//{ "Equal" , Key. },
|
|||
{ "Backspace", Key.Back }, |
|||
{ "Tab", Key.Tab }, |
|||
{ "KeyQ", Key.Q }, |
|||
{ "KeyW", Key.W }, |
|||
{ "KeyE", Key.E }, |
|||
{ "KeyR", Key.R }, |
|||
{ "KeyT", Key.T }, |
|||
{ "KeyY", Key.Y }, |
|||
{ "KeyU", Key.U }, |
|||
{ "KeyI", Key.I }, |
|||
{ "KeyO", Key.O }, |
|||
{ "KeyP", Key.P }, |
|||
{ "BracketLeft", Key.OemOpenBrackets }, |
|||
{ "BracketRight", Key.OemCloseBrackets }, |
|||
{ "Enter", Key.Enter }, |
|||
{ "ControlLeft", Key.LeftCtrl }, |
|||
{ "KeyA", Key.A }, |
|||
{ "KeyS", Key.S }, |
|||
{ "KeyD", Key.D }, |
|||
{ "KeyF", Key.F }, |
|||
{ "KeyG", Key.G }, |
|||
{ "KeyH", Key.H }, |
|||
{ "KeyJ", Key.J }, |
|||
{ "KeyK", Key.K }, |
|||
{ "KeyL", Key.L }, |
|||
{ "Semicolon", Key.OemSemicolon }, |
|||
{ "Quote", Key.OemQuotes }, |
|||
//{ "Backquote" , Key. },
|
|||
{ "ShiftLeft", Key.LeftShift }, |
|||
{ "Backslash", Key.OemBackslash }, |
|||
{ "KeyZ", Key.Z }, |
|||
{ "KeyX", Key.X }, |
|||
{ "KeyC", Key.C }, |
|||
{ "KeyV", Key.V }, |
|||
{ "KeyB", Key.B }, |
|||
{ "KeyN", Key.N }, |
|||
{ "KeyM", Key.M }, |
|||
{ "Comma", Key.OemComma }, |
|||
{ "Period", Key.OemPeriod }, |
|||
//{ "Slash" , Key. },
|
|||
{ "ShiftRight", Key.RightShift }, |
|||
{ "NumpadMultiply", Key.Multiply }, |
|||
{ "AltLeft", Key.LeftAlt }, |
|||
{ "Space", Key.Space }, |
|||
{ "CapsLock", Key.CapsLock }, |
|||
{ "F1", Key.F1 }, |
|||
{ "F2", Key.F2 }, |
|||
{ "F3", Key.F3 }, |
|||
{ "F4", Key.F4 }, |
|||
{ "F5", Key.F5 }, |
|||
{ "F6", Key.F6 }, |
|||
{ "F7", Key.F7 }, |
|||
{ "F8", Key.F8 }, |
|||
{ "F9", Key.F9 }, |
|||
{ "F10", Key.F10 }, |
|||
{ "NumLock", Key.NumLock }, |
|||
{ "ScrollLock", Key.Scroll }, |
|||
{ "Numpad7", Key.NumPad7 }, |
|||
{ "Numpad8", Key.NumPad8 }, |
|||
{ "Numpad9", Key.NumPad9 }, |
|||
{ "NumpadSubtract", Key.Subtract }, |
|||
{ "Numpad4", Key.NumPad4 }, |
|||
{ "Numpad5", Key.NumPad5 }, |
|||
{ "Numpad6", Key.NumPad6 }, |
|||
{ "NumpadAdd", Key.Add }, |
|||
{ "Numpad1", Key.NumPad1 }, |
|||
{ "Numpad2", Key.NumPad2 }, |
|||
{ "Numpad3", Key.NumPad3 }, |
|||
{ "Numpad0", Key.NumPad0 }, |
|||
{ "NumpadDecimal", Key.Decimal }, |
|||
{ "Unidentified", Key.NoName }, |
|||
//{ "IntlBackslash" , Key.bac },
|
|||
{ "F11", Key.F11 }, |
|||
{ "F12", Key.F12 }, |
|||
//{ "IntlRo" , Key.Ro },
|
|||
//{ "Unidentified" , Key. },
|
|||
{ "Convert", Key.ImeConvert }, |
|||
{ "KanaMode", Key.KanaMode }, |
|||
{ "NonConvert", Key.ImeNonConvert }, |
|||
//{ "Unidentified" , Key. },
|
|||
{ "NumpadEnter", Key.Enter }, |
|||
{ "ControlRight", Key.RightCtrl }, |
|||
{ "NumpadDivide", Key.Divide }, |
|||
{ "PrintScreen", Key.PrintScreen }, |
|||
{ "AltRight", Key.RightAlt }, |
|||
//{ "Unidentified" , Key. },
|
|||
{ "Home", Key.Home }, |
|||
{ "ArrowUp", Key.Up }, |
|||
{ "PageUp", Key.PageUp }, |
|||
{ "ArrowLeft", Key.Left }, |
|||
{ "ArrowRight", Key.Right }, |
|||
{ "End", Key.End }, |
|||
{ "ArrowDown", Key.Down }, |
|||
{ "PageDown", Key.PageDown }, |
|||
{ "Insert", Key.Insert }, |
|||
{ "Delete", Key.Delete }, |
|||
//{ "Unidentified" , Key. },
|
|||
{ "AudioVolumeMute", Key.VolumeMute }, |
|||
{ "AudioVolumeDown", Key.VolumeDown }, |
|||
{ "AudioVolumeUp", Key.VolumeUp }, |
|||
//{ "NumpadEqual" , Key. },
|
|||
{ "Pause", Key.Pause }, |
|||
{ "NumpadComma", Key.OemComma } |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using System.Diagnostics; |
|||
using Avalonia.Rendering; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
public class ManualTriggerRenderTimer : IRenderTimer |
|||
{ |
|||
private static readonly Stopwatch s_sw = Stopwatch.StartNew(); |
|||
|
|||
public static ManualTriggerRenderTimer Instance { get; } = new(); |
|||
|
|||
public void RaiseTick() => Tick?.Invoke(s_sw.Elapsed); |
|||
|
|||
public event Action<TimeSpan>? Tick; |
|||
} |
|||
} |
|||
@ -0,0 +1,165 @@ |
|||
using System.Diagnostics; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Raw; |
|||
using Avalonia.Input.TextInput; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.Web.Blazor.Interop; |
|||
using SkiaSharp; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod |
|||
{ |
|||
private Size _clientSize; |
|||
private BlazorSkiaSurface? _currentSurface; |
|||
private IInputRoot? _inputRoot; |
|||
private readonly Stopwatch _sw = Stopwatch.StartNew(); |
|||
private readonly ITextInputMethodImpl _textInputMethod; |
|||
private readonly TouchDevice _touchDevice; |
|||
|
|||
public RazorViewTopLevelImpl(ITextInputMethodImpl textInputMethod) |
|||
{ |
|||
_textInputMethod = textInputMethod; |
|||
TransparencyLevel = WindowTransparencyLevel.None; |
|||
AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1); |
|||
_touchDevice = new TouchDevice(); |
|||
} |
|||
|
|||
public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds; |
|||
|
|||
|
|||
internal void SetSurface(GRContext context, SKHtmlCanvasInterop.GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling) |
|||
{ |
|||
_currentSurface = |
|||
new BlazorSkiaSurface(context, glInfo, colorType, size, scaling, GRSurfaceOrigin.BottomLeft); |
|||
} |
|||
|
|||
public void SetClientSize(SKSize size, double dpi) |
|||
{ |
|||
var newSize = new Size(size.Width, size.Height); |
|||
|
|||
if (newSize != _clientSize) |
|||
{ |
|||
_clientSize = newSize; |
|||
|
|||
if (_currentSurface is { }) |
|||
{ |
|||
_currentSurface.Size = new PixelSize((int)size.Width, (int)size.Height); |
|||
} |
|||
|
|||
Resized?.Invoke(newSize, PlatformResizeReason.User); |
|||
} |
|||
} |
|||
|
|||
public void RawTouchEvent(RawPointerEventType type, Point p, RawInputModifiers modifiers, long touchPointId) |
|||
{ |
|||
if (_inputRoot is { }) |
|||
{ |
|||
Input?.Invoke(new RawTouchEventArgs(_touchDevice, Timestamp, _inputRoot, type, p, modifiers, touchPointId)); |
|||
} |
|||
} |
|||
|
|||
public void RawMouseEvent(RawPointerEventType type, Point p, RawInputModifiers modifiers) |
|||
{ |
|||
if (_inputRoot is { }) |
|||
{ |
|||
Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, _inputRoot, type, p, modifiers)); |
|||
} |
|||
} |
|||
|
|||
public void RawMouseWheelEvent(Point p, Vector v, RawInputModifiers modifiers) |
|||
{ |
|||
if (_inputRoot is { }) |
|||
{ |
|||
Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, _inputRoot, p, v, modifiers)); |
|||
} |
|||
} |
|||
|
|||
public void RawKeyboardEvent(RawKeyEventType type, string key, RawInputModifiers modifiers) |
|||
{ |
|||
if (Keycodes.KeyCodes.TryGetValue(key, out var avkey)) |
|||
{ |
|||
if (_inputRoot is { }) |
|||
{ |
|||
Input?.Invoke(new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void RawTextEvent(string text) |
|||
{ |
|||
if (_inputRoot is { }) |
|||
{ |
|||
Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice, Timestamp, _inputRoot, text)); |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public IRenderer CreateRenderer(IRenderRoot root) |
|||
{ |
|||
var loop = AvaloniaLocator.Current.GetService<IRenderLoop>(); |
|||
|
|||
return new DeferredRenderer(root, loop); |
|||
} |
|||
|
|||
public void Invalidate(Rect rect) |
|||
{ |
|||
//Console.WriteLine("invalidate rect called");
|
|||
} |
|||
|
|||
public void SetInputRoot(IInputRoot inputRoot) |
|||
{ |
|||
_inputRoot = inputRoot; |
|||
} |
|||
|
|||
public Point PointToClient(PixelPoint point) => new Point(point.X, point.Y); |
|||
|
|||
public PixelPoint PointToScreen(Point point) => new PixelPoint((int)point.X, (int)point.Y); |
|||
|
|||
public void SetCursor(ICursorImpl cursor) |
|||
{ |
|||
// nop
|
|||
|
|||
} |
|||
|
|||
public IPopupImpl? CreatePopup() |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public Size ClientSize => _clientSize; |
|||
public Size? FrameSize => null; |
|||
public double RenderScaling => 1; |
|||
|
|||
public IEnumerable<object> Surfaces => new object[] { _currentSurface! }; |
|||
|
|||
public Action<RawInputEventArgs>? Input { get; set; } |
|||
public Action<Rect>? Paint { get; set; } |
|||
public Action<Size, PlatformResizeReason>? Resized { get; set; } |
|||
public Action<double>? ScalingChanged { get; set; } |
|||
public Action<WindowTransparencyLevel>? TransparencyLevelChanged { get; set; } |
|||
public Action? Closed { get; set; } |
|||
public Action? LostFocus { get; set; } |
|||
public IMouseDevice MouseDevice { get; } = new MouseDevice(); |
|||
|
|||
public IKeyboardDevice KeyboardDevice { get; } = BlazorWindowingPlatform.Keyboard; |
|||
public WindowTransparencyLevel TransparencyLevel { get; } |
|||
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } |
|||
|
|||
public ITextInputMethodImpl TextInputMethod => _textInputMethod; |
|||
} |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Platform; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
internal class ClipboardStub : IClipboard |
|||
{ |
|||
public Task<string> GetTextAsync() => Task.FromResult(""); |
|||
|
|||
public Task SetTextAsync(string text) => Task.CompletedTask; |
|||
|
|||
public Task ClearAsync() => Task.CompletedTask; |
|||
|
|||
public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; |
|||
|
|||
public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>()); |
|||
|
|||
public Task<object> GetDataAsync(string format) => Task.FromResult<object>(new ()); |
|||
} |
|||
|
|||
internal class CursorStub : ICursorImpl |
|||
{ |
|||
public void Dispose() |
|||
{ |
|||
|
|||
} |
|||
} |
|||
|
|||
internal class CursorFactoryStub : ICursorFactory |
|||
{ |
|||
public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) |
|||
{ |
|||
return new CursorStub(); |
|||
} |
|||
|
|||
ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType) |
|||
{ |
|||
return new CursorStub(); |
|||
} |
|||
} |
|||
|
|||
internal class IconLoaderStub : IPlatformIconLoader |
|||
{ |
|||
private class IconStub : IWindowIconImpl |
|||
{ |
|||
public void Save(Stream outputStream) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
|
|||
public IWindowIconImpl LoadIcon(string fileName) => new IconStub(); |
|||
|
|||
public IWindowIconImpl LoadIcon(Stream stream) => new IconStub(); |
|||
|
|||
public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) => new IconStub(); |
|||
} |
|||
|
|||
internal class SystemDialogsStub : ISystemDialogImpl |
|||
{ |
|||
public Task<string[]?> ShowFileDialogAsync(FileDialog dialog, Window parent) => |
|||
Task.FromResult((string[]?)null); |
|||
|
|||
public Task<string?> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) => |
|||
Task.FromResult((string?)null); |
|||
} |
|||
|
|||
internal class ScreenStub : IScreenImpl |
|||
{ |
|||
public int ScreenCount => 1; |
|||
|
|||
public IReadOnlyList<Screen> AllScreens { get; } = |
|||
new[] { new Screen(96, new PixelRect(0, 0, 4000, 4000), new PixelRect(0, 0, 4000, 4000), true) }; |
|||
} |
|||
} |
|||
@ -0,0 +1,100 @@ |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.Threading; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
public class BlazorWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface |
|||
{ |
|||
private bool _signaled; |
|||
private static int s_uiThreadId = -1; |
|||
|
|||
public IWindowImpl CreateWindow() => throw new NotSupportedException(); |
|||
|
|||
IWindowImpl IWindowingPlatform.CreateEmbeddableWindow() |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
public ITrayIconImpl? CreateTrayIcon() |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
public static KeyboardDevice Keyboard { get; private set; } |
|||
|
|||
public static void Register() |
|||
{ |
|||
var instance = new BlazorWindowingPlatform(); |
|||
Keyboard = new KeyboardDevice(); |
|||
AvaloniaLocator.CurrentMutable |
|||
.Bind<IClipboard>().ToSingleton<ClipboardStub>() |
|||
.Bind<ICursorFactory>().ToSingleton<CursorFactoryStub>() |
|||
.Bind<IKeyboardDevice>().ToConstant(Keyboard) |
|||
.Bind<IPlatformSettings>().ToConstant(instance) |
|||
.Bind<IPlatformThreadingInterface>().ToConstant(instance) |
|||
.Bind<IRenderLoop>().ToConstant(new RenderLoop()) |
|||
.Bind<IRenderTimer>().ToConstant(ManualTriggerRenderTimer.Instance) |
|||
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialogsStub>() |
|||
.Bind<IWindowingPlatform>().ToConstant(instance) |
|||
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>() |
|||
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>(); |
|||
} |
|||
|
|||
public Size DoubleClickSize { get; } = new Size(2, 2); |
|||
|
|||
public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500); |
|||
|
|||
public void RunLoop(CancellationToken cancellationToken) |
|||
{ |
|||
throw new NotSupportedException(); |
|||
} |
|||
|
|||
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) |
|||
{ |
|||
return AvaloniaLocator.Current.GetService<IRuntimePlatform>() |
|||
.StartSystemTimer(interval, () => |
|||
{ |
|||
Dispatcher.UIThread.RunJobs(priority); |
|||
tick(); |
|||
}); |
|||
} |
|||
|
|||
public void Signal(DispatcherPriority priority) |
|||
{ |
|||
if (_signaled) |
|||
return; |
|||
|
|||
_signaled = true; |
|||
|
|||
IDisposable? disp = null; |
|||
|
|||
disp = AvaloniaLocator.Current.GetService<IRuntimePlatform>() |
|||
.StartSystemTimer(TimeSpan.FromMilliseconds(1), |
|||
() => |
|||
{ |
|||
_signaled = false; |
|||
disp?.Dispose(); |
|||
|
|||
Signaled?.Invoke(null); |
|||
}); |
|||
} |
|||
|
|||
public bool CurrentThreadIsLoopThread |
|||
{ |
|||
get |
|||
{ |
|||
return true; // Blazor is single threaded.
|
|||
} |
|||
} |
|||
|
|||
public event Action<DispatcherPriority?>? Signaled; |
|||
|
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
@using Microsoft.AspNetCore.Components.Web |
|||
@ -0,0 +1,14 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"noImplicitAny": false, |
|||
"noEmitOnError": true, |
|||
"removeComments": false, |
|||
"sourceMap": true, |
|||
"target": "ES2020", |
|||
"module": "ES2020", |
|||
"outDir": "wwwroot" |
|||
}, |
|||
"exclude": [ |
|||
"node_modules" |
|||
] |
|||
} |
|||
Loading…
Reference in new issue