Merge pull request #9142 from AvaloniaUI/wasm-updates
Remove old blazor backend. Keep blazor components with new WASM implementation.
This commit is contained in:
+18
-18
@@ -198,8 +198,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{86A3F706-DC3
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Web.Blazor", "src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.csproj", "{25831348-EB2A-483E-9576-E8F6528674A5}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Web", "samples\ControlCatalog.Web\ControlCatalog.Web.csproj", "{C08E9894-AA92-426E-BF56-033E262CAD3E}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsInteropTest", "samples\interop\WindowsInteropTest\WindowsInteropTest.csproj", "{26A98DA1-D89D-4A95-8152-349F404DA2E2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\SampleControls\ControlSamples.csproj", "{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}"
|
||||
@@ -216,15 +214,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevGenerators", "src\tools\
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Web", "src\Web\Avalonia.Web\Avalonia.Web.csproj", "{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Web.Sample", "src\Web\Avalonia.Web.Sample\Avalonia.Web.Sample.csproj", "{1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox", "samples\MobileSandbox\MobileSandbox.csproj", "{3B8519C1-2F51-4F12-A348-120AB91D4532}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MobileSandbox", "samples\MobileSandbox\MobileSandbox.csproj", "{3B8519C1-2F51-4F12-A348-120AB91D4532}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox.Android", "samples\MobileSandbox.Android\MobileSandbox.Android.csproj", "{C90FE60B-B01E-4F35-91D6-379D6966030F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MobileSandbox.Android", "samples\MobileSandbox.Android\MobileSandbox.Android.csproj", "{C90FE60B-B01E-4F35-91D6-379D6966030F}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox.iOS", "samples\MobileSandbox.iOS\MobileSandbox.iOS.csproj", "{FED9A71D-00D7-4F40-A9E4-1229EEA28EEB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MobileSandbox.iOS", "samples\MobileSandbox.iOS\MobileSandbox.iOS.csproj", "{FED9A71D-00D7-4F40-A9E4-1229EEA28EEB}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox.Desktop", "samples\MobileSandbox.Desktop\MobileSandbox.Desktop.csproj", "{62D392C9-81CF-487F-92E8-598B2AF3FDCE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MobileSandbox.Desktop", "samples\MobileSandbox.Desktop\MobileSandbox.Desktop.csproj", "{62D392C9-81CF-487F-92E8-598B2AF3FDCE}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Blazor.Web", "samples\ControlCatalog.Blazor.Web\ControlCatalog.Blazor.Web.csproj", "{6A710364-AE6D-40BD-968B-024311527AC2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Web", "samples\ControlCatalog.Web\ControlCatalog.Web.csproj", "{8B3E8405-DE18-4048-A459-9CA4AC3319A2}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -480,10 +480,6 @@ Global
|
||||
{25831348-EB2A-483E-9576-E8F6528674A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{25831348-EB2A-483E-9576-E8F6528674A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{25831348-EB2A-483E-9576-E8F6528674A5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C08E9894-AA92-426E-BF56-033E262CAD3E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{26A98DA1-D89D-4A95-8152-349F404DA2E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{26A98DA1-D89D-4A95-8152-349F404DA2E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{26A98DA1-D89D-4A95-8152-349F404DA2E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -516,10 +512,6 @@ Global
|
||||
{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3B8519C1-2F51-4F12-A348-120AB91D4532}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3B8519C1-2F51-4F12-A348-120AB91D4532}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3B8519C1-2F51-4F12-A348-120AB91D4532}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -536,6 +528,14 @@ Global
|
||||
{62D392C9-81CF-487F-92E8-598B2AF3FDCE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{62D392C9-81CF-487F-92E8-598B2AF3FDCE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{62D392C9-81CF-487F-92E8-598B2AF3FDCE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6A710364-AE6D-40BD-968B-024311527AC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6A710364-AE6D-40BD-968B-024311527AC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6A710364-AE6D-40BD-968B-024311527AC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6A710364-AE6D-40BD-968B-024311527AC2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8B3E8405-DE18-4048-A459-9CA4AC3319A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8B3E8405-DE18-4048-A459-9CA4AC3319A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8B3E8405-DE18-4048-A459-9CA4AC3319A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8B3E8405-DE18-4048-A459-9CA4AC3319A2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -579,6 +579,7 @@ Global
|
||||
{41B02319-965D-4945-8005-C1A3D1224165} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
|
||||
{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
|
||||
{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
|
||||
{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
|
||||
{351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
|
||||
{909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C}
|
||||
{11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098}
|
||||
@@ -586,7 +587,6 @@ Global
|
||||
{676D6BFD-029D-4E43-BFC7-3892265CE251} = {9B9E3891-2366-4253-A952-D08BCEB71098}
|
||||
{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
|
||||
{25831348-EB2A-483E-9576-E8F6528674A5} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
|
||||
{C08E9894-AA92-426E-BF56-033E262CAD3E} = {9B9E3891-2366-4253-A952-D08BCEB71098}
|
||||
{26A98DA1-D89D-4A95-8152-349F404DA2E2} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
|
||||
{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270} = {9B9E3891-2366-4253-A952-D08BCEB71098}
|
||||
{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
|
||||
@@ -594,12 +594,12 @@ Global
|
||||
{EABE2161-989B-42BF-BD8D-1E34B20C21F1} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
|
||||
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
|
||||
{76D39FF6-6B4F-46C4-93CD-E6FC4665739E} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
|
||||
{1F61B6F1-B881-4E27-A5B0-09D1F543F7AC} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
|
||||
{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
|
||||
{3B8519C1-2F51-4F12-A348-120AB91D4532} = {9B9E3891-2366-4253-A952-D08BCEB71098}
|
||||
{C90FE60B-B01E-4F35-91D6-379D6966030F} = {9B9E3891-2366-4253-A952-D08BCEB71098}
|
||||
{FED9A71D-00D7-4F40-A9E4-1229EEA28EEB} = {9B9E3891-2366-4253-A952-D08BCEB71098}
|
||||
{62D392C9-81CF-487F-92E8-598B2AF3FDCE} = {9B9E3891-2366-4253-A952-D08BCEB71098}
|
||||
{6A710364-AE6D-40BD-968B-024311527AC2} = {9B9E3891-2366-4253-A952-D08BCEB71098}
|
||||
{8B3E8405-DE18-4048-A459-9CA4AC3319A2} = {9B9E3891-2366-4253-A952-D08BCEB71098}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
+1
-2
@@ -6,6 +6,5 @@
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using ControlCatalog.Web
|
||||
@using ControlCatalog.Web.Shared
|
||||
@using ControlCatalog.Blazor.Web.Shared
|
||||
@using SkiaSharp
|
||||
|
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,57 +1,45 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<!--Temporal hack that fixes compilation in VS-->
|
||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||
<EmccTotalMemory>16777216</EmccTotalMemory>
|
||||
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
|
||||
<BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData>
|
||||
<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>
|
||||
|
||||
<!-- In debug, make builds faster by reducing optimizations -->
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
|
||||
<WasmNativeStrip>false</WasmNativeStrip>
|
||||
<EmccCompileOptimizationFlag>-O1</EmccCompileOptimizationFlag>
|
||||
<RunAOTCompilation>false</RunAOTCompilation>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<Optimize>true</Optimize>
|
||||
<WasmNativeStrip>true</WasmNativeStrip>
|
||||
<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>
|
||||
<RunAOTCompilation>false</RunAOTCompilation>
|
||||
<DebuggerSupport>false</DebuggerSupport>
|
||||
<EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
|
||||
<EnableUnsafeUTF7Encoding>false</EnableUnsafeUTF7Encoding>
|
||||
<EventSourceSupport>false</EventSourceSupport>
|
||||
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
|
||||
<UseNativeHttpHandler>true</UseNativeHttpHandler>
|
||||
<UseSystemResourceKeys>true</UseSystemResourceKeys>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<TrimMode>link</TrimMode>
|
||||
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0" PrivateAssets="all" />
|
||||
<TrimmerRootDescriptor Include="Roots.xml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
|
||||
<ProjectReference Include="..\..\src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.csproj" />
|
||||
<ProjectReference Include="..\..\src\Web\Avalonia.Web\Avalonia.Web.csproj" />
|
||||
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\..\build\ReferenceCoreLibraries.props" />
|
||||
<Import Project="..\..\build\BuildTargets.targets" />
|
||||
|
||||
<Import Project="..\..\src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.targets" />
|
||||
<Import Project="..\..\src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.CompilationTuning.props" />
|
||||
<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="..\..\src\Web\Avalonia.Web\Avalonia.Web.props" />
|
||||
<Import Project="..\..\src\Web\Avalonia.Web\Avalonia.Web.targets" />
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -1,34 +1,42 @@
|
||||
using System;
|
||||
|
||||
using Avalonia;
|
||||
using System.Runtime.InteropServices.JavaScript;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Web.Blazor;
|
||||
using Avalonia.Web;
|
||||
|
||||
using ControlCatalog.Pages;
|
||||
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace ControlCatalog.Web;
|
||||
|
||||
public class EmbedSampleWeb : INativeDemoControl
|
||||
{
|
||||
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault)
|
||||
{
|
||||
var runtime = AvaloniaLocator.Current.GetRequiredService<IJSInProcessRuntime>();
|
||||
|
||||
if (isSecond)
|
||||
{
|
||||
var iframe = runtime.Invoke<IJSInProcessObjectReference>("document.createElement", "iframe");
|
||||
iframe.InvokeVoid("setAttribute", "src", "https://www.youtube.com/embed/kZCIporjJ70");
|
||||
var iframe = EmbedInterop.CreateElement("iframe");
|
||||
iframe.SetProperty("src", "https://www.youtube.com/embed/kZCIporjJ70");
|
||||
|
||||
return new JSObjectControlHandle(iframe);
|
||||
}
|
||||
else
|
||||
{
|
||||
// window.createAppButton source is defined in "app.js" file.
|
||||
var button = runtime.Invoke<IJSInProcessObjectReference>("window.createAppButton");
|
||||
var defaultHandle = (JSObjectControlHandle)createDefault();
|
||||
|
||||
return new JSObjectControlHandle(button);
|
||||
_ = 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);
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1,29 +1,19 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Avalonia;
|
||||
using Avalonia.Web;
|
||||
using ControlCatalog;
|
||||
using ControlCatalog.Web;
|
||||
|
||||
public class Program
|
||||
internal partial class Program
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
private static void Main(string[] args)
|
||||
{
|
||||
await CreateHostBuilder(args).Build().RunAsync();
|
||||
BuildAvaloniaApp()
|
||||
.AfterSetup(_ =>
|
||||
{
|
||||
ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb();
|
||||
}).SetupBrowserApp("out");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -4,12 +4,15 @@
|
||||
}
|
||||
|
||||
#avalonia-splash {
|
||||
position: absolute;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
color: whitesmoke;
|
||||
background: #171C2C;
|
||||
font-family: 'Nunito', sans-serif;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
#avalonia-splash a{
|
||||
@@ -24,15 +27,23 @@
|
||||
}
|
||||
|
||||
.splash-close {
|
||||
animation: fadeOut 1s forwards;
|
||||
animation: slide 0.5s linear 1s forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
@keyframes slide {
|
||||
0% {
|
||||
top: 0%;
|
||||
}
|
||||
|
||||
to {
|
||||
50% {
|
||||
opacity: 80%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 100%;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
display: none;
|
||||
visibility: collapse;
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 172 KiB |
@@ -4,7 +4,7 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Avalonia.Web.Sample</title>
|
||||
<title>AvaloniaUI - ControlCatalog</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="modulepreload" href="./main.js" />
|
||||
@@ -2,7 +2,7 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
import { dotnet } from './dotnet.js'
|
||||
import { createAvaloniaRuntime } from './avalonia.js';
|
||||
import { registerAvaloniaModule } from './avalonia.js';
|
||||
|
||||
const is_browser = typeof window != "undefined";
|
||||
if (!is_browser) throw new Error(`Expected to be running in a browser`);
|
||||
@@ -12,7 +12,7 @@ const dotnetRuntime = await dotnet
|
||||
.withApplicationArgumentsFromQuery()
|
||||
.create();
|
||||
|
||||
await createAvaloniaRuntime(dotnetRuntime);
|
||||
await registerAvaloniaModule(dotnetRuntime);
|
||||
|
||||
const config = dotnetRuntime.getConfig();
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<EmccTotalMemory>16777216</EmccTotalMemory>
|
||||
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
|
||||
<BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -1,53 +1,40 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<PackageId>Avalonia.Web.Blazor</PackageId>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<MSBuildEnableWorkloadResolver>false</MSBuildEnableWorkloadResolver>
|
||||
<StaticWebAssetsDisableProjectBuildPropsFileGeneration>true</StaticWebAssetsDisableProjectBuildPropsFileGeneration>
|
||||
<ResolveStaticWebAssetsInputsDependsOn>_IncludeGeneratedAvaloniaStaticFiles;$(ResolveStaticWebAssetsInputsDependsOn)</ResolveStaticWebAssetsInputsDependsOn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<SupportedPlatform Include="browser" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\..\..\build\BuildTargets.targets" />
|
||||
<Import Project="..\..\..\build\SkiaSharp.props" />
|
||||
<Import Project="..\..\..\build\HarfBuzzSharp.props" />
|
||||
<Import Project="..\..\..\build\NullableEnable.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="*.props">
|
||||
<Pack>true</Pack>
|
||||
<PackagePath>build\</PackagePath>
|
||||
</Content>
|
||||
<Content Include="*.targets">
|
||||
<Pack>true</Pack>
|
||||
<PackagePath>build\;buildTransitive\</PackagePath>
|
||||
</Content>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="7.0.0-*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.8" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />
|
||||
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
|
||||
<ProjectReference Include="..\Avalonia.Web\Avalonia.Web.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="NpmInstall" Inputs="webapp/package.json" Outputs="webapp/node_modules/.install-stamp">
|
||||
<Exec Command="npm install" WorkingDirectory="webapp" />
|
||||
<!-- Write the stamp file, so incremental builds work -->
|
||||
<Touch Files="webapp/node_modules/.install-stamp" AlwaysCreate="true" />
|
||||
</Target>
|
||||
<Target Name="NpmRunBuild" DependsOnTargets="NpmInstall" BeforeTargets="BeforeBuild">
|
||||
<Exec Command="npm run build" WorkingDirectory="webapp" />
|
||||
<Target Name="_IncludeGeneratedAvaloniaStaticFiles">
|
||||
<ItemGroup>
|
||||
<_AvaloniaWebAssets Include="$(MSBuildThisFileDirectory)../Avalonia.Web/wwwroot/**/*.*" />
|
||||
</ItemGroup>
|
||||
<DefineStaticWebAssets SourceId="$(PackageId)"
|
||||
SourceType="Computed"
|
||||
AssetKind="All"
|
||||
AssetRole="Primary"
|
||||
CopyToOutputDirectory="PreserveNewest"
|
||||
CopyToPublishDirectory="PreserveNewest"
|
||||
ContentRoot="$(MSBuildThisFileDirectory)../Avalonia.Web/wwwroot/"
|
||||
BasePath="_content\$(PackageId)"
|
||||
CandidateAssets="@(_AvaloniaWebAssets)"
|
||||
RelativePathFilter="**.js">
|
||||
<Output TaskParameter="Assets" ItemName="StaticWebAsset" />
|
||||
</DefineStaticWebAssets>
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<Project>
|
||||
<Import Project="Microsoft.AspNetCore.StaticWebAssets.props" />
|
||||
<Import Project="Avalonia.Web.Blazor.CompilationTuning.props" />
|
||||
</Project>
|
||||
@@ -1,6 +0,0 @@
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\libHarfBuzzSharp.a" />
|
||||
<NativeFileReference Include="$(SkiaSharpStaticLibraryPath)\2.0.23\libSkiaSharp.a" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,20 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform;
|
||||
|
||||
namespace Avalonia.Web.Blazor
|
||||
{
|
||||
public class AvaloniaBlazorAppBuilder : AppBuilderBase<AvaloniaBlazorAppBuilder>
|
||||
{
|
||||
public AvaloniaBlazorAppBuilder(IRuntimePlatform platform, Action<AvaloniaBlazorAppBuilder> platformServices)
|
||||
: base(platform, platformServices)
|
||||
{
|
||||
}
|
||||
|
||||
public AvaloniaBlazorAppBuilder()
|
||||
: base(new StandardRuntimePlatform(),
|
||||
builder => StandardRuntimePlatformServices.Register(builder.ApplicationType!.Assembly))
|
||||
{
|
||||
UseWindowingSubsystem(BlazorWindowingPlatform.Register);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Runtime.InteropServices.JavaScript;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using BrowserView = Avalonia.Web.AvaloniaView;
|
||||
|
||||
namespace Avalonia.Web.Blazor;
|
||||
|
||||
[SupportedOSPlatform("browser")]
|
||||
public class AvaloniaView : ComponentBase
|
||||
{
|
||||
private BrowserView? _browserView;
|
||||
private readonly string _containerId;
|
||||
|
||||
public AvaloniaView()
|
||||
{
|
||||
_containerId = "av_container_" + Guid.NewGuid();
|
||||
}
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.OpenElement(0, "div");
|
||||
builder.AddAttribute(1, "id", _containerId);
|
||||
builder.AddAttribute(2, "style", "width:100vw;height:100vh");
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (OperatingSystem.IsBrowser())
|
||||
{
|
||||
await Avalonia.Web.Interop.AvaloniaModule.ImportMain();
|
||||
|
||||
_browserView = new BrowserView(_containerId);
|
||||
if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime lifetime)
|
||||
{
|
||||
_browserView.Content = lifetime.MainView;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
<div id="container" @ref="_containerElement"
|
||||
class="avalonia-container"
|
||||
tabindex="0" oncontextmenu="return false;"
|
||||
@onwheel="OnWheel"
|
||||
@onkeydown="OnKeyDown"
|
||||
@onkeyup="OnKeyUp"
|
||||
@onkeyup:preventDefault="@KeyPreventDefault"
|
||||
@onkeydown:preventDefault="@KeyPreventDefault"
|
||||
@onpointerdown="OnPointerDown"
|
||||
@onpointerup="OnPointerUp"
|
||||
@onpointermove="OnPointerMove"
|
||||
@onpointercancel="OnPointerCancel"
|
||||
@onfocus="OnFocus">
|
||||
|
||||
<canvas id="htmlCanvas" @ref="_htmlCanvas" @attributes="AdditionalAttributes"/>
|
||||
|
||||
<div id="nativeControlsContainer" @ref="_nativeControlsContainer" />
|
||||
|
||||
<input id="inputElement" @ref="_inputElement" type="text"
|
||||
spellcheck="false" onpaste="return false;"
|
||||
oncopy="return false;"
|
||||
oncut="return false;"
|
||||
autocapitalize="none"/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#container{
|
||||
touch-action: none;
|
||||
}
|
||||
#htmlCanvas {
|
||||
opacity: 1;
|
||||
background-color: #ccc;
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
#nativeControlsContainer {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
z-index: 700;
|
||||
}
|
||||
|
||||
#inputElement {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
height: 20px;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
caret-color: transparent;
|
||||
border-top-style: hidden;
|
||||
border-bottom-style: hidden;
|
||||
border-right-style: hidden;
|
||||
border-left-style: hidden;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,500 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Controls.Embedding;
|
||||
using Avalonia.Controls.Platform;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Input.Raw;
|
||||
using Avalonia.Input.TextInput;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Rendering;
|
||||
using Avalonia.Rendering.Composition;
|
||||
using Avalonia.Web.Blazor.Interop;
|
||||
using Avalonia.Web.Blazor.Interop.Storage;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
using SkiaSharp;
|
||||
using HTMLPointerEventArgs = Microsoft.AspNetCore.Components.Web.PointerEventArgs;
|
||||
|
||||
namespace Avalonia.Web.Blazor
|
||||
{
|
||||
public partial class AvaloniaView : ITextInputMethodImpl
|
||||
{
|
||||
private readonly RazorViewTopLevelImpl _topLevelImpl;
|
||||
private EmbeddableControlRoot _topLevel;
|
||||
|
||||
// Interop
|
||||
private SKHtmlCanvasInterop? _interop = null;
|
||||
private SizeWatcherInterop? _sizeWatcher = null;
|
||||
private DpiWatcherInterop? _dpiWatcher = null;
|
||||
private SKHtmlCanvasInterop.GLInfo? _jsGlInfo = null;
|
||||
private AvaloniaModule? _avaloniaModule = null;
|
||||
private InputHelperInterop? _inputHelper = null;
|
||||
private FocusHelperInterop? _canvasHelper = null;
|
||||
private FocusHelperInterop? _containerHelper = null;
|
||||
private NativeControlHostInterop? _nativeControlHost = null;
|
||||
private StorageProviderInterop? _storageProvider = null;
|
||||
private ElementReference _htmlCanvas;
|
||||
private ElementReference _inputElement;
|
||||
private ElementReference _containerElement;
|
||||
private ElementReference _nativeControlsContainer;
|
||||
private double _dpi = 1;
|
||||
private SKSize _canvasSize = new (100, 100);
|
||||
|
||||
private GRContext? _context;
|
||||
private GRGlInterface? _glInterface;
|
||||
private const SKColorType ColorType = SKColorType.Rgba8888;
|
||||
|
||||
private bool _useGL;
|
||||
private bool _inputElementFocused;
|
||||
|
||||
private ITextInputMethodClient? _client;
|
||||
|
||||
|
||||
[Inject] private IJSRuntime Js { get; set; } = null!;
|
||||
|
||||
public AvaloniaView()
|
||||
{
|
||||
_topLevelImpl = new RazorViewTopLevelImpl(this);
|
||||
|
||||
_topLevel = new EmbeddableControlRoot(_topLevelImpl);
|
||||
|
||||
if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime lifetime)
|
||||
{
|
||||
_topLevel.Content = lifetime.MainView;
|
||||
}
|
||||
}
|
||||
|
||||
public bool KeyPreventDefault { get; set; }
|
||||
|
||||
public ITextInputMethodClient? Client => _client;
|
||||
|
||||
public bool IsActive => _client != null;
|
||||
|
||||
public bool IsComposing { get; private set; }
|
||||
|
||||
internal INativeControlHostImpl GetNativeControlHostImpl()
|
||||
{
|
||||
return _nativeControlHost ?? throw new InvalidOperationException("Blazor View wasn't initialized yet");
|
||||
}
|
||||
|
||||
internal IStorageProvider GetStorageProvider()
|
||||
{
|
||||
return _storageProvider ?? throw new InvalidOperationException("Blazor View wasn't initialized yet");
|
||||
}
|
||||
|
||||
private void OnPointerCancel(HTMLPointerEventArgs e)
|
||||
{
|
||||
if (e.PointerType == "touch")
|
||||
{
|
||||
_topLevelImpl.RawPointerEvent(RawPointerEventType.TouchCancel, e.PointerType, GetPointFromEventArgs(e),
|
||||
GetModifiers(e), e.PointerId);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPointerMove(HTMLPointerEventArgs e)
|
||||
{
|
||||
var type = e.PointerType switch
|
||||
{
|
||||
"touch" => RawPointerEventType.TouchUpdate,
|
||||
_ => RawPointerEventType.Move
|
||||
};
|
||||
|
||||
_topLevelImpl.RawPointerEvent(type, e.PointerType, GetPointFromEventArgs(e), GetModifiers(e), e.PointerId);
|
||||
}
|
||||
|
||||
private void OnPointerUp(HTMLPointerEventArgs e)
|
||||
{
|
||||
var type = e.PointerType switch
|
||||
{
|
||||
"touch" => RawPointerEventType.TouchEnd,
|
||||
_ => e.Button switch
|
||||
{
|
||||
0 => RawPointerEventType.LeftButtonUp,
|
||||
1 => RawPointerEventType.MiddleButtonUp,
|
||||
2 => RawPointerEventType.RightButtonUp,
|
||||
3 => RawPointerEventType.XButton1Up,
|
||||
4 => RawPointerEventType.XButton2Up,
|
||||
// 5 => Pen eraser button,
|
||||
_ => RawPointerEventType.Move
|
||||
}
|
||||
};
|
||||
|
||||
_topLevelImpl.RawPointerEvent(type, e.PointerType, GetPointFromEventArgs(e), GetModifiers(e), e.PointerId);
|
||||
}
|
||||
|
||||
private void OnPointerDown(HTMLPointerEventArgs e)
|
||||
{
|
||||
var type = e.PointerType switch
|
||||
{
|
||||
"touch" => RawPointerEventType.TouchBegin,
|
||||
_ => e.Button switch
|
||||
{
|
||||
0 => RawPointerEventType.LeftButtonDown,
|
||||
1 => RawPointerEventType.MiddleButtonDown,
|
||||
2 => RawPointerEventType.RightButtonDown,
|
||||
3 => RawPointerEventType.XButton1Down,
|
||||
4 => RawPointerEventType.XButton2Down,
|
||||
// 5 => Pen eraser button,
|
||||
_ => RawPointerEventType.Move
|
||||
}
|
||||
};
|
||||
|
||||
_topLevelImpl.RawPointerEvent(type, e.PointerType, GetPointFromEventArgs(e), GetModifiers(e), e.PointerId);
|
||||
}
|
||||
|
||||
private static RawPointerPoint GetPointFromEventArgs(HTMLPointerEventArgs args)
|
||||
{
|
||||
return new RawPointerPoint
|
||||
{
|
||||
Position = new Point(args.ClientX, args.ClientY),
|
||||
Pressure = args.Pressure,
|
||||
XTilt = args.TiltX,
|
||||
YTilt = args.TiltY
|
||||
// Twist = args.Twist - read from JS code directly when
|
||||
};
|
||||
}
|
||||
|
||||
private void OnWheel(WheelEventArgs e)
|
||||
{
|
||||
_topLevelImpl.RawMouseWheelEvent( new Point(e.ClientX, e.ClientY),
|
||||
new Vector(-(e.DeltaX / 50), -(e.DeltaY / 50)), GetModifiers(e));
|
||||
}
|
||||
|
||||
private static RawInputModifiers GetModifiers(MouseEventArgs e)
|
||||
{
|
||||
var modifiers = RawInputModifiers.None;
|
||||
|
||||
if (e.CtrlKey)
|
||||
modifiers |= RawInputModifiers.Control;
|
||||
if (e.AltKey)
|
||||
modifiers |= RawInputModifiers.Alt;
|
||||
if (e.ShiftKey)
|
||||
modifiers |= RawInputModifiers.Shift;
|
||||
if (e.MetaKey)
|
||||
modifiers |= RawInputModifiers.Meta;
|
||||
|
||||
if ((e.Buttons & 1L) == 1)
|
||||
modifiers |= RawInputModifiers.LeftMouseButton;
|
||||
|
||||
if ((e.Buttons & 2L) == 2)
|
||||
modifiers |= e.Type == "pen" ? RawInputModifiers.PenBarrelButton : RawInputModifiers.RightMouseButton;
|
||||
|
||||
if ((e.Buttons & 4L) == 4)
|
||||
modifiers |= RawInputModifiers.MiddleMouseButton;
|
||||
|
||||
if ((e.Buttons & 8L) == 8)
|
||||
modifiers |= RawInputModifiers.XButton1MouseButton;
|
||||
|
||||
if ((e.Buttons & 16L) == 16)
|
||||
modifiers |= RawInputModifiers.XButton2MouseButton;
|
||||
|
||||
if ((e.Buttons & 32L) == 32)
|
||||
modifiers |= RawInputModifiers.PenEraser;
|
||||
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
private static RawInputModifiers GetModifiers(KeyboardEventArgs e)
|
||||
{
|
||||
var modifiers = RawInputModifiers.None;
|
||||
|
||||
if (e.CtrlKey)
|
||||
modifiers |= RawInputModifiers.Control;
|
||||
if (e.AltKey)
|
||||
modifiers |= RawInputModifiers.Alt;
|
||||
if (e.ShiftKey)
|
||||
modifiers |= RawInputModifiers.Shift;
|
||||
if (e.MetaKey)
|
||||
modifiers |= RawInputModifiers.Meta;
|
||||
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
private void OnKeyDown(KeyboardEventArgs e)
|
||||
{
|
||||
KeyPreventDefault = _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, e.Code, e.Key, GetModifiers(e));
|
||||
}
|
||||
|
||||
private void OnKeyUp(KeyboardEventArgs e)
|
||||
{
|
||||
KeyPreventDefault = _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, e.Code, e.Key, GetModifiers(e));
|
||||
}
|
||||
|
||||
private void OnFocus(FocusEventArgs e)
|
||||
{
|
||||
// if focus has unexpectedly moved from the input element to the container element,
|
||||
// shift it back to the input element
|
||||
if (_inputElementFocused && _inputHelper is not null)
|
||||
{
|
||||
_inputHelper.Focus();
|
||||
}
|
||||
}
|
||||
|
||||
[Parameter(CaptureUnmatchedValues = true)]
|
||||
public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
AvaloniaLocator.CurrentMutable.Bind<IJSInProcessRuntime>().ToConstant((IJSInProcessRuntime)Js);
|
||||
|
||||
_avaloniaModule = await AvaloniaModule.ImportAsync(Js);
|
||||
|
||||
_canvasHelper = new FocusHelperInterop(_avaloniaModule, _htmlCanvas);
|
||||
_containerHelper = new FocusHelperInterop(_avaloniaModule, _containerElement);
|
||||
_inputHelper = new InputHelperInterop(_avaloniaModule, _inputElement);
|
||||
|
||||
_inputHelper.CompositionEvent += InputHelperOnCompositionEvent;
|
||||
_inputHelper.InputEvent += InputHelperOnInputEvent;
|
||||
|
||||
HideIme();
|
||||
_canvasHelper.SetCursor("default");
|
||||
_topLevelImpl.SetCssCursor = x =>
|
||||
{
|
||||
_inputHelper.SetCursor(x); //macOS
|
||||
_canvasHelper.SetCursor(x); //windows
|
||||
};
|
||||
|
||||
_nativeControlHost = new NativeControlHostInterop(_avaloniaModule, _nativeControlsContainer);
|
||||
_storageProvider = await StorageProviderInterop.ImportAsync(Js);
|
||||
|
||||
Console.WriteLine("starting html canvas setup");
|
||||
_interop = new SKHtmlCanvasInterop(_avaloniaModule, _htmlCanvas, OnRenderFrame);
|
||||
|
||||
Console.WriteLine("Interop created");
|
||||
|
||||
var skiaOptions = AvaloniaLocator.Current.GetService<SkiaOptions>();
|
||||
_useGL = skiaOptions?.CustomGpuFactory != null;
|
||||
|
||||
if (_useGL)
|
||||
{
|
||||
_jsGlInfo = _interop.InitGL();
|
||||
Console.WriteLine("jsglinfo created - init gl");
|
||||
}
|
||||
else
|
||||
{
|
||||
var rasterInitialized = _interop.InitRaster();
|
||||
Console.WriteLine("raster initialized: {0}", rasterInitialized);
|
||||
}
|
||||
|
||||
if (_useGL)
|
||||
{
|
||||
// create the SkiaSharp context
|
||||
if (_context == null)
|
||||
{
|
||||
_glInterface = GRGlInterface.Create();
|
||||
_context = GRContext.CreateGl(_glInterface);
|
||||
|
||||
|
||||
// bump the default resource cache limit
|
||||
_context.SetResourceCacheLimit(skiaOptions?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024);
|
||||
Console.WriteLine("glcontext created and resource limit set");
|
||||
}
|
||||
|
||||
_topLevelImpl.SetSurface(_context, _jsGlInfo!, ColorType,
|
||||
new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi);
|
||||
}
|
||||
else
|
||||
{
|
||||
_topLevelImpl.SetSurface(ColorType,
|
||||
new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData);
|
||||
}
|
||||
|
||||
_interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
|
||||
|
||||
Threading.Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
_interop.RequestAnimationFrame(true);
|
||||
|
||||
_sizeWatcher = new SizeWatcherInterop(_avaloniaModule, _htmlCanvas, OnSizeChanged);
|
||||
_dpiWatcher = new DpiWatcherInterop(_avaloniaModule, OnDpiChanged);
|
||||
|
||||
_sizeWatcher.Start();
|
||||
_topLevel.Prepare();
|
||||
|
||||
_topLevel.Renderer.Start();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void InputHelperOnInputEvent(object? sender, WebInputEventArgs e)
|
||||
{
|
||||
if (IsComposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_topLevelImpl.RawTextEvent(e.Data);
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void InputHelperOnCompositionEvent(object? sender, WebCompositionEventArgs e)
|
||||
{
|
||||
if(_client == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.Type)
|
||||
{
|
||||
case WebCompositionEventArgs.WebCompositionEventType.Start:
|
||||
_client.SetPreeditText(null);
|
||||
IsComposing = true;
|
||||
break;
|
||||
case WebCompositionEventArgs.WebCompositionEventType.Update:
|
||||
_client.SetPreeditText(e.Data);
|
||||
break;
|
||||
case WebCompositionEventArgs.WebCompositionEventType.End:
|
||||
IsComposing = false;
|
||||
_client.SetPreeditText(null);
|
||||
_topLevelImpl.RawTextEvent(e.Data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRenderFrame()
|
||||
{
|
||||
if (_useGL && (_jsGlInfo == null))
|
||||
{
|
||||
Console.WriteLine("nothing to render");
|
||||
return;
|
||||
}
|
||||
if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0)
|
||||
{
|
||||
Console.WriteLine("nothing to render");
|
||||
return;
|
||||
}
|
||||
|
||||
ManualTriggerRenderTimer.Instance.RaiseTick();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_dpiWatcher?.Unsubscribe(OnDpiChanged);
|
||||
_sizeWatcher?.Dispose();
|
||||
_interop?.Dispose();
|
||||
}
|
||||
|
||||
private void ForceBlit()
|
||||
{
|
||||
// Note: this is technically a hack, but it's a kinda unique use case when
|
||||
// we want to blit the previous frame
|
||||
// renderer doesn't have much control over the render target
|
||||
// we render on the UI thread
|
||||
// We also don't want to have it as a meaningful public API.
|
||||
// Therefore we have InternalsVisibleTo hack here.
|
||||
|
||||
if (_topLevel.Renderer is CompositingRenderer dr)
|
||||
{
|
||||
dr.CompositionTarget.ImmediateUIThreadRender();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDpiChanged(double newDpi)
|
||||
{
|
||||
if (Math.Abs(_dpi - newDpi) > 0.0001)
|
||||
{
|
||||
_dpi = newDpi;
|
||||
|
||||
_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
|
||||
|
||||
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
|
||||
|
||||
ForceBlit();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSizeChanged(SKSize newSize)
|
||||
{
|
||||
if (_canvasSize != newSize)
|
||||
{
|
||||
_canvasSize = newSize;
|
||||
|
||||
_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi));
|
||||
|
||||
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
|
||||
|
||||
ForceBlit();
|
||||
}
|
||||
}
|
||||
|
||||
private void HideIme()
|
||||
{
|
||||
_inputHelper?.Hide();
|
||||
_containerHelper?.Focus();
|
||||
}
|
||||
|
||||
public void SetClient(ITextInputMethodClient? client)
|
||||
{
|
||||
if (_inputHelper is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(_client != null)
|
||||
{
|
||||
_client.SurroundingTextChanged -= SurroundingTextChanged;
|
||||
}
|
||||
|
||||
if(client != null)
|
||||
{
|
||||
client.SurroundingTextChanged += SurroundingTextChanged;
|
||||
}
|
||||
|
||||
_inputHelper.Clear();
|
||||
|
||||
_client = client;
|
||||
|
||||
if (IsActive && _client != null)
|
||||
{
|
||||
_inputHelper.Show();
|
||||
_inputElementFocused = true;
|
||||
_inputHelper.Focus();
|
||||
|
||||
var surroundingText = _client.SurroundingText;
|
||||
|
||||
_inputHelper?.SetSurroundingText(surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
_inputElementFocused = false;
|
||||
HideIme();
|
||||
}
|
||||
}
|
||||
|
||||
private void SurroundingTextChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if(_client != null)
|
||||
{
|
||||
var surroundingText = _client.SurroundingText;
|
||||
|
||||
_inputHelper?.SetSurroundingText(surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCursorRect(Rect rect)
|
||||
{
|
||||
_inputHelper?.Focus();
|
||||
var bounds = new PixelRect((int)rect.X, (int) rect.Y, (int) rect.Width, (int) rect.Height);
|
||||
|
||||
_inputHelper?.SetBounds(bounds, _client?.SurroundingText.CursorOffset ?? 0);
|
||||
_inputHelper?.Focus();
|
||||
}
|
||||
|
||||
public void SetOptions(TextInputOptions options)
|
||||
{
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_inputHelper?.Clear();
|
||||
_inputHelper?.SetSurroundingText("", 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,39 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Media;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Avalonia.Web.Blazor
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
|
||||
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()
|
||||
{
|
||||
return builder.SetupWithLifetime(new BlazorSingleViewLifetime());
|
||||
}
|
||||
|
||||
public static T UseBlazor<T>(this T builder) where T : AppBuilderBase<T>, new()
|
||||
{
|
||||
return builder
|
||||
.UseBrowser()
|
||||
.With(new BrowserPlatformOptions
|
||||
{
|
||||
FrameworkAssetPathResolver = new(filePath => $"/_content/Avalonia.Web.Blazor/{filePath}")
|
||||
});
|
||||
}
|
||||
|
||||
public static AppBuilder Configure<TApp>()
|
||||
where TApp : Application, new()
|
||||
{
|
||||
return AppBuilder.Configure<TApp>()
|
||||
.UseBlazor();
|
||||
}
|
||||
|
||||
internal class BlazorSingleViewLifetime : ISingleViewApplicationLifetime
|
||||
{
|
||||
public Control? MainView { get; set; }
|
||||
}
|
||||
|
||||
public static class WebAppBuilder
|
||||
{
|
||||
public static T SetupWithSingleViewLifetime<T>(
|
||||
this T builder)
|
||||
where T : AppBuilderBase<T>, new()
|
||||
{
|
||||
return builder.SetupWithLifetime(new BlazorSingleViewLifetime());
|
||||
}
|
||||
|
||||
public static AvaloniaBlazorAppBuilder Configure<TApp>()
|
||||
where TApp : Application, new()
|
||||
{
|
||||
var builder = AvaloniaBlazorAppBuilder.Configure<TApp>()
|
||||
.UseSkia()
|
||||
.With(new SkiaOptions { CustomGpuFactory = () => new BlazorSkiaGpu() });
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -39,10 +39,6 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="NpmInstall" Inputs="webapp/package.json" Outputs="webapp/node_modules/.install-stamp">
|
||||
<Exec Command="npm install" WorkingDirectory="webapp" />
|
||||
<!-- Write the stamp file, so incremental builds work -->
|
||||
@@ -52,4 +48,8 @@
|
||||
<Exec Command="npm run build" WorkingDirectory="webapp" />
|
||||
</Target>
|
||||
|
||||
<ItemGroup Label="InternalsVisibleTo">
|
||||
<InternalsVisibleTo Include="Avalonia.Web.Blazor, PublicKey=$(AvaloniaPublicKey)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -37,16 +37,14 @@ namespace Avalonia.Web
|
||||
|
||||
private bool _useGL;
|
||||
private ITextInputMethodClient? _client;
|
||||
private static int _canvasCount;
|
||||
|
||||
public AvaloniaView(string divId)
|
||||
: this(DomHelper.GetElementById(divId) ?? throw new Exception($"Element with id {divId} was not found in the html document."))
|
||||
{
|
||||
var host = DomHelper.GetElementById(divId);
|
||||
if (host == null)
|
||||
{
|
||||
throw new Exception($"Element with id {divId} was not found in the html document.");
|
||||
}
|
||||
}
|
||||
|
||||
public AvaloniaView(JSObject host)
|
||||
{
|
||||
var hostContent = DomHelper.CreateAvaloniaHost(host);
|
||||
if (hostContent == null)
|
||||
{
|
||||
@@ -64,8 +62,6 @@ namespace Avalonia.Web
|
||||
|
||||
_splash = DomHelper.GetElementById("avalonia-splash");
|
||||
|
||||
_canvas.SetProperty("id", $"avaloniaCanvas{_canvasCount++}");
|
||||
|
||||
_topLevelImpl = new BrowserTopLevelImpl(this);
|
||||
|
||||
_topLevel = new WebEmbeddableControlRoot(_topLevelImpl, () =>
|
||||
@@ -137,7 +133,7 @@ namespace Avalonia.Web
|
||||
|
||||
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
|
||||
|
||||
DomHelper.ObserveSize(host, divId, OnSizeChanged);
|
||||
DomHelper.ObserveSize(host, null, OnSizeChanged);
|
||||
|
||||
CanvasHelper.RequestAnimationFrame(_canvas, true);
|
||||
}
|
||||
@@ -387,7 +383,7 @@ namespace Avalonia.Web
|
||||
InputHelper.FocusElement(_containerElement);
|
||||
}
|
||||
|
||||
public void SetClient(ITextInputMethodClient? client)
|
||||
void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client)
|
||||
{
|
||||
Console.WriteLine("Set Client");
|
||||
if (_client != null)
|
||||
@@ -431,18 +427,18 @@ namespace Avalonia.Web
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCursorRect(Rect rect)
|
||||
void ITextInputMethodImpl.SetCursorRect(Rect rect)
|
||||
{
|
||||
InputHelper.FocusElement(_inputElement);
|
||||
InputHelper.SetBounds(_inputElement, (int)rect.X, (int)rect.Y, (int)rect.Width, (int)rect.Height, _client?.SurroundingText.CursorOffset ?? 0);
|
||||
InputHelper.FocusElement(_inputElement);
|
||||
}
|
||||
|
||||
public void SetOptions(TextInputOptions options)
|
||||
void ITextInputMethodImpl.SetOptions(TextInputOptions options)
|
||||
{
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
void ITextInputMethodImpl.Reset()
|
||||
{
|
||||
InputHelper.ClearInputElement(_inputElement);
|
||||
InputHelper.SetSurroundingText(_inputElement, "", 0, 0);
|
||||
|
||||
@@ -1,41 +1,54 @@
|
||||
using System.Runtime.InteropServices.JavaScript;
|
||||
using System;
|
||||
using System;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Web.Skia;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Avalonia.Web
|
||||
namespace Avalonia.Web;
|
||||
|
||||
[SupportedOSPlatform("browser")]
|
||||
public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime
|
||||
{
|
||||
[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings
|
||||
public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime
|
||||
public AvaloniaView? View;
|
||||
|
||||
public Control? MainView
|
||||
{
|
||||
public AvaloniaView? View;
|
||||
|
||||
public Control? MainView
|
||||
{
|
||||
get => View!.Content;
|
||||
set => View!.Content = value;
|
||||
}
|
||||
}
|
||||
|
||||
public static partial class WebAppBuilder
|
||||
{
|
||||
public static T SetupBrowserApp<T>(
|
||||
this T builder, string mainDivId)
|
||||
where T : AppBuilderBase<T>, new()
|
||||
{
|
||||
var lifetime = new BrowserSingleViewLifetime();
|
||||
|
||||
return builder
|
||||
.UseWindowingSubsystem(BrowserWindowingPlatform.Register)
|
||||
.UseSkia()
|
||||
.With(new SkiaOptions { CustomGpuFactory = () => new BrowserSkiaGpu() })
|
||||
.AfterSetup(b =>
|
||||
{
|
||||
lifetime.View = new AvaloniaView(mainDivId);
|
||||
})
|
||||
.SetupWithLifetime(lifetime);
|
||||
}
|
||||
get => View!.Content;
|
||||
set => View!.Content = value;
|
||||
}
|
||||
}
|
||||
|
||||
public class BrowserPlatformOptions
|
||||
{
|
||||
public Func<string, string> FrameworkAssetPathResolver { get; set; } = new(fileName => $"./{fileName}");
|
||||
}
|
||||
|
||||
|
||||
[SupportedOSPlatform("browser")]
|
||||
public static class WebAppBuilder
|
||||
{
|
||||
public static T SetupBrowserApp<T>(
|
||||
this T builder, string mainDivId)
|
||||
where T : AppBuilderBase<T>, new()
|
||||
{
|
||||
var lifetime = new BrowserSingleViewLifetime();
|
||||
|
||||
return builder
|
||||
.UseBrowser()
|
||||
.AfterSetup(b =>
|
||||
{
|
||||
lifetime.View = new AvaloniaView(mainDivId);
|
||||
})
|
||||
.SetupWithLifetime(lifetime);
|
||||
}
|
||||
|
||||
public static T UseBrowser<T>(
|
||||
this T builder)
|
||||
where T : AppBuilderBase<T>, new()
|
||||
{
|
||||
return builder
|
||||
.UseWindowingSubsystem(BrowserWindowingPlatform.Register)
|
||||
.UseSkia()
|
||||
.With(new SkiaOptions { CustomGpuFactory = () => new BrowserSkiaGpu() });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ using Avalonia.Platform;
|
||||
|
||||
namespace Avalonia.Web
|
||||
{
|
||||
public class CssCursor : ICursorImpl
|
||||
internal class CssCursor : ICursorImpl
|
||||
{
|
||||
public const string Default = "default";
|
||||
public string? Value { get; set; }
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -29,13 +29,13 @@ internal static partial class CanvasHelper
|
||||
return glInfo;
|
||||
}
|
||||
|
||||
[JSImport("Canvas.requestAnimationFrame", "avalonia.ts")]
|
||||
[JSImport("Canvas.requestAnimationFrame", AvaloniaModule.MainModuleName)]
|
||||
public static partial void RequestAnimationFrame(JSObject canvas, bool renderLoop);
|
||||
|
||||
[JSImport("Canvas.setCanvasSize", "avalonia.ts")]
|
||||
[JSImport("Canvas.setCanvasSize", AvaloniaModule.MainModuleName)]
|
||||
public static partial void SetCanvasSize(JSObject canvas, int height, int width);
|
||||
|
||||
[JSImport("Canvas.initGL", "avalonia.ts")]
|
||||
[JSImport("Canvas.initGL", AvaloniaModule.MainModuleName)]
|
||||
private static partial JSObject InitGL(
|
||||
JSObject canvas,
|
||||
string canvasId,
|
||||
|
||||
@@ -8,20 +8,20 @@ internal static partial class DomHelper
|
||||
[JSImport("globalThis.document.getElementById")]
|
||||
internal static partial JSObject? GetElementById(string id);
|
||||
|
||||
[JSImport("AvaloniaDOM.createAvaloniaHost", "avalonia.ts")]
|
||||
[JSImport("AvaloniaDOM.createAvaloniaHost", AvaloniaModule.MainModuleName)]
|
||||
public static partial JSObject CreateAvaloniaHost(JSObject element);
|
||||
|
||||
[JSImport("AvaloniaDOM.addClass", "avalonia.ts")]
|
||||
[JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)]
|
||||
public static partial void AddCssClass(JSObject element, string className);
|
||||
|
||||
[JSImport("SizeWatcher.observe", "avalonia.ts")]
|
||||
[JSImport("SizeWatcher.observe", AvaloniaModule.MainModuleName)]
|
||||
public static partial JSObject ObserveSize(
|
||||
JSObject canvas,
|
||||
string canvasId,
|
||||
string? canvasId,
|
||||
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number>>]
|
||||
Action<int, int> onSizeChanged);
|
||||
|
||||
[JSImport("DpiWatcher.start", "avalonia.ts")]
|
||||
[JSImport("DpiWatcher.start", AvaloniaModule.MainModuleName)]
|
||||
public static partial double ObserveDpi(
|
||||
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number>>]
|
||||
Action<double, double> onDpiChanged);
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Avalonia.Web.Interop;
|
||||
|
||||
internal static partial class InputHelper
|
||||
{
|
||||
[JSImport("InputHelper.subscribeKeyEvents", "avalonia.ts")]
|
||||
[JSImport("InputHelper.subscribeKeyEvents", AvaloniaModule.MainModuleName)]
|
||||
public static partial void SubscribeKeyEvents(
|
||||
JSObject htmlElement,
|
||||
[JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.Number, JSType.Boolean>>]
|
||||
@@ -14,7 +14,7 @@ internal static partial class InputHelper
|
||||
[JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.Number, JSType.Boolean>>]
|
||||
Func<string, string, int, bool> keyUp);
|
||||
|
||||
[JSImport("InputHelper.subscribeTextEvents", "avalonia.ts")]
|
||||
[JSImport("InputHelper.subscribeTextEvents", AvaloniaModule.MainModuleName)]
|
||||
public static partial void SubscribeTextEvents(
|
||||
JSObject htmlElement,
|
||||
[JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.Boolean>>]
|
||||
@@ -26,7 +26,7 @@ internal static partial class InputHelper
|
||||
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
|
||||
Func<JSObject, bool> onCompositionEnd);
|
||||
|
||||
[JSImport("InputHelper.subscribePointerEvents", "avalonia.ts")]
|
||||
[JSImport("InputHelper.subscribePointerEvents", AvaloniaModule.MainModuleName)]
|
||||
public static partial void SubscribePointerEvents(
|
||||
JSObject htmlElement,
|
||||
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
|
||||
@@ -39,35 +39,35 @@ internal static partial class InputHelper
|
||||
Func<JSObject, bool> wheel);
|
||||
|
||||
|
||||
[JSImport("InputHelper.subscribeInputEvents", "avalonia.ts")]
|
||||
[JSImport("InputHelper.subscribeInputEvents", AvaloniaModule.MainModuleName)]
|
||||
public static partial void SubscribeInputEvents(
|
||||
JSObject htmlElement,
|
||||
[JSMarshalAs<JSType.Function<JSType.String, JSType.Boolean>>]
|
||||
Func<string, bool> input);
|
||||
|
||||
|
||||
[JSImport("InputHelper.clearInput", "avalonia.ts")]
|
||||
[JSImport("InputHelper.clearInput", AvaloniaModule.MainModuleName)]
|
||||
public static partial void ClearInputElement(JSObject htmlElement);
|
||||
|
||||
[JSImport("InputHelper.isInputElement", "avalonia.ts")]
|
||||
[JSImport("InputHelper.isInputElement", AvaloniaModule.MainModuleName)]
|
||||
public static partial void IsInputElement(JSObject htmlElement);
|
||||
|
||||
[JSImport("InputHelper.focusElement", "avalonia.ts")]
|
||||
[JSImport("InputHelper.focusElement", AvaloniaModule.MainModuleName)]
|
||||
public static partial void FocusElement(JSObject htmlElement);
|
||||
|
||||
[JSImport("InputHelper.setCursor", "avalonia.ts")]
|
||||
[JSImport("InputHelper.setCursor", AvaloniaModule.MainModuleName)]
|
||||
public static partial void SetCursor(JSObject htmlElement, string kind);
|
||||
|
||||
[JSImport("InputHelper.hide", "avalonia.ts")]
|
||||
[JSImport("InputHelper.hide", AvaloniaModule.MainModuleName)]
|
||||
public static partial void HideElement(JSObject htmlElement);
|
||||
|
||||
[JSImport("InputHelper.show", "avalonia.ts")]
|
||||
[JSImport("InputHelper.show", AvaloniaModule.MainModuleName)]
|
||||
public static partial void ShowElement(JSObject htmlElement);
|
||||
|
||||
[JSImport("InputHelper.setSurroundingText", "avalonia.ts")]
|
||||
[JSImport("InputHelper.setSurroundingText", AvaloniaModule.MainModuleName)]
|
||||
public static partial void SetSurroundingText(JSObject htmlElement, string text, int start, int end);
|
||||
|
||||
[JSImport("InputHelper.setBounds", "avalonia.ts")]
|
||||
[JSImport("InputHelper.setBounds", AvaloniaModule.MainModuleName)]
|
||||
public static partial void SetBounds(JSObject htmlElement, int x, int y, int width, int height, int caret);
|
||||
|
||||
[JSImport("globalThis.navigator.clipboard.readText")]
|
||||
|
||||
@@ -5,24 +5,24 @@ namespace Avalonia.Web.Interop;
|
||||
|
||||
internal static partial class NativeControlHostHelper
|
||||
{
|
||||
[JSImport("NativeControlHost.createDefaultChild", "avalonia.ts")]
|
||||
[JSImport("NativeControlHost.createDefaultChild", AvaloniaModule.MainModuleName)]
|
||||
internal static partial JSObject CreateDefaultChild(JSObject? parent);
|
||||
|
||||
[JSImport("NativeControlHost.createAttachment", "avalonia.ts")]
|
||||
[JSImport("NativeControlHost.createAttachment", AvaloniaModule.MainModuleName)]
|
||||
internal static partial JSObject CreateAttachment();
|
||||
|
||||
[JSImport("NativeControlHost.initializeWithChildHandle", "avalonia.ts")]
|
||||
[JSImport("NativeControlHost.initializeWithChildHandle", AvaloniaModule.MainModuleName)]
|
||||
internal static partial void InitializeWithChildHandle(JSObject element, JSObject child);
|
||||
|
||||
[JSImport("NativeControlHost.attachTo", "avalonia.ts")]
|
||||
[JSImport("NativeControlHost.attachTo", AvaloniaModule.MainModuleName)]
|
||||
internal static partial void AttachTo(JSObject element, JSObject? host);
|
||||
|
||||
[JSImport("NativeControlHost.showInBounds", "avalonia.ts")]
|
||||
[JSImport("NativeControlHost.showInBounds", AvaloniaModule.MainModuleName)]
|
||||
internal static partial void ShowInBounds(JSObject element, double x, double y, double width, double height);
|
||||
|
||||
[JSImport("NativeControlHost.hideWithSize", "avalonia.ts")]
|
||||
[JSImport("NativeControlHost.hideWithSize", AvaloniaModule.MainModuleName)]
|
||||
internal static partial void HideWithSize(JSObject element, double width, double height);
|
||||
|
||||
[JSImport("NativeControlHost.releaseChild", "avalonia.ts")]
|
||||
[JSImport("NativeControlHost.releaseChild", AvaloniaModule.MainModuleName)]
|
||||
internal static partial void ReleaseChild(JSObject element);
|
||||
}
|
||||
|
||||
@@ -5,51 +5,51 @@ namespace Avalonia.Web.Interop;
|
||||
|
||||
internal static partial class StorageHelper
|
||||
{
|
||||
[JSImport("Caniuse.canShowOpenFilePicker", "avalonia.ts")]
|
||||
[JSImport("Caniuse.canShowOpenFilePicker", AvaloniaModule.MainModuleName)]
|
||||
public static partial bool CanShowOpenFilePicker();
|
||||
|
||||
[JSImport("Caniuse.canShowSaveFilePicker", "avalonia.ts")]
|
||||
[JSImport("Caniuse.canShowSaveFilePicker", AvaloniaModule.MainModuleName)]
|
||||
public static partial bool CanShowSaveFilePicker();
|
||||
|
||||
[JSImport("Caniuse.canShowDirectoryPicker", "avalonia.ts")]
|
||||
[JSImport("Caniuse.canShowDirectoryPicker", AvaloniaModule.MainModuleName)]
|
||||
public static partial bool CanShowDirectoryPicker();
|
||||
|
||||
[JSImport("StorageProvider.selectFolderDialog", "storage.ts")]
|
||||
[JSImport("StorageProvider.selectFolderDialog", AvaloniaModule.StorageModuleName)]
|
||||
public static partial Task<JSObject?> SelectFolderDialog(JSObject? startIn);
|
||||
|
||||
[JSImport("StorageProvider.openFileDialog", "storage.ts")]
|
||||
[JSImport("StorageProvider.openFileDialog", AvaloniaModule.StorageModuleName)]
|
||||
public static partial Task<JSObject?> OpenFileDialog(JSObject? startIn, bool multiple,
|
||||
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption);
|
||||
|
||||
[JSImport("StorageProvider.saveFileDialog", "storage.ts")]
|
||||
[JSImport("StorageProvider.saveFileDialog", AvaloniaModule.StorageModuleName)]
|
||||
public static partial Task<JSObject?> SaveFileDialog(JSObject? startIn, string? suggestedName,
|
||||
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption);
|
||||
|
||||
[JSImport("StorageProvider.openBookmark", "storage.ts")]
|
||||
[JSImport("StorageProvider.openBookmark", AvaloniaModule.StorageModuleName)]
|
||||
public static partial Task<JSObject?> OpenBookmark(string key);
|
||||
|
||||
[JSImport("StorageItem.saveBookmark", "storage.ts")]
|
||||
[JSImport("StorageItem.saveBookmark", AvaloniaModule.StorageModuleName)]
|
||||
public static partial Task<string?> SaveBookmark(JSObject item);
|
||||
|
||||
[JSImport("StorageItem.deleteBookmark", "storage.ts")]
|
||||
[JSImport("StorageItem.deleteBookmark", AvaloniaModule.StorageModuleName)]
|
||||
public static partial Task DeleteBookmark(JSObject item);
|
||||
|
||||
[JSImport("StorageItem.getProperties", "storage.ts")]
|
||||
[JSImport("StorageItem.getProperties", AvaloniaModule.StorageModuleName)]
|
||||
public static partial Task<JSObject?> GetProperties(JSObject item);
|
||||
|
||||
[JSImport("StorageItem.openWrite", "storage.ts")]
|
||||
[JSImport("StorageItem.openWrite", AvaloniaModule.StorageModuleName)]
|
||||
public static partial Task<JSObject> OpenWrite(JSObject item);
|
||||
|
||||
[JSImport("StorageItem.openRead", "storage.ts")]
|
||||
[JSImport("StorageItem.openRead", AvaloniaModule.StorageModuleName)]
|
||||
public static partial Task<JSObject> OpenRead(JSObject item);
|
||||
|
||||
[JSImport("StorageItem.getItems", "storage.ts")]
|
||||
[JSImport("StorageItem.getItems", AvaloniaModule.StorageModuleName)]
|
||||
[return: JSMarshalAs<JSType.Promise<JSType.Object>>]
|
||||
public static partial Task<JSObject> GetItems(JSObject item);
|
||||
|
||||
[JSImport("StorageItems.itemsArray", "storage.ts")]
|
||||
[JSImport("StorageItems.itemsArray", AvaloniaModule.StorageModuleName)]
|
||||
public static partial JSObject[] ItemsArray(JSObject item);
|
||||
|
||||
[JSImport("StorageProvider.createAcceptType", "storage.ts")]
|
||||
[JSImport("StorageProvider.createAcceptType", AvaloniaModule.StorageModuleName)]
|
||||
public static partial JSObject CreateAcceptType(string description, string[] mimeTypes);
|
||||
}
|
||||
|
||||
@@ -2,33 +2,33 @@
|
||||
using System.Runtime.InteropServices.JavaScript;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Avalonia.Web.Storage;
|
||||
namespace Avalonia.Web.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Set of FileSystemWritableFileStream and Blob methods.
|
||||
/// </summary>
|
||||
internal static partial class StreamHelper
|
||||
{
|
||||
[JSImport("StreamHelper.seek", "avalonia.ts")]
|
||||
[JSImport("StreamHelper.seek", AvaloniaModule.MainModuleName)]
|
||||
public static partial void Seek(JSObject stream, [JSMarshalAs<JSType.Number>] long position);
|
||||
|
||||
[JSImport("StreamHelper.truncate", "avalonia.ts")]
|
||||
[JSImport("StreamHelper.truncate", AvaloniaModule.MainModuleName)]
|
||||
public static partial void Truncate(JSObject stream, [JSMarshalAs<JSType.Number>] long size);
|
||||
|
||||
[JSImport("StreamHelper.write", "avalonia.ts")]
|
||||
[JSImport("StreamHelper.write", AvaloniaModule.MainModuleName)]
|
||||
public static partial Task WriteAsync(JSObject stream, [JSMarshalAs<JSType.MemoryView>] ArraySegment<byte> data);
|
||||
|
||||
[JSImport("StreamHelper.close", "avalonia.ts")]
|
||||
[JSImport("StreamHelper.close", AvaloniaModule.MainModuleName)]
|
||||
public static partial Task CloseAsync(JSObject stream);
|
||||
|
||||
[JSImport("StreamHelper.byteLength", "avalonia.ts")]
|
||||
[JSImport("StreamHelper.byteLength", AvaloniaModule.MainModuleName)]
|
||||
[return: JSMarshalAs<JSType.Number>]
|
||||
public static partial long ByteLength(JSObject stream);
|
||||
|
||||
[JSImport("StreamHelper.sliceArrayBuffer", "avalonia.ts")]
|
||||
[JSImport("StreamHelper.sliceArrayBuffer", AvaloniaModule.MainModuleName)]
|
||||
private static partial Task<JSObject> SliceToArrayBuffer(JSObject stream, [JSMarshalAs<JSType.Number>] long offset, int count);
|
||||
|
||||
[JSImport("StreamHelper.toMemoryView", "avalonia.ts")]
|
||||
[JSImport("StreamHelper.toMemoryView", AvaloniaModule.MainModuleName)]
|
||||
[return: JSMarshalAs<JSType.Array<JSType.Number>>]
|
||||
private static partial byte[] ArrayBufferToMemoryView(JSObject stream);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ using Avalonia.Rendering;
|
||||
|
||||
namespace Avalonia.Web
|
||||
{
|
||||
public class ManualTriggerRenderTimer : IRenderTimer
|
||||
internal class ManualTriggerRenderTimer : IRenderTimer
|
||||
{
|
||||
private static readonly Stopwatch s_sw = Stopwatch.StartNew();
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ using System.Runtime.InteropServices.JavaScript;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Avalonia.Web.Interop;
|
||||
|
||||
namespace Avalonia.Web.Storage;
|
||||
|
||||
[System.Runtime.Versioning.SupportedOSPlatform("browser")]
|
||||
|
||||
@@ -20,7 +20,7 @@ internal class BrowserStorageProvider : IStorageProvider
|
||||
internal const string PickerCancelMessage = "The user aborted a request";
|
||||
internal const string NoPermissionsMessage = "Permissions denied";
|
||||
|
||||
private readonly Lazy<Task<JSObject>> _lazyModule = new(() => JSHost.ImportAsync("storage.ts", "./storage.js"));
|
||||
private readonly Lazy<Task> _lazyModule = new(() => AvaloniaModule.ImportStorage());
|
||||
|
||||
public bool CanOpen => StorageHelper.CanShowOpenFilePicker();
|
||||
public bool CanSave => StorageHelper.CanShowSaveFilePicker();
|
||||
@@ -28,7 +28,7 @@ internal class BrowserStorageProvider : IStorageProvider
|
||||
|
||||
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
|
||||
{
|
||||
_ = await _lazyModule.Value;
|
||||
await _lazyModule.Value;
|
||||
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
|
||||
|
||||
var (types, exludeAll) = ConvertFileTypes(options.FileTypeFilter);
|
||||
@@ -62,7 +62,7 @@ internal class BrowserStorageProvider : IStorageProvider
|
||||
|
||||
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
|
||||
{
|
||||
_ = await _lazyModule.Value;
|
||||
await _lazyModule.Value;
|
||||
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
|
||||
|
||||
var (types, exludeAll) = ConvertFileTypes(options.FileTypeChoices);
|
||||
@@ -90,7 +90,7 @@ internal class BrowserStorageProvider : IStorageProvider
|
||||
|
||||
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
|
||||
{
|
||||
_ = await _lazyModule.Value;
|
||||
await _lazyModule.Value;
|
||||
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
|
||||
|
||||
try
|
||||
@@ -106,14 +106,14 @@ internal class BrowserStorageProvider : IStorageProvider
|
||||
|
||||
public async Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
|
||||
{
|
||||
_ = await _lazyModule.Value;
|
||||
await _lazyModule.Value;
|
||||
var item = await StorageHelper.OpenBookmark(bookmark);
|
||||
return item is not null ? new JSStorageFile(item) : null;
|
||||
}
|
||||
|
||||
public async Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
|
||||
{
|
||||
_ = await _lazyModule.Value;
|
||||
await _lazyModule.Value;
|
||||
var item = await StorageHelper.OpenBookmark(bookmark);
|
||||
return item is not null ? new JSStorageFolder(item) : null;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ using System.Runtime.InteropServices.JavaScript;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Avalonia.Web.Interop;
|
||||
|
||||
namespace Avalonia.Web.Storage;
|
||||
|
||||
[System.Runtime.Versioning.SupportedOSPlatform("browser")]
|
||||
|
||||
@@ -8,7 +8,7 @@ using Avalonia.Threading;
|
||||
|
||||
namespace Avalonia.Web
|
||||
{
|
||||
public class BrowserWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface
|
||||
internal class BrowserWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface
|
||||
{
|
||||
private bool _signaled;
|
||||
private static KeyboardDevice? s_keyboard;
|
||||
|
||||
@@ -6,8 +6,8 @@ import { Caniuse } from "./avalonia/caniuse";
|
||||
import { StreamHelper } from "./avalonia/stream";
|
||||
import { NativeControlHost } from "./avalonia/nativeControlHost";
|
||||
|
||||
export async function createAvaloniaRuntime(api: RuntimeAPI): Promise<void> {
|
||||
api.setModuleImports("avalonia.ts", {
|
||||
async function registerAvaloniaModule(api: RuntimeAPI): Promise<void> {
|
||||
api.setModuleImports("avalonia", {
|
||||
Caniuse,
|
||||
Canvas,
|
||||
InputHelper,
|
||||
@@ -18,3 +18,15 @@ export async function createAvaloniaRuntime(api: RuntimeAPI): Promise<void> {
|
||||
NativeControlHost
|
||||
});
|
||||
}
|
||||
export {
|
||||
Caniuse,
|
||||
Canvas,
|
||||
InputHelper,
|
||||
SizeWatcher,
|
||||
DpiWatcher,
|
||||
AvaloniaDOM,
|
||||
StreamHelper,
|
||||
NativeControlHost,
|
||||
|
||||
registerAvaloniaModule
|
||||
};
|
||||
|
||||
@@ -211,7 +211,7 @@ export class SizeWatcher {
|
||||
static observer: ResizeObserver;
|
||||
static elements: Map<string, HTMLElement>;
|
||||
|
||||
public static observe(element: HTMLElement, elementId: string, callback: (width: number, height: number) => void): void {
|
||||
public static observe(element: HTMLElement, elementId: string | undefined, callback: (width: number, height: number) => void): void {
|
||||
if (!element || !callback) {
|
||||
return;
|
||||
}
|
||||
@@ -223,7 +223,7 @@ export class SizeWatcher {
|
||||
callback
|
||||
};
|
||||
|
||||
SizeWatcher.elements.set(elementId, element);
|
||||
SizeWatcher.elements.set(elementId ?? element.id, element);
|
||||
SizeWatcher.observer.observe(element);
|
||||
|
||||
SizeWatcher.invoke(element);
|
||||
|
||||
@@ -4,13 +4,17 @@ export class AvaloniaDOM {
|
||||
}
|
||||
|
||||
static createAvaloniaHost(host: HTMLElement) {
|
||||
const randomIdPart = Math.random().toString(36).replace(/[^a-z]+/g, "").substr(2, 10);
|
||||
|
||||
// Root element
|
||||
host.classList.add("avalonia-container");
|
||||
host.tabIndex = 0;
|
||||
host.oncontextmenu = function () { return false; };
|
||||
host.style.overflow = "hidden";
|
||||
|
||||
// Rendering target canvas
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.id = `canvas${randomIdPart}`;
|
||||
canvas.classList.add("avalonia-canvas");
|
||||
canvas.style.backgroundColor = "#ccc";
|
||||
canvas.style.width = "100%";
|
||||
@@ -19,6 +23,7 @@ export class AvaloniaDOM {
|
||||
|
||||
// Native controls host
|
||||
const nativeHost = document.createElement("div");
|
||||
canvas.id = `nativeHost${randomIdPart}`;
|
||||
nativeHost.classList.add("avalonia-native-host");
|
||||
nativeHost.style.left = "0px";
|
||||
nativeHost.style.top = "0px";
|
||||
@@ -28,6 +33,7 @@ export class AvaloniaDOM {
|
||||
|
||||
// IME
|
||||
const inputElement = document.createElement("input");
|
||||
canvas.id = `input${randomIdPart}`;
|
||||
inputElement.classList.add("avalonia-input-element");
|
||||
inputElement.autocapitalize = "none";
|
||||
inputElement.type = "text";
|
||||
@@ -42,6 +48,7 @@ export class AvaloniaDOM {
|
||||
inputElement.style.color = "transparent";
|
||||
inputElement.style.display = "none";
|
||||
inputElement.style.height = "20px";
|
||||
inputElement.style.zIndex = "-1";
|
||||
inputElement.onpaste = function () { return false; };
|
||||
inputElement.oncopy = function () { return false; };
|
||||
inputElement.oncut = function () { return false; };
|
||||
|
||||
Reference in New Issue
Block a user