committed by
GitHub
72 changed files with 6433 additions and 24 deletions
@ -0,0 +1,41 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net7.0</TargetFramework> |
||||
|
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier> |
||||
|
<WasmMainJSPath>main.js</WasmMainJSPath> |
||||
|
<OutputType>Exe</OutputType> |
||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> |
||||
|
<MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver> |
||||
|
<WasmBuildNative>true</WasmBuildNative> |
||||
|
<EmccFlags>-sVERBOSE -sERROR_ON_UNDEFINED_SYMBOLS=0</EmccFlags> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<PropertyGroup Condition="'$(Configuration)'=='Release'"> |
||||
|
<RunAOTCompilation>true</RunAOTCompilation> |
||||
|
<PublishTrimmed>true</PublishTrimmed> |
||||
|
<TrimMode>full</TrimMode> |
||||
|
<WasmBuildNative>true</WasmBuildNative> |
||||
|
<InvariantGlobalization>true</InvariantGlobalization> |
||||
|
<WasmEnableSIMD>true</WasmEnableSIMD> |
||||
|
<EmccCompileOptimizationFlag>-O3</EmccCompileOptimizationFlag> |
||||
|
<EmccLinkOptimizationFlag>-O3</EmccLinkOptimizationFlag> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\..\samples\ControlCatalog\ControlCatalog.csproj" /> |
||||
|
<ProjectReference Include="..\Avalonia.Web\Avalonia.Web.csproj" /> |
||||
|
<ProjectReference Include="..\..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<WasmExtraFilesToDeploy Include="index.html" /> |
||||
|
<WasmExtraFilesToDeploy Include="main.js" /> |
||||
|
<WasmExtraFilesToDeploy Include="embed.js" /> |
||||
|
<WasmExtraFilesToDeploy Include="favicon.ico" /> |
||||
|
<WasmExtraFilesToDeploy Include="Logo.svg" /> |
||||
|
<WasmExtraFilesToDeploy Include="app.css" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<Import Project="..\Avalonia.Web\Avalonia.Web.props" /> |
||||
|
<Import Project="..\Avalonia.Web\Avalonia.Web.targets" /> |
||||
|
</Project> |
||||
@ -0,0 +1,44 @@ |
|||||
|
using System; |
||||
|
using System.Runtime.InteropServices.JavaScript; |
||||
|
|
||||
|
using Avalonia; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Web; |
||||
|
|
||||
|
using ControlCatalog.Pages; |
||||
|
|
||||
|
namespace ControlCatalog.Web; |
||||
|
|
||||
|
public class EmbedSampleWeb : INativeDemoControl |
||||
|
{ |
||||
|
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault) |
||||
|
{ |
||||
|
if (isSecond) |
||||
|
{ |
||||
|
var iframe = EmbedInterop.CreateElement("iframe"); |
||||
|
iframe.SetProperty("src", "https://www.youtube.com/embed/kZCIporjJ70"); |
||||
|
|
||||
|
return new JSObjectControlHandle(iframe); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var defaultHandle = (JSObjectControlHandle)createDefault(); |
||||
|
|
||||
|
_ = JSHost.ImportAsync("embed.js", "./embed.js").ContinueWith(_ => |
||||
|
{ |
||||
|
EmbedInterop.AddAppButton(defaultHandle.Object); |
||||
|
}); |
||||
|
|
||||
|
return defaultHandle; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal static partial class EmbedInterop |
||||
|
{ |
||||
|
[JSImport("globalThis.document.createElement")] |
||||
|
public static partial JSObject CreateElement(string tagName); |
||||
|
|
||||
|
[JSImport("addAppButton", "embed.js")] |
||||
|
public static partial void AddAppButton(JSObject parentObject); |
||||
|
} |
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,19 @@ |
|||||
|
using Avalonia; |
||||
|
using Avalonia.Web; |
||||
|
using ControlCatalog; |
||||
|
using ControlCatalog.Web; |
||||
|
|
||||
|
internal partial class Program |
||||
|
{ |
||||
|
private static void Main(string[] args) |
||||
|
{ |
||||
|
BuildAvaloniaApp() |
||||
|
.AfterSetup(_ => |
||||
|
{ |
||||
|
ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb(); |
||||
|
}).SetupBrowserApp("out"); |
||||
|
} |
||||
|
|
||||
|
public static AppBuilder BuildAvaloniaApp() |
||||
|
=> AppBuilder.Configure<App>(); |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
#out { |
||||
|
height: 100vh; |
||||
|
width: 100vw |
||||
|
} |
||||
|
|
||||
|
#avalonia-splash { |
||||
|
position: absolute; |
||||
|
height: 100%; |
||||
|
width: 100%; |
||||
|
color: whitesmoke; |
||||
|
background: #171C2C; |
||||
|
font-family: 'Nunito', sans-serif; |
||||
|
} |
||||
|
|
||||
|
#avalonia-splash a{ |
||||
|
color: whitesmoke; |
||||
|
text-decoration: none; |
||||
|
} |
||||
|
|
||||
|
.center { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
height: 250px; |
||||
|
} |
||||
|
|
||||
|
.splash-close { |
||||
|
animation: fadeOut 1s forwards; |
||||
|
} |
||||
|
|
||||
|
@keyframes fadeOut { |
||||
|
from { |
||||
|
opacity: 1; |
||||
|
} |
||||
|
|
||||
|
to { |
||||
|
opacity: 0; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
export function addAppButton(parent) { |
||||
|
var button = globalThis.document.createElement('button'); |
||||
|
button.innerText = 'Hello world'; |
||||
|
var clickCount = 0; |
||||
|
button.onclick = () => { |
||||
|
clickCount++; |
||||
|
button.innerText = 'Click count ' + clickCount; |
||||
|
}; |
||||
|
parent.appendChild(button); |
||||
|
return button; |
||||
|
} |
||||
|
After Width: | Height: | Size: 172 KiB |
@ -0,0 +1,31 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<!-- Licensed to the .NET Foundation under one or more agreements. --> |
||||
|
<!-- The .NET Foundation licenses this file to you under the MIT license. --> |
||||
|
<html> |
||||
|
|
||||
|
<head> |
||||
|
<title>Avalonia.Web.Sample</title> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<link rel="modulepreload" href="./main.js" /> |
||||
|
<link rel="modulepreload" href="./dotnet.js" /> |
||||
|
<link rel="modulepreload" href="./avalonia.js" /> |
||||
|
<link rel="stylesheet" href="./app.css" /> |
||||
|
</head> |
||||
|
|
||||
|
<body style="margin: 0px"> |
||||
|
<div id="out"> |
||||
|
<div id="avalonia-splash"> |
||||
|
<div class="center"> |
||||
|
<h2>Powered by</h2> |
||||
|
<a class="navbar-brand" href="https://www.avaloniaui.net/" target="_blank"> |
||||
|
<img src="Logo.svg" alt="Avalonia Logo" width="30" height="24" /> |
||||
|
Avalonia |
||||
|
</a> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<script type='module' src="./main.js"></script> |
||||
|
</body> |
||||
|
|
||||
|
</html> |
||||
@ -0,0 +1,19 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
|
||||
|
import { dotnet } from './dotnet.js' |
||||
|
import { createAvaloniaRuntime } from './avalonia.js'; |
||||
|
|
||||
|
const is_browser = typeof window != "undefined"; |
||||
|
if (!is_browser) throw new Error(`Expected to be running in a browser`); |
||||
|
|
||||
|
const dotnetRuntime = await dotnet |
||||
|
.withDiagnosticTracing(false) |
||||
|
.withApplicationArgumentsFromQuery() |
||||
|
.create(); |
||||
|
|
||||
|
await createAvaloniaRuntime(dotnetRuntime); |
||||
|
|
||||
|
const config = dotnetRuntime.getConfig(); |
||||
|
|
||||
|
await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]); |
||||
@ -0,0 +1,11 @@ |
|||||
|
{ |
||||
|
"wasmHostProperties": { |
||||
|
"perHostConfig": [ |
||||
|
{ |
||||
|
"name": "browser", |
||||
|
"html-path": "index.html", |
||||
|
"Host": "browser" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net7.0</TargetFramework> |
||||
|
<LangVersion>preview</LangVersion> |
||||
|
<Nullable>enable</Nullable> |
||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<SupportedPlatform Include="browser" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<Import Project="..\..\..\build\BuildTargets.targets" /> |
||||
|
<Import Project="..\..\..\build\SkiaSharp.props" /> |
||||
|
<Import Project="..\..\..\build\HarfBuzzSharp.props" /> |
||||
|
<Import Project="..\..\..\build\NullableEnable.props" /> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" /> |
||||
|
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<Content Include="*.props"> |
||||
|
<Pack>true</Pack> |
||||
|
<PackagePath>build\</PackagePath> |
||||
|
</Content> |
||||
|
<Content Include="*.targets"> |
||||
|
<Pack>true</Pack> |
||||
|
<PackagePath>build\;buildTransitive\</PackagePath> |
||||
|
</Content> |
||||
|
<Content Include="interop.js"> |
||||
|
<Pack>true</Pack> |
||||
|
<PackagePath>build/interop.js;buildTransitive/interop.js</PackagePath> |
||||
|
</Content> |
||||
|
<Content Include="wwwroot/**/*.*"> |
||||
|
<Pack>true</Pack> |
||||
|
<PackagePath>build\wwwroot;buildTransitive\wwwroot</PackagePath> |
||||
|
</Content> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<Folder Include="wwwroot\" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<Target Name="NpmInstall" Inputs="webapp/package.json" Outputs="webapp/node_modules/.install-stamp"> |
||||
|
<Exec Command="npm install" WorkingDirectory="webapp" /> |
||||
|
<!-- Write the stamp file, so incremental builds work --> |
||||
|
<Touch Files="webapp/node_modules/.install-stamp" AlwaysCreate="true" /> |
||||
|
</Target> |
||||
|
<Target Name="NpmRunBuild" DependsOnTargets="NpmInstall" BeforeTargets="BeforeBuild"> |
||||
|
<Exec Command="npm run build" WorkingDirectory="webapp" /> |
||||
|
</Target> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<Project> |
||||
|
<PropertyGroup> |
||||
|
<EmccExtraLDFlags>$(EmccExtraLDFlags) --js-library="$(MSBuildThisFileDirectory)\interop.js"</EmccExtraLDFlags> |
||||
|
</PropertyGroup> |
||||
|
</Project> |
||||
@ -0,0 +1,7 @@ |
|||||
|
<Project> |
||||
|
<ItemGroup> |
||||
|
<WasmExtraFilesToDeploy Include="$(MSBuildThisFileDirectory)/wwwroot/**/*.*" /> |
||||
|
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\3.1.7\libHarfBuzzSharp.a" /> |
||||
|
<NativeFileReference Include="$(SkiaSharpStaticLibraryPath)\3.1.7\libSkiaSharp.a" /> |
||||
|
</ItemGroup> |
||||
|
</Project> |
||||
@ -0,0 +1,451 @@ |
|||||
|
using System; |
||||
|
using System.Runtime.InteropServices.JavaScript; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Controls.Embedding; |
||||
|
using Avalonia.Controls.Platform; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Input.Raw; |
||||
|
using Avalonia.Input.TextInput; |
||||
|
using Avalonia.Rendering.Composition; |
||||
|
using Avalonia.Threading; |
||||
|
using Avalonia.Web.Interop; |
||||
|
using Avalonia.Web.Skia; |
||||
|
|
||||
|
using SkiaSharp; |
||||
|
|
||||
|
namespace Avalonia.Web |
||||
|
{ |
||||
|
[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings
|
||||
|
public partial class AvaloniaView : ITextInputMethodImpl |
||||
|
{ |
||||
|
private readonly BrowserTopLevelImpl _topLevelImpl; |
||||
|
private EmbeddableControlRoot _topLevel; |
||||
|
|
||||
|
private readonly JSObject _containerElement; |
||||
|
private readonly JSObject _canvas; |
||||
|
private readonly JSObject _nativeControlsContainer; |
||||
|
private readonly JSObject _inputElement; |
||||
|
private readonly JSObject? _splash; |
||||
|
|
||||
|
private GLInfo? _jsGlInfo = null; |
||||
|
private double _dpi = 1; |
||||
|
private Size _canvasSize = new(100.0, 100.0); |
||||
|
|
||||
|
private GRContext? _context; |
||||
|
private GRGlInterface? _glInterface; |
||||
|
private const SKColorType ColorType = SKColorType.Rgba8888; |
||||
|
|
||||
|
private bool _useGL; |
||||
|
private ITextInputMethodClient? _client; |
||||
|
private static int _canvasCount; |
||||
|
|
||||
|
public AvaloniaView(string divId) |
||||
|
{ |
||||
|
var host = DomHelper.GetElementById(divId); |
||||
|
if (host == null) |
||||
|
{ |
||||
|
throw new Exception($"Element with id {divId} was not found in the html document."); |
||||
|
} |
||||
|
|
||||
|
var hostContent = DomHelper.CreateAvaloniaHost(host); |
||||
|
if (hostContent == null) |
||||
|
{ |
||||
|
throw new InvalidOperationException("Avalonia WASM host wasn't initialized."); |
||||
|
} |
||||
|
|
||||
|
_containerElement = hostContent.GetPropertyAsJSObject("host") |
||||
|
?? throw new InvalidOperationException("Host cannot be null"); |
||||
|
_canvas = hostContent.GetPropertyAsJSObject("canvas") |
||||
|
?? throw new InvalidOperationException("Canvas cannot be null"); |
||||
|
_nativeControlsContainer = hostContent.GetPropertyAsJSObject("nativeHost") |
||||
|
?? throw new InvalidOperationException("NativeHost cannot be null"); |
||||
|
_inputElement = hostContent.GetPropertyAsJSObject("inputElement") |
||||
|
?? throw new InvalidOperationException("InputElement cannot be null"); |
||||
|
|
||||
|
_splash = DomHelper.GetElementById("avalonia-splash"); |
||||
|
|
||||
|
_canvas.SetProperty("id", $"avaloniaCanvas{_canvasCount++}"); |
||||
|
|
||||
|
_topLevelImpl = new BrowserTopLevelImpl(this); |
||||
|
|
||||
|
_topLevel = new WebEmbeddableControlRoot(_topLevelImpl, () => |
||||
|
{ |
||||
|
Dispatcher.UIThread.Post(() => |
||||
|
{ |
||||
|
if (_splash != null) |
||||
|
{ |
||||
|
DomHelper.AddCssClass(_splash, "splash-close"); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
_topLevelImpl.SetCssCursor = (cursor) => |
||||
|
{ |
||||
|
InputHelper.SetCursor(_containerElement, cursor); // macOS
|
||||
|
InputHelper.SetCursor(_canvas, cursor); // windows
|
||||
|
}; |
||||
|
|
||||
|
_topLevel.Prepare(); |
||||
|
|
||||
|
_topLevel.Renderer.Start(); |
||||
|
|
||||
|
InputHelper.SubscribeKeyEvents( |
||||
|
_containerElement, |
||||
|
OnKeyDown, |
||||
|
OnKeyUp); |
||||
|
|
||||
|
InputHelper.SubscribeTextEvents( |
||||
|
_inputElement, |
||||
|
OnTextInput, |
||||
|
OnCompositionStart, |
||||
|
OnCompositionUpdate, |
||||
|
OnCompositionEnd); |
||||
|
|
||||
|
InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp, OnWheel); |
||||
|
|
||||
|
var skiaOptions = AvaloniaLocator.Current.GetService<SkiaOptions>(); |
||||
|
|
||||
|
_dpi = DomHelper.ObserveDpi(OnDpiChanged); |
||||
|
|
||||
|
_useGL = skiaOptions?.CustomGpuFactory != null; |
||||
|
|
||||
|
if (_useGL) |
||||
|
{ |
||||
|
_jsGlInfo = CanvasHelper.InitialiseGL(_canvas, OnRenderFrame); |
||||
|
// create the SkiaSharp context
|
||||
|
if (_context == null) |
||||
|
{ |
||||
|
_glInterface = GRGlInterface.Create(); |
||||
|
_context = GRContext.CreateGl(_glInterface); |
||||
|
|
||||
|
// bump the default resource cache limit
|
||||
|
_context.SetResourceCacheLimit(skiaOptions?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024); |
||||
|
} |
||||
|
|
||||
|
_topLevelImpl.Surfaces = new[] { new BrowserSkiaSurface(_context, _jsGlInfo, ColorType, new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, GRSurfaceOrigin.BottomLeft) }; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
//var rasterInitialized = _interop.InitRaster();
|
||||
|
//Console.WriteLine("raster initialized: {0}", rasterInitialized);
|
||||
|
|
||||
|
//_topLevelImpl.SetSurface(ColorType,
|
||||
|
// new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData);
|
||||
|
} |
||||
|
|
||||
|
CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); |
||||
|
|
||||
|
_topLevelImpl.SetClientSize(_canvasSize, _dpi); |
||||
|
|
||||
|
DomHelper.ObserveSize(host, divId, OnSizeChanged); |
||||
|
|
||||
|
CanvasHelper.RequestAnimationFrame(_canvas, true); |
||||
|
} |
||||
|
|
||||
|
private static RawPointerPoint ExtractRawPointerFromJSArgs(JSObject args) |
||||
|
{ |
||||
|
var point = new RawPointerPoint |
||||
|
{ |
||||
|
Position = new Point(args.GetPropertyAsDouble("offsetX"), args.GetPropertyAsDouble("offsetY")), |
||||
|
Pressure = (float)args.GetPropertyAsDouble("pressure"), |
||||
|
XTilt = (float)args.GetPropertyAsDouble("tiltX"), |
||||
|
YTilt = (float)args.GetPropertyAsDouble("tiltY"), |
||||
|
Twist = (float)args.GetPropertyAsDouble("twist") |
||||
|
}; |
||||
|
|
||||
|
return point; |
||||
|
} |
||||
|
|
||||
|
private bool OnPointerMove(JSObject args) |
||||
|
{ |
||||
|
var type = args.GetPropertyAsString("pointertype"); |
||||
|
|
||||
|
var point = ExtractRawPointerFromJSArgs(args); |
||||
|
|
||||
|
return _topLevelImpl.RawPointerEvent(RawPointerEventType.Move, type!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); |
||||
|
} |
||||
|
|
||||
|
private bool OnPointerDown(JSObject args) |
||||
|
{ |
||||
|
var pointerType = args.GetPropertyAsString("pointerType"); |
||||
|
|
||||
|
var type = pointerType switch |
||||
|
{ |
||||
|
"touch" => RawPointerEventType.TouchBegin, |
||||
|
_ => args.GetPropertyAsInt32("button") switch |
||||
|
{ |
||||
|
0 => RawPointerEventType.LeftButtonDown, |
||||
|
1 => RawPointerEventType.MiddleButtonDown, |
||||
|
2 => RawPointerEventType.RightButtonDown, |
||||
|
3 => RawPointerEventType.XButton1Down, |
||||
|
4 => RawPointerEventType.XButton2Down, |
||||
|
// 5 => Pen eraser button,
|
||||
|
_ => RawPointerEventType.Move |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
var point = ExtractRawPointerFromJSArgs(args); |
||||
|
|
||||
|
return _topLevelImpl.RawPointerEvent(type, pointerType!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); |
||||
|
} |
||||
|
|
||||
|
private bool OnPointerUp(JSObject args) |
||||
|
{ |
||||
|
var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse"; |
||||
|
|
||||
|
var type = pointerType switch |
||||
|
{ |
||||
|
"touch" => RawPointerEventType.TouchEnd, |
||||
|
_ => args.GetPropertyAsInt32("button") switch |
||||
|
{ |
||||
|
0 => RawPointerEventType.LeftButtonUp, |
||||
|
1 => RawPointerEventType.MiddleButtonUp, |
||||
|
2 => RawPointerEventType.RightButtonUp, |
||||
|
3 => RawPointerEventType.XButton1Up, |
||||
|
4 => RawPointerEventType.XButton2Up, |
||||
|
// 5 => Pen eraser button,
|
||||
|
_ => RawPointerEventType.Move |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
var point = ExtractRawPointerFromJSArgs(args); |
||||
|
|
||||
|
return _topLevelImpl.RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); |
||||
|
} |
||||
|
|
||||
|
private bool OnWheel(JSObject args) |
||||
|
{ |
||||
|
return _topLevelImpl.RawMouseWheelEvent(new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), |
||||
|
new Vector(-(args.GetPropertyAsDouble("deltaX") / 50), -(args.GetPropertyAsDouble("deltaY") / 50)), GetModifiers(args)); |
||||
|
} |
||||
|
|
||||
|
private static RawInputModifiers GetModifiers(JSObject e) |
||||
|
{ |
||||
|
var modifiers = RawInputModifiers.None; |
||||
|
|
||||
|
if (e.GetPropertyAsBoolean("ctrlKey")) |
||||
|
modifiers |= RawInputModifiers.Control; |
||||
|
if (e.GetPropertyAsBoolean("altKey")) |
||||
|
modifiers |= RawInputModifiers.Alt; |
||||
|
if (e.GetPropertyAsBoolean("shiftKey")) |
||||
|
modifiers |= RawInputModifiers.Shift; |
||||
|
if (e.GetPropertyAsBoolean("metaKey")) |
||||
|
modifiers |= RawInputModifiers.Meta; |
||||
|
|
||||
|
var buttons = e.GetPropertyAsInt32("buttons"); |
||||
|
if ((buttons & 1L) == 1) |
||||
|
modifiers |= RawInputModifiers.LeftMouseButton; |
||||
|
|
||||
|
if ((buttons & 2L) == 2) |
||||
|
modifiers |= e.GetPropertyAsString("type") == "pen" ? RawInputModifiers.PenBarrelButton : RawInputModifiers.RightMouseButton; |
||||
|
|
||||
|
if ((buttons & 4L) == 4) |
||||
|
modifiers |= RawInputModifiers.MiddleMouseButton; |
||||
|
|
||||
|
if ((buttons & 8L) == 8) |
||||
|
modifiers |= RawInputModifiers.XButton1MouseButton; |
||||
|
|
||||
|
if ((buttons & 16L) == 16) |
||||
|
modifiers |= RawInputModifiers.XButton2MouseButton; |
||||
|
|
||||
|
if ((buttons & 32L) == 32) |
||||
|
modifiers |= RawInputModifiers.PenEraser; |
||||
|
|
||||
|
return modifiers; |
||||
|
} |
||||
|
|
||||
|
private bool OnKeyDown (string code, string key, int modifier) |
||||
|
{ |
||||
|
return _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier); |
||||
|
} |
||||
|
|
||||
|
private bool OnKeyUp(string code, string key, int modifier) |
||||
|
{ |
||||
|
return _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)modifier); |
||||
|
} |
||||
|
|
||||
|
private bool OnTextInput (string type, string? data) |
||||
|
{ |
||||
|
if(data == null || IsComposing) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return _topLevelImpl.RawTextEvent(data); |
||||
|
} |
||||
|
|
||||
|
private bool OnCompositionStart (JSObject args) |
||||
|
{ |
||||
|
if (_client == null) |
||||
|
return false; |
||||
|
|
||||
|
_client.SetPreeditText(null); |
||||
|
IsComposing = true; |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private bool OnCompositionUpdate(JSObject args) |
||||
|
{ |
||||
|
if (_client == null) |
||||
|
return false; |
||||
|
|
||||
|
_client.SetPreeditText(args.GetPropertyAsString("data")); |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private bool OnCompositionEnd(JSObject args) |
||||
|
{ |
||||
|
if (_client == null) |
||||
|
return false; |
||||
|
|
||||
|
IsComposing = false; |
||||
|
_client.SetPreeditText(null); |
||||
|
_topLevelImpl.RawTextEvent(args.GetPropertyAsString("data")!); |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private void OnRenderFrame() |
||||
|
{ |
||||
|
if (_useGL && (_jsGlInfo == null)) |
||||
|
{ |
||||
|
Console.WriteLine("nothing to render"); |
||||
|
return; |
||||
|
} |
||||
|
if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0) |
||||
|
{ |
||||
|
Console.WriteLine("nothing to render"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
ManualTriggerRenderTimer.Instance.RaiseTick(); |
||||
|
} |
||||
|
|
||||
|
public Control? Content |
||||
|
{ |
||||
|
get => (Control)_topLevel.Content!; |
||||
|
set => _topLevel.Content = value; |
||||
|
} |
||||
|
|
||||
|
public bool IsComposing { get; private set; } |
||||
|
|
||||
|
internal INativeControlHostImpl GetNativeControlHostImpl() |
||||
|
{ |
||||
|
return new BrowserNativeControlHost(_nativeControlsContainer); |
||||
|
} |
||||
|
|
||||
|
private void ForceBlit() |
||||
|
{ |
||||
|
// Note: this is technically a hack, but it's a kinda unique use case when
|
||||
|
// we want to blit the previous frame
|
||||
|
// renderer doesn't have much control over the render target
|
||||
|
// we render on the UI thread
|
||||
|
// We also don't want to have it as a meaningful public API.
|
||||
|
// Therefore we have InternalsVisibleTo hack here.
|
||||
|
|
||||
|
if (_topLevel.Renderer is CompositingRenderer dr) |
||||
|
{ |
||||
|
dr.CompositionTarget.ImmediateUIThreadRender(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void OnDpiChanged(double oldDpi, double newDpi) |
||||
|
{ |
||||
|
if (Math.Abs(_dpi - newDpi) > 0.0001) |
||||
|
{ |
||||
|
_dpi = newDpi; |
||||
|
|
||||
|
CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); |
||||
|
|
||||
|
_topLevelImpl.SetClientSize(_canvasSize, _dpi); |
||||
|
|
||||
|
ForceBlit(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void OnSizeChanged(int height, int width) |
||||
|
{ |
||||
|
var newSize = new Size(height, width); |
||||
|
|
||||
|
if (_canvasSize != newSize) |
||||
|
{ |
||||
|
_canvasSize = newSize; |
||||
|
|
||||
|
CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); |
||||
|
|
||||
|
_topLevelImpl.SetClientSize(_canvasSize, _dpi); |
||||
|
|
||||
|
ForceBlit(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void HideIme() |
||||
|
{ |
||||
|
InputHelper.HideElement(_inputElement); |
||||
|
InputHelper.FocusElement(_containerElement); |
||||
|
} |
||||
|
|
||||
|
public void SetClient(ITextInputMethodClient? client) |
||||
|
{ |
||||
|
Console.WriteLine("Set Client"); |
||||
|
if (_client != null) |
||||
|
{ |
||||
|
_client.SurroundingTextChanged -= SurroundingTextChanged; |
||||
|
} |
||||
|
|
||||
|
if (client != null) |
||||
|
{ |
||||
|
client.SurroundingTextChanged += SurroundingTextChanged; |
||||
|
} |
||||
|
|
||||
|
InputHelper.ClearInputElement(_inputElement); |
||||
|
|
||||
|
_client = client; |
||||
|
|
||||
|
if (_client != null) |
||||
|
{ |
||||
|
InputHelper.ShowElement(_inputElement); |
||||
|
InputHelper.FocusElement(_inputElement); |
||||
|
|
||||
|
var surroundingText = _client.SurroundingText; |
||||
|
|
||||
|
InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset); |
||||
|
|
||||
|
Console.WriteLine("Shown, focused and surrounded."); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
HideIme(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void SurroundingTextChanged(object? sender, EventArgs e) |
||||
|
{ |
||||
|
if (_client != null) |
||||
|
{ |
||||
|
var surroundingText = _client.SurroundingText; |
||||
|
|
||||
|
InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void SetCursorRect(Rect rect) |
||||
|
{ |
||||
|
InputHelper.FocusElement(_inputElement); |
||||
|
InputHelper.SetBounds(_inputElement, (int)rect.X, (int)rect.Y, (int)rect.Width, (int)rect.Height, _client?.SurroundingText.CursorOffset ?? 0); |
||||
|
InputHelper.FocusElement(_inputElement); |
||||
|
} |
||||
|
|
||||
|
public void SetOptions(TextInputOptions options) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public void Reset() |
||||
|
{ |
||||
|
InputHelper.ClearInputElement(_inputElement); |
||||
|
InputHelper.SetSurroundingText(_inputElement, "", 0, 0); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,136 @@ |
|||||
|
using System; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using System.Runtime.InteropServices.JavaScript; |
||||
|
|
||||
|
using Avalonia.Controls.Platform; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Web.Interop; |
||||
|
|
||||
|
namespace Avalonia.Web |
||||
|
{ |
||||
|
internal class BrowserNativeControlHost : INativeControlHostImpl |
||||
|
{ |
||||
|
private readonly JSObject _hostElement; |
||||
|
|
||||
|
public BrowserNativeControlHost(JSObject element) |
||||
|
{ |
||||
|
_hostElement = element; |
||||
|
} |
||||
|
|
||||
|
public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent) |
||||
|
{ |
||||
|
var element = NativeControlHostHelper.CreateDefaultChild(null); |
||||
|
return new JSObjectControlHandle(element); |
||||
|
} |
||||
|
|
||||
|
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func<IPlatformHandle, IPlatformHandle> create) |
||||
|
{ |
||||
|
Attachment? a = null; |
||||
|
try |
||||
|
{ |
||||
|
var child = create(new JSObjectControlHandle(_hostElement)); |
||||
|
var attachmenetReference = NativeControlHostHelper.CreateAttachment(); |
||||
|
// It has to be assigned to the variable before property setter is called so we dispose it on exception
|
||||
|
#pragma warning disable IDE0017 // Simplify object initialization
|
||||
|
a = new Attachment(attachmenetReference, child); |
||||
|
#pragma warning restore IDE0017 // Simplify object initialization
|
||||
|
a.AttachedTo = this; |
||||
|
return a; |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
a?.Dispose(); |
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle) |
||||
|
{ |
||||
|
var attachmenetReference = NativeControlHostHelper.CreateAttachment(); |
||||
|
var a = new Attachment(attachmenetReference, handle); |
||||
|
a.AttachedTo = this; |
||||
|
return a; |
||||
|
} |
||||
|
|
||||
|
public bool IsCompatibleWith(IPlatformHandle handle) => handle is JSObjectControlHandle; |
||||
|
|
||||
|
private class Attachment : INativeControlHostControlTopLevelAttachment |
||||
|
{ |
||||
|
private const string InitializeWithChildHandleSymbol = "InitializeWithChildHandle"; |
||||
|
private const string AttachToSymbol = "AttachTo"; |
||||
|
private const string ShowInBoundsSymbol = "ShowInBounds"; |
||||
|
private const string HideWithSizeSymbol = "HideWithSize"; |
||||
|
private const string ReleaseChildSymbol = "ReleaseChild"; |
||||
|
|
||||
|
private JSObject? _native; |
||||
|
private BrowserNativeControlHost? _attachedTo; |
||||
|
|
||||
|
public Attachment(JSObject native, IPlatformHandle handle) |
||||
|
{ |
||||
|
_native = native; |
||||
|
NativeControlHostHelper.InitializeWithChildHandle(_native, ((JSObjectControlHandle)handle).Object); |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
if (_native != null) |
||||
|
{ |
||||
|
NativeControlHostHelper.ReleaseChild(_native); |
||||
|
_native.Dispose(); |
||||
|
_native = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public INativeControlHostImpl? AttachedTo |
||||
|
{ |
||||
|
get => _attachedTo!; |
||||
|
set |
||||
|
{ |
||||
|
CheckDisposed(); |
||||
|
|
||||
|
var host = (BrowserNativeControlHost?)value; |
||||
|
if (host == null) |
||||
|
{ |
||||
|
NativeControlHostHelper.AttachTo(_native, null); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
NativeControlHostHelper.AttachTo(_native, host._hostElement); |
||||
|
} |
||||
|
_attachedTo = host; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public bool IsCompatibleWith(INativeControlHostImpl host) => host is BrowserNativeControlHost; |
||||
|
|
||||
|
public void HideWithSize(Size size) |
||||
|
{ |
||||
|
CheckDisposed(); |
||||
|
if (_attachedTo == null) |
||||
|
return; |
||||
|
|
||||
|
NativeControlHostHelper.HideWithSize(_native, Math.Max(1, size.Width), Math.Max(1, size.Height)); |
||||
|
} |
||||
|
|
||||
|
public void ShowInBounds(Rect bounds) |
||||
|
{ |
||||
|
CheckDisposed(); |
||||
|
|
||||
|
if (_attachedTo == null) |
||||
|
throw new InvalidOperationException("Native control isn't attached to a toplevel"); |
||||
|
|
||||
|
bounds = new Rect(bounds.X, bounds.Y, Math.Max(1, bounds.Width), |
||||
|
Math.Max(1, bounds.Height)); |
||||
|
|
||||
|
NativeControlHostHelper.ShowInBounds(_native, bounds.X, bounds.Y, bounds.Width, bounds.Height); |
||||
|
} |
||||
|
|
||||
|
[MemberNotNull(nameof(_native))] |
||||
|
private void CheckDisposed() |
||||
|
{ |
||||
|
if (_native == null) |
||||
|
throw new ObjectDisposedException(nameof(Attachment)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
using System.Runtime.InteropServices.JavaScript; |
||||
|
using System; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Controls.ApplicationLifetimes; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Web.Skia; |
||||
|
|
||||
|
namespace Avalonia.Web |
||||
|
{ |
||||
|
[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings
|
||||
|
public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime |
||||
|
{ |
||||
|
public AvaloniaView? View; |
||||
|
|
||||
|
public Control? MainView |
||||
|
{ |
||||
|
get => View!.Content; |
||||
|
set => View!.Content = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static partial class WebAppBuilder |
||||
|
{ |
||||
|
public static T SetupBrowserApp<T>( |
||||
|
this T builder, string mainDivId) |
||||
|
where T : AppBuilderBase<T>, new() |
||||
|
{ |
||||
|
var lifetime = new BrowserSingleViewLifetime(); |
||||
|
|
||||
|
return builder |
||||
|
.UseWindowingSubsystem(BrowserWindowingPlatform.Register) |
||||
|
.UseSkia() |
||||
|
.With(new SkiaOptions { CustomGpuFactory = () => new BrowserSkiaGpu() }) |
||||
|
.AfterSetup(b => |
||||
|
{ |
||||
|
lifetime.View = new AvaloniaView(mainDivId); |
||||
|
}) |
||||
|
.SetupWithLifetime(lifetime); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,226 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Controls.Platform; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Input.Raw; |
||||
|
using Avalonia.Input.TextInput; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Platform.Storage; |
||||
|
using Avalonia.Rendering; |
||||
|
using Avalonia.Rendering.Composition; |
||||
|
using Avalonia.Web.Skia; |
||||
|
using Avalonia.Web.Storage; |
||||
|
|
||||
|
namespace Avalonia.Web |
||||
|
{ |
||||
|
[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings
|
||||
|
internal class BrowserTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider |
||||
|
{ |
||||
|
private Size _clientSize; |
||||
|
private IInputRoot? _inputRoot; |
||||
|
private readonly Stopwatch _sw = Stopwatch.StartNew(); |
||||
|
private readonly AvaloniaView _avaloniaView; |
||||
|
private readonly TouchDevice _touchDevice; |
||||
|
private readonly PenDevice _penDevice; |
||||
|
private string _currentCursor = CssCursor.Default; |
||||
|
|
||||
|
public BrowserTopLevelImpl(AvaloniaView avaloniaView) |
||||
|
{ |
||||
|
Surfaces = Enumerable.Empty<object>(); |
||||
|
_avaloniaView = avaloniaView; |
||||
|
TransparencyLevel = WindowTransparencyLevel.None; |
||||
|
AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1); |
||||
|
_touchDevice = new TouchDevice(); |
||||
|
_penDevice = new PenDevice(); |
||||
|
NativeControlHost = _avaloniaView.GetNativeControlHostImpl(); |
||||
|
} |
||||
|
|
||||
|
public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds; |
||||
|
|
||||
|
public void SetClientSize(Size newSize, double dpi) |
||||
|
{ |
||||
|
if (Math.Abs(RenderScaling - dpi) > 0.0001) |
||||
|
{ |
||||
|
if (Surfaces.FirstOrDefault() is BrowserSkiaSurface surface) |
||||
|
{ |
||||
|
surface.Scaling = dpi; |
||||
|
} |
||||
|
|
||||
|
ScalingChanged?.Invoke(dpi); |
||||
|
} |
||||
|
|
||||
|
if (newSize != _clientSize) |
||||
|
{ |
||||
|
_clientSize = newSize; |
||||
|
|
||||
|
if (Surfaces.FirstOrDefault() is BrowserSkiaSurface surface) |
||||
|
{ |
||||
|
surface.Size = new PixelSize((int)newSize.Width, (int)newSize.Height); |
||||
|
} |
||||
|
|
||||
|
Resized?.Invoke(newSize, PlatformResizeReason.User); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public bool RawPointerEvent( |
||||
|
RawPointerEventType eventType, string pointerType, |
||||
|
RawPointerPoint p, RawInputModifiers modifiers, long touchPointId) |
||||
|
{ |
||||
|
if (_inputRoot is { } |
||||
|
&& Input is { } input) |
||||
|
{ |
||||
|
var device = GetPointerDevice(pointerType); |
||||
|
var args = device is TouchDevice ? |
||||
|
new RawTouchEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers, touchPointId) : |
||||
|
new RawPointerEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers) |
||||
|
{ |
||||
|
RawPointerId = touchPointId |
||||
|
}; |
||||
|
|
||||
|
input.Invoke(args); |
||||
|
|
||||
|
return args.Handled; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private IPointerDevice GetPointerDevice(string pointerType) |
||||
|
{ |
||||
|
return pointerType switch |
||||
|
{ |
||||
|
"touch" => _touchDevice, |
||||
|
"pen" => _penDevice, |
||||
|
_ => MouseDevice |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public bool RawMouseWheelEvent(Point p, Vector v, RawInputModifiers modifiers) |
||||
|
{ |
||||
|
if (_inputRoot is { }) |
||||
|
{ |
||||
|
var args = new RawMouseWheelEventArgs(MouseDevice, Timestamp, _inputRoot, p, v, modifiers); |
||||
|
|
||||
|
Input?.Invoke(args); |
||||
|
|
||||
|
return args.Handled; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public bool RawKeyboardEvent(RawKeyEventType type, string code, string key, RawInputModifiers modifiers) |
||||
|
{ |
||||
|
if (Keycodes.KeyCodes.TryGetValue(code, out var avkey)) |
||||
|
{ |
||||
|
if (_inputRoot is { }) |
||||
|
{ |
||||
|
var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers); |
||||
|
|
||||
|
Input?.Invoke(args); |
||||
|
|
||||
|
return args.Handled; |
||||
|
} |
||||
|
} |
||||
|
else if (Keycodes.KeyCodes.TryGetValue(key, out avkey)) |
||||
|
{ |
||||
|
if (_inputRoot is { }) |
||||
|
{ |
||||
|
var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers); |
||||
|
|
||||
|
Input?.Invoke(args); |
||||
|
|
||||
|
return args.Handled; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public bool RawTextEvent(string text) |
||||
|
{ |
||||
|
if (_inputRoot is { }) |
||||
|
{ |
||||
|
var args = new RawTextInputEventArgs(KeyboardDevice, Timestamp, _inputRoot, text); |
||||
|
Input?.Invoke(args); |
||||
|
|
||||
|
return args.Handled; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public IRenderer CreateRenderer(IRenderRoot root) |
||||
|
{ |
||||
|
var loop = AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(); |
||||
|
return new CompositingRenderer(root, new Compositor(loop, null)); |
||||
|
} |
||||
|
|
||||
|
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) |
||||
|
{ |
||||
|
var val = (cursor as CssCursor)?.Value ?? CssCursor.Default; |
||||
|
if (_currentCursor != val) |
||||
|
{ |
||||
|
SetCssCursor?.Invoke(val); |
||||
|
_currentCursor = val; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public IPopupImpl? CreatePopup() |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public Size ClientSize => _clientSize; |
||||
|
public Size? FrameSize => null; |
||||
|
public double RenderScaling => (Surfaces.FirstOrDefault() as BrowserSkiaSurface)?.Scaling ?? 1; |
||||
|
|
||||
|
public IEnumerable<object> Surfaces { get; set; } |
||||
|
|
||||
|
public Action<string>? SetCssCursor { get; set; } |
||||
|
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; } = BrowserWindowingPlatform.Keyboard; |
||||
|
public WindowTransparencyLevel TransparencyLevel { get; } |
||||
|
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } |
||||
|
|
||||
|
public ITextInputMethodImpl TextInputMethod => _avaloniaView; |
||||
|
|
||||
|
public INativeControlHostImpl? NativeControlHost { get; } |
||||
|
public IStorageProvider StorageProvider { get; } = new BrowserStorageProvider(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Input.Platform; |
||||
|
using Avalonia.Web.Interop; |
||||
|
|
||||
|
namespace Avalonia.Web |
||||
|
{ |
||||
|
internal class ClipboardImpl : IClipboard |
||||
|
{ |
||||
|
public Task<string> GetTextAsync() |
||||
|
{ |
||||
|
return InputHelper.ReadClipboardTextAsync(); |
||||
|
} |
||||
|
|
||||
|
public Task SetTextAsync(string text) |
||||
|
{ |
||||
|
return InputHelper.WriteClipboardTextAsync(text); |
||||
|
} |
||||
|
|
||||
|
public async Task ClearAsync() => await SetTextAsync(""); |
||||
|
|
||||
|
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()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,95 @@ |
|||||
|
using System; |
||||
|
using System.IO; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.Web |
||||
|
{ |
||||
|
public class CssCursor : ICursorImpl |
||||
|
{ |
||||
|
public const string Default = "default"; |
||||
|
public string? Value { get; set; } |
||||
|
|
||||
|
public CssCursor(StandardCursorType type) |
||||
|
{ |
||||
|
Value = ToKeyword(type); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Create a cursor from base64 image
|
||||
|
/// </summary>
|
||||
|
public CssCursor(string base64, string format, PixelPoint hotspot, StandardCursorType fallback) |
||||
|
{ |
||||
|
Value = $"url(\"data:image/{format};base64,{base64}\") {hotspot.X} {hotspot.Y}, {ToKeyword(fallback)}"; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Create a cursor from url to *.cur file.
|
||||
|
/// </summary>
|
||||
|
public CssCursor(string url, StandardCursorType fallback) |
||||
|
{ |
||||
|
Value = $"url('{url}'), {ToKeyword(fallback)}"; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Create a cursor from png/svg and hotspot position
|
||||
|
/// </summary>
|
||||
|
public CssCursor(string url, PixelPoint hotSpot, StandardCursorType fallback) |
||||
|
{ |
||||
|
Value = $"url('{url}') {hotSpot.X} {hotSpot.Y}, {ToKeyword(fallback)}"; |
||||
|
} |
||||
|
|
||||
|
private static string ToKeyword(StandardCursorType type) => type switch |
||||
|
{ |
||||
|
StandardCursorType.Hand => "pointer", |
||||
|
StandardCursorType.Cross => "crosshair", |
||||
|
StandardCursorType.Help => "help", |
||||
|
StandardCursorType.Ibeam => "text", |
||||
|
StandardCursorType.No => "not-allowed", |
||||
|
StandardCursorType.None => "none", |
||||
|
StandardCursorType.Wait => "progress", |
||||
|
StandardCursorType.AppStarting => "wait", |
||||
|
|
||||
|
StandardCursorType.DragMove => "move", |
||||
|
StandardCursorType.DragCopy => "copy", |
||||
|
StandardCursorType.DragLink => "alias", |
||||
|
|
||||
|
StandardCursorType.UpArrow => "default",/*not found matching one*/ |
||||
|
StandardCursorType.SizeWestEast => "ew-resize", |
||||
|
StandardCursorType.SizeNorthSouth => "ns-resize", |
||||
|
StandardCursorType.SizeAll => "move", |
||||
|
|
||||
|
StandardCursorType.TopSide => "n-resize", |
||||
|
StandardCursorType.BottomSide => "s-resize", |
||||
|
StandardCursorType.LeftSide => "w-resize", |
||||
|
StandardCursorType.RightSide => "e-resize", |
||||
|
StandardCursorType.TopLeftCorner => "nw-resize", |
||||
|
StandardCursorType.TopRightCorner => "ne-resize", |
||||
|
StandardCursorType.BottomLeftCorner => "sw-resize", |
||||
|
StandardCursorType.BottomRightCorner => "se-resize", |
||||
|
|
||||
|
_ => Default, |
||||
|
}; |
||||
|
|
||||
|
public void Dispose() {} |
||||
|
} |
||||
|
|
||||
|
internal class CssCursorFactory : ICursorFactory |
||||
|
{ |
||||
|
public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) |
||||
|
{ |
||||
|
using var imageStream = new MemoryStream(); |
||||
|
cursor.Save(imageStream); |
||||
|
|
||||
|
//not memory optimized because CryptoStream with ToBase64Transform is not supported in the browser.
|
||||
|
var base64String = Convert.ToBase64String(imageStream.ToArray()); |
||||
|
return new CssCursor(base64String, "png", hotSpot, StandardCursorType.Arrow); |
||||
|
} |
||||
|
|
||||
|
ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType) |
||||
|
{ |
||||
|
return new CssCursor(cursorType); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,43 @@ |
|||||
|
using System; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Runtime.InteropServices.JavaScript; |
||||
|
|
||||
|
namespace Avalonia.Web.Interop; |
||||
|
|
||||
|
internal record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth); |
||||
|
|
||||
|
[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings
|
||||
|
internal static partial class CanvasHelper |
||||
|
{ |
||||
|
|
||||
|
[DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] |
||||
|
static extern JSObject InterceptGLObject(); |
||||
|
|
||||
|
public static GLInfo InitialiseGL(JSObject canvas, Action renderFrameCallback) |
||||
|
{ |
||||
|
InterceptGLObject(); |
||||
|
|
||||
|
var info = InitGL(canvas, canvas.GetPropertyAsString("id")!, renderFrameCallback); |
||||
|
|
||||
|
var glInfo = new GLInfo( |
||||
|
info.GetPropertyAsInt32("context"), |
||||
|
(uint)info.GetPropertyAsInt32("fboId"), |
||||
|
info.GetPropertyAsInt32("stencil"), |
||||
|
info.GetPropertyAsInt32("sample"), |
||||
|
info.GetPropertyAsInt32("depth")); |
||||
|
|
||||
|
return glInfo; |
||||
|
} |
||||
|
|
||||
|
[JSImport("Canvas.requestAnimationFrame", "avalonia.ts")] |
||||
|
public static partial void RequestAnimationFrame(JSObject canvas, bool renderLoop); |
||||
|
|
||||
|
[JSImport("Canvas.setCanvasSize", "avalonia.ts")] |
||||
|
public static partial void SetCanvasSize(JSObject canvas, int height, int width); |
||||
|
|
||||
|
[JSImport("Canvas.initGL", "avalonia.ts")] |
||||
|
private static partial JSObject InitGL( |
||||
|
JSObject canvas, |
||||
|
string canvasId, |
||||
|
[JSMarshalAs<JSType.Function>] Action renderFrameCallback); |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
using System; |
||||
|
using System.Runtime.InteropServices.JavaScript; |
||||
|
|
||||
|
namespace Avalonia.Web.Interop; |
||||
|
|
||||
|
internal static partial class DomHelper |
||||
|
{ |
||||
|
[JSImport("globalThis.document.getElementById")] |
||||
|
internal static partial JSObject? GetElementById(string id); |
||||
|
|
||||
|
[JSImport("AvaloniaDOM.createAvaloniaHost", "avalonia.ts")] |
||||
|
public static partial JSObject CreateAvaloniaHost(JSObject element); |
||||
|
|
||||
|
[JSImport("AvaloniaDOM.addClass", "avalonia.ts")] |
||||
|
public static partial void AddCssClass(JSObject element, string className); |
||||
|
|
||||
|
[JSImport("SizeWatcher.observe", "avalonia.ts")] |
||||
|
public static partial JSObject ObserveSize( |
||||
|
JSObject canvas, |
||||
|
string canvasId, |
||||
|
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number>>] |
||||
|
Action<int, int> onSizeChanged); |
||||
|
|
||||
|
[JSImport("DpiWatcher.start", "avalonia.ts")] |
||||
|
public static partial double ObserveDpi( |
||||
|
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number>>] |
||||
|
Action<double, double> onDpiChanged); |
||||
|
} |
||||
@ -0,0 +1,78 @@ |
|||||
|
using System; |
||||
|
using System.Runtime.InteropServices.JavaScript; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Avalonia.Web.Interop; |
||||
|
|
||||
|
internal static partial class InputHelper |
||||
|
{ |
||||
|
[JSImport("InputHelper.subscribeKeyEvents", "avalonia.ts")] |
||||
|
public static partial void SubscribeKeyEvents( |
||||
|
JSObject htmlElement, |
||||
|
[JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.Number, JSType.Boolean>>] |
||||
|
Func<string, string, int, bool> keyDown, |
||||
|
[JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.Number, JSType.Boolean>>] |
||||
|
Func<string, string, int, bool> keyUp); |
||||
|
|
||||
|
[JSImport("InputHelper.subscribeTextEvents", "avalonia.ts")] |
||||
|
public static partial void SubscribeTextEvents( |
||||
|
JSObject htmlElement, |
||||
|
[JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.Boolean>>] |
||||
|
Func<string, string?, bool> onInput, |
||||
|
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>] |
||||
|
Func<JSObject, bool> onCompositionStart, |
||||
|
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>] |
||||
|
Func<JSObject, bool> onCompositionUpdate, |
||||
|
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>] |
||||
|
Func<JSObject, bool> onCompositionEnd); |
||||
|
|
||||
|
[JSImport("InputHelper.subscribePointerEvents", "avalonia.ts")] |
||||
|
public static partial void SubscribePointerEvents( |
||||
|
JSObject htmlElement, |
||||
|
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>] |
||||
|
Func<JSObject, bool> pointerMove, |
||||
|
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>] |
||||
|
Func<JSObject, bool> pointerDown, |
||||
|
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>] |
||||
|
Func<JSObject, bool> pointerUp, |
||||
|
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>] |
||||
|
Func<JSObject, bool> wheel); |
||||
|
|
||||
|
|
||||
|
[JSImport("InputHelper.subscribeInputEvents", "avalonia.ts")] |
||||
|
public static partial void SubscribeInputEvents( |
||||
|
JSObject htmlElement, |
||||
|
[JSMarshalAs<JSType.Function<JSType.String, JSType.Boolean>>] |
||||
|
Func<string, bool> input); |
||||
|
|
||||
|
|
||||
|
[JSImport("InputHelper.clearInput", "avalonia.ts")] |
||||
|
public static partial void ClearInputElement(JSObject htmlElement); |
||||
|
|
||||
|
[JSImport("InputHelper.isInputElement", "avalonia.ts")] |
||||
|
public static partial void IsInputElement(JSObject htmlElement); |
||||
|
|
||||
|
[JSImport("InputHelper.focusElement", "avalonia.ts")] |
||||
|
public static partial void FocusElement(JSObject htmlElement); |
||||
|
|
||||
|
[JSImport("InputHelper.setCursor", "avalonia.ts")] |
||||
|
public static partial void SetCursor(JSObject htmlElement, string kind); |
||||
|
|
||||
|
[JSImport("InputHelper.hide", "avalonia.ts")] |
||||
|
public static partial void HideElement(JSObject htmlElement); |
||||
|
|
||||
|
[JSImport("InputHelper.show", "avalonia.ts")] |
||||
|
public static partial void ShowElement(JSObject htmlElement); |
||||
|
|
||||
|
[JSImport("InputHelper.setSurroundingText", "avalonia.ts")] |
||||
|
public static partial void SetSurroundingText(JSObject htmlElement, string text, int start, int end); |
||||
|
|
||||
|
[JSImport("InputHelper.setBounds", "avalonia.ts")] |
||||
|
public static partial void SetBounds(JSObject htmlElement, int x, int y, int width, int height, int caret); |
||||
|
|
||||
|
[JSImport("globalThis.navigator.clipboard.readText")] |
||||
|
public static partial Task<string> ReadClipboardTextAsync(); |
||||
|
|
||||
|
[JSImport("globalThis.navigator.clipboard.writeText")] |
||||
|
public static partial Task WriteClipboardTextAsync(string text); |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
using System; |
||||
|
using System.Runtime.InteropServices.JavaScript; |
||||
|
|
||||
|
namespace Avalonia.Web.Interop; |
||||
|
|
||||
|
internal static partial class NativeControlHostHelper |
||||
|
{ |
||||
|
[JSImport("NativeControlHost.createDefaultChild", "avalonia.ts")] |
||||
|
internal static partial JSObject CreateDefaultChild(JSObject? parent); |
||||
|
|
||||
|
[JSImport("NativeControlHost.createAttachment", "avalonia.ts")] |
||||
|
internal static partial JSObject CreateAttachment(); |
||||
|
|
||||
|
[JSImport("NativeControlHost.initializeWithChildHandle", "avalonia.ts")] |
||||
|
internal static partial void InitializeWithChildHandle(JSObject element, JSObject child); |
||||
|
|
||||
|
[JSImport("NativeControlHost.attachTo", "avalonia.ts")] |
||||
|
internal static partial void AttachTo(JSObject element, JSObject? host); |
||||
|
|
||||
|
[JSImport("NativeControlHost.showInBounds", "avalonia.ts")] |
||||
|
internal static partial void ShowInBounds(JSObject element, double x, double y, double width, double height); |
||||
|
|
||||
|
[JSImport("NativeControlHost.hideWithSize", "avalonia.ts")] |
||||
|
internal static partial void HideWithSize(JSObject element, double width, double height); |
||||
|
|
||||
|
[JSImport("NativeControlHost.releaseChild", "avalonia.ts")] |
||||
|
internal static partial void ReleaseChild(JSObject element); |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
using System.Runtime.InteropServices.JavaScript; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Avalonia.Web.Interop; |
||||
|
|
||||
|
internal static partial class StorageHelper |
||||
|
{ |
||||
|
[JSImport("Caniuse.canShowOpenFilePicker", "avalonia.ts")] |
||||
|
public static partial bool CanShowOpenFilePicker(); |
||||
|
|
||||
|
[JSImport("Caniuse.canShowSaveFilePicker", "avalonia.ts")] |
||||
|
public static partial bool CanShowSaveFilePicker(); |
||||
|
|
||||
|
[JSImport("Caniuse.canShowDirectoryPicker", "avalonia.ts")] |
||||
|
public static partial bool CanShowDirectoryPicker(); |
||||
|
|
||||
|
[JSImport("StorageProvider.selectFolderDialog", "storage.ts")] |
||||
|
public static partial Task<JSObject?> SelectFolderDialog(JSObject? startIn); |
||||
|
|
||||
|
[JSImport("StorageProvider.openFileDialog", "storage.ts")] |
||||
|
public static partial Task<JSObject?> OpenFileDialog(JSObject? startIn, bool multiple, |
||||
|
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption); |
||||
|
|
||||
|
[JSImport("StorageProvider.saveFileDialog", "storage.ts")] |
||||
|
public static partial Task<JSObject?> SaveFileDialog(JSObject? startIn, string? suggestedName, |
||||
|
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption); |
||||
|
|
||||
|
[JSImport("StorageProvider.openBookmark", "storage.ts")] |
||||
|
public static partial Task<JSObject?> OpenBookmark(string key); |
||||
|
|
||||
|
[JSImport("StorageItem.saveBookmark", "storage.ts")] |
||||
|
public static partial Task<string?> SaveBookmark(JSObject item); |
||||
|
|
||||
|
[JSImport("StorageItem.deleteBookmark", "storage.ts")] |
||||
|
public static partial Task DeleteBookmark(JSObject item); |
||||
|
|
||||
|
[JSImport("StorageItem.getProperties", "storage.ts")] |
||||
|
public static partial Task<JSObject?> GetProperties(JSObject item); |
||||
|
|
||||
|
[JSImport("StorageItem.openWrite", "storage.ts")] |
||||
|
public static partial Task<JSObject> OpenWrite(JSObject item); |
||||
|
|
||||
|
[JSImport("StorageItem.openRead", "storage.ts")] |
||||
|
public static partial Task<JSObject> OpenRead(JSObject item); |
||||
|
|
||||
|
[JSImport("StorageItem.getItems", "storage.ts")] |
||||
|
[return: JSMarshalAs<JSType.Promise<JSType.Object>>] |
||||
|
public static partial Task<JSObject> GetItems(JSObject item); |
||||
|
|
||||
|
[JSImport("StorageItems.itemsArray", "storage.ts")] |
||||
|
public static partial JSObject[] ItemsArray(JSObject item); |
||||
|
|
||||
|
[JSImport("StorageProvider.createAcceptType", "storage.ts")] |
||||
|
public static partial JSObject CreateAcceptType(string description, string[] mimeTypes); |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
using System; |
||||
|
using System.Runtime.InteropServices.JavaScript; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Avalonia.Web.Storage; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Set of FileSystemWritableFileStream and Blob methods.
|
||||
|
/// </summary>
|
||||
|
internal static partial class StreamHelper |
||||
|
{ |
||||
|
[JSImport("StreamHelper.seek", "avalonia.ts")] |
||||
|
public static partial void Seek(JSObject stream, [JSMarshalAs<JSType.Number>] long position); |
||||
|
|
||||
|
[JSImport("StreamHelper.truncate", "avalonia.ts")] |
||||
|
public static partial void Truncate(JSObject stream, [JSMarshalAs<JSType.Number>] long size); |
||||
|
|
||||
|
[JSImport("StreamHelper.write", "avalonia.ts")] |
||||
|
public static partial Task WriteAsync(JSObject stream, [JSMarshalAs<JSType.MemoryView>] ArraySegment<byte> data); |
||||
|
|
||||
|
[JSImport("StreamHelper.close", "avalonia.ts")] |
||||
|
public static partial Task CloseAsync(JSObject stream); |
||||
|
|
||||
|
[JSImport("StreamHelper.byteLength", "avalonia.ts")] |
||||
|
[return: JSMarshalAs<JSType.Number>] |
||||
|
public static partial long ByteLength(JSObject stream); |
||||
|
|
||||
|
[JSImport("StreamHelper.sliceArrayBuffer", "avalonia.ts")] |
||||
|
private static partial Task<JSObject> SliceToArrayBuffer(JSObject stream, [JSMarshalAs<JSType.Number>] long offset, int count); |
||||
|
|
||||
|
[JSImport("StreamHelper.toMemoryView", "avalonia.ts")] |
||||
|
[return: JSMarshalAs<JSType.Array<JSType.Number>>] |
||||
|
private static partial byte[] ArrayBufferToMemoryView(JSObject stream); |
||||
|
|
||||
|
public static async Task<byte[]> SliceAsync(JSObject stream, long offset, int count) |
||||
|
{ |
||||
|
using var buffer = await SliceToArrayBuffer(stream, offset, count); |
||||
|
return ArrayBufferToMemoryView(buffer); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
using System; |
||||
|
using System.Runtime.InteropServices.JavaScript; |
||||
|
|
||||
|
using Avalonia.Controls.Platform; |
||||
|
|
||||
|
namespace Avalonia.Web; |
||||
|
|
||||
|
public class JSObjectControlHandle : INativeControlHostDestroyableControlHandle |
||||
|
{ |
||||
|
internal const string ElementReferenceDescriptor = "JSObject"; |
||||
|
|
||||
|
public JSObjectControlHandle(JSObject reference) |
||||
|
{ |
||||
|
Object = reference; |
||||
|
} |
||||
|
|
||||
|
public JSObject Object { get; } |
||||
|
|
||||
|
public IntPtr Handle => throw new NotSupportedException(); |
||||
|
|
||||
|
public string? HandleDescriptor => ElementReferenceDescriptor; |
||||
|
|
||||
|
public void Destroy() |
||||
|
{ |
||||
|
if (Object is JSObject inProcess && !inProcess.IsDisposed) |
||||
|
{ |
||||
|
inProcess.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,129 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
using Avalonia.Input; |
||||
|
|
||||
|
namespace Avalonia.Web |
||||
|
{ |
||||
|
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,18 @@ |
|||||
|
using System; |
||||
|
using System.Diagnostics; |
||||
|
using Avalonia.Rendering; |
||||
|
|
||||
|
namespace Avalonia.Web |
||||
|
{ |
||||
|
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; |
||||
|
public bool RunsInBackground => false; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Skia; |
||||
|
|
||||
|
namespace Avalonia.Web.Skia |
||||
|
{ |
||||
|
public class BrowserSkiaGpu : ISkiaGpu |
||||
|
{ |
||||
|
public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable<object> surfaces) |
||||
|
{ |
||||
|
foreach (var surface in surfaces) |
||||
|
{ |
||||
|
if (surface is BrowserSkiaSurface browserSkiaSurface) |
||||
|
{ |
||||
|
return new BrowserSkiaGpuRenderTarget(browserSkiaSurface); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
public ISkiaSurface? TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
using Avalonia.Skia; |
||||
|
using SkiaSharp; |
||||
|
|
||||
|
namespace Avalonia.Web.Skia |
||||
|
{ |
||||
|
internal class BrowserSkiaGpuRenderSession : ISkiaGpuRenderSession |
||||
|
{ |
||||
|
private readonly SKSurface _surface; |
||||
|
|
||||
|
public BrowserSkiaGpuRenderSession(BrowserSkiaSurface browserSkiaSurface, GRBackendRenderTarget renderTarget) |
||||
|
{ |
||||
|
_surface = SKSurface.Create(browserSkiaSurface.Context, renderTarget, browserSkiaSurface.Origin, browserSkiaSurface.ColorType); |
||||
|
|
||||
|
GrContext = browserSkiaSurface.Context; |
||||
|
|
||||
|
ScaleFactor = browserSkiaSurface.Scaling; |
||||
|
|
||||
|
SurfaceOrigin = browserSkiaSurface.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,39 @@ |
|||||
|
using Avalonia.Skia; |
||||
|
using SkiaSharp; |
||||
|
|
||||
|
namespace Avalonia.Web.Skia |
||||
|
{ |
||||
|
internal class BrowserSkiaGpuRenderTarget : ISkiaGpuRenderTarget |
||||
|
{ |
||||
|
private readonly GRBackendRenderTarget _renderTarget; |
||||
|
private readonly BrowserSkiaSurface _browserSkiaSurface; |
||||
|
private readonly PixelSize _size; |
||||
|
|
||||
|
public BrowserSkiaGpuRenderTarget(BrowserSkiaSurface browserSkiaSurface) |
||||
|
{ |
||||
|
_size = browserSkiaSurface.Size; |
||||
|
|
||||
|
var glFbInfo = new GRGlFramebufferInfo(browserSkiaSurface.GlInfo.FboId, browserSkiaSurface.ColorType.ToGlSizedFormat()); |
||||
|
{ |
||||
|
_browserSkiaSurface = browserSkiaSurface; |
||||
|
_renderTarget = new GRBackendRenderTarget( |
||||
|
(int)(browserSkiaSurface.Size.Width * browserSkiaSurface.Scaling), |
||||
|
(int)(browserSkiaSurface.Size.Height * browserSkiaSurface.Scaling), |
||||
|
browserSkiaSurface.GlInfo.Samples, |
||||
|
browserSkiaSurface.GlInfo.Stencils, glFbInfo); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_renderTarget.Dispose(); |
||||
|
} |
||||
|
|
||||
|
public ISkiaGpuRenderSession BeginRenderingSession() |
||||
|
{ |
||||
|
return new BrowserSkiaGpuRenderSession(_browserSkiaSurface, _renderTarget); |
||||
|
} |
||||
|
|
||||
|
public bool IsCorrupted => _browserSkiaSurface.Size != _size; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,88 @@ |
|||||
|
using System; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using Avalonia.Controls.Platform.Surfaces; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Skia; |
||||
|
using SkiaSharp; |
||||
|
|
||||
|
namespace Avalonia.Web.Skia |
||||
|
{ |
||||
|
internal class BrowserSkiaRasterSurface : IBrowserSkiaSurface, IFramebufferPlatformSurface, IDisposable |
||||
|
{ |
||||
|
public SKColorType ColorType { get; set; } |
||||
|
|
||||
|
public PixelSize Size { get; set; } |
||||
|
|
||||
|
public double Scaling { get; set; } |
||||
|
|
||||
|
private FramebufferData? _fbData; |
||||
|
private readonly Action<IntPtr, SKSizeI> _blitCallback; |
||||
|
private readonly Action _onDisposeAction; |
||||
|
|
||||
|
public BrowserSkiaRasterSurface( |
||||
|
SKColorType colorType, PixelSize size, double scaling, Action<IntPtr, SKSizeI> blitCallback) |
||||
|
{ |
||||
|
ColorType = colorType; |
||||
|
Size = size; |
||||
|
Scaling = scaling; |
||||
|
_blitCallback = blitCallback; |
||||
|
_onDisposeAction = Blit; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_fbData?.Dispose(); |
||||
|
_fbData = null; |
||||
|
} |
||||
|
|
||||
|
public ILockedFramebuffer Lock() |
||||
|
{ |
||||
|
var bytesPerPixel = 4; // TODO: derive from ColorType
|
||||
|
var dpi = Scaling * 96.0; |
||||
|
var width = (int)(Size.Width * Scaling); |
||||
|
var height = (int)(Size.Height * Scaling); |
||||
|
|
||||
|
if (_fbData is null || _fbData?.Size.Width != width || _fbData?.Size.Height != height) |
||||
|
{ |
||||
|
_fbData?.Dispose(); |
||||
|
_fbData = new FramebufferData(width, height, bytesPerPixel); |
||||
|
} |
||||
|
|
||||
|
var pixelFormat = ColorType.ToPixelFormat(); |
||||
|
var data = _fbData.Value; |
||||
|
return new LockedFramebuffer( |
||||
|
data.Address, data.Size, data.RowBytes, |
||||
|
new Vector(dpi, dpi), pixelFormat, _onDisposeAction); |
||||
|
} |
||||
|
|
||||
|
private void Blit() |
||||
|
{ |
||||
|
if (_fbData != null) |
||||
|
{ |
||||
|
var data = _fbData.Value; |
||||
|
_blitCallback(data.Address, new SKSizeI(data.Size.Width, data.Size.Height)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private readonly struct FramebufferData |
||||
|
{ |
||||
|
public PixelSize Size { get; } |
||||
|
|
||||
|
public int RowBytes { get; } |
||||
|
|
||||
|
public IntPtr Address { get; } |
||||
|
|
||||
|
public FramebufferData(int width, int height, int bytesPerPixel) |
||||
|
{ |
||||
|
Size = new PixelSize(width, height); |
||||
|
RowBytes = width * bytesPerPixel; |
||||
|
Address = Marshal.AllocHGlobal(width * height * bytesPerPixel); |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
Marshal.FreeHGlobal(Address); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
using Avalonia.Web.Interop; |
||||
|
using SkiaSharp; |
||||
|
|
||||
|
namespace Avalonia.Web.Skia |
||||
|
{ |
||||
|
internal class BrowserSkiaSurface : IBrowserSkiaSurface |
||||
|
{ |
||||
|
public BrowserSkiaSurface(GRContext context, 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 GLInfo GlInfo { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
namespace Avalonia.Web.Skia |
||||
|
{ |
||||
|
internal interface IBrowserSkiaSurface |
||||
|
{ |
||||
|
public PixelSize Size { get; set; } |
||||
|
|
||||
|
public double Scaling { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,88 @@ |
|||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Runtime.InteropServices.JavaScript; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Avalonia.Web.Storage; |
||||
|
|
||||
|
[System.Runtime.Versioning.SupportedOSPlatform("browser")] |
||||
|
internal class BlobReadableStream : Stream |
||||
|
{ |
||||
|
private JSObject? _jSReference; |
||||
|
private long _position; |
||||
|
private readonly long _length; |
||||
|
|
||||
|
public BlobReadableStream(JSObject jsStreamReference) |
||||
|
{ |
||||
|
_jSReference = jsStreamReference; |
||||
|
_position = 0; |
||||
|
_length = StreamHelper.ByteLength(JSReference); |
||||
|
} |
||||
|
|
||||
|
private JSObject JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(WriteableStream)); |
||||
|
|
||||
|
public override bool CanRead => true; |
||||
|
|
||||
|
public override bool CanSeek => false; |
||||
|
|
||||
|
public override bool CanWrite => false; |
||||
|
|
||||
|
public override long Length => _length; |
||||
|
|
||||
|
public override long Position |
||||
|
{ |
||||
|
get => _position; |
||||
|
set => throw new NotSupportedException(); |
||||
|
} |
||||
|
|
||||
|
public override void Flush() { } |
||||
|
|
||||
|
public override long Seek(long offset, SeekOrigin origin) |
||||
|
{ |
||||
|
return _position = origin switch |
||||
|
{ |
||||
|
SeekOrigin.Current => _position + offset, |
||||
|
SeekOrigin.End => _length + offset, |
||||
|
_ => offset |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public override void SetLength(long value) |
||||
|
=> throw new NotSupportedException(); |
||||
|
|
||||
|
public override void Write(byte[] buffer, int offset, int count) |
||||
|
=> throw new NotSupportedException(); |
||||
|
|
||||
|
public override int Read(byte[] buffer, int offset, int count) |
||||
|
{ |
||||
|
throw new InvalidOperationException("Browser supports only ReadAsync"); |
||||
|
} |
||||
|
|
||||
|
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) |
||||
|
=> await ReadAsync(buffer.AsMemory(offset, count), cancellationToken); |
||||
|
|
||||
|
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
var numBytesToRead = (int)Math.Min(buffer.Length, Length - _position); |
||||
|
var bytesRead = await StreamHelper.SliceAsync(JSReference, _position, numBytesToRead); |
||||
|
if (bytesRead.Length != numBytesToRead) |
||||
|
{ |
||||
|
throw new EndOfStreamException("Failed to read the requested number of bytes from the stream."); |
||||
|
} |
||||
|
|
||||
|
_position += bytesRead.Length; |
||||
|
bytesRead.CopyTo(buffer); |
||||
|
|
||||
|
return bytesRead.Length; |
||||
|
} |
||||
|
|
||||
|
protected override void Dispose(bool disposing) |
||||
|
{ |
||||
|
if (_jSReference is { } jsReference) |
||||
|
{ |
||||
|
_jSReference = null; |
||||
|
jsReference.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,257 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Runtime.InteropServices.JavaScript; |
||||
|
using System.Runtime.Versioning; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
using Avalonia.Platform.Storage; |
||||
|
using Avalonia.Web.Interop; |
||||
|
|
||||
|
namespace Avalonia.Web.Storage; |
||||
|
|
||||
|
internal record FilePickerAcceptType(string Description, IReadOnlyDictionary<string, IReadOnlyList<string>> Accept); |
||||
|
|
||||
|
[SupportedOSPlatform("browser")] |
||||
|
internal class BrowserStorageProvider : IStorageProvider |
||||
|
{ |
||||
|
internal const string PickerCancelMessage = "The user aborted a request"; |
||||
|
internal const string NoPermissionsMessage = "Permissions denied"; |
||||
|
|
||||
|
private readonly Lazy<Task<JSObject>> _lazyModule = new(() => JSHost.ImportAsync("storage.ts", "./storage.js")); |
||||
|
|
||||
|
public bool CanOpen => StorageHelper.CanShowOpenFilePicker(); |
||||
|
public bool CanSave => StorageHelper.CanShowSaveFilePicker(); |
||||
|
public bool CanPickFolder => StorageHelper.CanShowDirectoryPicker(); |
||||
|
|
||||
|
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options) |
||||
|
{ |
||||
|
_ = await _lazyModule.Value; |
||||
|
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; |
||||
|
|
||||
|
var (types, exludeAll) = ConvertFileTypes(options.FileTypeFilter); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
using var items = await StorageHelper.OpenFileDialog(startIn, options.AllowMultiple, types, exludeAll); |
||||
|
if (items is null) |
||||
|
{ |
||||
|
return Array.Empty<IStorageFile>(); |
||||
|
} |
||||
|
|
||||
|
var itemsArray = StorageHelper.ItemsArray(items); |
||||
|
return itemsArray.Select(item => new JSStorageFile(item)).ToArray(); |
||||
|
} |
||||
|
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) |
||||
|
{ |
||||
|
return Array.Empty<IStorageFile>(); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
if (types is not null) |
||||
|
{ |
||||
|
foreach (var type in types) |
||||
|
{ |
||||
|
type.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options) |
||||
|
{ |
||||
|
_ = await _lazyModule.Value; |
||||
|
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; |
||||
|
|
||||
|
var (types, exludeAll) = ConvertFileTypes(options.FileTypeChoices); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
var item = await StorageHelper.SaveFileDialog(startIn, options.SuggestedFileName, types, exludeAll); |
||||
|
return item is not null ? new JSStorageFile(item) : null; |
||||
|
} |
||||
|
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
if (types is not null) |
||||
|
{ |
||||
|
foreach (var type in types) |
||||
|
{ |
||||
|
type.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options) |
||||
|
{ |
||||
|
_ = await _lazyModule.Value; |
||||
|
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
var item = await StorageHelper.SelectFolderDialog(startIn); |
||||
|
return item is not null ? new[] { new JSStorageFolder(item) } : Array.Empty<IStorageFolder>(); |
||||
|
} |
||||
|
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) |
||||
|
{ |
||||
|
return Array.Empty<IStorageFolder>(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark) |
||||
|
{ |
||||
|
_ = await _lazyModule.Value; |
||||
|
var item = await StorageHelper.OpenBookmark(bookmark); |
||||
|
return item is not null ? new JSStorageFile(item) : null; |
||||
|
} |
||||
|
|
||||
|
public async Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark) |
||||
|
{ |
||||
|
_ = await _lazyModule.Value; |
||||
|
var item = await StorageHelper.OpenBookmark(bookmark); |
||||
|
return item is not null ? new JSStorageFolder(item) : null; |
||||
|
} |
||||
|
|
||||
|
private static (JSObject[]? types, bool excludeAllOption) ConvertFileTypes(IEnumerable<FilePickerFileType>? input) |
||||
|
{ |
||||
|
var types = input? |
||||
|
.Where(t => t.MimeTypes?.Any() == true && t != FilePickerFileTypes.All) |
||||
|
.Select(t => StorageHelper.CreateAcceptType(t.Name, t.MimeTypes!.ToArray())) |
||||
|
.ToArray(); |
||||
|
if (types?.Length == 0) |
||||
|
{ |
||||
|
types = null; |
||||
|
} |
||||
|
|
||||
|
var inlcudeAll = input?.Contains(FilePickerFileTypes.All) == true || types is null; |
||||
|
|
||||
|
return (types, !inlcudeAll); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal abstract class JSStorageItem : IStorageBookmarkItem |
||||
|
{ |
||||
|
internal JSObject? _fileHandle; |
||||
|
|
||||
|
protected JSStorageItem(JSObject fileHandle) |
||||
|
{ |
||||
|
_fileHandle = fileHandle ?? throw new ArgumentNullException(nameof(fileHandle)); |
||||
|
} |
||||
|
|
||||
|
internal JSObject FileHandle => _fileHandle ?? throw new ObjectDisposedException(nameof(JSStorageItem)); |
||||
|
|
||||
|
public string Name => FileHandle.GetPropertyAsString("name") ?? string.Empty; |
||||
|
|
||||
|
public bool TryGetUri([NotNullWhen(true)] out Uri? uri) |
||||
|
{ |
||||
|
uri = new Uri(Name, UriKind.Relative); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public async Task<StorageItemProperties> GetBasicPropertiesAsync() |
||||
|
{ |
||||
|
using var properties = await StorageHelper.GetProperties(FileHandle); |
||||
|
var size = (long?)properties?.GetPropertyAsDouble("Size"); |
||||
|
var lastModified = (long?)properties?.GetPropertyAsDouble("LastModified"); |
||||
|
|
||||
|
return new StorageItemProperties( |
||||
|
(ulong?)size, |
||||
|
dateCreated: null, |
||||
|
dateModified: lastModified > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(lastModified.Value) : null); |
||||
|
} |
||||
|
|
||||
|
public bool CanBookmark => true; |
||||
|
|
||||
|
public Task<string?> SaveBookmarkAsync() |
||||
|
{ |
||||
|
return StorageHelper.SaveBookmark(FileHandle); |
||||
|
} |
||||
|
|
||||
|
public Task<IStorageFolder?> GetParentAsync() |
||||
|
{ |
||||
|
return Task.FromResult<IStorageFolder?>(null); |
||||
|
} |
||||
|
|
||||
|
public Task ReleaseBookmarkAsync() |
||||
|
{ |
||||
|
return StorageHelper.DeleteBookmark(FileHandle); |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_fileHandle?.Dispose(); |
||||
|
_fileHandle = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal class JSStorageFile : JSStorageItem, IStorageBookmarkFile |
||||
|
{ |
||||
|
public JSStorageFile(JSObject fileHandle) : base(fileHandle) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public bool CanOpenRead => true; |
||||
|
public async Task<Stream> OpenReadAsync() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var blob = await StorageHelper.OpenRead(FileHandle); |
||||
|
return new BlobReadableStream(blob); |
||||
|
} |
||||
|
catch (JSException ex) when (ex.Message == BrowserStorageProvider.NoPermissionsMessage) |
||||
|
{ |
||||
|
throw new UnauthorizedAccessException("User denied permissions to open the file", ex); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public bool CanOpenWrite => true; |
||||
|
public async Task<Stream> OpenWriteAsync() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
using var properties = await StorageHelper.GetProperties(FileHandle); |
||||
|
var streamWriter = await StorageHelper.OpenWrite(FileHandle); |
||||
|
var size = (long?)properties?.GetPropertyAsDouble("Size") ?? 0; |
||||
|
|
||||
|
return new WriteableStream(streamWriter, size); |
||||
|
} |
||||
|
catch (JSException ex) when (ex.Message == BrowserStorageProvider.NoPermissionsMessage) |
||||
|
{ |
||||
|
throw new UnauthorizedAccessException("User denied permissions to open the file", ex); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal class JSStorageFolder : JSStorageItem, IStorageBookmarkFolder |
||||
|
{ |
||||
|
public JSStorageFolder(JSObject fileHandle) : base(fileHandle) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync() |
||||
|
{ |
||||
|
using var items = await StorageHelper.GetItems(FileHandle); |
||||
|
if (items is null) |
||||
|
{ |
||||
|
return Array.Empty<IStorageItem>(); |
||||
|
} |
||||
|
|
||||
|
var itemsArray = StorageHelper.ItemsArray(items); |
||||
|
|
||||
|
return itemsArray |
||||
|
.Select(reference => reference.GetPropertyAsString("kind") switch |
||||
|
{ |
||||
|
"directory" => (IStorageItem)new JSStorageFolder(reference), |
||||
|
"file" => new JSStorageFile(reference), |
||||
|
_ => null |
||||
|
}) |
||||
|
.Where(i => i is not null) |
||||
|
.ToArray()!; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,124 @@ |
|||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Runtime.InteropServices.JavaScript; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Avalonia.Web.Storage; |
||||
|
|
||||
|
[System.Runtime.Versioning.SupportedOSPlatform("browser")] |
||||
|
// Loose wrapper implementaion of a stream on top of FileAPI FileSystemWritableFileStream
|
||||
|
internal sealed class WriteableStream : Stream |
||||
|
{ |
||||
|
private JSObject? _jSReference; |
||||
|
|
||||
|
// Unfortunatelly we can't read current length/position, so we need to keep it C#-side only.
|
||||
|
private long _length, _position; |
||||
|
|
||||
|
internal WriteableStream(JSObject jSReference, long initialLength) |
||||
|
{ |
||||
|
_jSReference = jSReference; |
||||
|
_length = initialLength; |
||||
|
} |
||||
|
|
||||
|
private JSObject JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(WriteableStream)); |
||||
|
|
||||
|
public override bool CanRead => false; |
||||
|
|
||||
|
public override bool CanSeek => true; |
||||
|
|
||||
|
public override bool CanWrite => true; |
||||
|
|
||||
|
public override long Length => _length; |
||||
|
|
||||
|
public override long Position |
||||
|
{ |
||||
|
get => _position; |
||||
|
set => Seek(_position, SeekOrigin.Begin); |
||||
|
} |
||||
|
|
||||
|
public override void Flush() |
||||
|
{ |
||||
|
// no-op
|
||||
|
} |
||||
|
|
||||
|
public override int Read(byte[] buffer, int offset, int count) |
||||
|
{ |
||||
|
throw new NotSupportedException(); |
||||
|
} |
||||
|
|
||||
|
public override long Seek(long offset, SeekOrigin origin) |
||||
|
{ |
||||
|
var position = origin switch |
||||
|
{ |
||||
|
SeekOrigin.Current => _position + offset, |
||||
|
SeekOrigin.End => _length + offset, |
||||
|
_ => offset |
||||
|
}; |
||||
|
StreamHelper.Seek(JSReference, position); |
||||
|
return position; |
||||
|
} |
||||
|
|
||||
|
public override void SetLength(long value) |
||||
|
{ |
||||
|
_length = value; |
||||
|
|
||||
|
// See https://docs.w3cub.com/dom/filesystemwritablefilestream/truncate
|
||||
|
// If the offset is smaller than the size, it remains unchanged. If the offset is larger than size, the offset is set to that size
|
||||
|
if (_position > _length) |
||||
|
{ |
||||
|
_position = _length; |
||||
|
} |
||||
|
|
||||
|
StreamHelper.Truncate(JSReference, value); |
||||
|
} |
||||
|
|
||||
|
public override void Write(byte[] buffer, int offset, int count) |
||||
|
{ |
||||
|
throw new InvalidOperationException("Browser supports only WriteAsync"); |
||||
|
} |
||||
|
|
||||
|
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
return new ValueTask(WriteAsyncInternal(buffer.ToArray(), cancellationToken)); |
||||
|
} |
||||
|
|
||||
|
private Task WriteAsyncInternal(byte[] buffer, CancellationToken _) |
||||
|
{ |
||||
|
_position += buffer.Length; |
||||
|
|
||||
|
return StreamHelper.WriteAsync(JSReference, buffer); |
||||
|
} |
||||
|
|
||||
|
protected override void Dispose(bool disposing) |
||||
|
{ |
||||
|
if (_jSReference is { } jsReference) |
||||
|
{ |
||||
|
_jSReference = null; |
||||
|
try |
||||
|
{ |
||||
|
_ = StreamHelper.CloseAsync(jsReference); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
jsReference.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override async ValueTask DisposeAsync() |
||||
|
{ |
||||
|
if (_jSReference is { } jsReference) |
||||
|
{ |
||||
|
_jSReference = null; |
||||
|
try |
||||
|
{ |
||||
|
await StreamHelper.CloseAsync(jsReference); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
jsReference.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,68 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Controls.Embedding; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Rendering.SceneGraph; |
||||
|
|
||||
|
namespace Avalonia.Web |
||||
|
{ |
||||
|
internal class WebEmbeddableControlRoot : EmbeddableControlRoot |
||||
|
{ |
||||
|
class SplashScreenCloseCustomDrawingOperation : ICustomDrawOperation |
||||
|
{ |
||||
|
private bool _hasRendered; |
||||
|
private Action _onFirstRender; |
||||
|
|
||||
|
public SplashScreenCloseCustomDrawingOperation(Action onFirstRender) |
||||
|
{ |
||||
|
_onFirstRender = onFirstRender; |
||||
|
} |
||||
|
|
||||
|
public Rect Bounds => Rect.Empty; |
||||
|
|
||||
|
public bool HasRendered => _hasRendered; |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public bool Equals(ICustomDrawOperation? other) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public bool HitTest(Point p) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public void Render(IDrawingContextImpl context) |
||||
|
{ |
||||
|
_hasRendered = true; |
||||
|
_onFirstRender(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public WebEmbeddableControlRoot(ITopLevelImpl impl, Action onFirstRender) : base(impl) |
||||
|
{ |
||||
|
_splashCloseOp = new SplashScreenCloseCustomDrawingOperation(() => |
||||
|
{ |
||||
|
_splashCloseOp = null; |
||||
|
onFirstRender(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private SplashScreenCloseCustomDrawingOperation? _splashCloseOp; |
||||
|
|
||||
|
public override void Render(DrawingContext context) |
||||
|
{ |
||||
|
base.Render(context); |
||||
|
|
||||
|
if (_splashCloseOp != null) |
||||
|
{ |
||||
|
context.Custom(_splashCloseOp); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
#nullable enable |
||||
|
|
||||
|
namespace Avalonia.Web |
||||
|
{ |
||||
|
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 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) }; |
||||
|
|
||||
|
public Screen? ScreenFromPoint(PixelPoint point) |
||||
|
{ |
||||
|
return ScreenHelper.ScreenFromPoint(point, AllScreens); |
||||
|
} |
||||
|
|
||||
|
public Screen? ScreenFromRect(PixelRect rect) |
||||
|
{ |
||||
|
return ScreenHelper.ScreenFromRect(rect, AllScreens); |
||||
|
} |
||||
|
|
||||
|
public Screen? ScreenFromWindow(IWindowBaseImpl window) |
||||
|
{ |
||||
|
return ScreenHelper.ScreenFromWindow(window, AllScreens); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,105 @@ |
|||||
|
using System; |
||||
|
using System.Threading; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Input.Platform; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Rendering; |
||||
|
using Avalonia.Threading; |
||||
|
|
||||
|
namespace Avalonia.Web |
||||
|
{ |
||||
|
public class BrowserWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface |
||||
|
{ |
||||
|
private bool _signaled; |
||||
|
private static KeyboardDevice? s_keyboard; |
||||
|
|
||||
|
public IWindowImpl CreateWindow() => throw new NotSupportedException(); |
||||
|
|
||||
|
IWindowImpl IWindowingPlatform.CreateEmbeddableWindow() |
||||
|
{ |
||||
|
throw new NotImplementedException(); |
||||
|
} |
||||
|
|
||||
|
public ITrayIconImpl? CreateTrayIcon() |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
public static KeyboardDevice Keyboard => s_keyboard ?? |
||||
|
throw new InvalidOperationException("BrowserWindowingPlatform not registered."); |
||||
|
|
||||
|
public static void Register() |
||||
|
{ |
||||
|
var instance = new BrowserWindowingPlatform(); |
||||
|
s_keyboard = new KeyboardDevice(); |
||||
|
AvaloniaLocator.CurrentMutable |
||||
|
.Bind<IClipboard>().ToSingleton<ClipboardImpl>() |
||||
|
.Bind<ICursorFactory>().ToSingleton<CssCursorFactory>() |
||||
|
.Bind<IKeyboardDevice>().ToConstant(s_keyboard) |
||||
|
.Bind<IPlatformSettings>().ToConstant(instance) |
||||
|
.Bind<IPlatformThreadingInterface>().ToConstant(instance) |
||||
|
.Bind<IRenderLoop>().ToConstant(new RenderLoop()) |
||||
|
.Bind<IRenderTimer>().ToConstant(ManualTriggerRenderTimer.Instance) |
||||
|
.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 Size TouchDoubleClickSize => new Size(16, 16); |
||||
|
|
||||
|
public TimeSpan TouchDoubleClickTime => DoubleClickTime; |
||||
|
public void RunLoop(CancellationToken cancellationToken) |
||||
|
{ |
||||
|
throw new NotSupportedException(); |
||||
|
} |
||||
|
|
||||
|
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) |
||||
|
{ |
||||
|
return GetRuntimePlatform() |
||||
|
.StartSystemTimer(interval, () => |
||||
|
{ |
||||
|
Dispatcher.UIThread.RunJobs(priority); |
||||
|
tick(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public void Signal(DispatcherPriority priority) |
||||
|
{ |
||||
|
if (_signaled) |
||||
|
return; |
||||
|
|
||||
|
_signaled = true; |
||||
|
|
||||
|
IDisposable? disp = null; |
||||
|
|
||||
|
disp = GetRuntimePlatform() |
||||
|
.StartSystemTimer(TimeSpan.FromMilliseconds(1), |
||||
|
() => |
||||
|
{ |
||||
|
_signaled = false; |
||||
|
disp?.Dispose(); |
||||
|
|
||||
|
Signaled?.Invoke(null); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public bool CurrentThreadIsLoopThread |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
return true; // Browser is single threaded.
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public event Action<DispatcherPriority?>? Signaled; |
||||
|
|
||||
|
private static IRuntimePlatform GetRuntimePlatform() |
||||
|
{ |
||||
|
return AvaloniaLocator.Current.GetRequiredService<IRuntimePlatform>(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
var LibraryExample = { |
||||
|
// Internal functions
|
||||
|
$EXAMPLE: { |
||||
|
internal_func: function () { |
||||
|
} |
||||
|
}, |
||||
|
InterceptGLObject: function () { |
||||
|
globalThis.AvaloniaGL = GL |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
autoAddDeps(LibraryExample, '$EXAMPLE') |
||||
|
mergeInto(LibraryManager.library, LibraryExample) |
||||
@ -0,0 +1,47 @@ |
|||||
|
{ |
||||
|
"env": { |
||||
|
"browser": true, |
||||
|
"es6": true |
||||
|
}, |
||||
|
"extends": "standard-with-typescript", |
||||
|
"overrides": [], |
||||
|
"parserOptions": { |
||||
|
"ecmaVersion": "latest", |
||||
|
"sourceType": "module", |
||||
|
"project": [ |
||||
|
"tsconfig.json" |
||||
|
] |
||||
|
}, |
||||
|
"rules": { |
||||
|
"indent": [ |
||||
|
"warn", |
||||
|
4 |
||||
|
], |
||||
|
"@typescript-eslint/indent": [ |
||||
|
"warn", |
||||
|
4 |
||||
|
], |
||||
|
"quotes": ["warn", "double"], |
||||
|
"semi": ["error", "always"], |
||||
|
"@typescript-eslint/quotes": ["warn", "double"], |
||||
|
"@typescript-eslint/explicit-function-return-type": "off", |
||||
|
"@typescript-eslint/no-extraneous-class": "off", |
||||
|
"@typescript-eslint/strict-boolean-expressions": "off", |
||||
|
"@typescript-eslint/space-before-function-paren": "off", |
||||
|
"@typescript-eslint/semi": ["error", "always"], |
||||
|
"@typescript-eslint/member-delimiter-style": [ |
||||
|
"error", |
||||
|
{ |
||||
|
"multiline": { |
||||
|
"delimiter": "semi", |
||||
|
"requireLast": true |
||||
|
}, |
||||
|
"singleline": { |
||||
|
"delimiter": "semi", |
||||
|
"requireLast": false |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
"ignorePatterns": ["types/*"] |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
require("esbuild").build({ |
||||
|
entryPoints: [ |
||||
|
"./modules/avalonia.ts", |
||||
|
"./modules/storage.ts" |
||||
|
], |
||||
|
outdir: "../wwwroot", |
||||
|
bundle: true, |
||||
|
minify: false, |
||||
|
format: "esm", |
||||
|
target: "es2016", |
||||
|
platform: "browser", |
||||
|
sourcemap: "linked", |
||||
|
loader: { ".ts": "ts" } |
||||
|
}) |
||||
|
.then(() => console.log("⚡ Done")) |
||||
|
.catch(() => process.exit(1)); |
||||
@ -0,0 +1,20 @@ |
|||||
|
import { RuntimeAPI } from "../types/dotnet"; |
||||
|
import { SizeWatcher, DpiWatcher, Canvas } from "./avalonia/canvas"; |
||||
|
import { InputHelper } from "./avalonia/input"; |
||||
|
import { AvaloniaDOM } from "./avalonia/dom"; |
||||
|
import { Caniuse } from "./avalonia/caniuse"; |
||||
|
import { StreamHelper } from "./avalonia/stream"; |
||||
|
import { NativeControlHost } from "./avalonia/nativeControlHost"; |
||||
|
|
||||
|
export async function createAvaloniaRuntime(api: RuntimeAPI): Promise<void> { |
||||
|
api.setModuleImports("avalonia.ts", { |
||||
|
Caniuse, |
||||
|
Canvas, |
||||
|
InputHelper, |
||||
|
SizeWatcher, |
||||
|
DpiWatcher, |
||||
|
AvaloniaDOM, |
||||
|
StreamHelper, |
||||
|
NativeControlHost |
||||
|
}); |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
export class Caniuse { |
||||
|
public static canShowOpenFilePicker(): boolean { |
||||
|
return typeof window.showOpenFilePicker !== "undefined"; |
||||
|
} |
||||
|
|
||||
|
public static canShowSaveFilePicker(): boolean { |
||||
|
return typeof window.showSaveFilePicker !== "undefined"; |
||||
|
} |
||||
|
|
||||
|
public static canShowDirectoryPicker(): boolean { |
||||
|
return typeof window.showDirectoryPicker !== "undefined"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,303 @@ |
|||||
|
interface SKGLViewInfo { |
||||
|
context: WebGLRenderingContext | WebGL2RenderingContext | undefined; |
||||
|
fboId: number; |
||||
|
stencil: number; |
||||
|
sample: number; |
||||
|
depth: number; |
||||
|
} |
||||
|
|
||||
|
type CanvasElement = { |
||||
|
Canvas: Canvas | undefined; |
||||
|
} & HTMLCanvasElement; |
||||
|
|
||||
|
export class Canvas { |
||||
|
static elements: Map<string, HTMLCanvasElement>; |
||||
|
|
||||
|
htmlCanvas: HTMLCanvasElement; |
||||
|
glInfo?: SKGLViewInfo; |
||||
|
renderFrameCallback: () => void; |
||||
|
renderLoopEnabled: boolean = false; |
||||
|
renderLoopRequest: number = 0; |
||||
|
newWidth?: number; |
||||
|
newHeight?: number; |
||||
|
|
||||
|
public static initGL(element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): SKGLViewInfo | null { |
||||
|
const view = Canvas.init(true, element, elementId, renderFrameCallback); |
||||
|
if (!view || !view.glInfo) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return view.glInfo; |
||||
|
} |
||||
|
|
||||
|
static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): Canvas | null { |
||||
|
const htmlCanvas = element as CanvasElement; |
||||
|
if (!htmlCanvas) { |
||||
|
console.error("No canvas element was provided."); |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
if (!Canvas.elements) { |
||||
|
Canvas.elements = new Map<string, HTMLCanvasElement>(); |
||||
|
} |
||||
|
Canvas.elements.set(elementId, element); |
||||
|
|
||||
|
const view = new Canvas(useGL, element, renderFrameCallback); |
||||
|
|
||||
|
htmlCanvas.Canvas = view; |
||||
|
|
||||
|
return view; |
||||
|
} |
||||
|
|
||||
|
public constructor(useGL: boolean, element: HTMLCanvasElement, renderFrameCallback: () => void) { |
||||
|
this.htmlCanvas = element; |
||||
|
this.renderFrameCallback = renderFrameCallback; |
||||
|
|
||||
|
if (useGL) { |
||||
|
const ctx = Canvas.createWebGLContext(element); |
||||
|
if (!ctx) { |
||||
|
console.error("Failed to create WebGL context"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const GL = (globalThis as any).AvaloniaGL; |
||||
|
|
||||
|
// make current
|
||||
|
GL.makeContextCurrent(ctx); |
||||
|
|
||||
|
const GLctx = GL.currentContext.GLctx as WebGLRenderingContext; |
||||
|
|
||||
|
// 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 setEnableRenderLoop(enable: boolean): void { |
||||
|
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 requestAnimationFrame(renderLoop?: boolean): void { |
||||
|
// optionally update the render loop
|
||||
|
if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) { |
||||
|
this.setEnableRenderLoop(renderLoop); |
||||
|
} |
||||
|
|
||||
|
// 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) { |
||||
|
const GL = (globalThis as any).AvaloniaGL; |
||||
|
// make current
|
||||
|
GL.makeContextCurrent(this.glInfo.context); |
||||
|
} |
||||
|
|
||||
|
if (this.htmlCanvas.width !== this.newWidth) { |
||||
|
this.htmlCanvas.width = this.newWidth ?? 0; |
||||
|
} |
||||
|
|
||||
|
if (this.htmlCanvas.height !== this.newHeight) { |
||||
|
this.htmlCanvas.height = this.newHeight ?? 0; |
||||
|
} |
||||
|
|
||||
|
this.renderFrameCallback(); |
||||
|
this.renderLoopRequest = 0; |
||||
|
|
||||
|
// we may want to draw the next frame
|
||||
|
if (this.renderLoopEnabled) { |
||||
|
this.requestAnimationFrame(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public setCanvasSize(width: number, height: number): void { |
||||
|
this.newWidth = width; |
||||
|
this.newHeight = height; |
||||
|
|
||||
|
if (this.htmlCanvas.width !== this.newWidth) { |
||||
|
this.htmlCanvas.width = this.newWidth; |
||||
|
} |
||||
|
|
||||
|
if (this.htmlCanvas.height !== this.newHeight) { |
||||
|
this.htmlCanvas.height = this.newHeight; |
||||
|
} |
||||
|
|
||||
|
if (this.glInfo) { |
||||
|
const GL = (globalThis as any).AvaloniaGL; |
||||
|
// make current
|
||||
|
GL.makeContextCurrent(this.glInfo.context); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number): void { |
||||
|
const htmlCanvas = element as CanvasElement; |
||||
|
if (!htmlCanvas || !htmlCanvas.Canvas) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
htmlCanvas.Canvas.setCanvasSize(width, height); |
||||
|
} |
||||
|
|
||||
|
public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean): void { |
||||
|
const htmlCanvas = element as CanvasElement; |
||||
|
if (!htmlCanvas || !htmlCanvas.Canvas) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
htmlCanvas.Canvas.requestAnimationFrame(renderLoop); |
||||
|
} |
||||
|
|
||||
|
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 |
||||
|
}; |
||||
|
|
||||
|
const GL = (globalThis as any).AvaloniaGL; |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
type SizeWatcherElement = { |
||||
|
SizeWatcher: SizeWatcherInstance; |
||||
|
} & HTMLElement; |
||||
|
|
||||
|
interface SizeWatcherInstance { |
||||
|
callback: (width: number, height: number) => void; |
||||
|
} |
||||
|
|
||||
|
export class SizeWatcher { |
||||
|
static observer: ResizeObserver; |
||||
|
static elements: Map<string, HTMLElement>; |
||||
|
|
||||
|
public static observe(element: HTMLElement, elementId: string, callback: (width: number, height: number) => void): void { |
||||
|
if (!element || !callback) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
SizeWatcher.init(); |
||||
|
|
||||
|
const watcherElement = element as SizeWatcherElement; |
||||
|
watcherElement.SizeWatcher = { |
||||
|
callback |
||||
|
}; |
||||
|
|
||||
|
SizeWatcher.elements.set(elementId, element); |
||||
|
SizeWatcher.observer.observe(element); |
||||
|
|
||||
|
SizeWatcher.invoke(element); |
||||
|
} |
||||
|
|
||||
|
public static unobserve(elementId: string): void { |
||||
|
if (!elementId || !SizeWatcher.observer) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const element = SizeWatcher.elements.get(elementId); |
||||
|
if (element) { |
||||
|
SizeWatcher.elements.delete(elementId); |
||||
|
SizeWatcher.observer.unobserve(element); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
static init(): void { |
||||
|
if (SizeWatcher.observer) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
SizeWatcher.elements = new Map<string, HTMLElement>(); |
||||
|
SizeWatcher.observer = new ResizeObserver((entries) => { |
||||
|
for (const entry of entries) { |
||||
|
SizeWatcher.invoke(entry.target); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
static invoke(element: Element): void { |
||||
|
const watcherElement = element as SizeWatcherElement; |
||||
|
const instance = watcherElement.SizeWatcher; |
||||
|
|
||||
|
if (!instance || !instance.callback) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
return instance.callback(element.clientWidth, element.clientHeight); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class DpiWatcher { |
||||
|
static lastDpi: number; |
||||
|
static timerId: number; |
||||
|
static callback: (old: number, newdpi: number) => void; |
||||
|
|
||||
|
public static getDpi(): number { |
||||
|
return window.devicePixelRatio; |
||||
|
} |
||||
|
|
||||
|
public static start(callback: (old: number, newdpi: number) => void): number { |
||||
|
DpiWatcher.lastDpi = window.devicePixelRatio; |
||||
|
DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000); |
||||
|
DpiWatcher.callback = callback; |
||||
|
|
||||
|
return DpiWatcher.lastDpi; |
||||
|
} |
||||
|
|
||||
|
public static stop(): void { |
||||
|
window.clearInterval(DpiWatcher.timerId); |
||||
|
} |
||||
|
|
||||
|
static update(): void { |
||||
|
if (!DpiWatcher.callback) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const currentDpi = window.devicePixelRatio; |
||||
|
const lastDpi = DpiWatcher.lastDpi; |
||||
|
DpiWatcher.lastDpi = currentDpi; |
||||
|
|
||||
|
if (Math.abs(lastDpi - currentDpi) > 0.001) { |
||||
|
DpiWatcher.callback(lastDpi, currentDpi); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,149 @@ |
|||||
|
// Based on https://github.com/component/textarea-caret-position/blob/master/index.js
|
||||
|
export class CaretHelper { |
||||
|
public static getCaretCoordinates( |
||||
|
element: HTMLInputElement | HTMLTextAreaElement, |
||||
|
position: number, |
||||
|
options?: { debug: boolean } |
||||
|
) { |
||||
|
if (!isBrowser) { |
||||
|
throw new Error( |
||||
|
"textarea-caret-position#getCaretCoordinates should only be called in a browser" |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const debug = options?.debug ?? false; |
||||
|
if (debug) { |
||||
|
const el = document.querySelector( |
||||
|
"#input-textarea-caret-position-mirror-div" |
||||
|
); |
||||
|
if (el) el.parentNode?.removeChild(el); |
||||
|
} |
||||
|
|
||||
|
// The mirror div will replicate the textarea's style
|
||||
|
const div = document.createElement("div"); |
||||
|
div.id = "input-textarea-caret-position-mirror-div"; |
||||
|
document.body.appendChild(div); |
||||
|
|
||||
|
const style = div.style; |
||||
|
const computed = window.getComputedStyle |
||||
|
? window.getComputedStyle(element) |
||||
|
: ((element as any).currentStyle as CSSStyleDeclaration); // currentStyle for IE < 9
|
||||
|
const isInput = element.nodeName === "INPUT"; |
||||
|
|
||||
|
// Default textarea styles
|
||||
|
style.whiteSpace = "pre-wrap"; |
||||
|
if (!isInput) style.wordWrap = "break-word"; // only for textarea-s
|
||||
|
|
||||
|
// Position off-screen
|
||||
|
style.position = "absolute"; // required to return coordinates properly
|
||||
|
if (!debug) style.visibility = "hidden"; // not 'display: none' because we want rendering
|
||||
|
|
||||
|
// Transfer the element's properties to the div
|
||||
|
properties.forEach((prop: string) => { |
||||
|
if (isInput && prop === "lineHeight") { |
||||
|
// Special case for <input>s because text is rendered centered and line height may be != height
|
||||
|
if (computed.boxSizing === "border-box") { |
||||
|
const height = parseInt(computed.height); |
||||
|
const outerHeight = |
||||
|
parseInt(computed.paddingTop) + |
||||
|
parseInt(computed.paddingBottom) + |
||||
|
parseInt(computed.borderTopWidth) + |
||||
|
parseInt(computed.borderBottomWidth); |
||||
|
const targetHeight = outerHeight + parseInt(computed.lineHeight); |
||||
|
if (height > targetHeight) { |
||||
|
style.lineHeight = `${height - outerHeight}px`; |
||||
|
} else if (height === targetHeight) { |
||||
|
style.lineHeight = computed.lineHeight; |
||||
|
} else { |
||||
|
style.lineHeight = "0"; |
||||
|
} |
||||
|
} else { |
||||
|
style.lineHeight = computed.height; |
||||
|
} |
||||
|
} else { |
||||
|
(style as any)[prop] = (computed as any)[prop]; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
if (isFirefox) { |
||||
|
// Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
|
||||
|
if (element.scrollHeight > parseInt(computed.height)) { |
||||
|
style.overflowY = "scroll"; |
||||
|
} |
||||
|
} else { |
||||
|
style.overflow = "hidden"; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
|
||||
|
} |
||||
|
|
||||
|
div.textContent = element.value.substring(0, position); |
||||
|
// The second special handling for input type="text" vs textarea:
|
||||
|
// spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
|
||||
|
if (isInput) div.textContent = div.textContent.replace(/\s/g, "\u00a0"); |
||||
|
|
||||
|
const span = document.createElement("span"); |
||||
|
// Wrapping must be replicated *exactly*, including when a long word gets
|
||||
|
// onto the next line, with whitespace at the end of the line before (#7).
|
||||
|
// The *only* reliable way to do that is to copy the *entire* rest of the
|
||||
|
// textarea's content into the <span> created at the caret position.
|
||||
|
// For inputs, just '.' would be enough, but no need to bother.
|
||||
|
span.textContent = element.value.substring(position) || "."; // || because a completely empty faux span doesn't render at all
|
||||
|
div.appendChild(span); |
||||
|
|
||||
|
const coordinates = { |
||||
|
top: span.offsetTop + parseInt(computed.borderTopWidth), |
||||
|
left: span.offsetLeft + parseInt(computed.borderLeftWidth), |
||||
|
height: parseInt(computed.lineHeight) |
||||
|
}; |
||||
|
|
||||
|
if (debug) { |
||||
|
span.style.backgroundColor = "#aaa"; |
||||
|
} else { |
||||
|
document.body.removeChild(div); |
||||
|
} |
||||
|
|
||||
|
return coordinates; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const properties = [ |
||||
|
"direction", // RTL support
|
||||
|
"boxSizing", |
||||
|
"width", // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
|
||||
|
"height", |
||||
|
"overflowX", |
||||
|
"overflowY", // copy the scrollbar for IE
|
||||
|
|
||||
|
"borderTopWidth", |
||||
|
"borderRightWidth", |
||||
|
"borderBottomWidth", |
||||
|
"borderLeftWidth", |
||||
|
"borderStyle", |
||||
|
|
||||
|
"paddingTop", |
||||
|
"paddingRight", |
||||
|
"paddingBottom", |
||||
|
"paddingLeft", |
||||
|
|
||||
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/font
|
||||
|
"fontStyle", |
||||
|
"fontVariant", |
||||
|
"fontWeight", |
||||
|
"fontStretch", |
||||
|
"fontSize", |
||||
|
"fontSizeAdjust", |
||||
|
"lineHeight", |
||||
|
"fontFamily", |
||||
|
|
||||
|
"textAlign", |
||||
|
"textTransform", |
||||
|
"textIndent", |
||||
|
"textDecoration", // might not make a difference, but better be safe
|
||||
|
|
||||
|
"letterSpacing", |
||||
|
"wordSpacing", |
||||
|
|
||||
|
"tabSize", |
||||
|
"MozTabSize" |
||||
|
]; |
||||
|
|
||||
|
const isBrowser = typeof window !== "undefined"; |
||||
|
const isFirefox = isBrowser && (window as any).mozInnerScreenX != null; |
||||
@ -0,0 +1,60 @@ |
|||||
|
export class AvaloniaDOM { |
||||
|
public static addClass(element: HTMLElement, className: string): void { |
||||
|
element.classList.add(className); |
||||
|
} |
||||
|
|
||||
|
static createAvaloniaHost(host: HTMLElement) { |
||||
|
// Root element
|
||||
|
host.classList.add("avalonia-container"); |
||||
|
host.tabIndex = 0; |
||||
|
host.oncontextmenu = function () { return false; }; |
||||
|
|
||||
|
// Rendering target canvas
|
||||
|
const canvas = document.createElement("canvas"); |
||||
|
canvas.classList.add("avalonia-canvas"); |
||||
|
canvas.style.backgroundColor = "#ccc"; |
||||
|
canvas.style.width = "100%"; |
||||
|
canvas.style.height = "100%"; |
||||
|
canvas.style.position = "absolute"; |
||||
|
|
||||
|
// Native controls host
|
||||
|
const nativeHost = document.createElement("div"); |
||||
|
nativeHost.classList.add("avalonia-native-host"); |
||||
|
nativeHost.style.left = "0px"; |
||||
|
nativeHost.style.top = "0px"; |
||||
|
nativeHost.style.width = "100%"; |
||||
|
nativeHost.style.height = "100%"; |
||||
|
nativeHost.style.position = "absolute"; |
||||
|
|
||||
|
// IME
|
||||
|
const inputElement = document.createElement("input"); |
||||
|
inputElement.classList.add("avalonia-input-element"); |
||||
|
inputElement.autocapitalize = "none"; |
||||
|
inputElement.type = "text"; |
||||
|
inputElement.spellcheck = false; |
||||
|
inputElement.style.padding = "0"; |
||||
|
inputElement.style.margin = "0"; |
||||
|
inputElement.style.position = "absolute"; |
||||
|
inputElement.style.overflow = "hidden"; |
||||
|
inputElement.style.borderStyle = "hidden"; |
||||
|
inputElement.style.outline = "none"; |
||||
|
inputElement.style.background = "transparent"; |
||||
|
inputElement.style.color = "transparent"; |
||||
|
inputElement.style.display = "none"; |
||||
|
inputElement.style.height = "20px"; |
||||
|
inputElement.onpaste = function () { return false; }; |
||||
|
inputElement.oncopy = function () { return false; }; |
||||
|
inputElement.oncut = function () { return false; }; |
||||
|
|
||||
|
host.prepend(inputElement); |
||||
|
host.prepend(nativeHost); |
||||
|
host.prepend(canvas); |
||||
|
|
||||
|
return { |
||||
|
host, |
||||
|
canvas, |
||||
|
nativeHost, |
||||
|
inputElement |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,204 @@ |
|||||
|
import { CaretHelper } from "./caretHelper"; |
||||
|
|
||||
|
enum RawInputModifiers { |
||||
|
None = 0, |
||||
|
Alt = 1, |
||||
|
Control = 2, |
||||
|
Shift = 4, |
||||
|
Meta = 8, |
||||
|
|
||||
|
LeftMouseButton = 16, |
||||
|
RightMouseButton = 32, |
||||
|
MiddleMouseButton = 64, |
||||
|
XButton1MouseButton = 128, |
||||
|
XButton2MouseButton = 256, |
||||
|
KeyboardMask = Alt | Control | Shift | Meta, |
||||
|
|
||||
|
PenInverted = 512, |
||||
|
PenEraser = 1024, |
||||
|
PenBarrelButton = 2048 |
||||
|
} |
||||
|
|
||||
|
export class InputHelper { |
||||
|
public static subscribeKeyEvents( |
||||
|
element: HTMLInputElement, |
||||
|
keyDownCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, |
||||
|
keyUpCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean) { |
||||
|
const keyDownHandler = (args: KeyboardEvent) => { |
||||
|
if (keyDownCallback(args.code, args.key, this.getModifiers(args))) { |
||||
|
args.preventDefault(); |
||||
|
} |
||||
|
}; |
||||
|
element.addEventListener("keydown", keyDownHandler); |
||||
|
|
||||
|
const keyUpHandler = (args: KeyboardEvent) => { |
||||
|
if (keyUpCallback(args.code, args.key, this.getModifiers(args))) { |
||||
|
args.preventDefault(); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
element.addEventListener("keyup", keyUpHandler); |
||||
|
|
||||
|
return () => { |
||||
|
element.removeEventListener("keydown", keyDownHandler); |
||||
|
element.removeEventListener("keyup", keyUpHandler); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public static subscribeTextEvents( |
||||
|
element: HTMLInputElement, |
||||
|
inputCallback: (type: string, data: string | null) => boolean, |
||||
|
compositionStartCallback: (args: CompositionEvent) => boolean, |
||||
|
compositionUpdateCallback: (args: CompositionEvent) => boolean, |
||||
|
compositionEndCallback: (args: CompositionEvent) => boolean) { |
||||
|
const inputHandler = (args: Event) => { |
||||
|
const inputEvent = args as InputEvent; |
||||
|
|
||||
|
// todo check cast
|
||||
|
if (inputCallback(inputEvent.type, inputEvent.data)) { |
||||
|
args.preventDefault(); |
||||
|
} |
||||
|
}; |
||||
|
element.addEventListener("input", inputHandler); |
||||
|
|
||||
|
const compositionStartHandler = (args: CompositionEvent) => { |
||||
|
if (compositionStartCallback(args)) { |
||||
|
args.preventDefault(); |
||||
|
} |
||||
|
}; |
||||
|
element.addEventListener("compositionstart", compositionStartHandler); |
||||
|
|
||||
|
const compositionUpdateHandler = (args: CompositionEvent) => { |
||||
|
if (compositionUpdateCallback(args)) { |
||||
|
args.preventDefault(); |
||||
|
} |
||||
|
}; |
||||
|
element.addEventListener("compositionupdate", compositionUpdateHandler); |
||||
|
|
||||
|
const compositionEndHandler = (args: CompositionEvent) => { |
||||
|
if (compositionEndCallback(args)) { |
||||
|
args.preventDefault(); |
||||
|
} |
||||
|
}; |
||||
|
element.addEventListener("compositionend", compositionEndHandler); |
||||
|
|
||||
|
return () => { |
||||
|
element.removeEventListener("input", inputHandler); |
||||
|
element.removeEventListener("compositionstart", compositionStartHandler); |
||||
|
element.removeEventListener("compositionupdate", compositionUpdateHandler); |
||||
|
element.removeEventListener("compositionend", compositionEndHandler); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public static subscribePointerEvents( |
||||
|
element: HTMLInputElement, |
||||
|
pointerMoveCallback: (args: PointerEvent) => boolean, |
||||
|
pointerDownCallback: (args: PointerEvent) => boolean, |
||||
|
pointerUpCallback: (args: PointerEvent) => boolean, |
||||
|
wheelCallback: (args: WheelEvent) => boolean |
||||
|
) { |
||||
|
const pointerMoveHandler = (args: PointerEvent) => { |
||||
|
if (pointerMoveCallback(args)) { |
||||
|
args.preventDefault(); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const pointerDownHandler = (args: PointerEvent) => { |
||||
|
if (pointerDownCallback(args)) { |
||||
|
args.preventDefault(); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const pointerUpHandler = (args: PointerEvent) => { |
||||
|
if (pointerUpCallback(args)) { |
||||
|
args.preventDefault(); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const wheelHandler = (args: WheelEvent) => { |
||||
|
if (wheelCallback(args)) { |
||||
|
args.preventDefault(); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
element.addEventListener("pointermove", pointerMoveHandler); |
||||
|
element.addEventListener("pointerdown", pointerDownHandler); |
||||
|
element.addEventListener("pointerup", pointerUpHandler); |
||||
|
element.addEventListener("wheel", wheelHandler); |
||||
|
|
||||
|
return () => { |
||||
|
element.removeEventListener("pointerover", pointerMoveHandler); |
||||
|
element.removeEventListener("pointerdown", pointerDownHandler); |
||||
|
element.removeEventListener("pointerup", pointerUpHandler); |
||||
|
element.removeEventListener("wheel", wheelHandler); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public static subscribeInputEvents( |
||||
|
element: HTMLInputElement, |
||||
|
inputCallback: (value: string) => boolean |
||||
|
) { |
||||
|
const inputHandler = (args: Event) => { |
||||
|
if (inputCallback((args as any).value)) { |
||||
|
args.preventDefault(); |
||||
|
} |
||||
|
}; |
||||
|
element.addEventListener("input", inputHandler); |
||||
|
|
||||
|
return () => { |
||||
|
element.removeEventListener("input", inputHandler); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public static clearInput(inputElement: HTMLInputElement) { |
||||
|
inputElement.value = ""; |
||||
|
} |
||||
|
|
||||
|
public static focusElement(inputElement: HTMLElement) { |
||||
|
inputElement.focus(); |
||||
|
} |
||||
|
|
||||
|
public static setCursor(inputElement: HTMLInputElement, kind: string) { |
||||
|
inputElement.style.cursor = kind; |
||||
|
} |
||||
|
|
||||
|
public static setBounds(inputElement: HTMLInputElement, x: number, y: number, caretWidth: number, caretHeight: number, caret: number) { |
||||
|
inputElement.style.left = (x).toFixed(0) + "px"; |
||||
|
inputElement.style.top = (y).toFixed(0) + "px"; |
||||
|
|
||||
|
const { left, top } = CaretHelper.getCaretCoordinates(inputElement, caret); |
||||
|
|
||||
|
inputElement.style.left = (x - left).toFixed(0) + "px"; |
||||
|
inputElement.style.top = (y - top).toFixed(0) + "px"; |
||||
|
} |
||||
|
|
||||
|
public static hide(inputElement: HTMLInputElement) { |
||||
|
inputElement.style.display = "none"; |
||||
|
} |
||||
|
|
||||
|
public static show(inputElement: HTMLInputElement) { |
||||
|
inputElement.style.display = "block"; |
||||
|
} |
||||
|
|
||||
|
public static setSurroundingText(inputElement: HTMLInputElement, text: string, start: number, end: number) { |
||||
|
if (!inputElement) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
inputElement.value = text; |
||||
|
inputElement.setSelectionRange(start, end); |
||||
|
inputElement.style.width = "20px"; |
||||
|
inputElement.style.width = `${inputElement.scrollWidth}px`; |
||||
|
} |
||||
|
|
||||
|
private static getModifiers(args: KeyboardEvent): RawInputModifiers { |
||||
|
let modifiers = RawInputModifiers.None; |
||||
|
|
||||
|
if (args.ctrlKey) { modifiers |= RawInputModifiers.Control; } |
||||
|
if (args.altKey) { modifiers |= RawInputModifiers.Alt; } |
||||
|
if (args.shiftKey) { modifiers |= RawInputModifiers.Shift; } |
||||
|
if (args.metaKey) { modifiers |= RawInputModifiers.Meta; } |
||||
|
|
||||
|
return modifiers; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
class NativeControlHostTopLevelAttachment { |
||||
|
_child?: HTMLElement; |
||||
|
_host?: HTMLElement; |
||||
|
} |
||||
|
|
||||
|
export class NativeControlHost { |
||||
|
public static createDefaultChild(parent?: HTMLElement): HTMLElement { |
||||
|
return document.createElement("div"); |
||||
|
} |
||||
|
|
||||
|
public static createAttachment(): NativeControlHostTopLevelAttachment { |
||||
|
return new NativeControlHostTopLevelAttachment(); |
||||
|
} |
||||
|
|
||||
|
public static initializeWithChildHandle(element: NativeControlHostTopLevelAttachment, child: HTMLElement): void { |
||||
|
element._child = child; |
||||
|
element._child.style.position = "absolute"; |
||||
|
} |
||||
|
|
||||
|
public static attachTo(element: NativeControlHostTopLevelAttachment, host?: HTMLElement): void { |
||||
|
if (element._host && element._child) { |
||||
|
element._host.removeChild(element._child); |
||||
|
} |
||||
|
|
||||
|
element._host = host; |
||||
|
|
||||
|
if (element._host && element._child) { |
||||
|
element._host.appendChild(element._child); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static showInBounds(element: NativeControlHostTopLevelAttachment, x: number, y: number, width: number, height: number): void { |
||||
|
if (element._child) { |
||||
|
element._child.style.top = `${y}px`; |
||||
|
element._child.style.left = `${x}px`; |
||||
|
element._child.style.width = `${width}px`; |
||||
|
element._child.style.height = `${height}px`; |
||||
|
element._child.style.display = "block"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static hideWithSize(element: NativeControlHostTopLevelAttachment, width: number, height: number): void { |
||||
|
if (element._child) { |
||||
|
element._child.style.width = `${width}px`; |
||||
|
element._child.style.height = `${height}px`; |
||||
|
element._child.style.display = "none"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static releaseChild(element: NativeControlHostTopLevelAttachment): void { |
||||
|
if (element._child) { |
||||
|
element._child = undefined; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
import { IMemoryView } from "../../types/dotnet"; |
||||
|
|
||||
|
export class StreamHelper { |
||||
|
public static async seek(stream: FileSystemWritableFileStream, position: number) { |
||||
|
return await stream.seek(position); |
||||
|
} |
||||
|
|
||||
|
public static async truncate(stream: FileSystemWritableFileStream, size: number) { |
||||
|
return await stream.truncate(size); |
||||
|
} |
||||
|
|
||||
|
public static async close(stream: FileSystemWritableFileStream) { |
||||
|
return await stream.close(); |
||||
|
} |
||||
|
|
||||
|
public static async write(stream: FileSystemWritableFileStream, span: IMemoryView) { |
||||
|
const array = new Uint8Array(span.byteLength); |
||||
|
span.copyTo(array); |
||||
|
|
||||
|
const data: WriteParams = { |
||||
|
type: "write", |
||||
|
data: array |
||||
|
}; |
||||
|
|
||||
|
return await stream.write(data); |
||||
|
} |
||||
|
|
||||
|
public static byteLength(stream: Blob) { |
||||
|
return stream.size; |
||||
|
} |
||||
|
|
||||
|
public static async sliceArrayBuffer(stream: Blob, offset: number, count: number) { |
||||
|
const buffer = await stream.slice(offset, offset + count).arrayBuffer(); |
||||
|
return new Uint8Array(buffer); |
||||
|
} |
||||
|
|
||||
|
public static toMemoryView(buffer: Uint8Array): Uint8Array { |
||||
|
return buffer; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,2 @@ |
|||||
|
export { StorageItem, StorageItems } from "./storage/storageItem"; |
||||
|
export { StorageProvider } from "./storage/storageProvider"; |
||||
@ -0,0 +1,84 @@ |
|||||
|
class InnerDbConnection { |
||||
|
constructor(private readonly database: IDBDatabase) { } |
||||
|
|
||||
|
private openStore(store: string, mode: IDBTransactionMode): IDBObjectStore { |
||||
|
const tx = this.database.transaction(store, mode); |
||||
|
return tx.objectStore(store); |
||||
|
} |
||||
|
|
||||
|
public async put(store: string, obj: any, key?: IDBValidKey): Promise<IDBValidKey> { |
||||
|
const os = this.openStore(store, "readwrite"); |
||||
|
|
||||
|
return await new Promise((resolve, reject) => { |
||||
|
const response = os.put(obj, key); |
||||
|
response.onsuccess = () => { |
||||
|
resolve(response.result); |
||||
|
}; |
||||
|
response.onerror = () => { |
||||
|
reject(response.error); |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public get(store: string, key: IDBValidKey): any { |
||||
|
const os = this.openStore(store, "readonly"); |
||||
|
|
||||
|
return new Promise((resolve, reject) => { |
||||
|
const response = os.get(key); |
||||
|
response.onsuccess = () => { |
||||
|
resolve(response.result); |
||||
|
}; |
||||
|
response.onerror = () => { |
||||
|
reject(response.error); |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async delete(store: string, key: IDBValidKey): Promise<void> { |
||||
|
const os = this.openStore(store, "readwrite"); |
||||
|
|
||||
|
return await new Promise((resolve, reject) => { |
||||
|
const response = os.delete(key); |
||||
|
response.onsuccess = () => { |
||||
|
resolve(); |
||||
|
}; |
||||
|
response.onerror = () => { |
||||
|
reject(response.error); |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public close() { |
||||
|
this.database.close(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class IndexedDbWrapper { |
||||
|
constructor(private readonly databaseName: string, private readonly objectStores: [string]) { |
||||
|
} |
||||
|
|
||||
|
public async connect(): Promise<InnerDbConnection> { |
||||
|
const conn = window.indexedDB.open(this.databaseName, 1); |
||||
|
|
||||
|
conn.onupgradeneeded = event => { |
||||
|
const db = (event.target as IDBRequest<IDBDatabase>).result; |
||||
|
this.objectStores.forEach(store => { |
||||
|
db.createObjectStore(store); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
return await new Promise((resolve, reject) => { |
||||
|
conn.onsuccess = event => { |
||||
|
resolve(new InnerDbConnection((event.target as IDBRequest<IDBDatabase>).result)); |
||||
|
}; |
||||
|
conn.onerror = event => { |
||||
|
reject((event.target as IDBRequest<IDBDatabase>).error); |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const fileBookmarksStore: string = "fileBookmarks"; |
||||
|
export const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [ |
||||
|
fileBookmarksStore |
||||
|
]); |
||||
@ -0,0 +1,111 @@ |
|||||
|
import { avaloniaDb, fileBookmarksStore } from "./indexedDb"; |
||||
|
|
||||
|
export class StorageItem { |
||||
|
constructor(public handle: FileSystemHandle, private readonly bookmarkId?: string) { } |
||||
|
|
||||
|
public get name(): string { |
||||
|
return this.handle.name; |
||||
|
} |
||||
|
|
||||
|
public get kind(): string { |
||||
|
return this.handle.kind; |
||||
|
} |
||||
|
|
||||
|
public static async openRead(item: StorageItem): Promise<Blob> { |
||||
|
if (!(item.handle instanceof FileSystemFileHandle)) { |
||||
|
throw new Error("StorageItem is not a file"); |
||||
|
} |
||||
|
|
||||
|
await item.verityPermissions("read"); |
||||
|
|
||||
|
const file = await item.handle.getFile(); |
||||
|
return file; |
||||
|
} |
||||
|
|
||||
|
public static async openWrite(item: StorageItem): Promise<FileSystemWritableFileStream> { |
||||
|
if (!(item.handle instanceof FileSystemFileHandle)) { |
||||
|
throw new Error("StorageItem is not a file"); |
||||
|
} |
||||
|
|
||||
|
await item.verityPermissions("readwrite"); |
||||
|
|
||||
|
return await item.handle.createWritable({ keepExistingData: true }); |
||||
|
} |
||||
|
|
||||
|
public static async getProperties(item: StorageItem): Promise<{ Size: number; LastModified: number; Type: string } | null> { |
||||
|
const file = item.handle instanceof FileSystemFileHandle && |
||||
|
await item.handle.getFile(); |
||||
|
|
||||
|
if (!file) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
Size: file.size, |
||||
|
LastModified: file.lastModified, |
||||
|
Type: file.type |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public static async getItems(item: StorageItem): Promise<StorageItems> { |
||||
|
if (item.handle.kind !== "directory") { |
||||
|
return new StorageItems([]); |
||||
|
} |
||||
|
|
||||
|
const items: StorageItem[] = []; |
||||
|
for await (const [, value] of (item.handle as any).entries()) { |
||||
|
items.push(new StorageItem(value)); |
||||
|
} |
||||
|
return new StorageItems(items); |
||||
|
} |
||||
|
|
||||
|
private async verityPermissions(mode: FileSystemPermissionMode): Promise<void | never> { |
||||
|
if (await this.handle.queryPermission({ mode }) === "granted") { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (await this.handle.requestPermission({ mode }) === "denied") { |
||||
|
throw new Error("Permissions denied"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static async saveBookmark(item: StorageItem): Promise<string> { |
||||
|
// If file was previously bookmarked, just return old one.
|
||||
|
if (item.bookmarkId) { |
||||
|
return item.bookmarkId; |
||||
|
} |
||||
|
|
||||
|
const connection = await avaloniaDb.connect(); |
||||
|
try { |
||||
|
const key = await connection.put(fileBookmarksStore, item.handle, item.generateBookmarkId()); |
||||
|
return key as string; |
||||
|
} finally { |
||||
|
connection.close(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static async deleteBookmark(item: StorageItem): Promise<void> { |
||||
|
if (!item.bookmarkId) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const connection = await avaloniaDb.connect(); |
||||
|
try { |
||||
|
await connection.delete(fileBookmarksStore, item.bookmarkId); |
||||
|
} finally { |
||||
|
connection.close(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private generateBookmarkId(): string { |
||||
|
return Date.now().toString(36) + Math.random().toString(36).substring(2); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class StorageItems { |
||||
|
constructor(private readonly items: StorageItem[]) { } |
||||
|
|
||||
|
public static itemsArray(instance: StorageItems): StorageItem[] { |
||||
|
return instance.items; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,70 @@ |
|||||
|
import { avaloniaDb, fileBookmarksStore } from "./indexedDb"; |
||||
|
import { StorageItem, StorageItems } from "./storageItem"; |
||||
|
|
||||
|
declare global { |
||||
|
type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; |
||||
|
type StartInDirectory = WellKnownDirectory | FileSystemHandle; |
||||
|
interface OpenFilePickerOptions { |
||||
|
startIn?: StartInDirectory; |
||||
|
} |
||||
|
interface SaveFilePickerOptions { |
||||
|
startIn?: StartInDirectory; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class StorageProvider { |
||||
|
public static async selectFolderDialog( |
||||
|
startIn: StorageItem | null): Promise<StorageItem> { |
||||
|
// 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined.
|
||||
|
const options: DirectoryPickerOptions = { |
||||
|
startIn: (startIn?.handle ?? undefined) |
||||
|
}; |
||||
|
|
||||
|
const handle = await window.showDirectoryPicker(options); |
||||
|
return new StorageItem(handle); |
||||
|
} |
||||
|
|
||||
|
public static async openFileDialog( |
||||
|
startIn: StorageItem | null, multiple: boolean, |
||||
|
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItems> { |
||||
|
const options: OpenFilePickerOptions = { |
||||
|
startIn: (startIn?.handle ?? undefined), |
||||
|
multiple, |
||||
|
excludeAcceptAllOption, |
||||
|
types: (types ?? undefined) |
||||
|
}; |
||||
|
|
||||
|
const handles = await window.showOpenFilePicker(options); |
||||
|
return new StorageItems(handles.map((handle: FileSystemHandle) => new StorageItem(handle))); |
||||
|
} |
||||
|
|
||||
|
public static async saveFileDialog( |
||||
|
startIn: StorageItem | null, suggestedName: string | null, |
||||
|
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItem> { |
||||
|
const options: SaveFilePickerOptions = { |
||||
|
startIn: (startIn?.handle ?? undefined), |
||||
|
suggestedName: (suggestedName ?? undefined), |
||||
|
excludeAcceptAllOption, |
||||
|
types: (types ?? undefined) |
||||
|
}; |
||||
|
|
||||
|
const handle = await window.showSaveFilePicker(options); |
||||
|
return new StorageItem(handle); |
||||
|
} |
||||
|
|
||||
|
public static async openBookmark(key: string): Promise<StorageItem | null> { |
||||
|
const connection = await avaloniaDb.connect(); |
||||
|
try { |
||||
|
const handle = await connection.get(fileBookmarksStore, key); |
||||
|
return handle && new StorageItem(handle, key); |
||||
|
} finally { |
||||
|
connection.close(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static createAcceptType(description: string, mimeTypes: string[]): FilePickerAcceptType { |
||||
|
const accept: Record<string, string[]> = {}; |
||||
|
mimeTypes.forEach(a => { accept[a] = []; }); |
||||
|
return { description, accept }; |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,22 @@ |
|||||
|
{ |
||||
|
"name": "avalonia.web", |
||||
|
"scripts": { |
||||
|
"typecheck": "npx tsc -noEmit", |
||||
|
"eslint": "npx eslint . --fix", |
||||
|
"prebuild": "npm-run-all typecheck eslint", |
||||
|
"build": "node build.js" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"@types/emscripten": "^1.39.6", |
||||
|
"@types/wicg-file-system-access": "^2020.9.5", |
||||
|
"@typescript-eslint/eslint-plugin": "^5.38.1", |
||||
|
"esbuild": "^0.15.7", |
||||
|
"eslint": "^8.24.0", |
||||
|
"eslint-config-standard-with-typescript": "^23.0.0", |
||||
|
"eslint-plugin-import": "^2.26.0", |
||||
|
"eslint-plugin-n": "^15.3.0", |
||||
|
"eslint-plugin-promise": "^6.0.1", |
||||
|
"npm-run-all": "^4.1.5", |
||||
|
"typescript": "^4.8.3" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
{ |
||||
|
"compilerOptions": { |
||||
|
"target": "es2016", |
||||
|
"module": "es2020", |
||||
|
"strict": true, |
||||
|
"sourceMap": true, |
||||
|
"noEmitOnError": true, |
||||
|
"isolatedModules": true, // we need it for esbuild |
||||
|
"lib": [ |
||||
|
"dom", |
||||
|
"es2016", |
||||
|
"esnext.asynciterable" |
||||
|
] |
||||
|
}, |
||||
|
"exclude": [ |
||||
|
"node_modules" |
||||
|
] |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,270 @@ |
|||||
|
// See https://raw.githubusercontent.com/dotnet/runtime/main/src/mono/wasm/runtime/dotnet.d.ts
|
||||
|
|
||||
|
//! Licensed to the .NET Foundation under one or more agreements.
|
||||
|
//! The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
//!
|
||||
|
//! This is generated file, see src/mono/wasm/runtime/rollup.config.js
|
||||
|
|
||||
|
//! This is not considered public API with backward compatibility guarantees.
|
||||
|
|
||||
|
interface DotnetHostBuilder { |
||||
|
withConfig(config: MonoConfig): DotnetHostBuilder; |
||||
|
withConfigSrc(configSrc: string): DotnetHostBuilder; |
||||
|
withApplicationArguments(...args: string[]): DotnetHostBuilder; |
||||
|
withEnvironmentVariable(name: string, value: string): DotnetHostBuilder; |
||||
|
withEnvironmentVariables(variables: { |
||||
|
[i: string]: string; |
||||
|
}): DotnetHostBuilder; |
||||
|
withVirtualWorkingDirectory(vfsPath: string): DotnetHostBuilder; |
||||
|
withDiagnosticTracing(enabled: boolean): DotnetHostBuilder; |
||||
|
withDebugging(level: number): DotnetHostBuilder; |
||||
|
withMainAssembly(mainAssemblyName: string): DotnetHostBuilder; |
||||
|
withApplicationArgumentsFromQuery(): DotnetHostBuilder; |
||||
|
create(): Promise<RuntimeAPI>; |
||||
|
run(): Promise<number>; |
||||
|
} |
||||
|
|
||||
|
declare interface NativePointer { |
||||
|
__brandNativePointer: "NativePointer"; |
||||
|
} |
||||
|
declare interface VoidPtr extends NativePointer { |
||||
|
__brand: "VoidPtr"; |
||||
|
} |
||||
|
declare interface CharPtr extends NativePointer { |
||||
|
__brand: "CharPtr"; |
||||
|
} |
||||
|
declare interface Int32Ptr extends NativePointer { |
||||
|
__brand: "Int32Ptr"; |
||||
|
} |
||||
|
declare interface EmscriptenModule { |
||||
|
HEAP8: Int8Array; |
||||
|
HEAP16: Int16Array; |
||||
|
HEAP32: Int32Array; |
||||
|
HEAPU8: Uint8Array; |
||||
|
HEAPU16: Uint16Array; |
||||
|
HEAPU32: Uint32Array; |
||||
|
HEAPF32: Float32Array; |
||||
|
HEAPF64: Float64Array; |
||||
|
_malloc(size: number): VoidPtr; |
||||
|
_free(ptr: VoidPtr): void; |
||||
|
print(message: string): void; |
||||
|
printErr(message: string): void; |
||||
|
ccall<T>(ident: string, returnType?: string | null, argTypes?: string[], args?: any[], opts?: any): T; |
||||
|
cwrap<T extends Function>(ident: string, returnType: string, argTypes?: string[], opts?: any): T; |
||||
|
cwrap<T extends Function>(ident: string, ...args: any[]): T; |
||||
|
setValue(ptr: VoidPtr, value: number, type: string, noSafe?: number | boolean): void; |
||||
|
setValue(ptr: Int32Ptr, value: number, type: string, noSafe?: number | boolean): void; |
||||
|
getValue(ptr: number, type: string, noSafe?: number | boolean): number; |
||||
|
UTF8ToString(ptr: CharPtr, maxBytesToRead?: number): string; |
||||
|
UTF8ArrayToString(u8Array: Uint8Array, idx?: number, maxBytesToRead?: number): string; |
||||
|
FS_createPath(parent: string, path: string, canRead?: boolean, canWrite?: boolean): string; |
||||
|
FS_createDataFile(parent: string, name: string, data: TypedArray, canRead: boolean, canWrite: boolean, canOwn?: boolean): string; |
||||
|
FS_readFile(filename: string, opts: any): any; |
||||
|
removeRunDependency(id: string): void; |
||||
|
addRunDependency(id: string): void; |
||||
|
stackSave(): VoidPtr; |
||||
|
stackRestore(stack: VoidPtr): void; |
||||
|
stackAlloc(size: number): VoidPtr; |
||||
|
ready: Promise<unknown>; |
||||
|
instantiateWasm?: InstantiateWasmCallBack; |
||||
|
preInit?: (() => any)[] | (() => any); |
||||
|
preRun?: (() => any)[] | (() => any); |
||||
|
onRuntimeInitialized?: () => any; |
||||
|
postRun?: (() => any)[] | (() => any); |
||||
|
onAbort?: { |
||||
|
(error: any): void; |
||||
|
}; |
||||
|
} |
||||
|
declare type InstantiateWasmSuccessCallback = (instance: WebAssembly.Instance, module: WebAssembly.Module) => void; |
||||
|
declare type InstantiateWasmCallBack = (imports: WebAssembly.Imports, successCallback: InstantiateWasmSuccessCallback) => any; |
||||
|
declare type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array; |
||||
|
|
||||
|
declare type MonoConfig = { |
||||
|
/** |
||||
|
* The subfolder containing managed assemblies and pdbs. This is relative to dotnet.js script. |
||||
|
*/ |
||||
|
assemblyRootFolder?: string; |
||||
|
/** |
||||
|
* A list of assets to load along with the runtime. |
||||
|
*/ |
||||
|
assets?: AssetEntry[]; |
||||
|
/** |
||||
|
* Additional search locations for assets. |
||||
|
*/ |
||||
|
remoteSources?: string[]; |
||||
|
/** |
||||
|
* It will not fail the startup is .pdb files can't be downloaded |
||||
|
*/ |
||||
|
ignorePdbLoadErrors?: boolean; |
||||
|
/** |
||||
|
* We are throttling parallel downloads in order to avoid net::ERR_INSUFFICIENT_RESOURCES on chrome. The default value is 16. |
||||
|
*/ |
||||
|
maxParallelDownloads?: number; |
||||
|
/** |
||||
|
* Name of the assembly with main entrypoint |
||||
|
*/ |
||||
|
mainAssemblyName?: string; |
||||
|
/** |
||||
|
* Configures the runtime's globalization mode |
||||
|
*/ |
||||
|
globalizationMode?: GlobalizationMode; |
||||
|
/** |
||||
|
* debugLevel > 0 enables debugging and sets the debug log level to debugLevel |
||||
|
* debugLevel == 0 disables debugging and enables interpreter optimizations |
||||
|
* debugLevel < 0 enabled debugging and disables debug logging. |
||||
|
*/ |
||||
|
debugLevel?: number; |
||||
|
/** |
||||
|
* Enables diagnostic log messages during startup |
||||
|
*/ |
||||
|
diagnosticTracing?: boolean; |
||||
|
/** |
||||
|
* Dictionary-style Object containing environment variables |
||||
|
*/ |
||||
|
environmentVariables?: { |
||||
|
[i: string]: string; |
||||
|
}; |
||||
|
/** |
||||
|
* initial number of workers to add to the emscripten pthread pool |
||||
|
*/ |
||||
|
pthreadPoolSize?: number; |
||||
|
}; |
||||
|
interface ResourceRequest { |
||||
|
name: string; |
||||
|
behavior: AssetBehaviours; |
||||
|
resolvedUrl?: string; |
||||
|
hash?: string; |
||||
|
} |
||||
|
interface LoadingResource { |
||||
|
name: string; |
||||
|
url: string; |
||||
|
response: Promise<Response>; |
||||
|
} |
||||
|
interface AssetEntry extends ResourceRequest { |
||||
|
/** |
||||
|
* If specified, overrides the path of the asset in the virtual filesystem and similar data structures once downloaded. |
||||
|
*/ |
||||
|
virtualPath?: string; |
||||
|
/** |
||||
|
* Culture code |
||||
|
*/ |
||||
|
culture?: string; |
||||
|
/** |
||||
|
* If true, an attempt will be made to load the asset from each location in MonoConfig.remoteSources. |
||||
|
*/ |
||||
|
loadRemote?: boolean; |
||||
|
/** |
||||
|
* If true, the runtime startup would not fail if the asset download was not successful. |
||||
|
*/ |
||||
|
isOptional?: boolean; |
||||
|
/** |
||||
|
* If provided, runtime doesn't have to fetch the data. |
||||
|
* Runtime would set the buffer to null after instantiation to free the memory. |
||||
|
*/ |
||||
|
buffer?: ArrayBuffer; |
||||
|
/** |
||||
|
* It's metadata + fetch-like Promise<Response> |
||||
|
* If provided, the runtime doesn't have to initiate the download. It would just await the response. |
||||
|
*/ |
||||
|
pendingDownload?: LoadingResource; |
||||
|
} |
||||
|
declare type AssetBehaviours = "resource" | "assembly" | "pdb" | "heap" | "icu" | "vfs" | "dotnetwasm" | "js-module-threads"; |
||||
|
declare type GlobalizationMode = "icu" | // load ICU globalization data from any runtime assets with behavior "icu".
|
||||
|
"invariant" | // operate in invariant globalization mode.
|
||||
|
"auto"; |
||||
|
declare type DotnetModuleConfig = { |
||||
|
disableDotnet6Compatibility?: boolean; |
||||
|
config?: MonoConfig; |
||||
|
configSrc?: string; |
||||
|
onConfigLoaded?: (config: MonoConfig) => void | Promise<void>; |
||||
|
onDotnetReady?: () => void | Promise<void>; |
||||
|
imports?: any; |
||||
|
exports?: string[]; |
||||
|
downloadResource?: (request: ResourceRequest) => LoadingResource | undefined; |
||||
|
} & Partial<EmscriptenModule>; |
||||
|
declare type APIType = { |
||||
|
runMain: (mainAssemblyName: string, args: string[]) => Promise<number>; |
||||
|
runMainAndExit: (mainAssemblyName: string, args: string[]) => Promise<number>; |
||||
|
setEnvironmentVariable: (name: string, value: string) => void; |
||||
|
getAssemblyExports(assemblyName: string): Promise<any>; |
||||
|
setModuleImports(moduleName: string, moduleImports: any): void; |
||||
|
getConfig: () => MonoConfig; |
||||
|
setHeapB32: (offset: NativePointer, value: number | boolean) => void; |
||||
|
setHeapU8: (offset: NativePointer, value: number) => void; |
||||
|
setHeapU16: (offset: NativePointer, value: number) => void; |
||||
|
setHeapU32: (offset: NativePointer, value: NativePointer | number) => void; |
||||
|
setHeapI8: (offset: NativePointer, value: number) => void; |
||||
|
setHeapI16: (offset: NativePointer, value: number) => void; |
||||
|
setHeapI32: (offset: NativePointer, value: number) => void; |
||||
|
setHeapI52: (offset: NativePointer, value: number) => void; |
||||
|
setHeapU52: (offset: NativePointer, value: number) => void; |
||||
|
setHeapI64Big: (offset: NativePointer, value: bigint) => void; |
||||
|
setHeapF32: (offset: NativePointer, value: number) => void; |
||||
|
setHeapF64: (offset: NativePointer, value: number) => void; |
||||
|
getHeapB32: (offset: NativePointer) => boolean; |
||||
|
getHeapU8: (offset: NativePointer) => number; |
||||
|
getHeapU16: (offset: NativePointer) => number; |
||||
|
getHeapU32: (offset: NativePointer) => number; |
||||
|
getHeapI8: (offset: NativePointer) => number; |
||||
|
getHeapI16: (offset: NativePointer) => number; |
||||
|
getHeapI32: (offset: NativePointer) => number; |
||||
|
getHeapI52: (offset: NativePointer) => number; |
||||
|
getHeapU52: (offset: NativePointer) => number; |
||||
|
getHeapI64Big: (offset: NativePointer) => bigint; |
||||
|
getHeapF32: (offset: NativePointer) => number; |
||||
|
getHeapF64: (offset: NativePointer) => number; |
||||
|
}; |
||||
|
declare type RuntimeAPI = { |
||||
|
/** |
||||
|
* @deprecated Please use API object instead. See also MONOType in dotnet-legacy.d.ts |
||||
|
*/ |
||||
|
MONO: any; |
||||
|
/** |
||||
|
* @deprecated Please use API object instead. See also BINDINGType in dotnet-legacy.d.ts |
||||
|
*/ |
||||
|
BINDING: any; |
||||
|
INTERNAL: any; |
||||
|
Module: EmscriptenModule; |
||||
|
runtimeId: number; |
||||
|
runtimeBuildInfo: { |
||||
|
productVersion: string; |
||||
|
gitHash: string; |
||||
|
buildConfiguration: string; |
||||
|
}; |
||||
|
} & APIType; |
||||
|
declare type ModuleAPI = { |
||||
|
dotnet: DotnetHostBuilder; |
||||
|
exit: (code: number, reason?: any) => void; |
||||
|
}; |
||||
|
declare function createDotnetRuntime(moduleFactory: DotnetModuleConfig | ((api: RuntimeAPI) => DotnetModuleConfig)): Promise<RuntimeAPI>; |
||||
|
declare type CreateDotnetRuntimeType = typeof createDotnetRuntime; |
||||
|
|
||||
|
declare global { |
||||
|
function getDotnetRuntime(runtimeId: number): RuntimeAPI | undefined; |
||||
|
} |
||||
|
|
||||
|
declare const dotnet: ModuleAPI["dotnet"]; |
||||
|
declare const exit: ModuleAPI["exit"]; |
||||
|
|
||||
|
export { CreateDotnetRuntimeType, DotnetModuleConfig, EmscriptenModule, ModuleAPI, MonoConfig, RuntimeAPI, createDotnetRuntime as default, dotnet, exit }; |
||||
|
|
||||
|
export interface IMemoryView { |
||||
|
/** |
||||
|
* copies elements from provided source to the wasm memory. |
||||
|
* target has to have the elements of the same type as the underlying C# array. |
||||
|
* same as TypedArray.set() |
||||
|
*/ |
||||
|
set(source: TypedArray, targetOffset?: number): void; |
||||
|
/** |
||||
|
* copies elements from wasm memory to provided target. |
||||
|
* target has to have the elements of the same type as the underlying C# array. |
||||
|
*/ |
||||
|
copyTo(target: TypedArray, sourceOffset?: number): void; |
||||
|
/** |
||||
|
* same as TypedArray.slice() |
||||
|
*/ |
||||
|
slice(start?: number, end?: number): TypedArray; |
||||
|
|
||||
|
get length(): number; |
||||
|
get byteLength(): number; |
||||
|
} |
||||
Loading…
Reference in new issue