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 System; |
||||
|
using System.Runtime.InteropServices.JavaScript; |
||||
using Avalonia; |
|
||||
using Avalonia.Platform; |
using Avalonia.Platform; |
||||
using Avalonia.Web.Blazor; |
using Avalonia.Web; |
||||
|
|
||||
using ControlCatalog.Pages; |
using ControlCatalog.Pages; |
||||
|
|
||||
using Microsoft.JSInterop; |
|
||||
|
|
||||
namespace ControlCatalog.Web; |
namespace ControlCatalog.Web; |
||||
|
|
||||
public class EmbedSampleWeb : INativeDemoControl |
public class EmbedSampleWeb : INativeDemoControl |
||||
{ |
{ |
||||
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault) |
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault) |
||||
{ |
{ |
||||
var runtime = AvaloniaLocator.Current.GetRequiredService<IJSInProcessRuntime>(); |
|
||||
|
|
||||
if (isSecond) |
if (isSecond) |
||||
{ |
{ |
||||
var iframe = runtime.Invoke<IJSInProcessObjectReference>("document.createElement", "iframe"); |
var iframe = EmbedInterop.CreateElement("iframe"); |
||||
iframe.InvokeVoid("setAttribute", "src", "https://www.youtube.com/embed/kZCIporjJ70"); |
iframe.SetProperty("src", "https://www.youtube.com/embed/kZCIporjJ70"); |
||||
|
|
||||
return new JSObjectControlHandle(iframe); |
return new JSObjectControlHandle(iframe); |
||||
} |
} |
||||
else |
else |
||||
{ |
{ |
||||
// window.createAppButton source is defined in "app.js" file.
|
var defaultHandle = (JSObjectControlHandle)createDefault(); |
||||
var button = runtime.Invoke<IJSInProcessObjectReference>("window.createAppButton"); |
|
||||
|
_ = 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"> |
<Project Sdk="Microsoft.NET.Sdk.Razor"> |
||||
|
|
||||
<PropertyGroup> |
<PropertyGroup> |
||||
<TargetFramework>net6.0</TargetFramework> |
<TargetFramework>net7.0</TargetFramework> |
||||
<ImplicitUsings>enable</ImplicitUsings> |
|
||||
<PackageId>Avalonia.Web.Blazor</PackageId> |
<PackageId>Avalonia.Web.Blazor</PackageId> |
||||
<LangVersion>preview</LangVersion> |
<ResolveStaticWebAssetsInputsDependsOn>_IncludeGeneratedAvaloniaStaticFiles;$(ResolveStaticWebAssetsInputsDependsOn)</ResolveStaticWebAssetsInputsDependsOn> |
||||
<MSBuildEnableWorkloadResolver>false</MSBuildEnableWorkloadResolver> |
|
||||
<StaticWebAssetsDisableProjectBuildPropsFileGeneration>true</StaticWebAssetsDisableProjectBuildPropsFileGeneration> |
|
||||
</PropertyGroup> |
</PropertyGroup> |
||||
|
|
||||
<ItemGroup> |
<ItemGroup> |
||||
<SupportedPlatform Include="browser" /> |
<SupportedPlatform Include="browser" /> |
||||
</ItemGroup> |
</ItemGroup> |
||||
|
|
||||
<Import Project="..\..\..\build\BuildTargets.targets" /> |
|
||||
<Import Project="..\..\..\build\SkiaSharp.props" /> |
|
||||
<Import Project="..\..\..\build\HarfBuzzSharp.props" /> |
|
||||
<Import Project="..\..\..\build\NullableEnable.props" /> |
<Import Project="..\..\..\build\NullableEnable.props" /> |
||||
|
|
||||
<ItemGroup> |
<ItemGroup> |
||||
<Content Include="*.props"> |
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="7.0.0-*" /> |
||||
<Pack>true</Pack> |
|
||||
<PackagePath>build\</PackagePath> |
|
||||
</Content> |
|
||||
<Content Include="*.targets"> |
|
||||
<Pack>true</Pack> |
|
||||
<PackagePath>build\;buildTransitive\</PackagePath> |
|
||||
</Content> |
|
||||
</ItemGroup> |
</ItemGroup> |
||||
|
|
||||
<ItemGroup> |
<ItemGroup> |
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.8" /> |
<ProjectReference Include="..\Avalonia.Web\Avalonia.Web.csproj" /> |
||||
</ItemGroup> |
|
||||
|
|
||||
<ItemGroup> |
|
||||
<ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" /> |
|
||||
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" /> |
|
||||
</ItemGroup> |
|
||||
|
|
||||
<ItemGroup> |
|
||||
<Folder Include="wwwroot\" /> |
|
||||
</ItemGroup> |
</ItemGroup> |
||||
|
|
||||
<Target Name="NpmInstall" Inputs="webapp/package.json" Outputs="webapp/node_modules/.install-stamp"> |
<Target Name="_IncludeGeneratedAvaloniaStaticFiles"> |
||||
<Exec Command="npm install" WorkingDirectory="webapp" /> |
<ItemGroup> |
||||
<!-- Write the stamp file, so incremental builds work --> |
<_AvaloniaWebAssets Include="$(MSBuildThisFileDirectory)../Avalonia.Web/wwwroot/**/*.*" /> |
||||
<Touch Files="webapp/node_modules/.install-stamp" AlwaysCreate="true" /> |
</ItemGroup> |
||||
</Target> |
<DefineStaticWebAssets SourceId="$(PackageId)" |
||||
<Target Name="NpmRunBuild" DependsOnTargets="NpmInstall" BeforeTargets="BeforeBuild"> |
SourceType="Computed" |
||||
<Exec Command="npm run build" WorkingDirectory="webapp" /> |
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> |
</Target> |
||||
</Project> |
</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.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>( |
return builder |
||||
this T builder) |
.UseBrowser() |
||||
where T : AppBuilderBase<T>, new() |
.With(new BrowserPlatformOptions |
||||
{ |
{ |
||||
return builder.SetupWithLifetime(new BlazorSingleViewLifetime()); |
FrameworkAssetPathResolver = new(filePath => $"/_content/Avalonia.Web.Blazor/{filePath}") |
||||
} |
}); |
||||
|
} |
||||
|
|
||||
public static AvaloniaBlazorAppBuilder Configure<TApp>() |
public static AppBuilder Configure<TApp>() |
||||
where TApp : Application, new() |
where TApp : Application, new() |
||||
{ |
{ |
||||
var builder = AvaloniaBlazorAppBuilder.Configure<TApp>() |
return AppBuilder.Configure<TApp>() |
||||
.UseSkia() |
.UseBlazor(); |
||||
.With(new SkiaOptions { CustomGpuFactory = () => new BlazorSkiaGpu() }); |
} |
||||
|
|
||||
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; |
||||
using Avalonia.Controls.ApplicationLifetimes; |
using Avalonia.Controls.ApplicationLifetimes; |
||||
using Avalonia.Media; |
|
||||
using Avalonia.Web.Skia; |
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 AvaloniaView? View; |
||||
public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime |
|
||||
{ |
|
||||
public AvaloniaView? View; |
|
||||
|
|
||||
public Control? MainView |
public Control? MainView |
||||
{ |
{ |
||||
get => View!.Content; |
get => View!.Content; |
||||
set => View!.Content = value; |
set => View!.Content = value; |
||||
} |
|
||||
} |
} |
||||
|
} |
||||
|
|
||||
public static partial class WebAppBuilder |
public class BrowserPlatformOptions |
||||
{ |
{ |
||||
public static T SetupBrowserApp<T>( |
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) |
this T builder, string mainDivId) |
||||
where T : AppBuilderBase<T>, new() |
where T : AppBuilderBase<T>, new() |
||||
{ |
{ |
||||
var lifetime = new BrowserSingleViewLifetime(); |
var lifetime = new BrowserSingleViewLifetime(); |
||||
|
|
||||
return builder |
return builder |
||||
.UseWindowingSubsystem(BrowserWindowingPlatform.Register) |
.UseBrowser() |
||||
.UseSkia() |
.AfterSetup(b => |
||||
.With(new SkiaOptions { CustomGpuFactory = () => new BrowserSkiaGpu() }) |
{ |
||||
.AfterSetup(b => |
lifetime.View = new AvaloniaView(mainDivId); |
||||
{ |
}) |
||||
lifetime.View = new AvaloniaView(mainDivId); |
.SetupWithLifetime(lifetime); |
||||
}) |
} |
||||
.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