committed by
GitHub
83 changed files with 3603 additions and 143 deletions
@ -1,6 +1,7 @@ |
|||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
||||
<ItemGroup> |
<ItemGroup> |
||||
<PackageReference Include="HarfBuzzSharp" 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.155" /> |
<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> |
</ItemGroup> |
||||
</Project> |
</Project> |
||||
|
|||||
@ -1,6 +1,7 @@ |
|||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
||||
<ItemGroup> |
<ItemGroup> |
||||
<PackageReference Include="SkiaSharp" 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.155" /> |
<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> |
</ItemGroup> |
||||
</Project> |
</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