Browse Source
Remove old blazor backend. Keep blazor components with new WASM implementation.pull/9161/head
committed by
GitHub
96 changed files with 435 additions and 3954 deletions
@ -0,0 +1,17 @@ |
|||
using Avalonia; |
|||
using Avalonia.Web.Blazor; |
|||
|
|||
namespace ControlCatalog.Blazor.Web; |
|||
|
|||
public partial class App |
|||
{ |
|||
protected override void OnParametersSet() |
|||
{ |
|||
AppBuilder.Configure<ControlCatalog.App>() |
|||
.UseBlazor() |
|||
// .With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering
|
|||
.SetupWithSingleViewLifetime(); |
|||
|
|||
base.OnParametersSet(); |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> |
|||
<PropertyGroup> |
|||
<TargetFramework>net7.0</TargetFramework> |
|||
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier> |
|||
<Nullable>enable</Nullable> |
|||
<EmccTotalMemory>16777216</EmccTotalMemory> |
|||
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport> |
|||
<BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.0-rc.1.22427.2" /> |
|||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.0-rc.1.22427.2" PrivateAssets="all" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" /> |
|||
<ProjectReference Include="..\..\src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.csproj" /> |
|||
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<Import Project="..\..\build\ReferenceCoreLibraries.props" /> |
|||
<Import Project="..\..\build\BuildTargets.targets" /> |
|||
|
|||
<Import Project="..\..\src\Web\Avalonia.Web\Avalonia.Web.props" /> |
|||
<Import Project="..\..\src\Web\Avalonia.Web\Avalonia.Web.targets" /> |
|||
|
|||
</Project> |
|||
|
|||
@ -0,0 +1,29 @@ |
|||
using System; |
|||
using System.Net.Http; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using ControlCatalog.Blazor.Web; |
|||
|
|||
public class Program |
|||
{ |
|||
public static async Task Main(string[] args) |
|||
{ |
|||
await CreateHostBuilder(args).Build().RunAsync(); |
|||
} |
|||
|
|||
public static WebAssemblyHostBuilder CreateHostBuilder(string[] args) |
|||
{ |
|||
var builder = WebAssemblyHostBuilder.CreateDefault(args); |
|||
|
|||
builder.RootComponents.Add<App>("#app"); |
|||
|
|||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); |
|||
|
|||
return builder; |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 172 KiB |
@ -1,20 +0,0 @@ |
|||
using Avalonia; |
|||
using Avalonia.Web.Blazor; |
|||
|
|||
namespace ControlCatalog.Web; |
|||
|
|||
public partial class App |
|||
{ |
|||
protected override void OnParametersSet() |
|||
{ |
|||
WebAppBuilder.Configure<ControlCatalog.App>() |
|||
.AfterSetup(_ => |
|||
{ |
|||
ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb(); |
|||
}) |
|||
.With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering
|
|||
.SetupWithSingleViewLifetime(); |
|||
|
|||
base.OnParametersSet(); |
|||
} |
|||
} |
|||
@ -1,34 +1,42 @@ |
|||
using System; |
|||
|
|||
using Avalonia; |
|||
using System.Runtime.InteropServices.JavaScript; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Web.Blazor; |
|||
using Avalonia.Web; |
|||
|
|||
using ControlCatalog.Pages; |
|||
|
|||
using Microsoft.JSInterop; |
|||
|
|||
namespace ControlCatalog.Web; |
|||
|
|||
public class EmbedSampleWeb : INativeDemoControl |
|||
{ |
|||
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault) |
|||
{ |
|||
var runtime = AvaloniaLocator.Current.GetRequiredService<IJSInProcessRuntime>(); |
|||
|
|||
if (isSecond) |
|||
{ |
|||
var iframe = runtime.Invoke<IJSInProcessObjectReference>("document.createElement", "iframe"); |
|||
iframe.InvokeVoid("setAttribute", "src", "https://www.youtube.com/embed/kZCIporjJ70"); |
|||
var iframe = EmbedInterop.CreateElement("iframe"); |
|||
iframe.SetProperty("src", "https://www.youtube.com/embed/kZCIporjJ70"); |
|||
|
|||
return new JSObjectControlHandle(iframe); |
|||
} |
|||
else |
|||
{ |
|||
// window.createAppButton source is defined in "app.js" file.
|
|||
var button = runtime.Invoke<IJSInProcessObjectReference>("window.createAppButton"); |
|||
var defaultHandle = (JSObjectControlHandle)createDefault(); |
|||
|
|||
_ = JSHost.ImportAsync("embed.js", "./embed.js").ContinueWith(_ => |
|||
{ |
|||
EmbedInterop.AddAppButton(defaultHandle.Object); |
|||
}); |
|||
|
|||
return new JSObjectControlHandle(button); |
|||
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); |
|||
} |
|||
|
|||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,6 @@ |
|||
<linker> |
|||
<assembly fullname="ControlCatalog" preserve="All" /> |
|||
<assembly fullname="ControlCatalog.Web" preserve="All" /> |
|||
<assembly fullname="Avalonia.Themes.Fluent" preserve="All" /> |
|||
<assembly fullname="Avalonia.Themes.Simple" preserve="All" /> |
|||
</linker> |
|||
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 172 KiB |
@ -1,7 +0,0 @@ |
|||
<Project> |
|||
<PropertyGroup> |
|||
<EmccTotalMemory>16777216</EmccTotalMemory> |
|||
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport> |
|||
<BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData> |
|||
</PropertyGroup> |
|||
</Project> |
|||
@ -1,53 +1,40 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk.Razor"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<ImplicitUsings>enable</ImplicitUsings> |
|||
<TargetFramework>net7.0</TargetFramework> |
|||
<PackageId>Avalonia.Web.Blazor</PackageId> |
|||
<LangVersion>preview</LangVersion> |
|||
<MSBuildEnableWorkloadResolver>false</MSBuildEnableWorkloadResolver> |
|||
<StaticWebAssetsDisableProjectBuildPropsFileGeneration>true</StaticWebAssetsDisableProjectBuildPropsFileGeneration> |
|||
<ResolveStaticWebAssetsInputsDependsOn>_IncludeGeneratedAvaloniaStaticFiles;$(ResolveStaticWebAssetsInputsDependsOn)</ResolveStaticWebAssetsInputsDependsOn> |
|||
</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> |
|||
<Content Include="*.props"> |
|||
<Pack>true</Pack> |
|||
<PackagePath>build\</PackagePath> |
|||
</Content> |
|||
<Content Include="*.targets"> |
|||
<Pack>true</Pack> |
|||
<PackagePath>build\;buildTransitive\</PackagePath> |
|||
</Content> |
|||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="7.0.0-*" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.8" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" /> |
|||
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Folder Include="wwwroot\" /> |
|||
<ProjectReference Include="..\Avalonia.Web\Avalonia.Web.csproj" /> |
|||
</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 Name="_IncludeGeneratedAvaloniaStaticFiles"> |
|||
<ItemGroup> |
|||
<_AvaloniaWebAssets Include="$(MSBuildThisFileDirectory)../Avalonia.Web/wwwroot/**/*.*" /> |
|||
</ItemGroup> |
|||
<DefineStaticWebAssets SourceId="$(PackageId)" |
|||
SourceType="Computed" |
|||
AssetKind="All" |
|||
AssetRole="Primary" |
|||
CopyToOutputDirectory="PreserveNewest" |
|||
CopyToPublishDirectory="PreserveNewest" |
|||
ContentRoot="$(MSBuildThisFileDirectory)../Avalonia.Web/wwwroot/" |
|||
BasePath="_content\$(PackageId)" |
|||
CandidateAssets="@(_AvaloniaWebAssets)" |
|||
RelativePathFilter="**.js"> |
|||
<Output TaskParameter="Assets" ItemName="StaticWebAsset" /> |
|||
</DefineStaticWebAssets> |
|||
</Target> |
|||
</Project> |
|||
|
|||
@ -1,4 +0,0 @@ |
|||
<Project> |
|||
<Import Project="Microsoft.AspNetCore.StaticWebAssets.props" /> |
|||
<Import Project="Avalonia.Web.Blazor.CompilationTuning.props" /> |
|||
</Project> |
|||
@ -1,6 +0,0 @@ |
|||
<Project> |
|||
<ItemGroup> |
|||
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\libHarfBuzzSharp.a" /> |
|||
<NativeFileReference Include="$(SkiaSharpStaticLibraryPath)\2.0.23\libSkiaSharp.a" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -1,20 +0,0 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
public class AvaloniaBlazorAppBuilder : AppBuilderBase<AvaloniaBlazorAppBuilder> |
|||
{ |
|||
public AvaloniaBlazorAppBuilder(IRuntimePlatform platform, Action<AvaloniaBlazorAppBuilder> platformServices) |
|||
: base(platform, platformServices) |
|||
{ |
|||
} |
|||
|
|||
public AvaloniaBlazorAppBuilder() |
|||
: base(new StandardRuntimePlatform(), |
|||
builder => StandardRuntimePlatformServices.Register(builder.ApplicationType!.Assembly)) |
|||
{ |
|||
UseWindowingSubsystem(BlazorWindowingPlatform.Register); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
using System.Runtime.InteropServices.JavaScript; |
|||
using System.Runtime.Versioning; |
|||
using System.Threading.Tasks; |
|||
using System; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Microsoft.AspNetCore.Components; |
|||
using Microsoft.AspNetCore.Components.Rendering; |
|||
using BrowserView = Avalonia.Web.AvaloniaView; |
|||
|
|||
namespace Avalonia.Web.Blazor; |
|||
|
|||
[SupportedOSPlatform("browser")] |
|||
public class AvaloniaView : ComponentBase |
|||
{ |
|||
private BrowserView? _browserView; |
|||
private readonly string _containerId; |
|||
|
|||
public AvaloniaView() |
|||
{ |
|||
_containerId = "av_container_" + Guid.NewGuid(); |
|||
} |
|||
|
|||
protected override void BuildRenderTree(RenderTreeBuilder builder) |
|||
{ |
|||
builder.OpenElement(0, "div"); |
|||
builder.AddAttribute(1, "id", _containerId); |
|||
builder.AddAttribute(2, "style", "width:100vw;height:100vh"); |
|||
builder.CloseElement(); |
|||
} |
|||
|
|||
protected override async Task OnInitializedAsync() |
|||
{ |
|||
if (OperatingSystem.IsBrowser()) |
|||
{ |
|||
await Avalonia.Web.Interop.AvaloniaModule.ImportMain(); |
|||
|
|||
_browserView = new BrowserView(_containerId); |
|||
if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime lifetime) |
|||
{ |
|||
_browserView.Content = lifetime.MainView; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,67 +0,0 @@ |
|||
<div id="container" @ref="_containerElement" |
|||
class="avalonia-container" |
|||
tabindex="0" oncontextmenu="return false;" |
|||
@onwheel="OnWheel" |
|||
@onkeydown="OnKeyDown" |
|||
@onkeyup="OnKeyUp" |
|||
@onkeyup:preventDefault="@KeyPreventDefault" |
|||
@onkeydown:preventDefault="@KeyPreventDefault" |
|||
@onpointerdown="OnPointerDown" |
|||
@onpointerup="OnPointerUp" |
|||
@onpointermove="OnPointerMove" |
|||
@onpointercancel="OnPointerCancel" |
|||
@onfocus="OnFocus"> |
|||
|
|||
<canvas id="htmlCanvas" @ref="_htmlCanvas" @attributes="AdditionalAttributes"/> |
|||
|
|||
<div id="nativeControlsContainer" @ref="_nativeControlsContainer" /> |
|||
|
|||
<input id="inputElement" @ref="_inputElement" type="text" |
|||
spellcheck="false" onpaste="return false;" |
|||
oncopy="return false;" |
|||
oncut="return false;" |
|||
autocapitalize="none"/> |
|||
</div> |
|||
|
|||
<style> |
|||
#container{ |
|||
touch-action: none; |
|||
} |
|||
#htmlCanvas { |
|||
opacity: 1; |
|||
background-color: #ccc; |
|||
position: fixed; |
|||
width: 100vw; |
|||
height: 100vh; |
|||
top: 0px; |
|||
left: 0px; |
|||
z-index: 500; |
|||
} |
|||
|
|||
#nativeControlsContainer { |
|||
position: fixed; |
|||
width: 100vw; |
|||
height: 100vh; |
|||
top: 0px; |
|||
left: 0px; |
|||
z-index: 700; |
|||
} |
|||
|
|||
#inputElement { |
|||
padding: 0; |
|||
margin: 0; |
|||
position: absolute; |
|||
height: 20px; |
|||
z-index: 1000; |
|||
overflow: hidden; |
|||
caret-color: transparent; |
|||
border-top-style: hidden; |
|||
border-bottom-style: hidden; |
|||
border-right-style: hidden; |
|||
border-left-style: hidden; |
|||
outline: none; |
|||
background: transparent; |
|||
color: transparent; |
|||
} |
|||
|
|||
</style> |
|||
@ -1,500 +0,0 @@ |
|||
using System.Diagnostics; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Avalonia.Controls.Embedding; |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Raw; |
|||
using Avalonia.Input.TextInput; |
|||
using Avalonia.Platform.Storage; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.Rendering.Composition; |
|||
using Avalonia.Web.Blazor.Interop; |
|||
using Avalonia.Web.Blazor.Interop.Storage; |
|||
|
|||
using Microsoft.AspNetCore.Components; |
|||
using Microsoft.AspNetCore.Components.Forms; |
|||
using Microsoft.AspNetCore.Components.Web; |
|||
using Microsoft.JSInterop; |
|||
|
|||
using SkiaSharp; |
|||
using HTMLPointerEventArgs = Microsoft.AspNetCore.Components.Web.PointerEventArgs; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
public partial class AvaloniaView : ITextInputMethodImpl |
|||
{ |
|||
private readonly RazorViewTopLevelImpl _topLevelImpl; |
|||
private EmbeddableControlRoot _topLevel; |
|||
|
|||
// Interop
|
|||
private SKHtmlCanvasInterop? _interop = null; |
|||
private SizeWatcherInterop? _sizeWatcher = null; |
|||
private DpiWatcherInterop? _dpiWatcher = null; |
|||
private SKHtmlCanvasInterop.GLInfo? _jsGlInfo = null; |
|||
private AvaloniaModule? _avaloniaModule = null; |
|||
private InputHelperInterop? _inputHelper = null; |
|||
private FocusHelperInterop? _canvasHelper = null; |
|||
private FocusHelperInterop? _containerHelper = null; |
|||
private NativeControlHostInterop? _nativeControlHost = null; |
|||
private StorageProviderInterop? _storageProvider = null; |
|||
private ElementReference _htmlCanvas; |
|||
private ElementReference _inputElement; |
|||
private ElementReference _containerElement; |
|||
private ElementReference _nativeControlsContainer; |
|||
private double _dpi = 1; |
|||
private SKSize _canvasSize = new (100, 100); |
|||
|
|||
private GRContext? _context; |
|||
private GRGlInterface? _glInterface; |
|||
private const SKColorType ColorType = SKColorType.Rgba8888; |
|||
|
|||
private bool _useGL; |
|||
private bool _inputElementFocused; |
|||
|
|||
private ITextInputMethodClient? _client; |
|||
|
|||
|
|||
[Inject] private IJSRuntime Js { get; set; } = null!; |
|||
|
|||
public AvaloniaView() |
|||
{ |
|||
_topLevelImpl = new RazorViewTopLevelImpl(this); |
|||
|
|||
_topLevel = new EmbeddableControlRoot(_topLevelImpl); |
|||
|
|||
if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime lifetime) |
|||
{ |
|||
_topLevel.Content = lifetime.MainView; |
|||
} |
|||
} |
|||
|
|||
public bool KeyPreventDefault { get; set; } |
|||
|
|||
public ITextInputMethodClient? Client => _client; |
|||
|
|||
public bool IsActive => _client != null; |
|||
|
|||
public bool IsComposing { get; private set; } |
|||
|
|||
internal INativeControlHostImpl GetNativeControlHostImpl() |
|||
{ |
|||
return _nativeControlHost ?? throw new InvalidOperationException("Blazor View wasn't initialized yet"); |
|||
} |
|||
|
|||
internal IStorageProvider GetStorageProvider() |
|||
{ |
|||
return _storageProvider ?? throw new InvalidOperationException("Blazor View wasn't initialized yet"); |
|||
} |
|||
|
|||
private void OnPointerCancel(HTMLPointerEventArgs e) |
|||
{ |
|||
if (e.PointerType == "touch") |
|||
{ |
|||
_topLevelImpl.RawPointerEvent(RawPointerEventType.TouchCancel, e.PointerType, GetPointFromEventArgs(e), |
|||
GetModifiers(e), e.PointerId); |
|||
} |
|||
} |
|||
|
|||
private void OnPointerMove(HTMLPointerEventArgs e) |
|||
{ |
|||
var type = e.PointerType switch |
|||
{ |
|||
"touch" => RawPointerEventType.TouchUpdate, |
|||
_ => RawPointerEventType.Move |
|||
}; |
|||
|
|||
_topLevelImpl.RawPointerEvent(type, e.PointerType, GetPointFromEventArgs(e), GetModifiers(e), e.PointerId); |
|||
} |
|||
|
|||
private void OnPointerUp(HTMLPointerEventArgs e) |
|||
{ |
|||
var type = e.PointerType switch |
|||
{ |
|||
"touch" => RawPointerEventType.TouchEnd, |
|||
_ => e.Button switch |
|||
{ |
|||
0 => RawPointerEventType.LeftButtonUp, |
|||
1 => RawPointerEventType.MiddleButtonUp, |
|||
2 => RawPointerEventType.RightButtonUp, |
|||
3 => RawPointerEventType.XButton1Up, |
|||
4 => RawPointerEventType.XButton2Up, |
|||
// 5 => Pen eraser button,
|
|||
_ => RawPointerEventType.Move |
|||
} |
|||
}; |
|||
|
|||
_topLevelImpl.RawPointerEvent(type, e.PointerType, GetPointFromEventArgs(e), GetModifiers(e), e.PointerId); |
|||
} |
|||
|
|||
private void OnPointerDown(HTMLPointerEventArgs e) |
|||
{ |
|||
var type = e.PointerType switch |
|||
{ |
|||
"touch" => RawPointerEventType.TouchBegin, |
|||
_ => e.Button switch |
|||
{ |
|||
0 => RawPointerEventType.LeftButtonDown, |
|||
1 => RawPointerEventType.MiddleButtonDown, |
|||
2 => RawPointerEventType.RightButtonDown, |
|||
3 => RawPointerEventType.XButton1Down, |
|||
4 => RawPointerEventType.XButton2Down, |
|||
// 5 => Pen eraser button,
|
|||
_ => RawPointerEventType.Move |
|||
} |
|||
}; |
|||
|
|||
_topLevelImpl.RawPointerEvent(type, e.PointerType, GetPointFromEventArgs(e), GetModifiers(e), e.PointerId); |
|||
} |
|||
|
|||
private static RawPointerPoint GetPointFromEventArgs(HTMLPointerEventArgs args) |
|||
{ |
|||
return new RawPointerPoint |
|||
{ |
|||
Position = new Point(args.ClientX, args.ClientY), |
|||
Pressure = args.Pressure, |
|||
XTilt = args.TiltX, |
|||
YTilt = args.TiltY |
|||
// Twist = args.Twist - read from JS code directly when
|
|||
}; |
|||
} |
|||
|
|||
private void OnWheel(WheelEventArgs e) |
|||
{ |
|||
_topLevelImpl.RawMouseWheelEvent( new Point(e.ClientX, e.ClientY), |
|||
new Vector(-(e.DeltaX / 50), -(e.DeltaY / 50)), GetModifiers(e)); |
|||
} |
|||
|
|||
private static RawInputModifiers GetModifiers(MouseEventArgs e) |
|||
{ |
|||
var modifiers = RawInputModifiers.None; |
|||
|
|||
if (e.CtrlKey) |
|||
modifiers |= RawInputModifiers.Control; |
|||
if (e.AltKey) |
|||
modifiers |= RawInputModifiers.Alt; |
|||
if (e.ShiftKey) |
|||
modifiers |= RawInputModifiers.Shift; |
|||
if (e.MetaKey) |
|||
modifiers |= RawInputModifiers.Meta; |
|||
|
|||
if ((e.Buttons & 1L) == 1) |
|||
modifiers |= RawInputModifiers.LeftMouseButton; |
|||
|
|||
if ((e.Buttons & 2L) == 2) |
|||
modifiers |= e.Type == "pen" ? RawInputModifiers.PenBarrelButton : RawInputModifiers.RightMouseButton; |
|||
|
|||
if ((e.Buttons & 4L) == 4) |
|||
modifiers |= RawInputModifiers.MiddleMouseButton; |
|||
|
|||
if ((e.Buttons & 8L) == 8) |
|||
modifiers |= RawInputModifiers.XButton1MouseButton; |
|||
|
|||
if ((e.Buttons & 16L) == 16) |
|||
modifiers |= RawInputModifiers.XButton2MouseButton; |
|||
|
|||
if ((e.Buttons & 32L) == 32) |
|||
modifiers |= RawInputModifiers.PenEraser; |
|||
|
|||
return modifiers; |
|||
} |
|||
|
|||
private static RawInputModifiers GetModifiers(KeyboardEventArgs e) |
|||
{ |
|||
var modifiers = RawInputModifiers.None; |
|||
|
|||
if (e.CtrlKey) |
|||
modifiers |= RawInputModifiers.Control; |
|||
if (e.AltKey) |
|||
modifiers |= RawInputModifiers.Alt; |
|||
if (e.ShiftKey) |
|||
modifiers |= RawInputModifiers.Shift; |
|||
if (e.MetaKey) |
|||
modifiers |= RawInputModifiers.Meta; |
|||
|
|||
return modifiers; |
|||
} |
|||
|
|||
private void OnKeyDown(KeyboardEventArgs e) |
|||
{ |
|||
KeyPreventDefault = _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, e.Code, e.Key, GetModifiers(e)); |
|||
} |
|||
|
|||
private void OnKeyUp(KeyboardEventArgs e) |
|||
{ |
|||
KeyPreventDefault = _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, e.Code, e.Key, GetModifiers(e)); |
|||
} |
|||
|
|||
private void OnFocus(FocusEventArgs e) |
|||
{ |
|||
// if focus has unexpectedly moved from the input element to the container element,
|
|||
// shift it back to the input element
|
|||
if (_inputElementFocused && _inputHelper is not null) |
|||
{ |
|||
_inputHelper.Focus(); |
|||
} |
|||
} |
|||
|
|||
[Parameter(CaptureUnmatchedValues = true)] |
|||
public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; } |
|||
|
|||
protected override async Task OnAfterRenderAsync(bool firstRender) |
|||
{ |
|||
if (firstRender) |
|||
{ |
|||
AvaloniaLocator.CurrentMutable.Bind<IJSInProcessRuntime>().ToConstant((IJSInProcessRuntime)Js); |
|||
|
|||
_avaloniaModule = await AvaloniaModule.ImportAsync(Js); |
|||
|
|||
_canvasHelper = new FocusHelperInterop(_avaloniaModule, _htmlCanvas); |
|||
_containerHelper = new FocusHelperInterop(_avaloniaModule, _containerElement); |
|||
_inputHelper = new InputHelperInterop(_avaloniaModule, _inputElement); |
|||
|
|||
_inputHelper.CompositionEvent += InputHelperOnCompositionEvent; |
|||
_inputHelper.InputEvent += InputHelperOnInputEvent; |
|||
|
|||
HideIme(); |
|||
_canvasHelper.SetCursor("default"); |
|||
_topLevelImpl.SetCssCursor = x => |
|||
{ |
|||
_inputHelper.SetCursor(x); //macOS
|
|||
_canvasHelper.SetCursor(x); //windows
|
|||
}; |
|||
|
|||
_nativeControlHost = new NativeControlHostInterop(_avaloniaModule, _nativeControlsContainer); |
|||
_storageProvider = await StorageProviderInterop.ImportAsync(Js); |
|||
|
|||
Console.WriteLine("starting html canvas setup"); |
|||
_interop = new SKHtmlCanvasInterop(_avaloniaModule, _htmlCanvas, OnRenderFrame); |
|||
|
|||
Console.WriteLine("Interop created"); |
|||
|
|||
var skiaOptions = AvaloniaLocator.Current.GetService<SkiaOptions>(); |
|||
_useGL = skiaOptions?.CustomGpuFactory != null; |
|||
|
|||
if (_useGL) |
|||
{ |
|||
_jsGlInfo = _interop.InitGL(); |
|||
Console.WriteLine("jsglinfo created - init gl"); |
|||
} |
|||
else |
|||
{ |
|||
var rasterInitialized = _interop.InitRaster(); |
|||
Console.WriteLine("raster initialized: {0}", rasterInitialized); |
|||
} |
|||
|
|||
if (_useGL) |
|||
{ |
|||
// 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); |
|||
Console.WriteLine("glcontext created and resource limit set"); |
|||
} |
|||
|
|||
_topLevelImpl.SetSurface(_context, _jsGlInfo!, ColorType, |
|||
new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi); |
|||
} |
|||
else |
|||
{ |
|||
_topLevelImpl.SetSurface(ColorType, |
|||
new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData); |
|||
} |
|||
|
|||
_interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); |
|||
|
|||
Threading.Dispatcher.UIThread.Post(async () => |
|||
{ |
|||
_interop.RequestAnimationFrame(true); |
|||
|
|||
_sizeWatcher = new SizeWatcherInterop(_avaloniaModule, _htmlCanvas, OnSizeChanged); |
|||
_dpiWatcher = new DpiWatcherInterop(_avaloniaModule, OnDpiChanged); |
|||
|
|||
_sizeWatcher.Start(); |
|||
_topLevel.Prepare(); |
|||
|
|||
_topLevel.Renderer.Start(); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
private void InputHelperOnInputEvent(object? sender, WebInputEventArgs e) |
|||
{ |
|||
if (IsComposing) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
_topLevelImpl.RawTextEvent(e.Data); |
|||
|
|||
e.Handled = true; |
|||
} |
|||
|
|||
private void InputHelperOnCompositionEvent(object? sender, WebCompositionEventArgs e) |
|||
{ |
|||
if(_client == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
switch (e.Type) |
|||
{ |
|||
case WebCompositionEventArgs.WebCompositionEventType.Start: |
|||
_client.SetPreeditText(null); |
|||
IsComposing = true; |
|||
break; |
|||
case WebCompositionEventArgs.WebCompositionEventType.Update: |
|||
_client.SetPreeditText(e.Data); |
|||
break; |
|||
case WebCompositionEventArgs.WebCompositionEventType.End: |
|||
IsComposing = false; |
|||
_client.SetPreeditText(null); |
|||
_topLevelImpl.RawTextEvent(e.Data); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
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 void Dispose() |
|||
{ |
|||
_dpiWatcher?.Unsubscribe(OnDpiChanged); |
|||
_sizeWatcher?.Dispose(); |
|||
_interop?.Dispose(); |
|||
} |
|||
|
|||
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 newDpi) |
|||
{ |
|||
if (Math.Abs(_dpi - newDpi) > 0.0001) |
|||
{ |
|||
_dpi = newDpi; |
|||
|
|||
_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); |
|||
|
|||
_topLevelImpl.SetClientSize(_canvasSize, _dpi); |
|||
|
|||
ForceBlit(); |
|||
} |
|||
} |
|||
|
|||
private void OnSizeChanged(SKSize newSize) |
|||
{ |
|||
if (_canvasSize != newSize) |
|||
{ |
|||
_canvasSize = newSize; |
|||
|
|||
_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); |
|||
|
|||
_topLevelImpl.SetClientSize(_canvasSize, _dpi); |
|||
|
|||
ForceBlit(); |
|||
} |
|||
} |
|||
|
|||
private void HideIme() |
|||
{ |
|||
_inputHelper?.Hide(); |
|||
_containerHelper?.Focus(); |
|||
} |
|||
|
|||
public void SetClient(ITextInputMethodClient? client) |
|||
{ |
|||
if (_inputHelper is null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if(_client != null) |
|||
{ |
|||
_client.SurroundingTextChanged -= SurroundingTextChanged; |
|||
} |
|||
|
|||
if(client != null) |
|||
{ |
|||
client.SurroundingTextChanged += SurroundingTextChanged; |
|||
} |
|||
|
|||
_inputHelper.Clear(); |
|||
|
|||
_client = client; |
|||
|
|||
if (IsActive && _client != null) |
|||
{ |
|||
_inputHelper.Show(); |
|||
_inputElementFocused = true; |
|||
_inputHelper.Focus(); |
|||
|
|||
var surroundingText = _client.SurroundingText; |
|||
|
|||
_inputHelper?.SetSurroundingText(surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset); |
|||
} |
|||
else |
|||
{ |
|||
_inputElementFocused = false; |
|||
HideIme(); |
|||
} |
|||
} |
|||
|
|||
private void SurroundingTextChanged(object? sender, EventArgs e) |
|||
{ |
|||
if(_client != null) |
|||
{ |
|||
var surroundingText = _client.SurroundingText; |
|||
|
|||
_inputHelper?.SetSurroundingText(surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset); |
|||
} |
|||
} |
|||
|
|||
public void SetCursorRect(Rect rect) |
|||
{ |
|||
_inputHelper?.Focus(); |
|||
var bounds = new PixelRect((int)rect.X, (int) rect.Y, (int) rect.Width, (int) rect.Height); |
|||
|
|||
_inputHelper?.SetBounds(bounds, _client?.SurroundingText.CursorOffset ?? 0); |
|||
_inputHelper?.Focus(); |
|||
} |
|||
|
|||
public void SetOptions(TextInputOptions options) |
|||
{ |
|||
} |
|||
|
|||
public void Reset() |
|||
{ |
|||
_inputHelper?.Clear(); |
|||
_inputHelper?.SetSurroundingText("", 0, 0); |
|||
} |
|||
} |
|||
} |
|||
@ -1,31 +1,39 @@ |
|||
using Avalonia.Controls; |
|||
using System.Runtime.Versioning; |
|||
|
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
namespace Avalonia.Web.Blazor; |
|||
|
|||
[SupportedOSPlatform("browser")] |
|||
public static class WebAppBuilder |
|||
{ |
|||
public class BlazorSingleViewLifetime : ISingleViewApplicationLifetime |
|||
public static T SetupWithSingleViewLifetime<T>( |
|||
this T builder) |
|||
where T : AppBuilderBase<T>, new() |
|||
{ |
|||
public Control? MainView { get; set; } |
|||
return builder.SetupWithLifetime(new BlazorSingleViewLifetime()); |
|||
} |
|||
|
|||
public static class WebAppBuilder |
|||
public static T UseBlazor<T>(this T builder) where T : AppBuilderBase<T>, new() |
|||
{ |
|||
public static T SetupWithSingleViewLifetime<T>( |
|||
this T builder) |
|||
where T : AppBuilderBase<T>, new() |
|||
{ |
|||
return builder.SetupWithLifetime(new BlazorSingleViewLifetime()); |
|||
} |
|||
return builder |
|||
.UseBrowser() |
|||
.With(new BrowserPlatformOptions |
|||
{ |
|||
FrameworkAssetPathResolver = new(filePath => $"/_content/Avalonia.Web.Blazor/{filePath}") |
|||
}); |
|||
} |
|||
|
|||
public static AvaloniaBlazorAppBuilder Configure<TApp>() |
|||
where TApp : Application, new() |
|||
{ |
|||
var builder = AvaloniaBlazorAppBuilder.Configure<TApp>() |
|||
.UseSkia() |
|||
.With(new SkiaOptions { CustomGpuFactory = () => new BlazorSkiaGpu() }); |
|||
public static AppBuilder Configure<TApp>() |
|||
where TApp : Application, new() |
|||
{ |
|||
return AppBuilder.Configure<TApp>() |
|||
.UseBlazor(); |
|||
} |
|||
|
|||
return builder; |
|||
} |
|||
internal class BlazorSingleViewLifetime : ISingleViewApplicationLifetime |
|||
{ |
|||
public Control? MainView { get; set; } |
|||
} |
|||
} |
|||
|
|||
@ -1,25 +0,0 @@ |
|||
using Avalonia.Skia; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
public class BlazorSkiaGpu : ISkiaGpu |
|||
{ |
|||
public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable<object> surfaces) |
|||
{ |
|||
foreach (var surface in surfaces) |
|||
{ |
|||
if (surface is BlazorSkiaSurface blazorSkiaSurface) |
|||
{ |
|||
return new BlazorSkiaGpuRenderTarget(blazorSkiaSurface); |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public ISkiaSurface? TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session) |
|||
{ |
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
@ -1,37 +0,0 @@ |
|||
using Avalonia.Skia; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
internal class BlazorSkiaGpuRenderSession : ISkiaGpuRenderSession |
|||
{ |
|||
private readonly SKSurface _surface; |
|||
|
|||
|
|||
public BlazorSkiaGpuRenderSession(BlazorSkiaSurface blazorSkiaSurface, GRBackendRenderTarget renderTarget) |
|||
{ |
|||
_surface = SKSurface.Create(blazorSkiaSurface.Context, renderTarget, blazorSkiaSurface.Origin, blazorSkiaSurface.ColorType); |
|||
|
|||
GrContext = blazorSkiaSurface.Context; |
|||
|
|||
ScaleFactor = blazorSkiaSurface.Scaling; |
|||
|
|||
SurfaceOrigin = blazorSkiaSurface.Origin; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_surface.Flush(); |
|||
|
|||
_surface.Dispose(); |
|||
} |
|||
|
|||
public GRContext GrContext { get; } |
|||
|
|||
public SKSurface SkSurface => _surface; |
|||
|
|||
public double ScaleFactor { get; } |
|||
|
|||
public GRSurfaceOrigin SurfaceOrigin { get; } |
|||
} |
|||
} |
|||
@ -1,39 +0,0 @@ |
|||
using Avalonia.Skia; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
internal class BlazorSkiaGpuRenderTarget : ISkiaGpuRenderTarget |
|||
{ |
|||
private readonly GRBackendRenderTarget _renderTarget; |
|||
private readonly BlazorSkiaSurface _blazorSkiaSurface; |
|||
private readonly PixelSize _size; |
|||
|
|||
public BlazorSkiaGpuRenderTarget(BlazorSkiaSurface blazorSkiaSurface) |
|||
{ |
|||
_size = blazorSkiaSurface.Size; |
|||
|
|||
var glFbInfo = new GRGlFramebufferInfo(blazorSkiaSurface.GlInfo.FboId, blazorSkiaSurface.ColorType.ToGlSizedFormat()); |
|||
{ |
|||
_blazorSkiaSurface = blazorSkiaSurface; |
|||
_renderTarget = new GRBackendRenderTarget( |
|||
(int)(blazorSkiaSurface.Size.Width * blazorSkiaSurface.Scaling), |
|||
(int)(blazorSkiaSurface.Size.Height * blazorSkiaSurface.Scaling), |
|||
blazorSkiaSurface.GlInfo.Samples, |
|||
blazorSkiaSurface.GlInfo.Stencils, glFbInfo); |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_renderTarget.Dispose(); |
|||
} |
|||
|
|||
public ISkiaGpuRenderSession BeginRenderingSession() |
|||
{ |
|||
return new BlazorSkiaGpuRenderSession(_blazorSkiaSurface, _renderTarget); |
|||
} |
|||
|
|||
public bool IsCorrupted => _blazorSkiaSurface.Size != _size; |
|||
} |
|||
} |
|||
@ -1,87 +0,0 @@ |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Controls.Platform.Surfaces; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Skia; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
internal class BlazorSkiaRasterSurface : IBlazorSkiaSurface, 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 BlazorSkiaRasterSurface( |
|||
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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,30 +0,0 @@ |
|||
using Avalonia.Web.Blazor.Interop; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
internal class BlazorSkiaSurface : IBlazorSkiaSurface |
|||
{ |
|||
public BlazorSkiaSurface(GRContext context, SKHtmlCanvasInterop.GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling, GRSurfaceOrigin origin) |
|||
{ |
|||
Context = context; |
|||
GlInfo = glInfo; |
|||
ColorType = colorType; |
|||
Size = size; |
|||
Scaling = scaling; |
|||
Origin = origin; |
|||
} |
|||
|
|||
public SKColorType ColorType { get; set; } |
|||
|
|||
public PixelSize Size { get; set; } |
|||
|
|||
public GRContext Context { get; set; } |
|||
|
|||
public GRSurfaceOrigin Origin { get; set; } |
|||
|
|||
public double Scaling { get; set; } |
|||
|
|||
public SKHtmlCanvasInterop.GLInfo GlInfo { get; set; } |
|||
} |
|||
} |
|||
@ -1,34 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
using Microsoft.JSInterop; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
internal class ClipboardImpl : IClipboard |
|||
{ |
|||
public async Task<string> GetTextAsync() |
|||
{ |
|||
return await AvaloniaLocator.Current.GetRequiredService<IJSInProcessRuntime>(). |
|||
InvokeAsync<string>("navigator.clipboard.readText"); |
|||
} |
|||
|
|||
public async Task SetTextAsync(string text) |
|||
{ |
|||
await AvaloniaLocator.Current.GetRequiredService<IJSInProcessRuntime>(). |
|||
InvokeAsync<string>("navigator.clipboard.writeText",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()); |
|||
} |
|||
} |
|||
@ -1,93 +0,0 @@ |
|||
using Avalonia.Input; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
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); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -1,9 +0,0 @@ |
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
internal interface IBlazorSkiaSurface |
|||
{ |
|||
public PixelSize Size { get; set; } |
|||
|
|||
public double Scaling { get; set; } |
|||
} |
|||
} |
|||
@ -1,81 +0,0 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using Microsoft.JSInterop; |
|||
|
|||
namespace Avalonia.Web.Blazor.Interop |
|||
{ |
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public class ActionHelper |
|||
{ |
|||
private readonly Action action; |
|||
|
|||
public ActionHelper(Action action) |
|||
{ |
|||
this.action = action; |
|||
} |
|||
|
|||
[JSInvokable] |
|||
public void Invoke() => action?.Invoke(); |
|||
} |
|||
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public class ActionHelper<T> |
|||
{ |
|||
private readonly Action<T> action; |
|||
|
|||
public ActionHelper(Action<T> action) |
|||
{ |
|||
this.action = action; |
|||
} |
|||
|
|||
[JSInvokable] |
|||
public void Invoke(T param1) => action?.Invoke(param1); |
|||
} |
|||
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public class ActionHelper<T1, T2> |
|||
{ |
|||
private readonly Action<T1, T2> action; |
|||
|
|||
public ActionHelper(Action<T1, T2> action) |
|||
{ |
|||
this.action = action; |
|||
} |
|||
|
|||
[JSInvokable] |
|||
public void Invoke(T1 p1, T2 p2) => action?.Invoke(p1, p2); |
|||
} |
|||
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public class ActionHelper<T1, T2, T3> |
|||
{ |
|||
private readonly Action<T1, T2, T3> action; |
|||
|
|||
public ActionHelper(Action<T1, T2, T3> action) |
|||
{ |
|||
this.action = action; |
|||
} |
|||
|
|||
[JSInvokable] |
|||
public void Invoke(T1 p1, T2 p2, T3 p3) => action?.Invoke(p1, p2, p3); |
|||
} |
|||
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public class ActionHelper<T1, T2, T3, T4> |
|||
{ |
|||
private readonly Action<T1, T2, T3, T4> action; |
|||
|
|||
public ActionHelper(Action<T1, T2, T3, T4> action) |
|||
{ |
|||
this.action = action; |
|||
} |
|||
|
|||
[JSInvokable] |
|||
public void Invoke(T1 p1, T2 p2, T3 p3, T4 p4) => action?.Invoke(p1, p2, p3, p4); |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
} |
|||
@ -1,18 +0,0 @@ |
|||
using Microsoft.JSInterop; |
|||
|
|||
namespace Avalonia.Web.Blazor.Interop |
|||
{ |
|||
internal class AvaloniaModule : JSModuleInterop |
|||
{ |
|||
private AvaloniaModule(IJSRuntime js) : base(js, "./_content/Avalonia.Web.Blazor/Avalonia.js") |
|||
{ |
|||
} |
|||
|
|||
public static async Task<AvaloniaModule> ImportAsync(IJSRuntime js) |
|||
{ |
|||
var interop = new AvaloniaModule(js); |
|||
await interop.ImportAsync(); |
|||
return interop; |
|||
} |
|||
} |
|||
} |
|||
@ -1,72 +0,0 @@ |
|||
using Microsoft.JSInterop; |
|||
|
|||
namespace Avalonia.Web.Blazor.Interop |
|||
{ |
|||
internal class DpiWatcherInterop : IDisposable |
|||
{ |
|||
private const string StartSymbol = "DpiWatcher.start"; |
|||
private const string StopSymbol = "DpiWatcher.stop"; |
|||
private const string GetDpiSymbol = "DpiWatcher.getDpi"; |
|||
|
|||
private event Action<double>? callbacksEvent; |
|||
private readonly ActionHelper<float, float> _callbackHelper; |
|||
private readonly AvaloniaModule _module; |
|||
|
|||
private DotNetObjectReference<ActionHelper<float, float>>? callbackReference; |
|||
|
|||
public DpiWatcherInterop(AvaloniaModule module, Action<double>? callback = null) |
|||
{ |
|||
_module = module; |
|||
_callbackHelper = new ActionHelper<float, float>((o, n) => callbacksEvent?.Invoke(n)); |
|||
|
|||
if (callback != null) |
|||
Subscribe(callback); |
|||
} |
|||
|
|||
public void Dispose() => Stop(); |
|||
|
|||
public void Subscribe(Action<double> callback) |
|||
{ |
|||
var shouldStart = callbacksEvent == null; |
|||
|
|||
callbacksEvent += callback; |
|||
|
|||
var dpi = shouldStart |
|||
? Start() |
|||
: GetDpi(); |
|||
|
|||
callback(dpi); |
|||
} |
|||
|
|||
public void Unsubscribe(Action<double> callback) |
|||
{ |
|||
callbacksEvent -= callback; |
|||
|
|||
if (callbacksEvent == null) |
|||
Stop(); |
|||
} |
|||
|
|||
private double Start() |
|||
{ |
|||
if (callbackReference != null) |
|||
return GetDpi(); |
|||
|
|||
callbackReference = DotNetObjectReference.Create(_callbackHelper); |
|||
|
|||
return _module.Invoke<double>(StartSymbol, callbackReference); |
|||
} |
|||
|
|||
private void Stop() |
|||
{ |
|||
if (callbackReference == null) |
|||
return; |
|||
|
|||
_module.Invoke(StopSymbol); |
|||
|
|||
callbackReference?.Dispose(); |
|||
callbackReference = null; |
|||
} |
|||
|
|||
public double GetDpi() => _module.Invoke<double>(GetDpiSymbol); |
|||
} |
|||
} |
|||
@ -1,22 +0,0 @@ |
|||
using Microsoft.AspNetCore.Components; |
|||
|
|||
namespace Avalonia.Web.Blazor.Interop; |
|||
|
|||
internal class FocusHelperInterop |
|||
{ |
|||
private const string FocusSymbol = "FocusHelper.focus"; |
|||
private const string SetCursorSymbol = "FocusHelper.setCursor"; |
|||
|
|||
private readonly AvaloniaModule _module; |
|||
private readonly ElementReference _inputElement; |
|||
|
|||
public FocusHelperInterop(AvaloniaModule module, ElementReference inputElement) |
|||
{ |
|||
_module = module; |
|||
_inputElement = inputElement; |
|||
} |
|||
|
|||
public void Focus() => _module.Invoke(FocusSymbol, _inputElement); |
|||
|
|||
public void SetCursor(string kind) => _module.Invoke(SetCursorSymbol, _inputElement, kind); |
|||
} |
|||
@ -1,130 +0,0 @@ |
|||
using Microsoft.AspNetCore.Components; |
|||
using Microsoft.JSInterop; |
|||
|
|||
namespace Avalonia.Web.Blazor.Interop |
|||
{ |
|||
internal class WebCompositionEventArgs : EventArgs |
|||
{ |
|||
public enum WebCompositionEventType |
|||
{ |
|||
Start, |
|||
Update, |
|||
End |
|||
} |
|||
|
|||
public WebCompositionEventArgs(string type, string data) |
|||
{ |
|||
Type = type switch |
|||
{ |
|||
"compositionstart" => WebCompositionEventType.Start, |
|||
"compositionupdate" => WebCompositionEventType.Update, |
|||
"compositionend" => WebCompositionEventType.End, |
|||
_ => Type |
|||
}; |
|||
|
|||
Data = data; |
|||
} |
|||
|
|||
public WebCompositionEventType Type { get; } |
|||
|
|||
public string Data { get; } |
|||
} |
|||
|
|||
internal class WebInputEventArgs |
|||
{ |
|||
public WebInputEventArgs(string type, string data) |
|||
{ |
|||
Type = type; |
|||
Data = data; |
|||
} |
|||
|
|||
public string Type { get; } |
|||
|
|||
public string Data { get; } |
|||
|
|||
public bool Handled { get; set; } |
|||
} |
|||
|
|||
internal class InputHelperInterop |
|||
{ |
|||
private const string ClearSymbol = "InputHelper.clear"; |
|||
private const string FocusSymbol = "InputHelper.focus"; |
|||
private const string SetCursorSymbol = "InputHelper.setCursor"; |
|||
private const string HideSymbol = "InputHelper.hide"; |
|||
private const string ShowSymbol = "InputHelper.show"; |
|||
private const string StartSymbol = "InputHelper.start"; |
|||
private const string SetSurroundingTextSymbol = "InputHelper.setSurroundingText"; |
|||
private const string SetBoundsSymbol = "InputHelper.setBounds"; |
|||
|
|||
private readonly AvaloniaModule _module; |
|||
private readonly ElementReference _inputElement; |
|||
private readonly ActionHelper<string, string> _compositionAction; |
|||
private readonly ActionHelper<string, string> _inputAction; |
|||
|
|||
private DotNetObjectReference<ActionHelper<string, string>>? compositionActionReference; |
|||
private DotNetObjectReference<ActionHelper<string, string>>? inputActionReference; |
|||
|
|||
public InputHelperInterop(AvaloniaModule module, ElementReference inputElement) |
|||
{ |
|||
_module = module; |
|||
_inputElement = inputElement; |
|||
|
|||
_compositionAction = new ActionHelper<string, string>(OnCompositionEvent); |
|||
_inputAction = new ActionHelper<string, string>(OnInputEvent); |
|||
|
|||
Start(); |
|||
} |
|||
|
|||
public event EventHandler<WebCompositionEventArgs>? CompositionEvent; |
|||
public event EventHandler<WebInputEventArgs>? InputEvent; |
|||
|
|||
private void OnCompositionEvent(string type, string data) |
|||
{ |
|||
Console.WriteLine($"CompositionEvent Handler Helper {CompositionEvent == null} "); |
|||
CompositionEvent?.Invoke(this, new WebCompositionEventArgs(type, data)); |
|||
} |
|||
|
|||
private void OnInputEvent(string type, string data) |
|||
{ |
|||
Console.WriteLine($"InputEvent Handler Helper {InputEvent == null} "); |
|||
|
|||
var args = new WebInputEventArgs(type, data); |
|||
|
|||
InputEvent?.Invoke(this, args); |
|||
} |
|||
|
|||
public void Clear() => _module.Invoke(ClearSymbol, _inputElement); |
|||
|
|||
public void Focus() => _module.Invoke(FocusSymbol, _inputElement); |
|||
|
|||
public void SetCursor(string kind) => _module.Invoke(SetCursorSymbol, _inputElement, kind); |
|||
|
|||
public void Hide() => _module.Invoke(HideSymbol, _inputElement); |
|||
|
|||
public void Show() => _module.Invoke(ShowSymbol, _inputElement); |
|||
|
|||
private void Start() |
|||
{ |
|||
if(compositionActionReference != null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
compositionActionReference = DotNetObjectReference.Create(_compositionAction); |
|||
|
|||
inputActionReference = DotNetObjectReference.Create(_inputAction); |
|||
|
|||
_module.Invoke(StartSymbol, _inputElement, compositionActionReference, inputActionReference); |
|||
} |
|||
|
|||
public void SetSurroundingText(string text, int start, int end) |
|||
{ |
|||
_module.Invoke(SetSurroundingTextSymbol, _inputElement, text, start, end); |
|||
} |
|||
|
|||
public void SetBounds(PixelRect bounds, int caret) |
|||
{ |
|||
_module.Invoke(SetBoundsSymbol, _inputElement, bounds.X, bounds.Y, bounds.Width, bounds.Height, caret); |
|||
} |
|||
} |
|||
} |
|||
@ -1,46 +0,0 @@ |
|||
using Microsoft.JSInterop; |
|||
|
|||
namespace Avalonia.Web.Blazor.Interop |
|||
{ |
|||
internal class JSModuleInterop : IDisposable |
|||
{ |
|||
private readonly Task<IJSUnmarshalledObjectReference> moduleTask; |
|||
private IJSUnmarshalledObjectReference? module; |
|||
|
|||
public JSModuleInterop(IJSRuntime js, string filename) |
|||
{ |
|||
if (js is not IJSInProcessRuntime) |
|||
throw new NotSupportedException("SkiaSharp currently only works on Web Assembly."); |
|||
|
|||
moduleTask = js.InvokeAsync<IJSUnmarshalledObjectReference>("import", filename).AsTask(); |
|||
} |
|||
|
|||
public async Task ImportAsync() |
|||
{ |
|||
module = await moduleTask; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
OnDisposingModule(); |
|||
Module.Dispose(); |
|||
} |
|||
|
|||
protected IJSUnmarshalledObjectReference Module => |
|||
module ?? throw new InvalidOperationException("Make sure to run ImportAsync() first."); |
|||
|
|||
internal void Invoke(string identifier, params object?[]? args) => |
|||
Module.InvokeVoid(identifier, args); |
|||
|
|||
internal TValue Invoke<TValue>(string identifier, params object?[]? args) => |
|||
Module.Invoke<TValue>(identifier, args); |
|||
|
|||
internal ValueTask InvokeAsync(string identifier, params object?[]? args) => |
|||
Module.InvokeVoidAsync(identifier, args); |
|||
|
|||
internal ValueTask<TValue> InvokeAsync<TValue>(string identifier, params object?[]? args) => |
|||
Module.InvokeAsync<TValue>(identifier, args); |
|||
|
|||
protected virtual void OnDisposingModule() { } |
|||
} |
|||
} |
|||
@ -1,144 +0,0 @@ |
|||
using System.Diagnostics.CodeAnalysis; |
|||
|
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Platform; |
|||
|
|||
using Microsoft.AspNetCore.Components; |
|||
using Microsoft.JSInterop; |
|||
|
|||
namespace Avalonia.Web.Blazor.Interop |
|||
{ |
|||
|
|||
internal class NativeControlHostInterop : INativeControlHostImpl |
|||
{ |
|||
private const string CreateDefaultChildSymbol = "NativeControlHost.CreateDefaultChild"; |
|||
private const string CreateAttachmentSymbol = "NativeControlHost.CreateAttachment"; |
|||
private const string GetReferenceSymbol = "NativeControlHost.GetReference"; |
|||
|
|||
private readonly AvaloniaModule _module; |
|||
private readonly ElementReference _hostElement; |
|||
|
|||
public NativeControlHostInterop(AvaloniaModule module, ElementReference element) |
|||
{ |
|||
_module = module; |
|||
_hostElement = element; |
|||
} |
|||
|
|||
public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent) |
|||
{ |
|||
var element = _module.Invoke<IJSInProcessObjectReference>(CreateDefaultChildSymbol); |
|||
return new JSObjectControlHandle(element); |
|||
} |
|||
|
|||
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func<IPlatformHandle, IPlatformHandle> create) |
|||
{ |
|||
Attachment? a = null; |
|||
try |
|||
{ |
|||
using var hostElementJsReference = _module.Invoke<IJSInProcessObjectReference>(GetReferenceSymbol, _hostElement); |
|||
var child = create(new JSObjectControlHandle(hostElementJsReference)); |
|||
var attachmenetReference = _module.Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol); |
|||
// 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 = _module.Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol); |
|||
var a = new Attachment(attachmenetReference, handle); |
|||
a.AttachedTo = this; |
|||
return a; |
|||
} |
|||
|
|||
public bool IsCompatibleWith(IPlatformHandle handle) => handle is JSObjectControlHandle; |
|||
|
|||
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 IJSInProcessObjectReference? _native; |
|||
private NativeControlHostInterop? _attachedTo; |
|||
|
|||
public Attachment(IJSInProcessObjectReference native, IPlatformHandle handle) |
|||
{ |
|||
_native = native; |
|||
_native.InvokeVoid(InitializeWithChildHandleSymbol, ((JSObjectControlHandle)handle).Object); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (_native != null) |
|||
{ |
|||
_native.InvokeVoid(ReleaseChildSymbol); |
|||
_native.Dispose(); |
|||
_native = null; |
|||
} |
|||
} |
|||
|
|||
public INativeControlHostImpl? AttachedTo |
|||
{ |
|||
get => _attachedTo!; |
|||
set |
|||
{ |
|||
CheckDisposed(); |
|||
|
|||
var host = (NativeControlHostInterop?)value; |
|||
if (host == null) |
|||
{ |
|||
_native.InvokeVoid(AttachToSymbol); |
|||
} |
|||
else |
|||
{ |
|||
_native.InvokeVoid(AttachToSymbol, host._hostElement); |
|||
} |
|||
_attachedTo = host; |
|||
} |
|||
} |
|||
|
|||
public bool IsCompatibleWith(INativeControlHostImpl host) => host is NativeControlHostInterop; |
|||
|
|||
public void HideWithSize(Size size) |
|||
{ |
|||
CheckDisposed(); |
|||
if (_attachedTo == null) |
|||
return; |
|||
|
|||
_native.InvokeVoid(HideWithSizeSymbol, Math.Max(1, (float)size.Width), Math.Max(1, (float)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)); |
|||
|
|||
_native.InvokeVoid(ShowInBoundsSymbol, (float)bounds.X, (float)bounds.Y, (float)bounds.Width, (float)bounds.Height); |
|||
} |
|||
|
|||
[MemberNotNull(nameof(_native))] |
|||
private void CheckDisposed() |
|||
{ |
|||
if (_native == null) |
|||
throw new ObjectDisposedException(nameof(Attachment)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,76 +0,0 @@ |
|||
using Microsoft.AspNetCore.Components; |
|||
using Microsoft.JSInterop; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Web.Blazor.Interop |
|||
{ |
|||
internal class SKHtmlCanvasInterop : IDisposable |
|||
{ |
|||
private const string JsFilename = "./_content/Avalonia.Web.Blazor/SKHtmlCanvas.js"; |
|||
private const string InitGLSymbol = "SKHtmlCanvas.initGL"; |
|||
private const string InitRasterSymbol = "SKHtmlCanvas.initRaster"; |
|||
private const string DeinitSymbol = "SKHtmlCanvas.deinit"; |
|||
private const string RequestAnimationFrameSymbol = "SKHtmlCanvas.requestAnimationFrame"; |
|||
private const string SetCanvasSizeSymbol = "SKHtmlCanvas.setCanvasSize"; |
|||
private const string PutImageDataSymbol = "SKHtmlCanvas.putImageData"; |
|||
|
|||
private readonly AvaloniaModule _module; |
|||
private readonly ElementReference _htmlCanvas; |
|||
private readonly string _htmlElementId; |
|||
private readonly ActionHelper _callbackHelper; |
|||
|
|||
private DotNetObjectReference<ActionHelper>? callbackReference; |
|||
|
|||
public SKHtmlCanvasInterop(AvaloniaModule module, ElementReference element, Action renderFrameCallback) |
|||
{ |
|||
_module = module; |
|||
_htmlCanvas = element; |
|||
_htmlElementId = element.Id; |
|||
|
|||
_callbackHelper = new ActionHelper(renderFrameCallback); |
|||
} |
|||
|
|||
public void Dispose() => Deinit(); |
|||
|
|||
public GLInfo InitGL() |
|||
{ |
|||
if (callbackReference != null) |
|||
throw new InvalidOperationException("Unable to initialize the same canvas more than once."); |
|||
|
|||
callbackReference = DotNetObjectReference.Create(_callbackHelper); |
|||
|
|||
return _module.Invoke<GLInfo>(InitGLSymbol, _htmlCanvas, _htmlElementId, callbackReference); |
|||
} |
|||
|
|||
public bool InitRaster() |
|||
{ |
|||
if (callbackReference != null) |
|||
throw new InvalidOperationException("Unable to initialize the same canvas more than once."); |
|||
|
|||
callbackReference = DotNetObjectReference.Create(_callbackHelper); |
|||
|
|||
return _module.Invoke<bool>(InitRasterSymbol, _htmlCanvas, _htmlElementId, callbackReference); |
|||
} |
|||
|
|||
public void Deinit() |
|||
{ |
|||
if (callbackReference == null) |
|||
return; |
|||
|
|||
_module.Invoke(DeinitSymbol, _htmlElementId); |
|||
|
|||
callbackReference?.Dispose(); |
|||
} |
|||
|
|||
public void RequestAnimationFrame(bool enableRenderLoop) => |
|||
_module.Invoke(RequestAnimationFrameSymbol, _htmlCanvas, enableRenderLoop); |
|||
|
|||
public void SetCanvasSize(int rawWidth, int rawHeight) => |
|||
_module.Invoke(SetCanvasSizeSymbol, _htmlCanvas, rawWidth, rawHeight); |
|||
|
|||
public void PutImageData(IntPtr intPtr, SKSizeI rawSize) => |
|||
_module.Invoke(PutImageDataSymbol, _htmlCanvas, intPtr.ToInt64(), rawSize.Width, rawSize.Height); |
|||
|
|||
public record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth); |
|||
} |
|||
} |
|||
@ -1,50 +0,0 @@ |
|||
using Microsoft.AspNetCore.Components; |
|||
using Microsoft.JSInterop; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Web.Blazor.Interop |
|||
{ |
|||
internal class SizeWatcherInterop : IDisposable |
|||
{ |
|||
private const string ObserveSymbol = "SizeWatcher.observe"; |
|||
private const string UnobserveSymbol = "SizeWatcher.unobserve"; |
|||
|
|||
private readonly AvaloniaModule _module; |
|||
private readonly ElementReference _htmlElement; |
|||
private readonly string _htmlElementId; |
|||
private readonly ActionHelper<float, float> _callbackHelper; |
|||
|
|||
private DotNetObjectReference<ActionHelper<float, float>>? callbackReference; |
|||
|
|||
public SizeWatcherInterop(AvaloniaModule module, ElementReference element, Action<SKSize> callback) |
|||
{ |
|||
_module = module; |
|||
_htmlElement = element; |
|||
_htmlElementId = element.Id; |
|||
_callbackHelper = new ActionHelper<float, float>((x, y) => callback(new SKSize(x, y))); |
|||
} |
|||
|
|||
public void Dispose() => Stop(); |
|||
|
|||
public void Start() |
|||
{ |
|||
if (callbackReference != null) |
|||
return; |
|||
|
|||
callbackReference = DotNetObjectReference.Create(_callbackHelper); |
|||
|
|||
_module.Invoke(ObserveSymbol, _htmlElement, _htmlElementId, callbackReference); |
|||
} |
|||
|
|||
public void Stop() |
|||
{ |
|||
if (callbackReference == null) |
|||
return; |
|||
|
|||
_module.Invoke(UnobserveSymbol, _htmlElementId); |
|||
|
|||
callbackReference?.Dispose(); |
|||
callbackReference = null; |
|||
} |
|||
} |
|||
} |
|||
@ -1,225 +0,0 @@ |
|||
using System.Diagnostics.CodeAnalysis; |
|||
|
|||
using Avalonia.Platform.Storage; |
|||
|
|||
using Microsoft.JSInterop; |
|||
|
|||
namespace Avalonia.Web.Blazor.Interop.Storage |
|||
{ |
|||
internal record FilePickerAcceptType(string Description, IReadOnlyDictionary<string, IReadOnlyList<string>> Accept); |
|||
|
|||
internal record FileProperties(ulong Size, long LastModified, string? Type); |
|||
|
|||
internal class StorageProviderInterop : JSModuleInterop, IStorageProvider |
|||
{ |
|||
private const string JsFilename = "./_content/Avalonia.Web.Blazor/Storage.js"; |
|||
private const string PickerCancelMessage = "The user aborted a request"; |
|||
|
|||
public static async Task<StorageProviderInterop> ImportAsync(IJSRuntime js) |
|||
{ |
|||
var interop = new StorageProviderInterop(js); |
|||
await interop.ImportAsync(); |
|||
return interop; |
|||
} |
|||
|
|||
public StorageProviderInterop(IJSRuntime js) |
|||
: base(js, JsFilename) |
|||
{ |
|||
} |
|||
|
|||
public bool CanOpen => Invoke<bool>("StorageProvider.canOpen"); |
|||
public bool CanSave => Invoke<bool>("StorageProvider.canSave"); |
|||
public bool CanPickFolder => Invoke<bool>("StorageProvider.canPickFolder"); |
|||
|
|||
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options) |
|||
{ |
|||
try |
|||
{ |
|||
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; |
|||
|
|||
var (types, exludeAll) = ConvertFileTypes(options.FileTypeFilter); |
|||
var items = await InvokeAsync<IJSInProcessObjectReference>("StorageProvider.openFileDialog", startIn, options.AllowMultiple, types, exludeAll); |
|||
var count = items.Invoke<int>("count"); |
|||
|
|||
return Enumerable.Range(0, count) |
|||
.Select(index => new JSStorageFile(items.Invoke<IJSInProcessObjectReference>("at", index))) |
|||
.ToArray(); |
|||
} |
|||
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) |
|||
{ |
|||
return Array.Empty<IStorageFile>(); |
|||
} |
|||
} |
|||
|
|||
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options) |
|||
{ |
|||
try |
|||
{ |
|||
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; |
|||
|
|||
var (types, exludeAll) = ConvertFileTypes(options.FileTypeChoices); |
|||
var item = await InvokeAsync<IJSInProcessObjectReference>("StorageProvider.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; |
|||
} |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options) |
|||
{ |
|||
try |
|||
{ |
|||
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; |
|||
|
|||
var item = await InvokeAsync<IJSInProcessObjectReference>("StorageProvider.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) |
|||
{ |
|||
var item = await InvokeAsync<IJSInProcessObjectReference>("StorageProvider.openBookmark", bookmark); |
|||
return item is not null ? new JSStorageFile(item) : null; |
|||
} |
|||
|
|||
public async Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark) |
|||
{ |
|||
var item = await InvokeAsync<IJSInProcessObjectReference>("StorageProvider.openBookmark", bookmark); |
|||
return item is not null ? new JSStorageFolder(item) : null; |
|||
} |
|||
|
|||
private static (FilePickerAcceptType[]? types, bool excludeAllOption) ConvertFileTypes(IEnumerable<FilePickerFileType>? input) |
|||
{ |
|||
var types = input? |
|||
.Where(t => t.MimeTypes?.Any() == true && t != FilePickerFileTypes.All) |
|||
.Select(t => new FilePickerAcceptType(t.Name, t.MimeTypes! |
|||
.ToDictionary(m => m, _ => (IReadOnlyList<string>)Array.Empty<string>()))) |
|||
.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 IJSInProcessObjectReference? _fileHandle; |
|||
|
|||
protected JSStorageItem(IJSInProcessObjectReference fileHandle) |
|||
{ |
|||
_fileHandle = fileHandle ?? throw new ArgumentNullException(nameof(fileHandle)); |
|||
} |
|||
|
|||
internal IJSInProcessObjectReference FileHandle => _fileHandle ?? throw new ObjectDisposedException(nameof(JSStorageItem)); |
|||
|
|||
public string Name => FileHandle.Invoke<string>("getName"); |
|||
|
|||
public bool TryGetUri([NotNullWhen(true)] out Uri? uri) |
|||
{ |
|||
uri = new Uri(Name, UriKind.Relative); |
|||
return false; |
|||
} |
|||
|
|||
public async Task<StorageItemProperties> GetBasicPropertiesAsync() |
|||
{ |
|||
var properties = await FileHandle.InvokeAsync<FileProperties?>("getProperties"); |
|||
|
|||
return new StorageItemProperties( |
|||
properties?.Size, |
|||
dateCreated: null, |
|||
dateModified: properties?.LastModified > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(properties.LastModified) : null); |
|||
} |
|||
|
|||
public bool CanBookmark => true; |
|||
|
|||
public Task<string?> SaveBookmarkAsync() |
|||
{ |
|||
return FileHandle.InvokeAsync<string?>("saveBookmark").AsTask(); |
|||
} |
|||
|
|||
public Task<IStorageFolder?> GetParentAsync() |
|||
{ |
|||
return Task.FromResult<IStorageFolder?>(null); |
|||
} |
|||
|
|||
public Task ReleaseBookmarkAsync() |
|||
{ |
|||
return FileHandle.InvokeAsync<string?>("deleteBookmark").AsTask(); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_fileHandle?.Dispose(); |
|||
_fileHandle = null; |
|||
} |
|||
} |
|||
|
|||
internal class JSStorageFile : JSStorageItem, IStorageBookmarkFile |
|||
{ |
|||
public JSStorageFile(IJSInProcessObjectReference fileHandle) : base(fileHandle) |
|||
{ |
|||
} |
|||
|
|||
public bool CanOpenRead => true; |
|||
public async Task<Stream> OpenReadAsync() |
|||
{ |
|||
var stream = await FileHandle.InvokeAsync<IJSStreamReference>("openRead"); |
|||
// Remove maxAllowedSize limit, as developer can decide if they read only small part or everything.
|
|||
return await stream.OpenReadStreamAsync(long.MaxValue, CancellationToken.None); |
|||
} |
|||
|
|||
public bool CanOpenWrite => true; |
|||
public async Task<Stream> OpenWriteAsync() |
|||
{ |
|||
var properties = await FileHandle.InvokeAsync<FileProperties?>("getProperties"); |
|||
var streamWriter = await FileHandle.InvokeAsync<IJSInProcessObjectReference>("openWrite"); |
|||
|
|||
return new JSWriteableStream(streamWriter, (long)(properties?.Size ?? 0)); |
|||
} |
|||
} |
|||
|
|||
internal class JSStorageFolder : JSStorageItem, IStorageBookmarkFolder |
|||
{ |
|||
public JSStorageFolder(IJSInProcessObjectReference fileHandle) : base(fileHandle) |
|||
{ |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync() |
|||
{ |
|||
var items = await FileHandle.InvokeAsync<IJSInProcessObjectReference?>("getItems"); |
|||
if (items is null) |
|||
{ |
|||
return Array.Empty<IStorageItem>(); |
|||
} |
|||
|
|||
var count = items.Invoke<int>("count"); |
|||
|
|||
return Enumerable.Range(0, count) |
|||
.Select(index => |
|||
{ |
|||
var reference = items.Invoke<IJSInProcessObjectReference>("at", index); |
|||
return reference.Invoke<string>("getKind") switch |
|||
{ |
|||
"directory" => (IStorageItem)new JSStorageFolder(reference), |
|||
"file" => new JSStorageFile(reference), |
|||
_ => null |
|||
}; |
|||
}) |
|||
.Where(i => i is not null) |
|||
.ToArray()!; |
|||
} |
|||
} |
|||
} |
|||
@ -1,124 +0,0 @@ |
|||
using System.Buffers; |
|||
using System.Text.Json.Serialization; |
|||
|
|||
using Microsoft.JSInterop; |
|||
|
|||
namespace Avalonia.Web.Blazor.Interop.Storage |
|||
{ |
|||
// Loose wrapper implementaion of a stream on top of FileAPI FileSystemWritableFileStream
|
|||
internal sealed class JSWriteableStream : Stream |
|||
{ |
|||
private IJSInProcessObjectReference? _jSReference; |
|||
|
|||
// Unfortunatelly we can't read current length/position, so we need to keep it C#-side only.
|
|||
private long _length, _position; |
|||
|
|||
internal JSWriteableStream(IJSInProcessObjectReference jSReference, long initialLength) |
|||
{ |
|||
_jSReference = jSReference; |
|||
_length = initialLength; |
|||
} |
|||
|
|||
private IJSInProcessObjectReference JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(JSWriteableStream)); |
|||
|
|||
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 |
|||
}; |
|||
JSReference.InvokeVoid("seek", 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; |
|||
} |
|||
|
|||
JSReference.InvokeVoid("truncate", value); |
|||
} |
|||
|
|||
public override void Write(byte[] buffer, int offset, int count) |
|||
{ |
|||
throw new NotSupportedException("Synchronous writes are not supported."); |
|||
} |
|||
|
|||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) |
|||
{ |
|||
if (offset != 0 || count != buffer.Length) |
|||
{ |
|||
// TODO, we need to pass prepared buffer to the JS
|
|||
// Can't use ArrayPool as it can return bigger array than requested
|
|||
// Can't use Span/Memory, as it's not supported by JS interop yet.
|
|||
// Alternatively we can pass original buffer and offset+count, so it can be trimmed on the JS side (but is it more efficient tho?)
|
|||
buffer = buffer.AsMemory(offset, count).ToArray(); |
|||
} |
|||
return WriteAsyncInternal(buffer, cancellationToken).AsTask(); |
|||
} |
|||
|
|||
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) |
|||
{ |
|||
return WriteAsyncInternal(buffer.ToArray(), cancellationToken); |
|||
} |
|||
|
|||
private ValueTask WriteAsyncInternal(byte[] buffer, CancellationToken _) |
|||
{ |
|||
_position += buffer.Length; |
|||
|
|||
return JSReference.InvokeVoidAsync("write", buffer); |
|||
} |
|||
|
|||
protected override void Dispose(bool disposing) |
|||
{ |
|||
if (_jSReference is { } jsReference) |
|||
{ |
|||
_jSReference = null; |
|||
jsReference.InvokeVoid("close"); |
|||
jsReference.Dispose(); |
|||
} |
|||
} |
|||
|
|||
public override async ValueTask DisposeAsync() |
|||
{ |
|||
if (_jSReference is { } jsReference) |
|||
{ |
|||
_jSReference = null; |
|||
await jsReference.InvokeVoidAsync("close"); |
|||
await jsReference.DisposeAsync(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,35 +0,0 @@ |
|||
#nullable enable |
|||
using Avalonia.Controls.Platform; |
|||
|
|||
using Microsoft.JSInterop; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
public class JSObjectControlHandle : INativeControlHostDestroyableControlHandle |
|||
{ |
|||
internal const string ElementReferenceDescriptor = "JSObjectReference"; |
|||
|
|||
public JSObjectControlHandle(IJSObjectReference reference) |
|||
{ |
|||
Object = reference; |
|||
} |
|||
|
|||
public IJSObjectReference Object { get; } |
|||
|
|||
public IntPtr Handle => throw new NotSupportedException(); |
|||
|
|||
public string? HandleDescriptor => ElementReferenceDescriptor; |
|||
|
|||
public void Destroy() |
|||
{ |
|||
if (Object is IJSInProcessObjectReference inProcess) |
|||
{ |
|||
inProcess.Dispose(); |
|||
} |
|||
else |
|||
{ |
|||
_ = Object.DisposeAsync(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,127 +0,0 @@ |
|||
using Avalonia.Input; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
internal static class Keycodes |
|||
{ |
|||
public static Dictionary<string, Key> KeyCodes = new() |
|||
{ |
|||
{ "Escape", Key.Escape }, |
|||
{ "Digit1", Key.D1 }, |
|||
{ "Digit2", Key.D2 }, |
|||
{ "Digit3", Key.D3 }, |
|||
{ "Digit4", Key.D4 }, |
|||
{ "Digit5", Key.D5 }, |
|||
{ "Digit6", Key.D6 }, |
|||
{ "Digit7", Key.D7 }, |
|||
{ "Digit8", Key.D8 }, |
|||
{ "Digit9", Key.D9 }, |
|||
{ "Digit0", Key.D0 }, |
|||
{ "Minus", Key.OemMinus }, |
|||
//{ "Equal" , Key. },
|
|||
{ "Backspace", Key.Back }, |
|||
{ "Tab", Key.Tab }, |
|||
{ "KeyQ", Key.Q }, |
|||
{ "KeyW", Key.W }, |
|||
{ "KeyE", Key.E }, |
|||
{ "KeyR", Key.R }, |
|||
{ "KeyT", Key.T }, |
|||
{ "KeyY", Key.Y }, |
|||
{ "KeyU", Key.U }, |
|||
{ "KeyI", Key.I }, |
|||
{ "KeyO", Key.O }, |
|||
{ "KeyP", Key.P }, |
|||
{ "BracketLeft", Key.OemOpenBrackets }, |
|||
{ "BracketRight", Key.OemCloseBrackets }, |
|||
{ "Enter", Key.Enter }, |
|||
{ "ControlLeft", Key.LeftCtrl }, |
|||
{ "KeyA", Key.A }, |
|||
{ "KeyS", Key.S }, |
|||
{ "KeyD", Key.D }, |
|||
{ "KeyF", Key.F }, |
|||
{ "KeyG", Key.G }, |
|||
{ "KeyH", Key.H }, |
|||
{ "KeyJ", Key.J }, |
|||
{ "KeyK", Key.K }, |
|||
{ "KeyL", Key.L }, |
|||
{ "Semicolon", Key.OemSemicolon }, |
|||
{ "Quote", Key.OemQuotes }, |
|||
//{ "Backquote" , Key. },
|
|||
{ "ShiftLeft", Key.LeftShift }, |
|||
{ "Backslash", Key.OemBackslash }, |
|||
{ "KeyZ", Key.Z }, |
|||
{ "KeyX", Key.X }, |
|||
{ "KeyC", Key.C }, |
|||
{ "KeyV", Key.V }, |
|||
{ "KeyB", Key.B }, |
|||
{ "KeyN", Key.N }, |
|||
{ "KeyM", Key.M }, |
|||
{ "Comma", Key.OemComma }, |
|||
{ "Period", Key.OemPeriod }, |
|||
//{ "Slash" , Key. },
|
|||
{ "ShiftRight", Key.RightShift }, |
|||
{ "NumpadMultiply", Key.Multiply }, |
|||
{ "AltLeft", Key.LeftAlt }, |
|||
{ "Space", Key.Space }, |
|||
{ "CapsLock", Key.CapsLock }, |
|||
{ "F1", Key.F1 }, |
|||
{ "F2", Key.F2 }, |
|||
{ "F3", Key.F3 }, |
|||
{ "F4", Key.F4 }, |
|||
{ "F5", Key.F5 }, |
|||
{ "F6", Key.F6 }, |
|||
{ "F7", Key.F7 }, |
|||
{ "F8", Key.F8 }, |
|||
{ "F9", Key.F9 }, |
|||
{ "F10", Key.F10 }, |
|||
{ "NumLock", Key.NumLock }, |
|||
{ "ScrollLock", Key.Scroll }, |
|||
{ "Numpad7", Key.NumPad7 }, |
|||
{ "Numpad8", Key.NumPad8 }, |
|||
{ "Numpad9", Key.NumPad9 }, |
|||
{ "NumpadSubtract", Key.Subtract }, |
|||
{ "Numpad4", Key.NumPad4 }, |
|||
{ "Numpad5", Key.NumPad5 }, |
|||
{ "Numpad6", Key.NumPad6 }, |
|||
{ "NumpadAdd", Key.Add }, |
|||
{ "Numpad1", Key.NumPad1 }, |
|||
{ "Numpad2", Key.NumPad2 }, |
|||
{ "Numpad3", Key.NumPad3 }, |
|||
{ "Numpad0", Key.NumPad0 }, |
|||
{ "NumpadDecimal", Key.Decimal }, |
|||
{ "Unidentified", Key.NoName }, |
|||
//{ "IntlBackslash" , Key.bac },
|
|||
{ "F11", Key.F11 }, |
|||
{ "F12", Key.F12 }, |
|||
//{ "IntlRo" , Key.Ro },
|
|||
//{ "Unidentified" , Key. },
|
|||
{ "Convert", Key.ImeConvert }, |
|||
{ "KanaMode", Key.KanaMode }, |
|||
{ "NonConvert", Key.ImeNonConvert }, |
|||
//{ "Unidentified" , Key. },
|
|||
{ "NumpadEnter", Key.Enter }, |
|||
{ "ControlRight", Key.RightCtrl }, |
|||
{ "NumpadDivide", Key.Divide }, |
|||
{ "PrintScreen", Key.PrintScreen }, |
|||
{ "AltRight", Key.RightAlt }, |
|||
//{ "Unidentified" , Key. },
|
|||
{ "Home", Key.Home }, |
|||
{ "ArrowUp", Key.Up }, |
|||
{ "PageUp", Key.PageUp }, |
|||
{ "ArrowLeft", Key.Left }, |
|||
{ "ArrowRight", Key.Right }, |
|||
{ "End", Key.End }, |
|||
{ "ArrowDown", Key.Down }, |
|||
{ "PageDown", Key.PageDown }, |
|||
{ "Insert", Key.Insert }, |
|||
{ "Delete", Key.Delete }, |
|||
//{ "Unidentified" , Key. },
|
|||
{ "AudioVolumeMute", Key.VolumeMute }, |
|||
{ "AudioVolumeDown", Key.VolumeDown }, |
|||
{ "AudioVolumeUp", Key.VolumeUp }, |
|||
//{ "NumpadEqual" , Key. },
|
|||
{ "Pause", Key.Pause }, |
|||
{ "NumpadComma", Key.OemComma } |
|||
}; |
|||
} |
|||
} |
|||
@ -1,17 +0,0 @@ |
|||
using System.Diagnostics; |
|||
using Avalonia.Rendering; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
public class ManualTriggerRenderTimer : IRenderTimer |
|||
{ |
|||
private static readonly Stopwatch s_sw = Stopwatch.StartNew(); |
|||
|
|||
public static ManualTriggerRenderTimer Instance { get; } = new(); |
|||
|
|||
public void RaiseTick() => Tick?.Invoke(s_sw.Elapsed); |
|||
|
|||
public event Action<TimeSpan>? Tick; |
|||
public bool RunsInBackground => false; |
|||
} |
|||
} |
|||
@ -1,222 +0,0 @@ |
|||
using System.Diagnostics; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Raw; |
|||
using Avalonia.Input.TextInput; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Platform.Storage; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.Rendering.Composition; |
|||
using Avalonia.Web.Blazor.Interop; |
|||
using SkiaSharp; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider |
|||
{ |
|||
private Size _clientSize; |
|||
private IBlazorSkiaSurface? _currentSurface; |
|||
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 RazorViewTopLevelImpl(AvaloniaView avaloniaView) |
|||
{ |
|||
_avaloniaView = avaloniaView; |
|||
TransparencyLevel = WindowTransparencyLevel.None; |
|||
AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1); |
|||
_touchDevice = new TouchDevice(); |
|||
_penDevice = new PenDevice(); |
|||
} |
|||
|
|||
public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds; |
|||
|
|||
|
|||
internal void SetSurface(GRContext context, SKHtmlCanvasInterop.GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling) |
|||
{ |
|||
_currentSurface = |
|||
new BlazorSkiaSurface(context, glInfo, colorType, size, scaling, GRSurfaceOrigin.BottomLeft); |
|||
} |
|||
|
|||
internal void SetSurface(SKColorType colorType, PixelSize size, double scaling, Action<IntPtr, SKSizeI> blitCallback) |
|||
{ |
|||
_currentSurface = new BlazorSkiaRasterSurface(colorType, size, scaling, blitCallback); |
|||
} |
|||
|
|||
public void SetClientSize(SKSize size, double dpi) |
|||
{ |
|||
var newSize = new Size(size.Width, size.Height); |
|||
|
|||
if (Math.Abs(RenderScaling - dpi) > 0.0001) |
|||
{ |
|||
if (_currentSurface is { }) |
|||
{ |
|||
_currentSurface.Scaling = dpi; |
|||
} |
|||
|
|||
ScalingChanged?.Invoke(dpi); |
|||
} |
|||
|
|||
if (newSize != _clientSize) |
|||
{ |
|||
_clientSize = newSize; |
|||
|
|||
if (_currentSurface is { }) |
|||
{ |
|||
_currentSurface.Size = new PixelSize((int)size.Width, (int)size.Height); |
|||
} |
|||
|
|||
Resized?.Invoke(newSize, PlatformResizeReason.User); |
|||
} |
|||
} |
|||
|
|||
public void 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); |
|||
} |
|||
} |
|||
|
|||
private IPointerDevice GetPointerDevice(string pointerType) |
|||
{ |
|||
return pointerType switch |
|||
{ |
|||
"touch" => _touchDevice, |
|||
"pen" => _penDevice, |
|||
_ => MouseDevice |
|||
}; |
|||
} |
|||
|
|||
public void RawMouseWheelEvent(Point p, Vector v, RawInputModifiers modifiers) |
|||
{ |
|||
if (_inputRoot is { }) |
|||
{ |
|||
Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, _inputRoot, p, v, modifiers)); |
|||
} |
|||
} |
|||
|
|||
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 void RawTextEvent(string text) |
|||
{ |
|||
if (_inputRoot is { }) |
|||
{ |
|||
Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice, Timestamp, _inputRoot, text)); |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public IRenderer CreateRenderer(IRenderRoot root) |
|||
{ |
|||
var loop = AvaloniaLocator.Current.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 => _currentSurface?.Scaling ?? 1; |
|||
|
|||
public IEnumerable<object> Surfaces => new object[] { _currentSurface! }; |
|||
|
|||
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; } = BlazorWindowingPlatform.Keyboard; |
|||
public WindowTransparencyLevel TransparencyLevel { get; } |
|||
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } |
|||
|
|||
public ITextInputMethodImpl TextInputMethod => _avaloniaView; |
|||
|
|||
public INativeControlHostImpl? NativeControlHost => _avaloniaView.GetNativeControlHostImpl(); |
|||
public IStorageProvider StorageProvider => _avaloniaView.GetStorageProvider(); |
|||
} |
|||
} |
|||
@ -1,50 +0,0 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Platform; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
internal class 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); |
|||
} |
|||
} |
|||
} |
|||
@ -1,106 +0,0 @@ |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.Threading; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
public class BlazorWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface |
|||
{ |
|||
private bool _signaled; |
|||
private static 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("BlazorWindowingPlatform not registered."); |
|||
|
|||
public static void Register() |
|||
{ |
|||
var instance = new BlazorWindowingPlatform(); |
|||
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; // Blazor is single threaded.
|
|||
} |
|||
} |
|||
|
|||
public event Action<DispatcherPriority?>? Signaled; |
|||
|
|||
private static IRuntimePlatform GetRuntimePlatform() |
|||
{ |
|||
return AvaloniaLocator.Current.GetRequiredService<IRuntimePlatform>(); |
|||
} |
|||
} |
|||
} |
|||
@ -1 +0,0 @@ |
|||
@using Microsoft.AspNetCore.Components.Web |
|||
@ -1,16 +0,0 @@ |
|||
require("esbuild").build({ |
|||
entryPoints: [ |
|||
"./modules/Avalonia.ts", |
|||
"./modules/Storage.ts" |
|||
], |
|||
outdir: "../wwwroot", |
|||
bundle: true, |
|||
minify: true, |
|||
format: "esm", |
|||
target: "es2016", |
|||
platform: "browser", |
|||
sourcemap: "linked", |
|||
loader: {".ts": "ts"} |
|||
}) |
|||
.then(() => console.log("⚡ Done")) |
|||
.catch(() => process.exit(1)); |
|||
@ -1,7 +0,0 @@ |
|||
export { DpiWatcher } from "./Avalonia/DpiWatcher" |
|||
export { InputHelper } from "./Avalonia/InputHelper" |
|||
export { FocusHelper } from "./Avalonia/FocusHelper" |
|||
export { NativeControlHost } from "./Avalonia/NativeControlHost" |
|||
export { SizeWatcher } from "./Avalonia/SizeWatcher" |
|||
export { SKHtmlCanvas } from "./Avalonia/SKHtmlCanvas" |
|||
export { CaretHelper } from "./Avalonia/CaretHelper" |
|||
@ -1,149 +0,0 @@ |
|||
// 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 && 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 - https://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; |
|||
} |
|||
} |
|||
|
|||
|
|||
var 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; |
|||
@ -1,40 +0,0 @@ |
|||
export class DpiWatcher { |
|||
static lastDpi: number; |
|||
static timerId: number; |
|||
static callback?: DotNet.DotNetObject; |
|||
|
|||
public static getDpi() { |
|||
return window.devicePixelRatio; |
|||
} |
|||
|
|||
public static start(callback: DotNet.DotNetObject): number { |
|||
//console.info(`Starting DPI watcher with callback ${callback._id}...`);
|
|||
|
|||
DpiWatcher.lastDpi = window.devicePixelRatio; |
|||
DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000); |
|||
DpiWatcher.callback = callback; |
|||
|
|||
return DpiWatcher.lastDpi; |
|||
} |
|||
|
|||
public static stop() { |
|||
//console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`);
|
|||
|
|||
window.clearInterval(DpiWatcher.timerId); |
|||
|
|||
DpiWatcher.callback = undefined; |
|||
} |
|||
|
|||
static update() { |
|||
if (!DpiWatcher.callback) |
|||
return; |
|||
|
|||
const currentDpi = window.devicePixelRatio; |
|||
const lastDpi = DpiWatcher.lastDpi; |
|||
DpiWatcher.lastDpi = currentDpi; |
|||
|
|||
if (Math.abs(lastDpi - currentDpi) > 0.001) { |
|||
DpiWatcher.callback.invokeMethod('Invoke', lastDpi, currentDpi); |
|||
} |
|||
} |
|||
} |
|||
@ -1,9 +0,0 @@ |
|||
export class FocusHelper { |
|||
public static focus(inputElement: HTMLElement) { |
|||
inputElement.focus(); |
|||
} |
|||
|
|||
public static setCursor(inputElement: HTMLInputElement, kind: string) { |
|||
inputElement.style.cursor = kind; |
|||
} |
|||
} |
|||
@ -1,86 +0,0 @@ |
|||
import {CaretHelper} from "./CaretHelper"; |
|||
|
|||
export class InputHelper { |
|||
static inputCallback?: DotNet.DotNetObject; |
|||
static compositionCallback?: DotNet.DotNetObject |
|||
|
|||
public static start(inputElement: HTMLInputElement, compositionCallback: DotNet.DotNetObject, inputCallback: DotNet.DotNetObject) |
|||
{ |
|||
InputHelper.compositionCallback = compositionCallback; |
|||
|
|||
inputElement.addEventListener('compositionstart', InputHelper.onCompositionEvent); |
|||
inputElement.addEventListener('compositionupdate', InputHelper.onCompositionEvent); |
|||
inputElement.addEventListener('compositionend', InputHelper.onCompositionEvent); |
|||
|
|||
InputHelper.inputCallback = inputCallback; |
|||
|
|||
inputElement.addEventListener('input', InputHelper.onInputEvent); |
|||
} |
|||
|
|||
public static clear(inputElement: HTMLInputElement) { |
|||
inputElement.value = ""; |
|||
} |
|||
public static focus(inputElement: HTMLInputElement) { |
|||
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"; |
|||
|
|||
let {height, 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 onCompositionEvent(ev: CompositionEvent) |
|||
{ |
|||
if(!InputHelper.compositionCallback) |
|||
return; |
|||
|
|||
switch (ev.type) |
|||
{ |
|||
case "compositionstart": |
|||
case "compositionupdate": |
|||
case "compositionend": |
|||
InputHelper.compositionCallback.invokeMethod('Invoke', ev.type, ev.data); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
private static onInputEvent(ev: Event) { |
|||
if (!InputHelper.inputCallback) |
|||
return; |
|||
|
|||
var inputEvent = ev as InputEvent; |
|||
|
|||
InputHelper.inputCallback.invokeMethod('Invoke', ev.type, inputEvent.data); |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
@ -1,61 +0,0 @@ |
|||
export class NativeControlHost { |
|||
public static CreateDefaultChild(parent: HTMLElement): HTMLElement { |
|||
return document.createElement("div"); |
|||
} |
|||
|
|||
// Used to convert ElementReference to JSObjectReference.
|
|||
// Is there a better way?
|
|||
public static GetReference(element: Element): Element { |
|||
return element; |
|||
} |
|||
|
|||
public static CreateAttachment(): NativeControlHostTopLevelAttachment { |
|||
return new NativeControlHostTopLevelAttachment(); |
|||
} |
|||
} |
|||
|
|||
class NativeControlHostTopLevelAttachment { |
|||
_child?: HTMLElement; |
|||
_host?: HTMLElement; |
|||
|
|||
InitializeWithChildHandle(child: HTMLElement) { |
|||
this._child = child; |
|||
this._child.style.position = "absolute"; |
|||
} |
|||
|
|||
AttachTo(host: HTMLElement): void { |
|||
if (this._host && this._child) { |
|||
this._host.removeChild(this._child); |
|||
} |
|||
|
|||
this._host = host; |
|||
|
|||
if (this._host && this._child) { |
|||
this._host.appendChild(this._child); |
|||
} |
|||
} |
|||
|
|||
ShowInBounds(x: number, y: number, width: number, height: number): void { |
|||
if (this._child) { |
|||
this._child.style.top = y + "px"; |
|||
this._child.style.left = x + "px"; |
|||
this._child.style.width = width + "px"; |
|||
this._child.style.height = height + "px"; |
|||
this._child.style.display = "block"; |
|||
} |
|||
} |
|||
|
|||
HideWithSize(width: number, height: number): void { |
|||
if (this._child) { |
|||
this._child.style.width = width + "px"; |
|||
this._child.style.height = height + "px"; |
|||
this._child.style.display = "none"; |
|||
} |
|||
} |
|||
|
|||
ReleaseChild(): void { |
|||
if (this._child) { |
|||
this._child = undefined; |
|||
} |
|||
} |
|||
} |
|||
@ -1,255 +0,0 @@ |
|||
// aliases for emscripten
|
|||
declare let GL: any; |
|||
declare let GLctx: WebGLRenderingContext; |
|||
declare let Module: EmscriptenModule; |
|||
|
|||
// container for gl info
|
|||
type SKGLViewInfo = { |
|||
context: WebGLRenderingContext | WebGL2RenderingContext | undefined; |
|||
fboId: number; |
|||
stencil: number; |
|||
sample: number; |
|||
depth: number; |
|||
} |
|||
|
|||
// alias for a potential skia html canvas
|
|||
type SKHtmlCanvasElement = { |
|||
SKHtmlCanvas: SKHtmlCanvas | undefined |
|||
} & HTMLCanvasElement |
|||
|
|||
export class SKHtmlCanvas { |
|||
static elements: Map<string, HTMLCanvasElement>; |
|||
|
|||
htmlCanvas: HTMLCanvasElement; |
|||
glInfo?: SKGLViewInfo; |
|||
renderFrameCallback: DotNet.DotNetObject; |
|||
renderLoopEnabled: boolean = false; |
|||
renderLoopRequest: number = 0; |
|||
newWidth?: number; |
|||
newHeight?: number; |
|||
|
|||
public static initGL(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): SKGLViewInfo | null { |
|||
var view = SKHtmlCanvas.init(true, element, elementId, callback); |
|||
if (!view || !view.glInfo) |
|||
return null; |
|||
|
|||
return view.glInfo; |
|||
} |
|||
|
|||
public static initRaster(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): boolean { |
|||
var view = SKHtmlCanvas.init(false, element, elementId, callback); |
|||
if (!view) |
|||
return false; |
|||
|
|||
return true; |
|||
} |
|||
|
|||
static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): SKHtmlCanvas | null { |
|||
var htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas) { |
|||
console.error(`No canvas element was provided.`); |
|||
return null; |
|||
} |
|||
|
|||
if (!SKHtmlCanvas.elements) |
|||
SKHtmlCanvas.elements = new Map<string, HTMLCanvasElement>(); |
|||
SKHtmlCanvas.elements.set(elementId, element); |
|||
|
|||
const view = new SKHtmlCanvas(useGL, element, callback); |
|||
|
|||
htmlCanvas.SKHtmlCanvas = view; |
|||
|
|||
return view; |
|||
} |
|||
|
|||
public static deinit(elementId: string) { |
|||
if (!elementId) |
|||
return; |
|||
|
|||
const element = SKHtmlCanvas.elements.get(elementId); |
|||
SKHtmlCanvas.elements.delete(elementId); |
|||
|
|||
const htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) |
|||
return; |
|||
|
|||
htmlCanvas.SKHtmlCanvas.deinit(); |
|||
htmlCanvas.SKHtmlCanvas = undefined; |
|||
} |
|||
|
|||
public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean) { |
|||
const htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) |
|||
return; |
|||
|
|||
htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop); |
|||
} |
|||
|
|||
public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number) { |
|||
const htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) |
|||
return; |
|||
|
|||
htmlCanvas.SKHtmlCanvas.setCanvasSize(width, height); |
|||
} |
|||
|
|||
public static setEnableRenderLoop(element: HTMLCanvasElement, enable: boolean) { |
|||
const htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) |
|||
return; |
|||
|
|||
htmlCanvas.SKHtmlCanvas.setEnableRenderLoop(enable); |
|||
} |
|||
|
|||
public static putImageData(element: HTMLCanvasElement, pData: number, width: number, height: number) { |
|||
const htmlCanvas = element as SKHtmlCanvasElement; |
|||
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) |
|||
return; |
|||
|
|||
htmlCanvas.SKHtmlCanvas.putImageData(pData, width, height); |
|||
} |
|||
|
|||
public constructor(useGL: boolean, element: HTMLCanvasElement, callback: DotNet.DotNetObject) { |
|||
this.htmlCanvas = element; |
|||
this.renderFrameCallback = callback; |
|||
|
|||
if (useGL) { |
|||
const ctx = SKHtmlCanvas.createWebGLContext(this.htmlCanvas); |
|||
if (!ctx) { |
|||
console.error(`Failed to create WebGL context: err ${ctx}`); |
|||
return; |
|||
} |
|||
|
|||
// make current
|
|||
GL.makeContextCurrent(ctx); |
|||
|
|||
// read values
|
|||
const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); |
|||
this.glInfo = { |
|||
context: ctx, |
|||
fboId: fbo ? fbo.id : 0, |
|||
stencil: GLctx.getParameter(GLctx.STENCIL_BITS), |
|||
sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES)
|
|||
depth: GLctx.getParameter(GLctx.DEPTH_BITS), |
|||
}; |
|||
} |
|||
} |
|||
|
|||
public deinit() { |
|||
this.setEnableRenderLoop(false); |
|||
} |
|||
|
|||
public setCanvasSize(width: number, height: number) { |
|||
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) { |
|||
// make current
|
|||
GL.makeContextCurrent(this.glInfo.context); |
|||
} |
|||
} |
|||
|
|||
public requestAnimationFrame(renderLoop?: boolean) { |
|||
// 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) { |
|||
// 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.invokeMethod('Invoke'); |
|||
this.renderLoopRequest = 0; |
|||
|
|||
// we may want to draw the next frame
|
|||
if (this.renderLoopEnabled) |
|||
this.requestAnimationFrame(); |
|||
}); |
|||
} |
|||
|
|||
public setEnableRenderLoop(enable: boolean) { |
|||
this.renderLoopEnabled = enable; |
|||
|
|||
// either start the new frame or cancel the existing one
|
|||
if (enable) { |
|||
//console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`);
|
|||
this.requestAnimationFrame(); |
|||
} else if (this.renderLoopRequest !== 0) { |
|||
window.cancelAnimationFrame(this.renderLoopRequest); |
|||
this.renderLoopRequest = 0; |
|||
} |
|||
} |
|||
|
|||
public putImageData(pData: number, width: number, height: number): boolean { |
|||
if (this.glInfo || !pData || width <= 0 || width <= 0) |
|||
return false; |
|||
|
|||
var ctx = this.htmlCanvas.getContext('2d'); |
|||
if (!ctx) { |
|||
console.error(`Failed to obtain 2D canvas context.`); |
|||
return false; |
|||
} |
|||
|
|||
// make sure the canvas is scaled correctly for the drawing
|
|||
this.htmlCanvas.width = width; |
|||
this.htmlCanvas.height = height; |
|||
|
|||
// set the canvas to be the bytes
|
|||
var buffer = new Uint8ClampedArray(Module.HEAPU8.buffer, pData, width * height * 4); |
|||
var imageData = new ImageData(buffer, width, height); |
|||
ctx.putImageData(imageData, 0, 0); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext { |
|||
const contextAttributes = { |
|||
alpha: 1, |
|||
depth: 1, |
|||
stencil: 8, |
|||
antialias: 0, |
|||
premultipliedAlpha: 1, |
|||
preserveDrawingBuffer: 0, |
|||
preferLowPowerToHighPerformance: 0, |
|||
failIfMajorPerformanceCaveat: 0, |
|||
majorVersion: 2, |
|||
minorVersion: 0, |
|||
enableExtensionsByDefault: 1, |
|||
explicitSwapControl: 0, |
|||
renderViaOffscreenBackBuffer: 1, |
|||
}; |
|||
|
|||
let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes); |
|||
if (!ctx && contextAttributes.majorVersion > 1) { |
|||
console.warn('Falling back to WebGL 1.0'); |
|||
contextAttributes.majorVersion = 1; |
|||
contextAttributes.minorVersion = 0; |
|||
ctx = GL.createContext(htmlCanvas, contextAttributes); |
|||
} |
|||
|
|||
return ctx; |
|||
} |
|||
} |
|||
@ -1,67 +0,0 @@ |
|||
type SizeWatcherElement = { |
|||
SizeWatcher: SizeWatcherInstance; |
|||
} & HTMLElement |
|||
|
|||
type SizeWatcherInstance = { |
|||
callback: DotNet.DotNetObject; |
|||
} |
|||
|
|||
export class SizeWatcher { |
|||
static observer: ResizeObserver; |
|||
static elements: Map<string, HTMLElement>; |
|||
|
|||
public static observe(element: HTMLElement, elementId: string, callback: DotNet.DotNetObject) { |
|||
if (!element || !callback) |
|||
return; |
|||
|
|||
//console.info(`Adding size watcher observation with callback ${callback._id}...`);
|
|||
|
|||
SizeWatcher.init(); |
|||
|
|||
const watcherElement = element as SizeWatcherElement; |
|||
watcherElement.SizeWatcher = { |
|||
callback: callback |
|||
}; |
|||
|
|||
SizeWatcher.elements.set(elementId, element); |
|||
SizeWatcher.observer.observe(element); |
|||
|
|||
SizeWatcher.invoke(element); |
|||
} |
|||
|
|||
public static unobserve(elementId: string) { |
|||
if (!elementId || !SizeWatcher.observer) |
|||
return; |
|||
|
|||
//console.info('Removing size watcher observation...');
|
|||
|
|||
const element = SizeWatcher.elements.get(elementId)!; |
|||
|
|||
SizeWatcher.elements.delete(elementId); |
|||
SizeWatcher.observer.unobserve(element); |
|||
} |
|||
|
|||
static init() { |
|||
if (SizeWatcher.observer) |
|||
return; |
|||
|
|||
//console.info('Starting size watcher...');
|
|||
|
|||
SizeWatcher.elements = new Map<string, HTMLElement>(); |
|||
SizeWatcher.observer = new ResizeObserver((entries) => { |
|||
for (let entry of entries) { |
|||
SizeWatcher.invoke(entry.target); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
static invoke(element: Element) { |
|||
const watcherElement = element as SizeWatcherElement; |
|||
const instance = watcherElement.SizeWatcher; |
|||
|
|||
if (!instance || !instance.callback) |
|||
return; |
|||
|
|||
return instance.callback.invokeMethod('Invoke', element.clientWidth, element.clientHeight); |
|||
} |
|||
} |
|||
@ -1 +0,0 @@ |
|||
export { StorageProvider } from "./Storage/StorageProvider" |
|||
@ -1,79 +0,0 @@ |
|||
class InnerDbConnection { |
|||
constructor(private database: IDBDatabase) { } |
|||
|
|||
private openStore(store: string, mode: IDBTransactionMode): IDBObjectStore { |
|||
const tx = this.database.transaction(store, mode); |
|||
return tx.objectStore(store); |
|||
} |
|||
|
|||
public put(store: string, obj: any, key?: IDBValidKey): Promise<IDBValidKey> { |
|||
const os = this.openStore(store, "readwrite"); |
|||
|
|||
return 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 delete(store: string, key: IDBValidKey): Promise<void> { |
|||
const os = this.openStore(store, "readwrite"); |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
const response = os.delete(key); |
|||
response.onsuccess = () => { |
|||
resolve(); |
|||
}; |
|||
response.onerror = () => { |
|||
reject(response.error); |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
public close() { |
|||
this.database.close(); |
|||
} |
|||
} |
|||
|
|||
export class IndexedDbWrapper { |
|||
constructor(private databaseName: string, private objectStores: [string]) { |
|||
} |
|||
|
|||
public connect(): Promise<InnerDbConnection> { |
|||
const conn = window.indexedDB.open(this.databaseName, 1); |
|||
|
|||
conn.onupgradeneeded = event => { |
|||
const db = (<IDBRequest<IDBDatabase>>event.target).result; |
|||
this.objectStores.forEach(store => { |
|||
db.createObjectStore(store); |
|||
}); |
|||
}; |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
conn.onsuccess = event => { |
|||
resolve(new InnerDbConnection((<IDBRequest<IDBDatabase>>event.target).result)); |
|||
}; |
|||
conn.onerror = event => { |
|||
reject((<IDBRequest<IDBDatabase>>event.target).error); |
|||
}; |
|||
}); |
|||
} |
|||
} |
|||
@ -1,204 +0,0 @@ |
|||
import { IndexedDbWrapper } from "./IndexedDbWrapper"; |
|||
|
|||
declare global { |
|||
type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; |
|||
type StartInDirectory = WellKnownDirectory | FileSystemHandle; |
|||
interface OpenFilePickerOptions { |
|||
startIn?: StartInDirectory |
|||
} |
|||
interface SaveFilePickerOptions { |
|||
startIn?: StartInDirectory |
|||
} |
|||
} |
|||
|
|||
const fileBookmarksStore: string = "fileBookmarks"; |
|||
const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [ |
|||
fileBookmarksStore |
|||
]); |
|||
|
|||
class StorageItem { |
|||
constructor(public handle: FileSystemHandle, private bookmarkId?: string) { } |
|||
|
|||
public getName(): string { |
|||
return this.handle.name |
|||
} |
|||
|
|||
public getKind(): string { |
|||
return this.handle.kind; |
|||
} |
|||
|
|||
public async openRead(): Promise<Blob> { |
|||
if (!(this.handle instanceof FileSystemFileHandle)) { |
|||
throw new Error("StorageItem is not a file"); |
|||
} |
|||
|
|||
await this.verityPermissions('read'); |
|||
|
|||
const file = await this.handle.getFile(); |
|||
return file; |
|||
} |
|||
|
|||
public async openWrite(): Promise<FileSystemWritableFileStream> { |
|||
if (!(this.handle instanceof FileSystemFileHandle)) { |
|||
throw new Error("StorageItem is not a file"); |
|||
} |
|||
|
|||
await this.verityPermissions('readwrite'); |
|||
|
|||
return await this.handle.createWritable({ keepExistingData: true }); |
|||
} |
|||
|
|||
public async getProperties(): Promise<{ Size: number, LastModified: number, Type: string } | null> { |
|||
const file = this.handle instanceof FileSystemFileHandle |
|||
&& await this.handle.getFile(); |
|||
|
|||
if (!file) { |
|||
return null; |
|||
} |
|||
|
|||
return { |
|||
Size: file.size, |
|||
LastModified: file.lastModified, |
|||
Type: file.type |
|||
} |
|||
} |
|||
|
|||
public async getItems(): Promise<StorageItems> { |
|||
if (this.handle.kind !== "directory"){ |
|||
return new StorageItems([]); |
|||
} |
|||
|
|||
const items: StorageItem[] = []; |
|||
for await (const [key, value] of (this.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("Read permissions denied"); |
|||
} |
|||
} |
|||
|
|||
public async saveBookmark(): Promise<string> { |
|||
// If file was previously bookmarked, just return old one.
|
|||
if (this.bookmarkId) { |
|||
return this.bookmarkId; |
|||
} |
|||
|
|||
const connection = await avaloniaDb.connect(); |
|||
try { |
|||
const key = await connection.put(fileBookmarksStore, this.handle, this.generateBookmarkId()); |
|||
return <string>key; |
|||
} |
|||
finally { |
|||
connection.close(); |
|||
} |
|||
} |
|||
|
|||
public async deleteBookmark(): Promise<void> { |
|||
if (!this.bookmarkId) { |
|||
return; |
|||
} |
|||
|
|||
const connection = await avaloniaDb.connect(); |
|||
try { |
|||
const key = await connection.delete(fileBookmarksStore, this.bookmarkId); |
|||
} |
|||
finally { |
|||
connection.close(); |
|||
} |
|||
} |
|||
|
|||
private generateBookmarkId(): string { |
|||
return Date.now().toString(36) + Math.random().toString(36).substring(2); |
|||
} |
|||
} |
|||
|
|||
class StorageItems { |
|||
constructor(private items: StorageItem[]) { } |
|||
|
|||
public count(): number { |
|||
return this.items.length; |
|||
} |
|||
|
|||
public at(index: number): StorageItem { |
|||
return this.items[index]; |
|||
} |
|||
} |
|||
|
|||
export class StorageProvider { |
|||
|
|||
public static canOpen(): boolean { |
|||
return typeof window.showOpenFilePicker !== 'undefined'; |
|||
} |
|||
|
|||
public static canSave(): boolean { |
|||
return typeof window.showSaveFilePicker !== 'undefined'; |
|||
} |
|||
|
|||
public static canPickFolder(): boolean { |
|||
return typeof window.showDirectoryPicker !== 'undefined'; |
|||
} |
|||
|
|||
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(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,13 +0,0 @@ |
|||
{ |
|||
"name": "avalonia.web", |
|||
"scripts": { |
|||
"prebuild": "tsc -noEmit", |
|||
"build": "node build.js" |
|||
}, |
|||
"devDependencies": { |
|||
"@types/emscripten": "^1.39.6", |
|||
"@types/wicg-file-system-access": "^2020.9.5", |
|||
"typescript": "^4.7.4", |
|||
"esbuild": "^0.15.7" |
|||
} |
|||
} |
|||
@ -1,18 +0,0 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"target": "es2016", |
|||
"strict": true, |
|||
"sourceMap": true, |
|||
"outDir": "../wwwroot", |
|||
"noEmitOnError": true, |
|||
"isolatedModules": true, // we need it for esbuild |
|||
"lib": [ |
|||
"dom", |
|||
"es2016", |
|||
"esnext.asynciterable" |
|||
] |
|||
}, |
|||
"exclude": [ |
|||
"node_modules" |
|||
] |
|||
} |
|||
@ -1,56 +0,0 @@ |
|||
// Type definitions for non-npm package @blazor/javascript-interop 3.1
|
|||
// Project: https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interop?view=aspnetcore-3.1
|
|||
// Definitions by: Piotr Błażejewicz (Peter Blazejewicz) <https://github.com/peterblazejewicz>
|
|||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
|||
// Minimum TypeScript Version: 3.0
|
|||
|
|||
// Here be dragons!
|
|||
// This is community-maintained definition file intended to ease the process of developing
|
|||
// high quality JavaScript interop code to be used in Blazor application from your C# .NET code.
|
|||
// Could be removed without a notice in case official definition types ships with Blazor itself.
|
|||
|
|||
// tslint:disable:no-unnecessary-generics
|
|||
|
|||
declare namespace DotNet { |
|||
/** |
|||
* Invokes the specified .NET public method synchronously. Not all hosting scenarios support |
|||
* synchronous invocation, so if possible use invokeMethodAsync instead. |
|||
* |
|||
* @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method. |
|||
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. |
|||
* @param args Arguments to pass to the method, each of which must be JSON-serializable. |
|||
* @returns The result of the operation. |
|||
*/ |
|||
function invokeMethod<T>(assemblyName: string, methodIdentifier: string, ...args: any[]): T; |
|||
/** |
|||
* Invokes the specified .NET public method asynchronously. |
|||
* |
|||
* @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method. |
|||
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. |
|||
* @param args Arguments to pass to the method, each of which must be JSON-serializable. |
|||
* @returns A promise representing the result of the operation. |
|||
*/ |
|||
function invokeMethodAsync<T>(assemblyName: string, methodIdentifier: string, ...args: any[]): Promise<T>; |
|||
/** |
|||
* Represents the .NET instance passed by reference to JavaScript. |
|||
*/ |
|||
interface DotNetObject { |
|||
/** |
|||
* Invokes the specified .NET instance public method synchronously. Not all hosting scenarios support |
|||
* synchronous invocation, so if possible use invokeMethodAsync instead. |
|||
* |
|||
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. |
|||
* @param args Arguments to pass to the method, each of which must be JSON-serializable. |
|||
* @returns The result of the operation. |
|||
*/ |
|||
invokeMethod<T>(methodIdentifier: string, ...args: any[]): T; |
|||
/** |
|||
* Invokes the specified .NET instance public method asynchronously. |
|||
* |
|||
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. |
|||
* @param args Arguments to pass to the method, each of which must be JSON-serializable. |
|||
* @returns A promise representing the result of the operation. |
|||
*/ |
|||
invokeMethodAsync<T>(methodIdentifier: string, ...args: any[]): Promise<T>; |
|||
} |
|||
} |
|||
@ -1,41 +0,0 @@ |
|||
<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> |
|||
@ -1,44 +0,0 @@ |
|||
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); |
|||
} |
|||
@ -1,19 +0,0 @@ |
|||
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>(); |
|||
} |
|||
@ -1,41 +1,54 @@ |
|||
using System.Runtime.InteropServices.JavaScript; |
|||
using System; |
|||
using System; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Avalonia.Media; |
|||
using Avalonia.Web.Skia; |
|||
using System.Runtime.Versioning; |
|||
|
|||
namespace Avalonia.Web |
|||
namespace Avalonia.Web; |
|||
|
|||
[SupportedOSPlatform("browser")] |
|||
public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime |
|||
{ |
|||
[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings
|
|||
public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime |
|||
{ |
|||
public AvaloniaView? View; |
|||
public AvaloniaView? View; |
|||
|
|||
public Control? MainView |
|||
{ |
|||
get => View!.Content; |
|||
set => View!.Content = value; |
|||
} |
|||
public Control? MainView |
|||
{ |
|||
get => View!.Content; |
|||
set => View!.Content = value; |
|||
} |
|||
} |
|||
|
|||
public static partial class WebAppBuilder |
|||
{ |
|||
public static T SetupBrowserApp<T>( |
|||
public class BrowserPlatformOptions |
|||
{ |
|||
public Func<string, string> FrameworkAssetPathResolver { get; set; } = new(fileName => $"./{fileName}"); |
|||
} |
|||
|
|||
|
|||
[SupportedOSPlatform("browser")] |
|||
public static 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); |
|||
} |
|||
{ |
|||
var lifetime = new BrowserSingleViewLifetime(); |
|||
|
|||
return builder |
|||
.UseBrowser() |
|||
.AfterSetup(b => |
|||
{ |
|||
lifetime.View = new AvaloniaView(mainDivId); |
|||
}) |
|||
.SetupWithLifetime(lifetime); |
|||
} |
|||
|
|||
public static T UseBrowser<T>( |
|||
this T builder) |
|||
where T : AppBuilderBase<T>, new() |
|||
{ |
|||
return builder |
|||
.UseWindowingSubsystem(BrowserWindowingPlatform.Register) |
|||
.UseSkia() |
|||
.With(new SkiaOptions { CustomGpuFactory = () => new BrowserSkiaGpu() }); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,22 @@ |
|||
using System.Runtime.InteropServices.JavaScript; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Avalonia.Web.Interop; |
|||
|
|||
internal static class AvaloniaModule |
|||
{ |
|||
public const string MainModuleName = "avalonia"; |
|||
public const string StorageModuleName = "storage"; |
|||
|
|||
public static Task ImportMain() |
|||
{ |
|||
var options = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions(); |
|||
return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver("avalonia.js")); |
|||
} |
|||
|
|||
public static Task ImportStorage() |
|||
{ |
|||
var options = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions(); |
|||
return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver("storage.js")); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue