diff --git a/.editorconfig b/.editorconfig index 128b6f1712..179491eece 100644 --- a/.editorconfig +++ b/.editorconfig @@ -134,6 +134,9 @@ csharp_space_between_parentheses = false csharp_space_between_square_brackets = false space_within_single_line_array_initializer_braces = true +#Net Analyzer +dotnet_analyzer_diagnostic.category-Performance.severity = none #error - Uncomment when all violations are fixed. + # Wrapping preferences csharp_wrap_before_ternary_opsigns = false diff --git a/.gitignore b/.gitignore index 84faae1806..61a3b53de1 100644 --- a/.gitignore +++ b/.gitignore @@ -215,3 +215,4 @@ src/Web/Avalonia.Web.Blazor/Interop/Typescript/*.js node_modules src/Web/Avalonia.Web.Blazor/webapp/package-lock.json src/Web/Avalonia.Web.Blazor/wwwroot +src/Web/Avalonia.Web/wwwroot diff --git a/.gitmodules b/.gitmodules index 2d11fdfa9e..032bc879cc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github"] path = src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github url = https://github.com/kekekeks/XamlX.git +[submodule "nukebuild/il-repack"] + path = nukebuild/il-repack + url = https://github.com/Gillibald/il-repack diff --git a/.nuke b/.nuke deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json new file mode 100644 index 0000000000..5bbc3d6915 --- /dev/null +++ b/.nuke/build.schema.json @@ -0,0 +1,148 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Build Schema", + "$ref": "#/definitions/build", + "definitions": { + "build": { + "type": "object", + "properties": { + "Configuration": { + "type": "string", + "description": "configuration" + }, + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "ForceNugetVersion": { + "type": "string", + "description": "force-nuget-version" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "type": "string", + "description": "Host for execution. Default is 'automatic'", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "type": "string", + "enum": [ + "CiAzureLinux", + "CiAzureOSX", + "CiAzureWindows", + "Clean", + "Compile", + "CompileHtmlPreviewer", + "CompileNative", + "CreateIntermediateNugetPackages", + "CreateNugetPackages", + "GenerateCppHeaders", + "Package", + "RunCoreLibsTests", + "RunDesignerTests", + "RunHtmlPreviewerTests", + "RunLeakTests", + "RunRenderTests", + "RunTests", + "ZipFiles" + ] + } + }, + "SkipPreviewer": { + "type": "boolean", + "description": "skip-previewer" + }, + "SkipTests": { + "type": "boolean", + "description": "skip-tests" + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded. Default is Avalonia.sln" + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "type": "string", + "enum": [ + "CiAzureLinux", + "CiAzureOSX", + "CiAzureWindows", + "Clean", + "Compile", + "CompileHtmlPreviewer", + "CompileNative", + "CreateIntermediateNugetPackages", + "CreateNugetPackages", + "GenerateCppHeaders", + "Package", + "RunCoreLibsTests", + "RunDesignerTests", + "RunHtmlPreviewerTests", + "RunLeakTests", + "RunRenderTests", + "RunTests", + "ZipFiles" + ] + } + }, + "Verbosity": { + "type": "string", + "description": "Logging verbosity during build execution. Default is 'Normal'", + "enum": [ + "Minimal", + "Normal", + "Quiet", + "Verbose" + ] + } + } + } + } +} \ No newline at end of file diff --git a/.nuke/parameters.json b/.nuke/parameters.json new file mode 100644 index 0000000000..42521bb7dd --- /dev/null +++ b/.nuke/parameters.json @@ -0,0 +1,4 @@ +{ + "$schema": "./build.schema.json", + "Solution": "" +} \ No newline at end of file diff --git a/Avalonia.sln b/Avalonia.sln index 68335c672c..c000f56d09 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -90,6 +90,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}" ProjectSection(SolutionItems) = preProject build\ApiDiff.props = build\ApiDiff.props + build\AvaloniaPublicKey.props = build\AvaloniaPublicKey.props build\Base.props = build\Base.props build\Binding.props = build\Binding.props build\CoreLibraries.props = build\CoreLibraries.props @@ -102,8 +103,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\Microsoft.CSharp.props = build\Microsoft.CSharp.props build\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props build\Moq.props = build\Moq.props + build\NetAnalyzers.props = build\NetAnalyzers.props build\NetCore.props = build\NetCore.props build\NetFX.props = build\NetFX.props + build\NullableEnable.props = build\NullableEnable.props build\ReactiveUI.props = build\ReactiveUI.props build\ReferenceCoreLibraries.props = build\ReferenceCoreLibraries.props build\Rx.props = build\Rx.props @@ -198,8 +201,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}" @@ -212,15 +213,21 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ColorPick EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Tests", "tests\Avalonia.DesignerSupport.Tests\Avalonia.DesignerSupport.Tests.csproj", "{EABE2161-989B-42BF-BD8D-1E34B20C21F1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevGenerators", "src\tools\DevGenerators\DevGenerators.csproj", "{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevGenerators", "src\tools\DevGenerators\DevGenerators.csproj", "{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}" +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}") = "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 @@ -407,9 +414,7 @@ Global {BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|Any CPU.ActiveCfg = Release|Any CPU {BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|Any CPU.Build.0 = Release|Any CPU {3F00BC43-5095-477F-93D8-E65B08179A00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3F00BC43-5095-477F-93D8-E65B08179A00}.Debug|Any CPU.Build.0 = Debug|Any CPU {3F00BC43-5095-477F-93D8-E65B08179A00}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3F00BC43-5095-477F-93D8-E65B08179A00}.Release|Any CPU.Build.0 = Release|Any CPU {41B02319-965D-4945-8005-C1A3D1224165}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {41B02319-965D-4945-8005-C1A3D1224165}.Debug|Any CPU.Build.0 = Debug|Any CPU {41B02319-965D-4945-8005-C1A3D1224165}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -478,10 +483,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 @@ -510,6 +511,10 @@ Global {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Debug|Any CPU.Build.0 = Debug|Any CPU {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.ActiveCfg = Release|Any CPU {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.Build.0 = Release|Any CPU + {76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 {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 @@ -526,6 +531,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 @@ -569,6 +582,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} @@ -576,18 +590,19 @@ 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} {2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {EABE2161-989B-42BF-BD8D-1E34B20C21F1} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} - {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} + {76D39FF6-6B4F-46C4-93CD-E6FC4665739E} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268} {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} diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 0b79758c76..ee8abb75c1 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -41,9 +41,9 @@ jobs: steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.202' + displayName: 'Use .NET Core SDK 6.0.401' inputs: - version: 6.0.202 + version: 6.0.401 - task: Windows Application Driver@0 inputs: @@ -57,6 +57,7 @@ jobs: projects: 'samples/IntegrationTestApp/IntegrationTestApp.csproj' - task: DotNetCoreCLI@2 + retryCountOnTaskFailure: 3 inputs: command: 'test' projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj' diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 52fc8db53c..33b2dc670a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -6,7 +6,6 @@ jobs: variables: SolutionDir: '$(Build.SourcesDirectory)' steps: - - task: PowerShell@2 displayName: Get PR Number inputs: @@ -31,14 +30,20 @@ jobs: vmImage: 'ubuntu-20.04' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.418' + displayName: 'Use .NET Core SDK 6.0.401' inputs: - version: 3.1.418 + version: 6.0.401 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.202' + displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12' + inputs: + version: 7.0.100-rc.1.22431.12 + + - task: CmdLine@2 + displayName: 'Install Workloads' inputs: - version: 6.0.202 + script: | + dotnet workload install wasm-tools wasm-experimental - task: CmdLine@2 displayName: 'Run Build' @@ -62,22 +67,21 @@ jobs: vmImage: 'macos-12' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.418' + displayName: 'Use .NET Core SDK 6.0.401' inputs: - version: 3.1.418 + version: 6.0.401 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.202' + displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12' inputs: - version: 6.0.202 - + version: 7.0.100-rc.1.22431.12 + - task: CmdLine@2 - displayName: 'Install Mono 5.18' + displayName: 'Install Workloads' inputs: script: | - curl -o ./mono.pkg https://download.mono-project.com/archive/5.18.0/macos-10-universal/MonoFramework-MDK-5.18.0.225.macos10.xamarin.universal.pkg - sudo installer -verbose -pkg ./mono.pkg -target / - + dotnet workload install wasm-tools wasm-experimental + - task: CmdLine@2 displayName: 'Generate avalonia-native' inputs: @@ -134,26 +138,26 @@ jobs: SolutionDir: '$(Build.SourcesDirectory)' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.418' + displayName: 'Use .NET Core SDK 6.0.401' inputs: - version: 3.1.418 + version: 6.0.401 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.202' + displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12' inputs: - version: 6.0.202 + version: 7.0.100-rc.1.22431.12 - task: CmdLine@2 displayName: 'Install Workloads' inputs: script: | - dotnet workload install android ios + dotnet workload install android ios wasm-tools wasm-experimental - task: CmdLine@2 displayName: 'Install Nuke' inputs: script: | - dotnet tool install --global Nuke.GlobalTool --version 0.24.0 + dotnet tool install --global Nuke.GlobalTool --version 6.2.1 - task: CmdLine@2 displayName: 'Run Nuke' diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000000..b08cc590f4 --- /dev/null +++ b/build.cmd @@ -0,0 +1,7 @@ +:; set -eo pipefail +:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) +:; ${SCRIPT_DIR}/build.sh "$@" +:; exit $? + +@ECHO OFF +powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* diff --git a/build.ps1 b/build.ps1 index 985e8abcee..997e5b423f 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,13 +1,12 @@ [CmdletBinding()] Param( - #[switch]$CustomParam, [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] [string[]]$BuildArguments ) -Write-Output "Windows PowerShell $($Host.Version)" +Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)" -Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { exit 1 } +Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 } $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent ########################################################################### @@ -15,15 +14,15 @@ $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent ########################################################################### $BuildProjectFile = "$PSScriptRoot\nukebuild\_build.csproj" -$TempDirectory = "$PSScriptRoot\\.tmp" +$TempDirectory = "$PSScriptRoot\\.nuke\temp" $DotNetGlobalFile = "$PSScriptRoot\\global.json" -$DotNetInstallUrl = "https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.ps1" +$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" $DotNetChannel = "Current" $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 $env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 -$env:NUGET_XMLDOC_MODE = "skip" +$env:DOTNET_MULTILEVEL_LOOKUP = 0 ########################################################################### # EXECUTION @@ -34,38 +33,37 @@ function ExecSafe([scriptblock] $cmd) { if ($LASTEXITCODE) { exit $LASTEXITCODE } } -# If global.json exists, load expected version -if (Test-Path $DotNetGlobalFile) { - $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) - if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { - $DotNetVersion = $DotNetGlobal.sdk.version - } -} - -# If dotnet is installed locally, and expected version is not set or installation matches the expected version +# If dotnet CLI is installed globally and it matches requested version, use for execution if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` - (!(Test-Path variable:DotNetVersion) -or $(& dotnet --version) -eq $DotNetVersion)) { + $(dotnet --version) -and $LASTEXITCODE -eq 0) { $env:DOTNET_EXE = (Get-Command "dotnet").Path } else { - $DotNetDirectory = "$TempDirectory\dotnet-win" - $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" - # Download install script $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" - mkdir -force $TempDirectory > $null + New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) + # If global.json exists, load expected version + if (Test-Path $DotNetGlobalFile) { + $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) + if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { + $DotNetVersion = $DotNetGlobal.sdk.version + } + } + # Install by channel or version + $DotNetDirectory = "$TempDirectory\dotnet-win" if (!(Test-Path variable:DotNetVersion)) { - ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } } else { - ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } } - - $env:PATH="$DotNetDirectory;$env:PATH" + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" } -Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)" +Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" -ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile -- $BuildArguments } +ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } +ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } diff --git a/build.sh b/build.sh index 9532b4fbe0..76919a5351 100755 --- a/build.sh +++ b/build.sh @@ -1,16 +1,6 @@ #!/usr/bin/env bash -echo $(bash --version 2>&1 | head -n 1) - -#CUSTOMPARAM=0 -BUILD_ARGUMENTS=() -for i in "$@"; do - case $(echo $1 | awk '{print tolower($0)}') in - # -custom-param) CUSTOMPARAM=1;; - *) BUILD_ARGUMENTS+=("$1") ;; - esac - shift -done +bash --version 2>&1 | head -n 1 set -eo pipefail SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) @@ -20,11 +10,53 @@ SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) ########################################################################### BUILD_PROJECT_FILE="$SCRIPT_DIR/nukebuild/_build.csproj" +TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" + +DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" +DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" +DOTNET_CHANNEL="Current" export DOTNET_CLI_TELEMETRY_OPTOUT=1 export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 -export NUGET_XMLDOC_MODE="skip" +export DOTNET_MULTILEVEL_LOOKUP=0 -dotnet --info +########################################################################### +# EXECUTION +########################################################################### -dotnet run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]} +function FirstJsonValue { + perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}" +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then + export DOTNET_EXE="$(command -v dotnet)" +else + # Download install script + DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" + mkdir -p "$TEMP_DIRECTORY" + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" + + # If global.json exists, load expected version + if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then + DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")") + if [[ "$DOTNET_VERSION" == "" ]]; then + unset DOTNET_VERSION + fi + fi + + # Install by channel or version + DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" + if [[ -z ${DOTNET_VERSION+x} ]]; then + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path + else + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path + fi + export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" +fi + +echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" + +"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" diff --git a/build/NetAnalyzers.props b/build/NetAnalyzers.props new file mode 100644 index 0000000000..dfca9ecf9e --- /dev/null +++ b/build/NetAnalyzers.props @@ -0,0 +1,5 @@ + + + true + + diff --git a/dirs.proj b/dirs.proj index 47ad0dfd55..a2544ef951 100644 --- a/dirs.proj +++ b/dirs.proj @@ -23,12 +23,13 @@ + - + diff --git a/global.json b/global.json index a6792b05c7..44d4e10dbf 100644 --- a/global.json +++ b/global.json @@ -1,8 +1,4 @@ { - "sdk": { - "version": "6.0.202", - "rollForward": "latestFeature" - }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", "MSBuild.Sdk.Extras": "3.0.22", diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 4bbb667154..7425c344c3 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -23,6 +23,7 @@ using static Nuke.Common.Tools.MSBuild.MSBuildTasks; using static Nuke.Common.Tools.DotNet.DotNetTasks; using static Nuke.Common.Tools.Xunit.XunitTasks; using static Nuke.Common.Tools.VSWhere.VSWhereTasks; +using MicroCom.CodeGenerator; /* Before editing this file, install support plugin for your IDE, @@ -163,7 +164,7 @@ partial class Build : NukeBuild .EnableNoBuild() .EnableNoRestore() .When(Parameters.PublishTestResults, _ => _ - .SetLogger("trx") + .SetLoggers("trx") .SetResultsDirectory(Parameters.TestResultsRoot))); } } @@ -215,8 +216,6 @@ partial class Build : NukeBuild RunCoreTest("Avalonia.DesignerSupport.Tests"); }); - [PackageExecutable("JetBrains.dotMemoryUnit", "dotMemoryUnit.exe")] readonly Tool DotMemoryUnit; - Target RunLeakTests => _ => _ .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) .DependsOn(Compile) @@ -224,12 +223,9 @@ partial class Build : NukeBuild { void DoMemoryTest() { - var testAssembly = "tests\\Avalonia.LeakTests\\bin\\Release\\net461\\Avalonia.LeakTests.dll"; - DotMemoryUnit( - $"{XunitPath.DoubleQuoteIfNeeded()} --propagate-exit-code -- {testAssembly}", - timeout: 120_000); + RunCoreTest("Avalonia.LeakTests"); } - ControlFlow.ExecuteWithRetry(DoMemoryTest, waitInSeconds: 3); + ControlFlow.ExecuteWithRetry(DoMemoryTest, delay: TimeSpan.FromMilliseconds(3)); }); Target ZipFiles => _ => _ @@ -283,6 +279,14 @@ partial class Build : NukeBuild .DependsOn(Package) .DependsOn(ZipFiles); + Target GenerateCppHeaders => _ => _.Executes(() => + { + var file = MicroComCodeGenerator.Parse( + File.ReadAllText(RootDirectory / "src" / "Avalonia.Native" / "avn.idl")); + File.WriteAllText(RootDirectory / "native" / "Avalonia.Native" / "inc" / "avalonia-native.h", + file.GenerateCppHeader()); + }); + public static int Main() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs index 1826623674..dfa914d1db 100644 --- a/nukebuild/BuildParameters.cs +++ b/nukebuild/BuildParameters.cs @@ -74,11 +74,11 @@ public partial class Build MSBuildSolution = RootDirectory / "dirs.proj"; // PARAMETERS - IsLocalBuild = Host == HostType.Console; + IsLocalBuild = NukeBuild.IsLocalBuild; IsRunningOnUnix = Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX; IsRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - IsRunningOnAzure = Host == HostType.AzurePipelines || + IsRunningOnAzure = Host is AzurePipelines || Environment.GetEnvironmentVariable("LOGNAME") == "vsts"; if (IsRunningOnAzure) diff --git a/nukebuild/BuildTasksPatcher.cs b/nukebuild/BuildTasksPatcher.cs index e3766ae23f..5fd331035a 100644 --- a/nukebuild/BuildTasksPatcher.cs +++ b/nukebuild/BuildTasksPatcher.cs @@ -17,8 +17,12 @@ public class BuildTasksPatcher { if (entry.Name == "Avalonia.Build.Tasks.dll") { - var temp = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".dll"); + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + var temp = Path.Combine(tempDir, Guid.NewGuid() + ".dll"); var output = temp + ".output"; + File.Copy(typeof(Microsoft.Build.Framework.ITask).Assembly.GetModules()[0].FullyQualifiedName, + Path.Combine(tempDir, "Microsoft.Build.Framework.dll")); var patched = new MemoryStream(); try { @@ -57,10 +61,8 @@ public class BuildTasksPatcher { try { - if (File.Exists(temp)) - File.Delete(temp); - if (File.Exists(output)) - File.Delete(output); + if(Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); } catch { diff --git a/nukebuild/DotNetConfigHelper.cs b/nukebuild/DotNetConfigHelper.cs index 932525288c..eca1e2684d 100644 --- a/nukebuild/DotNetConfigHelper.cs +++ b/nukebuild/DotNetConfigHelper.cs @@ -46,7 +46,7 @@ public class DotNetConfigHelper public DotNetConfigHelper SetVerbosity(DotNetVerbosity verbosity) { Build = Build?.SetVerbosity(verbosity); - Pack = Pack?.SetVerbostiy(verbosity); + Pack = Pack?.SetVerbosity(verbosity); Test = Test?.SetVerbosity(verbosity); return this; } @@ -54,4 +54,4 @@ public class DotNetConfigHelper public static implicit operator DotNetConfigHelper(DotNetBuildSettings s) => new DotNetConfigHelper(s); public static implicit operator DotNetConfigHelper(DotNetPackSettings s) => new DotNetConfigHelper(s); public static implicit operator DotNetConfigHelper(DotNetTestSettings s) => new DotNetConfigHelper(s); -} \ No newline at end of file +} diff --git a/nukebuild/MicroComGen.cs b/nukebuild/MicroComGen.cs deleted file mode 100644 index b1e546cb97..0000000000 --- a/nukebuild/MicroComGen.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.IO; -using MicroCom.CodeGenerator; -using Nuke.Common; - -partial class Build : NukeBuild -{ - Target GenerateCppHeaders => _ => _.Executes(() => - { - var file = MicroComCodeGenerator.Parse( - File.ReadAllText(RootDirectory / "src" / "Avalonia.Native" / "avn.idl")); - File.WriteAllText(RootDirectory / "native" / "Avalonia.Native" / "inc" / "avalonia-native.h", - file.GenerateCppHeader()); - }); -} \ No newline at end of file diff --git a/nukebuild/Shims.cs b/nukebuild/Shims.cs index 1ac14bf622..6f79972ad6 100644 --- a/nukebuild/Shims.cs +++ b/nukebuild/Shims.cs @@ -49,7 +49,11 @@ public partial class Build { if (fsEntry is FileInfo) { +#if NET6 var relPath = Path.GetRelativePath(rootPath, fsEntry.FullName); +#else + var relPath = GetRelativePath(rootPath, fsEntry.FullName); +#endif AddFile(fsEntry.FullName, relPath); } } @@ -78,6 +82,17 @@ public partial class Build } } + private static string GetRelativePath(string relativeTo, string path) + { + var uri = new Uri(relativeTo); + var rel = Uri.UnescapeDataString(uri.MakeRelativeUri(new Uri(path)).ToString()).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + if (rel.Contains(Path.DirectorySeparatorChar.ToString()) == false) + { + rel = $".{Path.DirectorySeparatorChar}{rel}"; + } + return rel; + } + class NumergeNukeLogger : INumergeLogger { public void Log(NumergeLogLevel level, string message) diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index b2c58e2292..8c0d824298 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -1,41 +1,48 @@  - Exe - netcoreapp3.1 false False - CS0649;CS0169 + CS0649;CS0169;SYSLIB0011 + 1 + net7.0 + - - + - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - - - - - - - - - + + + + + + + + + + + + diff --git a/nukebuild/il-repack b/nukebuild/il-repack new file mode 160000 index 0000000000..892f079ea8 --- /dev/null +++ b/nukebuild/il-repack @@ -0,0 +1 @@ +Subproject commit 892f079ea8cb0c178f0a68f53a7a7eac13acdda9 diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs index 33ca511340..62c582610c 100644 --- a/samples/ControlCatalog.Android/MainActivity.cs +++ b/samples/ControlCatalog.Android/MainActivity.cs @@ -5,16 +5,8 @@ using Avalonia.Android; namespace ControlCatalog.Android { - [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleInstance, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] - public class MainActivity : AvaloniaActivity + [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] + public class MainActivity : AvaloniaMainActivity { - protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) - { - return base.CustomizeAppBuilder(builder) - .AfterSetup(_ => - { - Pages.EmbedSample.Implementation = new EmbedSampleAndroid(); - }); - } } } diff --git a/samples/ControlCatalog.Android/SplashActivity.cs b/samples/ControlCatalog.Android/SplashActivity.cs index dc292fd37b..908b5f082a 100644 --- a/samples/ControlCatalog.Android/SplashActivity.cs +++ b/samples/ControlCatalog.Android/SplashActivity.cs @@ -1,12 +1,23 @@ using Android.App; using Android.Content; +using Android.Content.PM; using Android.OS; +using Avalonia.Android; namespace ControlCatalog.Android { [Activity(Theme = "@style/MyTheme.Splash", MainLauncher = true, NoHistory = true)] - public class SplashActivity : Activity + public class SplashActivity : AvaloniaSplashActivity { + protected override Avalonia.AppBuilder CustomizeAppBuilder(Avalonia.AppBuilder builder) + { + return base.CustomizeAppBuilder(builder) + .AfterSetup(_ => + { + Pages.EmbedSample.Implementation = new EmbedSampleAndroid(); + }); + } + protected override void OnCreate(Bundle? savedInstanceState) { base.OnCreate(savedInstanceState); diff --git a/samples/ControlCatalog.Web/App.razor b/samples/ControlCatalog.Blazor.Web/App.razor similarity index 100% rename from samples/ControlCatalog.Web/App.razor rename to samples/ControlCatalog.Blazor.Web/App.razor diff --git a/samples/ControlCatalog.Blazor.Web/App.razor.cs b/samples/ControlCatalog.Blazor.Web/App.razor.cs new file mode 100644 index 0000000000..8cc0095f20 --- /dev/null +++ b/samples/ControlCatalog.Blazor.Web/App.razor.cs @@ -0,0 +1,17 @@ +using Avalonia; +using Avalonia.Web.Blazor; + +namespace ControlCatalog.Blazor.Web; + +public partial class App +{ + protected override void OnParametersSet() + { + AppBuilder.Configure() + .UseBlazor() + // .With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering + .SetupWithSingleViewLifetime(); + + base.OnParametersSet(); + } +} diff --git a/samples/ControlCatalog.Blazor.Web/ControlCatalog.Blazor.Web.csproj b/samples/ControlCatalog.Blazor.Web/ControlCatalog.Blazor.Web.csproj new file mode 100644 index 0000000000..03fb31f0d3 --- /dev/null +++ b/samples/ControlCatalog.Blazor.Web/ControlCatalog.Blazor.Web.csproj @@ -0,0 +1,29 @@ + + + net7.0 + browser-wasm + enable + 16777216 + false + false + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog.Web/Pages/Index.razor b/samples/ControlCatalog.Blazor.Web/Pages/Index.razor similarity index 100% rename from samples/ControlCatalog.Web/Pages/Index.razor rename to samples/ControlCatalog.Blazor.Web/Pages/Index.razor diff --git a/samples/ControlCatalog.Blazor.Web/Program.cs b/samples/ControlCatalog.Blazor.Web/Program.cs new file mode 100644 index 0000000000..d71b125fa1 --- /dev/null +++ b/samples/ControlCatalog.Blazor.Web/Program.cs @@ -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"); + + builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); + + return builder; + } +} + + + + diff --git a/samples/ControlCatalog.Web/Properties/launchSettings.json b/samples/ControlCatalog.Blazor.Web/Properties/launchSettings.json similarity index 100% rename from samples/ControlCatalog.Web/Properties/launchSettings.json rename to samples/ControlCatalog.Blazor.Web/Properties/launchSettings.json diff --git a/samples/ControlCatalog.Web/Shared/MainLayout.razor b/samples/ControlCatalog.Blazor.Web/Shared/MainLayout.razor similarity index 100% rename from samples/ControlCatalog.Web/Shared/MainLayout.razor rename to samples/ControlCatalog.Blazor.Web/Shared/MainLayout.razor diff --git a/samples/ControlCatalog.Web/_Imports.razor b/samples/ControlCatalog.Blazor.Web/_Imports.razor similarity index 85% rename from samples/ControlCatalog.Web/_Imports.razor rename to samples/ControlCatalog.Blazor.Web/_Imports.razor index 04c7a8690e..0e6d11b419 100644 --- a/samples/ControlCatalog.Web/_Imports.razor +++ b/samples/ControlCatalog.Blazor.Web/_Imports.razor @@ -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 diff --git a/samples/ControlCatalog.Web/wwwroot/css/app.css b/samples/ControlCatalog.Blazor.Web/wwwroot/css/app.css similarity index 100% rename from samples/ControlCatalog.Web/wwwroot/css/app.css rename to samples/ControlCatalog.Blazor.Web/wwwroot/css/app.css diff --git a/samples/ControlCatalog.Web/wwwroot/favicon.ico b/samples/ControlCatalog.Blazor.Web/wwwroot/favicon.ico similarity index 100% rename from samples/ControlCatalog.Web/wwwroot/favicon.ico rename to samples/ControlCatalog.Blazor.Web/wwwroot/favicon.ico diff --git a/samples/ControlCatalog.Web/wwwroot/index.html b/samples/ControlCatalog.Blazor.Web/wwwroot/index.html similarity index 100% rename from samples/ControlCatalog.Web/wwwroot/index.html rename to samples/ControlCatalog.Blazor.Web/wwwroot/index.html diff --git a/samples/ControlCatalog.Web/App.razor.cs b/samples/ControlCatalog.Web/App.razor.cs deleted file mode 100644 index bcd2a6fefc..0000000000 --- a/samples/ControlCatalog.Web/App.razor.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Avalonia; -using Avalonia.Web.Blazor; - -namespace ControlCatalog.Web; - -public partial class App -{ - protected override void OnParametersSet() - { - WebAppBuilder.Configure() - .AfterSetup(_ => - { - ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb(); - }) - .With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering - .SetupWithSingleViewLifetime(); - - base.OnParametersSet(); - } -} diff --git a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj index b2c9ec72eb..0ddec3444b 100644 --- a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj +++ b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj @@ -1,57 +1,45 @@ - + - net6.0 - enable - - true - 16777216 - false - false + net7.0 + browser-wasm + main.js + Exe + true + true + true + -sVERBOSE -sERROR_ON_UNDEFINED_SYMBOLS=0 - - - false - -O1 - false - - - - true - true + + true + true + full + true + true + true -O3 -O3 - false - false - false - false - false - false - true - false - true - true - true - link - true - - + - + - - - - - + + + + + + + + + + - diff --git a/samples/ControlCatalog.Web/EmbedSample.Browser.cs b/samples/ControlCatalog.Web/EmbedSample.Browser.cs index 5fe14409de..5cfbb608cc 100644 --- a/samples/ControlCatalog.Web/EmbedSample.Browser.cs +++ b/samples/ControlCatalog.Web/EmbedSample.Browser.cs @@ -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 createDefault) { - var runtime = AvaloniaLocator.Current.GetRequiredService(); - if (isSecond) { - var iframe = runtime.Invoke("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("window.createAppButton"); + var defaultHandle = (JSObjectControlHandle)createDefault(); + + _ = JSHost.ImportAsync("embed.js", "./embed.js").ContinueWith(_ => + { + EmbedInterop.AddAppButton(defaultHandle.Object); + }); - return new JSObjectControlHandle(button); + return defaultHandle; } } } + +internal static partial class EmbedInterop +{ + [JSImport("globalThis.document.createElement")] + public static partial JSObject CreateElement(string tagName); + + [JSImport("addAppButton", "embed.js")] + public static partial void AddAppButton(JSObject parentObject); +} diff --git a/samples/ControlCatalog.Web/Logo.svg b/samples/ControlCatalog.Web/Logo.svg new file mode 100644 index 0000000000..9685a23af1 --- /dev/null +++ b/samples/ControlCatalog.Web/Logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/samples/ControlCatalog.Web/Program.cs b/samples/ControlCatalog.Web/Program.cs index d1a7925813..7d05c8e462 100644 --- a/samples/ControlCatalog.Web/Program.cs +++ b/samples/ControlCatalog.Web/Program.cs @@ -1,29 +1,22 @@ -using System; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Components.WebAssembly.Hosting; -using Microsoft.Extensions.DependencyInjection; +using System.Runtime.Versioning; +using Avalonia; +using Avalonia.Web; +using ControlCatalog; using ControlCatalog.Web; -public class Program +[assembly:SupportedOSPlatform("browser")] + +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"); - - builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); - - return builder; - } + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure(); } - - - - diff --git a/samples/ControlCatalog.Web/Roots.xml b/samples/ControlCatalog.Web/Roots.xml new file mode 100644 index 0000000000..3c13098159 --- /dev/null +++ b/samples/ControlCatalog.Web/Roots.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/samples/ControlCatalog.Web/app.css b/samples/ControlCatalog.Web/app.css new file mode 100644 index 0000000000..27680f6e1a --- /dev/null +++ b/samples/ControlCatalog.Web/app.css @@ -0,0 +1,49 @@ +#out { + height: 100vh; + width: 100vw +} + +#avalonia-splash { + 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{ + color: whitesmoke; + text-decoration: none; +} + +.center { + display: flex; + justify-content: center; + height: 250px; +} + +.splash-close { + animation: slide 0.5s linear 1s forwards; +} + +@keyframes slide { + 0% { + top: 0%; + } + + 50% { + opacity: 80%; + } + + 100% { + top: 100%; + overflow: hidden; + opacity: 0; + display: none; + visibility: collapse; + } +} diff --git a/samples/ControlCatalog.Web/embed.js b/samples/ControlCatalog.Web/embed.js new file mode 100644 index 0000000000..f393c80314 --- /dev/null +++ b/samples/ControlCatalog.Web/embed.js @@ -0,0 +1,11 @@ +export function addAppButton(parent) { + var button = globalThis.document.createElement('button'); + button.innerText = 'Hello world'; + var clickCount = 0; + button.onclick = () => { + clickCount++; + button.innerText = 'Click count ' + clickCount; + }; + parent.appendChild(button); + return button; +} diff --git a/samples/ControlCatalog.Web/favicon.ico b/samples/ControlCatalog.Web/favicon.ico new file mode 100644 index 0000000000..da8d49ff9b Binary files /dev/null and b/samples/ControlCatalog.Web/favicon.ico differ diff --git a/samples/ControlCatalog.Web/index.html b/samples/ControlCatalog.Web/index.html new file mode 100644 index 0000000000..226ae70695 --- /dev/null +++ b/samples/ControlCatalog.Web/index.html @@ -0,0 +1,31 @@ + + + + + + + AvaloniaUI - ControlCatalog + + + + + + + + + +
+
+
+

Powered by

+ + Avalonia Logo + Avalonia + +
+
+
+ + + + diff --git a/samples/ControlCatalog.Web/main.js b/samples/ControlCatalog.Web/main.js new file mode 100644 index 0000000000..87f8a4f943 --- /dev/null +++ b/samples/ControlCatalog.Web/main.js @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { dotnet } from './dotnet.js' +import { registerAvaloniaModule } from './avalonia.js'; + +const is_browser = typeof window != "undefined"; +if (!is_browser) throw new Error(`Expected to be running in a browser`); + +const dotnetRuntime = await dotnet + .withDiagnosticTracing(false) + .withApplicationArgumentsFromQuery() + .create(); + +await registerAvaloniaModule(dotnetRuntime); + +const config = dotnetRuntime.getConfig(); + +await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]); diff --git a/samples/ControlCatalog.Web/runtimeconfig.template.json b/samples/ControlCatalog.Web/runtimeconfig.template.json new file mode 100644 index 0000000000..8f0557352c --- /dev/null +++ b/samples/ControlCatalog.Web/runtimeconfig.template.json @@ -0,0 +1,11 @@ +{ + "wasmHostProperties": { + "perHostConfig": [ + { + "name": "browser", + "html-path": "index.html", + "Host": "browser" + } + ] + } +} diff --git a/samples/MobileSandbox.Android/MainActivity.cs b/samples/MobileSandbox.Android/MainActivity.cs index ac9242dd52..d65f0dec92 100644 --- a/samples/MobileSandbox.Android/MainActivity.cs +++ b/samples/MobileSandbox.Android/MainActivity.cs @@ -5,8 +5,8 @@ using Avalonia.Android; namespace MobileSandbox.Android { - [Activity(Label = "MobileSandbox.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleInstance, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] - public class MainActivity : AvaloniaActivity + [Activity(Label = "MobileSandbox.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] + public class MainActivity : AvaloniaMainActivity { } } diff --git a/samples/MobileSandbox.Android/SplashActivity.cs b/samples/MobileSandbox.Android/SplashActivity.cs index c26371d6fe..ced092554d 100644 --- a/samples/MobileSandbox.Android/SplashActivity.cs +++ b/samples/MobileSandbox.Android/SplashActivity.cs @@ -1,11 +1,11 @@ using Android.App; using Android.Content; -using Android.OS; +using Avalonia.Android; namespace MobileSandbox.Android { [Activity(Theme = "@style/MyTheme.Splash", MainLauncher = true, NoHistory = true)] - public class SplashActivity : Activity + public class SplashActivity : AvaloniaSplashActivity { protected override void OnResume() { diff --git a/src/Android/Avalonia.Android/AvaloniaActivity.cs b/src/Android/Avalonia.Android/AvaloniaActivity.cs deleted file mode 100644 index 4ee4bc1375..0000000000 --- a/src/Android/Avalonia.Android/AvaloniaActivity.cs +++ /dev/null @@ -1,105 +0,0 @@ -using Android.OS; -using AndroidX.AppCompat.App; -using Android.Content.Res; -using AndroidX.Lifecycle; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Controls; -using Android.Runtime; -using Android.App; -using Android.Content; -using System; - -namespace Avalonia.Android -{ - public abstract class AvaloniaActivity : AppCompatActivity - { - internal class SingleViewLifetime : ISingleViewApplicationLifetime - { - public AvaloniaView View { get; internal set; } - - public Control MainView - { - get => (Control)View.Content; - set => View.Content = value; - } - } - - internal Action ActivityResult; - internal AvaloniaView View; - internal AvaloniaViewModel _viewModel; - - protected abstract AppBuilder CreateAppBuilder(); - - protected override void OnCreate(Bundle savedInstanceState) - { - var builder = CreateAppBuilder(); - - - var lifetime = new SingleViewLifetime(); - - builder.AfterSetup(x => - { - _viewModel = new ViewModelProvider(this).Get(Java.Lang.Class.FromType(typeof(AvaloniaViewModel))) as AvaloniaViewModel; - - View = new AvaloniaView(this); - if (_viewModel.Content != null) - { - View.Content = _viewModel.Content; - } - - SetContentView(View); - lifetime.View = View; - - View.Prepare(); - }); - - builder.SetupWithLifetime(lifetime); - - base.OnCreate(savedInstanceState); - } - public object Content - { - get - { - return _viewModel.Content; - } - set - { - _viewModel.Content = value; - if (View != null) - View.Content = value; - } - } - - public override void OnConfigurationChanged(Configuration newConfig) - { - base.OnConfigurationChanged(newConfig); - } - - protected override void OnDestroy() - { - View.Content = null; - - base.OnDestroy(); - } - - protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data) - { - base.OnActivityResult(requestCode, resultCode, data); - - ActivityResult?.Invoke(requestCode, resultCode, data); - } - } - - public abstract class AvaloniaActivity : AvaloniaActivity where TApp : Application, new() - { - protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder.UseAndroid(); - - protected override AppBuilder CreateAppBuilder() - { - var builder = AppBuilder.Configure(); - - return CustomizeAppBuilder(builder); - } - } -} diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs new file mode 100644 index 0000000000..705fa3c59d --- /dev/null +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs @@ -0,0 +1,72 @@ +using System; +using Android.App; +using Android.Content; +using Android.Content.Res; +using Android.OS; +using Android.Runtime; +using AndroidX.AppCompat.App; +using AndroidX.Lifecycle; + +namespace Avalonia.Android +{ + public abstract class AvaloniaMainActivity : AppCompatActivity + { + internal static object ViewContent; + + internal Action ActivityResult; + internal AvaloniaView View; + + protected override void OnCreate(Bundle savedInstanceState) + { + View = new AvaloniaView(this); + if (ViewContent != null) + { + View.Content = ViewContent; + } + + View.Prepare(); + + if (Avalonia.Application.Current.ApplicationLifetime is SingleViewLifetime lifetime) + { + lifetime.View = View; + } + + base.OnCreate(savedInstanceState); + + SetContentView(View); + } + + public object Content + { + get + { + return ViewContent; + } + set + { + ViewContent = value; + if (View != null) + View.Content = value; + } + } + + public override void OnConfigurationChanged(Configuration newConfig) + { + base.OnConfigurationChanged(newConfig); + } + + protected override void OnDestroy() + { + View.Content = null; + + base.OnDestroy(); + } + + protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data) + { + base.OnActivityResult(requestCode, resultCode, data); + + ActivityResult?.Invoke(requestCode, resultCode, data); + } + } +} diff --git a/src/Android/Avalonia.Android/AvaloniaSplashActivity.cs b/src/Android/Avalonia.Android/AvaloniaSplashActivity.cs new file mode 100644 index 0000000000..5b5ebd1bd9 --- /dev/null +++ b/src/Android/Avalonia.Android/AvaloniaSplashActivity.cs @@ -0,0 +1,34 @@ +using Android.OS; +using AndroidX.AppCompat.App; +using AndroidX.Lifecycle; + +namespace Avalonia.Android +{ + public abstract class AvaloniaSplashActivity : AppCompatActivity + { + protected abstract AppBuilder CreateAppBuilder(); + + protected override void OnCreate(Bundle? savedInstanceState) + { + base.OnCreate(savedInstanceState); + + var builder = CreateAppBuilder(); + + var lifetime = new SingleViewLifetime(); + + builder.SetupWithLifetime(lifetime); + } + } + + public abstract class AvaloniaSplashActivity : AvaloniaSplashActivity where TApp : Application, new() + { + protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder.UseAndroid(); + + protected override AppBuilder CreateAppBuilder() + { + var builder = AppBuilder.Configure(); + + return CustomizeAppBuilder(builder); + } + } +} diff --git a/src/Android/Avalonia.Android/AvaloniaViewModel.cs b/src/Android/Avalonia.Android/AvaloniaViewModel.cs deleted file mode 100644 index 1b2c00987a..0000000000 --- a/src/Android/Avalonia.Android/AvaloniaViewModel.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Avalonia.Android -{ - internal class AvaloniaViewModel : AndroidX.Lifecycle.ViewModel - { - public object Content { get; set; } - } -} diff --git a/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs b/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs index a9710039f8..e85ed11028 100644 --- a/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs +++ b/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs @@ -1,4 +1,5 @@ -using Avalonia.OpenGL.Egl; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; namespace Avalonia.Android.OpenGL @@ -19,7 +20,8 @@ namespace Avalonia.Android.OpenGL public static GlPlatformSurface TryCreate(IEglWindowGlPlatformSurfaceInfo info) { - if (EglPlatformOpenGlInterface.TryCreate() is EglPlatformOpenGlInterface egl) + var feature = AvaloniaLocator.Current.GetService(); + if (feature is EglPlatformOpenGlInterface egl) { return new GlPlatformSurface(egl, info); } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index f8eaeba897..7ce72aaca5 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -3,20 +3,14 @@ using System.Collections.Generic; using Android.Content; using Android.Graphics; -using Android.Media.TV; -using Android.OS; using Android.Runtime; -using Android.Text; using Android.Views; using Android.Views.InputMethods; -using Android.Widget; using Avalonia.Android.OpenGL; -using Avalonia.Android.Platform.Input; using Avalonia.Android.Platform.Specific; using Avalonia.Android.Platform.Specific.Helpers; using Avalonia.Android.Platform.Storage; using Avalonia.Controls; -using Avalonia.Controls.Documents; using Avalonia.Controls.Platform; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; @@ -29,7 +23,6 @@ using Avalonia.Platform.Storage; using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Java.Lang; -using static System.Net.Mime.MediaTypeNames; namespace Avalonia.Android.Platform.SkiaPlatform { @@ -59,7 +52,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling); NativeControlHost = new AndroidNativeControlHostImpl(avaloniaView); - StorageProvider = new AndroidStorageProvider((AvaloniaActivity)avaloniaView.Context); + StorageProvider = new AndroidStorageProvider((AvaloniaMainActivity)avaloniaView.Context); } public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) => @@ -301,7 +294,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform IsComposing = true; - _inputMethod.Client.SetPreeditText(ComposingText); + _inputMethod.Client?.SetPreeditText(ComposingText); return base.SetComposingText(text, newCursorPosition); } diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs index 653f450ec8..3a1a9e76ea 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs @@ -14,10 +14,10 @@ namespace Avalonia.Android.Platform.Storage; internal class AndroidStorageProvider : IStorageProvider { - private readonly AvaloniaActivity _activity; + private readonly AvaloniaMainActivity _activity; private int _lastRequestCode = 20000; - public AndroidStorageProvider(AvaloniaActivity activity) + public AndroidStorageProvider(AvaloniaMainActivity activity) { _activity = activity; } diff --git a/src/Android/Avalonia.Android/SingleViewLifetime.cs b/src/Android/Avalonia.Android/SingleViewLifetime.cs new file mode 100644 index 0000000000..eef763a932 --- /dev/null +++ b/src/Android/Avalonia.Android/SingleViewLifetime.cs @@ -0,0 +1,26 @@ +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; + +namespace Avalonia.Android +{ + internal class SingleViewLifetime : ISingleViewApplicationLifetime + { + private AvaloniaView _view; + + public AvaloniaView View + { + get => _view; internal set + { + if (_view != null) + { + _view.Content = null; + _view.Dispose(); + } + _view = value; + _view.Content = MainView; + } + } + + public Control MainView { get; set; } + } +} diff --git a/src/Avalonia.Base/Input/Cursor.cs b/src/Avalonia.Base/Input/Cursor.cs index 98c4258a90..8e79206f93 100644 --- a/src/Avalonia.Base/Input/Cursor.cs +++ b/src/Avalonia.Base/Input/Cursor.cs @@ -33,7 +33,7 @@ namespace Avalonia.Input DragLink, None, - // Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/ + // Not available in GTK directly, see https://www.pixelbeat.org/programming/x_cursors/ // We might enable them later, preferably, by loading pixmax directly from theme with fallback image // SizeNorthWestSouthEast, // SizeNorthEastSouthWest, diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs index 98ef6d2530..972a9a1e9d 100644 --- a/src/Avalonia.Base/Logging/LogArea.cs +++ b/src/Avalonia.Base/Logging/LogArea.cs @@ -8,51 +8,66 @@ namespace Avalonia.Logging /// /// The log event comes from the property system. /// - public const string Property = "Property"; + public const string Property = nameof(Property); /// /// The log event comes from the binding system. /// - public const string Binding = "Binding"; + public const string Binding = nameof(Binding); /// /// The log event comes from the animations system. /// - public const string Animations = "Animations"; + public const string Animations = nameof(Animations); /// /// The log event comes from the visual system. /// - public const string Visual = "Visual"; + public const string Visual = nameof(Visual); /// /// The log event comes from the layout system. /// - public const string Layout = "Layout"; + public const string Layout = nameof(Layout); /// /// The log event comes from the control system. /// - public const string Control = "Control"; + public const string Control = nameof(Control); /// - /// The log event comes from Win32Platform. + /// The log event comes from Win32 Platform. /// public const string Win32Platform = nameof(Win32Platform); /// - /// The log event comes from X11Platform. + /// The log event comes from X11 Platform. /// public const string X11Platform = nameof(X11Platform); /// - /// The log event comes from AndroidPlatform. + /// The log event comes from Android Platform. /// public const string AndroidPlatform = nameof(AndroidPlatform); /// - /// The log event comes from IOSPlatform. + /// The log event comes from iOS Platform. /// public const string IOSPlatform = nameof(IOSPlatform); + + /// + /// The log event comes from LinuxFramebuffer Platform + /// + public const string LinuxFramebufferPlatform = nameof(LinuxFramebufferPlatform); + + /// + /// The log event comes from FreeDesktop Platform + /// + public const string FreeDesktopPlatform = nameof(FreeDesktopPlatform); + + /// + /// The log event comes from macOS Platform + /// + public const string macOSPlatform = nameof(macOSPlatform); } } diff --git a/src/Avalonia.Base/Media/PreciseEllipticArcHelper.cs b/src/Avalonia.Base/Media/PreciseEllipticArcHelper.cs index 5dd647e8ca..49dfe3c7b3 100644 --- a/src/Avalonia.Base/Media/PreciseEllipticArcHelper.cs +++ b/src/Avalonia.Base/Media/PreciseEllipticArcHelper.cs @@ -1081,7 +1081,7 @@ namespace Avalonia.Media Point c = rest * (cs) + translation; - // See "http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter" to understand + // See "https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter" to understand // how the ellipse center is calculated diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BinaryReaderExtensions.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BinaryReaderExtensions.cs index 412007c6e0..ccbceb6a7b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BinaryReaderExtensions.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BinaryReaderExtensions.cs @@ -5,7 +5,7 @@ // not use this product except in compliance with the License. You may obtain // a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs index ab17263806..ce9cdde044 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs @@ -104,7 +104,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// Gets the canonical representation of a given codepoint. - /// + /// /// /// The code point to be mapped. /// The mapped canonical code point, or the passed . diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs index 34b14f008f..59c4df0a2e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs @@ -5,7 +5,7 @@ // not use this product except in compliance with the License. You may obtain // a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrie.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrie.cs index cf03ed7cd3..079f830ddc 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrie.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrie.cs @@ -5,7 +5,7 @@ // not use this product except in compliance with the License. You may obtain // a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrieBuilder.Constants.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrieBuilder.Constants.cs index 29ee45acc2..de0304f4c9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrieBuilder.Constants.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrieBuilder.Constants.cs @@ -5,7 +5,7 @@ // not use this product except in compliance with the License. You may obtain // a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrieBuilder.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrieBuilder.cs index 87f96984c5..755d603539 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrieBuilder.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrieBuilder.cs @@ -5,7 +5,7 @@ // not use this product except in compliance with the License. You may obtain // a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs index 46aa6efa72..4aa84e3ec4 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs @@ -18,6 +18,7 @@ public interface IStorageFile : IStorageItem /// /// Opens a stream for read access. /// + /// Task OpenReadAsync(); /// @@ -28,5 +29,6 @@ public interface IStorageFile : IStorageItem /// /// Opens stream for writing to the file. /// + /// Task OpenWriteAsync(); } diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs index c8368e6d7a..d5e01087ef 100644 --- a/src/Avalonia.Base/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Base/Properties/AssemblyInfo.cs @@ -30,4 +30,5 @@ using Avalonia.Metadata; [assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Web.Blazor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +[assembly: InternalsVisibleTo("Avalonia.Web, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs index bd3b86f06d..b8692bb771 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; using System.IO; -using System.Runtime.CompilerServices; using System.Runtime.Serialization; -using System.Runtime.Serialization.Json; using System.Xml.Linq; using System.Linq; diff --git a/src/Avalonia.Base/Utilities/BinarySearchExtension.cs b/src/Avalonia.Base/Utilities/BinarySearchExtension.cs index a4f6ae89c1..b7060d2e21 100644 --- a/src/Avalonia.Base/Utilities/BinarySearchExtension.cs +++ b/src/Avalonia.Base/Utilities/BinarySearchExtension.cs @@ -5,7 +5,7 @@ // not use this product except in compliance with the License. You may obtain // a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 8387c62cad..74debed828 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -9,6 +9,11 @@ NU1605;CS8632 + + + false + + Shared/AvaloniaResourcesIndex.cs @@ -105,7 +110,7 @@ - + diff --git a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs index c20b2f656e..75758d1315 100644 --- a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs +++ b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.Serialization; -using System.Runtime.Serialization.Json; using System.Text; using Avalonia.Markup.Xaml.PortableXaml; using Avalonia.Utilities; diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs new file mode 100644 index 0000000000..5cf5662ede --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs @@ -0,0 +1,359 @@ +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + /// + /// Implements a reduced version of the 2014 Material Design color palette. + /// + /// + /// This palette is based on the one outlined here: + /// + /// https://material.io/design/color/the-color-system.html#tools-for-picking-colors + /// + /// In order to make the palette uniform and rectangular the following + /// alterations were made: + /// + /// 1. The A100-A700 shades of each color are excluded. + /// These shades do not exist for all colors (brown/gray). + /// 2. Black/White are stand-alone and are also excluded. + /// + /// + public class MaterialColorPalette : IColorPalette + { + // See: https://material.io/design/color/the-color-system.html#tools-for-picking-colors + // This is a reduced palette for uniformity + private static Color[,]? _colorChart = null; + private static int _colorChartColorCount = 0; + private static int _colorChartShadeCount = 0; + private static object _colorChartMutex = new object(); + + /// + /// Initializes all color chart colors. + /// + /// + /// This is pulled out separately to lazy load for performance. + /// If no material color palette is ever used, no colors will be created. + /// + private void InitColorChart() + { + lock (_colorChartMutex) + { + _colorChart = new Color[,] + { + // Red + { + Color.FromArgb(0xFF, 0xFF, 0xEB, 0xEE), + Color.FromArgb(0xFF, 0xFF, 0xCD, 0xD2), + Color.FromArgb(0xFF, 0xEF, 0x9A, 0x9A), + Color.FromArgb(0xFF, 0xE5, 0x73, 0x73), + Color.FromArgb(0xFF, 0xEF, 0x53, 0x50), + Color.FromArgb(0xFF, 0xF4, 0x43, 0x36), + Color.FromArgb(0xFF, 0xE5, 0x39, 0x35), + Color.FromArgb(0xFF, 0xD3, 0x2F, 0x2F), + Color.FromArgb(0xFF, 0xC6, 0x28, 0x28), + Color.FromArgb(0xFF, 0xB7, 0x1C, 0x1C), + }, + + // Pink + { + Color.FromArgb(0xFF, 0xFC, 0xE4, 0xEC), + Color.FromArgb(0xFF, 0xF8, 0xBB, 0xD0), + Color.FromArgb(0xFF, 0xF4, 0x8F, 0xB1), + Color.FromArgb(0xFF, 0xF0, 0x62, 0x92), + Color.FromArgb(0xFF, 0xEC, 0x40, 0x7A), + Color.FromArgb(0xFF, 0xE9, 0x1E, 0x63), + Color.FromArgb(0xFF, 0xD8, 0x1B, 0x60), + Color.FromArgb(0xFF, 0xC2, 0x18, 0x5B), + Color.FromArgb(0xFF, 0xAD, 0x14, 0x57), + Color.FromArgb(0xFF, 0x88, 0x0E, 0x4F), + }, + + // Purple + { + Color.FromArgb(0xFF, 0xF3, 0xE5, 0xF5), + Color.FromArgb(0xFF, 0xE1, 0xBE, 0xE7), + Color.FromArgb(0xFF, 0xCE, 0x93, 0xD8), + Color.FromArgb(0xFF, 0xBA, 0x68, 0xC8), + Color.FromArgb(0xFF, 0xAB, 0x47, 0xBC), + Color.FromArgb(0xFF, 0x9C, 0x27, 0xB0), + Color.FromArgb(0xFF, 0x8E, 0x24, 0xAA), + Color.FromArgb(0xFF, 0x7B, 0x1F, 0xA2), + Color.FromArgb(0xFF, 0x6A, 0x1B, 0x9A), + Color.FromArgb(0xFF, 0x4A, 0x14, 0x8C), + }, + + // Deep Purple + { + Color.FromArgb(0xFF, 0xED, 0xE7, 0xF6), + Color.FromArgb(0xFF, 0xD1, 0xC4, 0xE9), + Color.FromArgb(0xFF, 0xB3, 0x9D, 0xDB), + Color.FromArgb(0xFF, 0x95, 0x75, 0xCD), + Color.FromArgb(0xFF, 0x7E, 0x57, 0xC2), + Color.FromArgb(0xFF, 0x67, 0x3A, 0xB7), + Color.FromArgb(0xFF, 0x5E, 0x35, 0xB1), + Color.FromArgb(0xFF, 0x51, 0x2D, 0xA8), + Color.FromArgb(0xFF, 0x45, 0x27, 0xA0), + Color.FromArgb(0xFF, 0x31, 0x1B, 0x92), + }, + + // Indigo + { + Color.FromArgb(0xFF, 0xE8, 0xEA, 0xF6), + Color.FromArgb(0xFF, 0xC5, 0xCA, 0xE9), + Color.FromArgb(0xFF, 0x9F, 0xA8, 0xDA), + Color.FromArgb(0xFF, 0x79, 0x86, 0xCB), + Color.FromArgb(0xFF, 0x5C, 0x6B, 0xC0), + Color.FromArgb(0xFF, 0x3F, 0x51, 0xB5), + Color.FromArgb(0xFF, 0x39, 0x49, 0xAB), + Color.FromArgb(0xFF, 0x30, 0x3F, 0x9F), + Color.FromArgb(0xFF, 0x28, 0x35, 0x93), + Color.FromArgb(0xFF, 0x1A, 0x23, 0x7E), + }, + + // Blue + { + Color.FromArgb(0xFF, 0xE3, 0xF2, 0xFD), + Color.FromArgb(0xFF, 0xBB, 0xDE, 0xFB), + Color.FromArgb(0xFF, 0x90, 0xCA, 0xF9), + Color.FromArgb(0xFF, 0x64, 0xB5, 0xF6), + Color.FromArgb(0xFF, 0x42, 0xA5, 0xF5), + Color.FromArgb(0xFF, 0x21, 0x96, 0xF3), + Color.FromArgb(0xFF, 0x1E, 0x88, 0xE5), + Color.FromArgb(0xFF, 0x19, 0x76, 0xD2), + Color.FromArgb(0xFF, 0x15, 0x65, 0xC0), + Color.FromArgb(0xFF, 0x0D, 0x47, 0xA1), + }, + + // Light Blue + { + Color.FromArgb(0xFF, 0xE1, 0xF5, 0xFE), + Color.FromArgb(0xFF, 0xB3, 0xE5, 0xFC), + Color.FromArgb(0xFF, 0x81, 0xD4, 0xFA), + Color.FromArgb(0xFF, 0x4F, 0xC3, 0xF7), + Color.FromArgb(0xFF, 0x29, 0xB6, 0xF6), + Color.FromArgb(0xFF, 0x03, 0xA9, 0xF4), + Color.FromArgb(0xFF, 0x03, 0x9B, 0xE5), + Color.FromArgb(0xFF, 0x02, 0x88, 0xD1), + Color.FromArgb(0xFF, 0x02, 0x77, 0xBD), + Color.FromArgb(0xFF, 0x01, 0x57, 0x9B), + }, + + // Cyan + { + Color.FromArgb(0xFF, 0xE0, 0xF7, 0xFA), + Color.FromArgb(0xFF, 0xB2, 0xEB, 0xF2), + Color.FromArgb(0xFF, 0x80, 0xDE, 0xEA), + Color.FromArgb(0xFF, 0x4D, 0xD0, 0xE1), + Color.FromArgb(0xFF, 0x26, 0xC6, 0xDA), + Color.FromArgb(0xFF, 0x00, 0xBC, 0xD4), + Color.FromArgb(0xFF, 0x00, 0xAC, 0xC1), + Color.FromArgb(0xFF, 0x00, 0x97, 0xA7), + Color.FromArgb(0xFF, 0x00, 0x83, 0x8F), + Color.FromArgb(0xFF, 0x00, 0x60, 0x64), + }, + + // Teal + { + Color.FromArgb(0xFF, 0xE0, 0xF2, 0xF1), + Color.FromArgb(0xFF, 0xB2, 0xDF, 0xDB), + Color.FromArgb(0xFF, 0x80, 0xCB, 0xC4), + Color.FromArgb(0xFF, 0x4D, 0xB6, 0xAC), + Color.FromArgb(0xFF, 0x26, 0xA6, 0x9A), + Color.FromArgb(0xFF, 0x00, 0x96, 0x88), + Color.FromArgb(0xFF, 0x00, 0x89, 0x7B), + Color.FromArgb(0xFF, 0x00, 0x79, 0x6B), + Color.FromArgb(0xFF, 0x00, 0x69, 0x5C), + Color.FromArgb(0xFF, 0x00, 0x4D, 0x40), + }, + + // Green + { + Color.FromArgb(0xFF, 0xE8, 0xF5, 0xE9), + Color.FromArgb(0xFF, 0xC8, 0xE6, 0xC9), + Color.FromArgb(0xFF, 0xA5, 0xD6, 0xA7), + Color.FromArgb(0xFF, 0x81, 0xC7, 0x84), + Color.FromArgb(0xFF, 0x66, 0xBB, 0x6A), + Color.FromArgb(0xFF, 0x4C, 0xAF, 0x50), + Color.FromArgb(0xFF, 0x43, 0xA0, 0x47), + Color.FromArgb(0xFF, 0x38, 0x8E, 0x3C), + Color.FromArgb(0xFF, 0x2E, 0x7D, 0x32), + Color.FromArgb(0xFF, 0x1B, 0x5E, 0x20), + }, + + // Light Green + { + Color.FromArgb(0xFF, 0xF1, 0xF8, 0xE9), + Color.FromArgb(0xFF, 0xDC, 0xED, 0xC8), + Color.FromArgb(0xFF, 0xC5, 0xE1, 0xA5), + Color.FromArgb(0xFF, 0xAE, 0xD5, 0x81), + Color.FromArgb(0xFF, 0x9C, 0xCC, 0x65), + Color.FromArgb(0xFF, 0x8B, 0xC3, 0x4A), + Color.FromArgb(0xFF, 0x7C, 0xB3, 0x42), + Color.FromArgb(0xFF, 0x68, 0x9F, 0x38), + Color.FromArgb(0xFF, 0x55, 0x8B, 0x2F), + Color.FromArgb(0xFF, 0x33, 0x69, 0x1E), + }, + + // Lime + { + Color.FromArgb(0xFF, 0xF9, 0xFB, 0xE7), + Color.FromArgb(0xFF, 0xF0, 0xF4, 0xC3), + Color.FromArgb(0xFF, 0xE6, 0xEE, 0x9C), + Color.FromArgb(0xFF, 0xDC, 0xE7, 0x75), + Color.FromArgb(0xFF, 0xD4, 0xE1, 0x57), + Color.FromArgb(0xFF, 0xCD, 0xDC, 0x39), + Color.FromArgb(0xFF, 0xC0, 0xCA, 0x33), + Color.FromArgb(0xFF, 0xAF, 0xB4, 0x2B), + Color.FromArgb(0xFF, 0x9E, 0x9D, 0x24), + Color.FromArgb(0xFF, 0x82, 0x77, 0x17), + }, + + // Yellow + { + Color.FromArgb(0xFF, 0xFF, 0xFD, 0xE7), + Color.FromArgb(0xFF, 0xFF, 0xF9, 0xC4), + Color.FromArgb(0xFF, 0xFF, 0xF5, 0x9D), + Color.FromArgb(0xFF, 0xFF, 0xF1, 0x76), + Color.FromArgb(0xFF, 0xFF, 0xEE, 0x58), + Color.FromArgb(0xFF, 0xFF, 0xEB, 0x3B), + Color.FromArgb(0xFF, 0xFD, 0xD8, 0x35), + Color.FromArgb(0xFF, 0xFB, 0xC0, 0x2D), + Color.FromArgb(0xFF, 0xF9, 0xA8, 0x25), + Color.FromArgb(0xFF, 0xF5, 0x7F, 0x17), + }, + + // Amber + { + Color.FromArgb(0xFF, 0xFF, 0xF8, 0xE1), + Color.FromArgb(0xFF, 0xFF, 0xEC, 0xB3), + Color.FromArgb(0xFF, 0xFF, 0xE0, 0x82), + Color.FromArgb(0xFF, 0xFF, 0xD5, 0x4F), + Color.FromArgb(0xFF, 0xFF, 0xCA, 0x28), + Color.FromArgb(0xFF, 0xFF, 0xC1, 0x07), + Color.FromArgb(0xFF, 0xFF, 0xB3, 0x00), + Color.FromArgb(0xFF, 0xFF, 0xA0, 0x00), + Color.FromArgb(0xFF, 0xFF, 0x8F, 0x00), + Color.FromArgb(0xFF, 0xFF, 0x6F, 0x00), + }, + + // Orange + { + Color.FromArgb(0xFF, 0xFF, 0xF3, 0xE0), + Color.FromArgb(0xFF, 0xFF, 0xE0, 0xB2), + Color.FromArgb(0xFF, 0xFF, 0xCC, 0x80), + Color.FromArgb(0xFF, 0xFF, 0xB7, 0x4D), + Color.FromArgb(0xFF, 0xFF, 0xA7, 0x26), + Color.FromArgb(0xFF, 0xFF, 0x98, 0x00), + Color.FromArgb(0xFF, 0xFB, 0x8C, 0x00), + Color.FromArgb(0xFF, 0xF5, 0x7C, 0x00), + Color.FromArgb(0xFF, 0xEF, 0x6C, 0x00), + Color.FromArgb(0xFF, 0xE6, 0x51, 0x00), + }, + + // Deep Orange + { + Color.FromArgb(0xFF, 0xFB, 0xE9, 0xE7), + Color.FromArgb(0xFF, 0xFF, 0xCC, 0xBC), + Color.FromArgb(0xFF, 0xFF, 0xAB, 0x91), + Color.FromArgb(0xFF, 0xFF, 0x8A, 0x65), + Color.FromArgb(0xFF, 0xFF, 0x70, 0x43), + Color.FromArgb(0xFF, 0xFF, 0x57, 0x22), + Color.FromArgb(0xFF, 0xF4, 0x51, 0x1E), + Color.FromArgb(0xFF, 0xE6, 0x4A, 0x19), + Color.FromArgb(0xFF, 0xD8, 0x43, 0x15), + Color.FromArgb(0xFF, 0xBF, 0x36, 0x0C), + }, + + // Brown + { + Color.FromArgb(0xFF, 0xEF, 0xEB, 0xE9), + Color.FromArgb(0xFF, 0xD7, 0xCC, 0xC8), + Color.FromArgb(0xFF, 0xBC, 0xAA, 0xA4), + Color.FromArgb(0xFF, 0xA1, 0x88, 0x7F), + Color.FromArgb(0xFF, 0x8D, 0x6E, 0x63), + Color.FromArgb(0xFF, 0x79, 0x55, 0x48), + Color.FromArgb(0xFF, 0x6D, 0x4C, 0x41), + Color.FromArgb(0xFF, 0x5D, 0x40, 0x37), + Color.FromArgb(0xFF, 0x4E, 0x34, 0x2E), + Color.FromArgb(0xFF, 0x3E, 0x27, 0x23), + }, + + // Gray + { + Color.FromArgb(0xFF, 0xFA, 0xFA, 0xFA), + Color.FromArgb(0xFF, 0xF5, 0xF5, 0xF5), + Color.FromArgb(0xFF, 0xEE, 0xEE, 0xEE), + Color.FromArgb(0xFF, 0xE0, 0xE0, 0xE0), + Color.FromArgb(0xFF, 0xBD, 0xBD, 0xBD), + Color.FromArgb(0xFF, 0x9E, 0x9E, 0x9E), + Color.FromArgb(0xFF, 0x75, 0x75, 0x75), + Color.FromArgb(0xFF, 0x61, 0x61, 0x61), + Color.FromArgb(0xFF, 0x42, 0x42, 0x42), + Color.FromArgb(0xFF, 0x21, 0x21, 0x21), + }, + + // Blue Gray + { + Color.FromArgb(0xFF, 0xEC, 0xEF, 0xF1), + Color.FromArgb(0xFF, 0xCF, 0xD8, 0xDC), + Color.FromArgb(0xFF, 0xB0, 0xBE, 0xC5), + Color.FromArgb(0xFF, 0x90, 0xA4, 0xAE), + Color.FromArgb(0xFF, 0x78, 0x90, 0x9C), + Color.FromArgb(0xFF, 0x60, 0x7D, 0x8B), + Color.FromArgb(0xFF, 0x54, 0x6E, 0x7A), + Color.FromArgb(0xFF, 0x45, 0x5A, 0x64), + Color.FromArgb(0xFF, 0x37, 0x47, 0x4F), + Color.FromArgb(0xFF, 0x26, 0x32, 0x38), + }, + }; + + _colorChartColorCount = _colorChart.GetLength(0); + _colorChartShadeCount = _colorChart.GetLength(1); + } + + return; + } + + /// + public int ColorCount + { + get + { + if (_colorChart == null) + { + InitColorChart(); + } + + return _colorChartColorCount; + } + } + + /// + public int ShadeCount + { + get + { + if (_colorChart == null) + { + InitColorChart(); + } + + return _colorChartShadeCount; + } + } + + /// + public Color GetColor(int colorIndex, int shadeIndex) + { + if (_colorChart == null) + { + InitColorChart(); + } + + return _colorChart![ + MathUtilities.Clamp(colorIndex, 0, _colorChartColorCount - 1), + MathUtilities.Clamp(shadeIndex, 0, _colorChartShadeCount - 1)]; + } + } +} diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index c675139831..8a8c4ead86 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 0de068a416..9c88bae5f6 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs b/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs index 5d883f2d14..a92feec509 100644 --- a/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs +++ b/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Controls/Calendar/CalendarButton.cs b/src/Avalonia.Controls/Calendar/CalendarButton.cs index 0a8e4dfae8..d8672cbf18 100644 --- a/src/Avalonia.Controls/Calendar/CalendarButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarButton.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using Avalonia.Controls.Metadata; diff --git a/src/Avalonia.Controls/Calendar/CalendarDateRange.cs b/src/Avalonia.Controls/Calendar/CalendarDateRange.cs index 88bc5ed7bd..793ef7a2ee 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDateRange.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDateRange.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs index 2ba4e36260..3d0befdba7 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Controls/Calendar/CalendarExtensions.cs b/src/Avalonia.Controls/Calendar/CalendarExtensions.cs index 00b5ce10bc..cb3ee06a9e 100644 --- a/src/Avalonia.Controls/Calendar/CalendarExtensions.cs +++ b/src/Avalonia.Controls/Calendar/CalendarExtensions.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using Avalonia.Input; diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index e2eabb5f28..eec3bdc9f2 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Controls/Calendar/DateTimeHelper.cs b/src/Avalonia.Controls/Calendar/DateTimeHelper.cs index 7a5c74a51b..bfff03a926 100644 --- a/src/Avalonia.Controls/Calendar/DateTimeHelper.cs +++ b/src/Avalonia.Controls/Calendar/DateTimeHelper.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs b/src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs index f4bc2528ba..211b5edb0d 100644 --- a/src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs +++ b/src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using Avalonia.Threading; diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs index 3d592e9ab5..ec1273ca98 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePickerDateValidationErrorEventArgs.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePickerDateValidationErrorEventArgs.cs index 647910cb6b..b58b549030 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePickerDateValidationErrorEventArgs.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePickerDateValidationErrorEventArgs.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePickerFormat.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePickerFormat.cs index 4d96859d75..ffd1f6f594 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePickerFormat.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePickerFormat.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. namespace Avalonia.Controls diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 05be5ad00d..54196bdf1a 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -21,8 +21,11 @@ namespace Avalonia.Controls /// A drop-down list control. /// [TemplatePart("PART_Popup", typeof(Popup))] + [PseudoClasses(pcDropdownOpen, pcPressed)] public class ComboBox : SelectingItemsControl { + public const string pcDropdownOpen = ":dropdownopen"; + public const string pcPressed = ":pressed"; /// /// The default value for the property. /// @@ -95,6 +98,7 @@ namespace Avalonia.Controls SelectedItemProperty.Changed.AddClassHandler((x, e) => x.SelectedItemChanged(e)); KeyDownEvent.AddClassHandler((x, e) => x.OnKeyDown(e), Interactivity.RoutingStrategies.Tunnel); IsTextSearchEnabledProperty.OverrideDefaultValue(true); + IsDropDownOpenProperty.Changed.AddClassHandler((x, e) => x.DropdownChanged(e)); } /// @@ -267,6 +271,20 @@ namespace Avalonia.Controls } } + /// + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + if(!e.Handled && e.Source is IVisual source) + { + if (_popup?.IsInsidePopup(source) == true) + { + return; + } + } + PseudoClasses.Set(pcPressed, true); + } + /// protected override void OnPointerReleased(PointerReleasedEventArgs e) { @@ -286,10 +304,12 @@ namespace Avalonia.Controls e.Handled = true; } } - + PseudoClasses.Set(pcPressed, false); base.OnPointerReleased(e); + } + /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { @@ -470,5 +490,11 @@ namespace Avalonia.Controls MoveSelection(NavigationDirection.Previous, WrapSelection); } } + + private void DropdownChanged(AvaloniaPropertyChangedEventArgs e) + { + bool newValue = e.GetNewValue(); + PseudoClasses.Set(pcDropdownOpen, newValue); + } } } diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index d8de813d47..615eb69fe3 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -35,7 +35,7 @@ DEALINGS IN THE SOFTWARE. The above is the version of the MIT "Expat" License used by X.org: - http://cgit.freedesktop.org/xorg/xserver/tree/COPYING + https://cgit.freedesktop.org/xorg/xserver/tree/COPYING Adjustments for Avalonia needs: diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 964a153c8b..da4e90fb66 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -18,6 +18,7 @@ using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Automation.Peers; using System.Diagnostics; +using Avalonia.Threading; namespace Avalonia.Controls { @@ -159,18 +160,41 @@ namespace Avalonia.Controls (o, v) => o.UndoLimit = v, unsetValue: -1); + /// + /// Defines the event. + /// public static readonly RoutedEvent CopyingToClipboardEvent = RoutedEvent.Register( nameof(CopyingToClipboard), RoutingStrategies.Bubble); + /// + /// Defines the event. + /// public static readonly RoutedEvent CuttingToClipboardEvent = RoutedEvent.Register( nameof(CuttingToClipboard), RoutingStrategies.Bubble); + /// + /// Defines the event. + /// public static readonly RoutedEvent PastingFromClipboardEvent = RoutedEvent.Register( nameof(PastingFromClipboard), RoutingStrategies.Bubble); + /// + /// Defines the event. + /// + public static readonly RoutedEvent TextChangedEvent = + RoutedEvent.Register( + nameof(TextChanged), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent TextChangingEvent = + RoutedEvent.Register( + nameof(TextChanging), RoutingStrategies.Bubble); + readonly struct UndoRedoState : IEquatable { public string? Text { get; } @@ -359,8 +383,8 @@ namespace Avalonia.Controls /// public double LineHeight { - get { return GetValue(LineHeightProperty); } - set { SetValue(LineHeightProperty, value); } + get => GetValue(LineHeightProperty); + set => SetValue(LineHeightProperty, value); } [Content] @@ -376,11 +400,19 @@ namespace Avalonia.Controls CaretIndex = CoerceCaretIndex(caretIndex, value); SelectionStart = CoerceCaretIndex(selectionStart, value); SelectionEnd = CoerceCaretIndex(selectionEnd, value); - if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing) + + var textChanged = SetAndRaise(TextProperty, ref _text, value); + + if (textChanged && IsUndoEnabled && !_isUndoingRedoing) { _undoRedoHelper.Clear(); SnapshotUndoRedo(); // so we always have an initial state } + + if (textChanged) + { + RaiseTextChangeEvents(); + } } } @@ -564,6 +596,27 @@ namespace Avalonia.Controls remove => RemoveHandler(PastingFromClipboardEvent, value); } + /// + /// Occurs asynchronously after text changes and the new text is rendered. + /// + public event EventHandler? TextChanged + { + add => AddHandler(TextChangedEvent, value); + remove => RemoveHandler(TextChangedEvent, value); + } + + /// + /// Occurs synchronously when text starts to change but before it is rendered. + /// + /// + /// This event occurs just after the property value has been updated. + /// + public event EventHandler? TextChanging + { + add => AddHandler(TextChangingEvent, value); + remove => RemoveHandler(TextChangingEvent, value); + } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { _presenter = e.NameScope.Get("PART_TextPresenter"); @@ -1252,7 +1305,7 @@ namespace Avalonia.Controls if (text != null && _wordSelectionStart >= 0) { - var distance = caretIndex - _wordSelectionStart; + var distance = caretIndex - _wordSelectionStart; if (distance <= 0) { @@ -1539,11 +1592,39 @@ namespace Avalonia.Controls return text.Substring(start, end - start); } + /// + /// Raises both the and events. + /// + /// + /// This must be called after the property is set. + /// + private void RaiseTextChangeEvents() + { + // Note the following sequence of these events (following WinUI) + // 1. TextChanging occurs synchronously when text starts to change but before it is rendered. + // This occurs after the Text property is set. + // 2. TextChanged occurs asynchronously after text changes and the new text is rendered. + + var textChangingEventArgs = new TextChangingEventArgs(TextChangingEvent); + RaiseEvent(textChangingEventArgs); + + Dispatcher.UIThread.Post(() => + { + var textChangedEventArgs = new TextChangedEventArgs(TextChangedEvent); + RaiseEvent(textChangedEventArgs); + }, DispatcherPriority.Normal); + } + private void SetTextInternal(string value, bool raiseTextChanged = true) { if (raiseTextChanged) { - SetAndRaise(TextProperty, ref _text, value); + bool textChanged = SetAndRaise(TextProperty, ref _text, value); + + if (textChanged) + { + RaiseTextChangeEvents(); + } } else { diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index d39d964277..5d5ffcc381 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -64,7 +64,7 @@ namespace Avalonia.Controls return new TextInputMethodSurroundingText { - Text = lineText ?? "", + Text = lineText ?? "", AnchorOffset = anchorOffset, CursorOffset = cursorOffset }; diff --git a/src/Avalonia.Controls/TextChangedEventArgs.cs b/src/Avalonia.Controls/TextChangedEventArgs.cs new file mode 100644 index 0000000000..77c609f19b --- /dev/null +++ b/src/Avalonia.Controls/TextChangedEventArgs.cs @@ -0,0 +1,20 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + /// + /// Provides data specific to a TextChanged event. + /// + public class TextChangedEventArgs : RoutedEventArgs + { + public TextChangedEventArgs(RoutedEvent? routedEvent) + : base (routedEvent) + { + } + + public TextChangedEventArgs(RoutedEvent? routedEvent, IInteractive? source) + : base(routedEvent, source) + { + } + } +} diff --git a/src/Avalonia.Controls/TextChangingEventArgs.cs b/src/Avalonia.Controls/TextChangingEventArgs.cs new file mode 100644 index 0000000000..4dedbc927b --- /dev/null +++ b/src/Avalonia.Controls/TextChangingEventArgs.cs @@ -0,0 +1,20 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + /// + /// Provides data specific to a TextChanging event. + /// + public class TextChangingEventArgs : RoutedEventArgs + { + public TextChangingEventArgs(RoutedEvent? routedEvent) + : base (routedEvent) + { + } + + public TextChangingEventArgs(RoutedEvent? routedEvent, IInteractive? source) + : base(routedEvent, source) + { + } + } +} diff --git a/src/Avalonia.Controls/Utils/ISelectionAdapter.cs b/src/Avalonia.Controls/Utils/ISelectionAdapter.cs index c5fb12197f..3ede518ffa 100644 --- a/src/Avalonia.Controls/Utils/ISelectionAdapter.cs +++ b/src/Avalonia.Controls/Utils/ISelectionAdapter.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs index 0288f99dce..3c1b1262ae 100644 --- a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs +++ b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Native/IconLoader.cs b/src/Avalonia.Native/IconLoader.cs index edb8b94e83..04779a43aa 100644 --- a/src/Avalonia.Native/IconLoader.cs +++ b/src/Avalonia.Native/IconLoader.cs @@ -6,7 +6,7 @@ namespace Avalonia.Native // OSX doesn't have a concept of *window* icon. // Icons in the title bar are only shown if there is // an opened file (on disk) associated with the current window - // see http://stackoverflow.com/a/7038671/2231814 + // see https://stackoverflow.com/a/7038671/2231814 class IconLoader : IPlatformIconLoader { class IconStub : IWindowIconImpl diff --git a/src/Avalonia.Themes.Fluent/Assets/Inter-Bold.ttf b/src/Avalonia.Themes.Fluent/Assets/Inter-Bold.ttf index 847ffd191b..a372c5fcca 100644 Binary files a/src/Avalonia.Themes.Fluent/Assets/Inter-Bold.ttf and b/src/Avalonia.Themes.Fluent/Assets/Inter-Bold.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/Inter-ExtraLight.ttf b/src/Avalonia.Themes.Fluent/Assets/Inter-ExtraLight.ttf index 33267cd799..13ef261fff 100644 Binary files a/src/Avalonia.Themes.Fluent/Assets/Inter-ExtraLight.ttf and b/src/Avalonia.Themes.Fluent/Assets/Inter-ExtraLight.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/Inter-Light.ttf b/src/Avalonia.Themes.Fluent/Assets/Inter-Light.ttf index c22eafe9fb..9f75ff2780 100644 Binary files a/src/Avalonia.Themes.Fluent/Assets/Inter-Light.ttf and b/src/Avalonia.Themes.Fluent/Assets/Inter-Light.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/Inter-Medium.ttf b/src/Avalonia.Themes.Fluent/Assets/Inter-Medium.ttf index f782894eea..2c5e25453c 100644 Binary files a/src/Avalonia.Themes.Fluent/Assets/Inter-Medium.ttf and b/src/Avalonia.Themes.Fluent/Assets/Inter-Medium.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/Inter-Regular.ttf b/src/Avalonia.Themes.Fluent/Assets/Inter-Regular.ttf index 3b7e686e54..c28fe4d744 100644 Binary files a/src/Avalonia.Themes.Fluent/Assets/Inter-Regular.ttf and b/src/Avalonia.Themes.Fluent/Assets/Inter-Regular.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/Inter-SemiBold.ttf b/src/Avalonia.Themes.Fluent/Assets/Inter-SemiBold.ttf index 556e972f48..d085eb4994 100644 Binary files a/src/Avalonia.Themes.Fluent/Assets/Inter-SemiBold.ttf and b/src/Avalonia.Themes.Fluent/Assets/Inter-SemiBold.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/Inter-Thin.ttf b/src/Avalonia.Themes.Fluent/Assets/Inter-Thin.ttf index e49058e33d..016a13482b 100644 Binary files a/src/Avalonia.Themes.Fluent/Assets/Inter-Thin.ttf and b/src/Avalonia.Themes.Fluent/Assets/Inter-Thin.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Controls/Calendar.xaml b/src/Avalonia.Themes.Fluent/Controls/Calendar.xaml index 7042f51c71..9c66ea9b84 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Calendar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Calendar.xaml @@ -1,7 +1,7 @@  diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml index d1aee7ee9a..06b6cf30c2 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml @@ -1,7 +1,7 @@  - - + - diff --git a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml index 0cfd491b21..6f1e4fccfe 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml @@ -1,7 +1,7 @@ @@ -153,11 +153,11 @@ - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml b/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml index be664b375d..7500ac7bca 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml @@ -1,7 +1,7 @@ diff --git a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml index fcd661a4b5..cb88eac2ba 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml @@ -1,7 +1,7 @@  @@ -181,11 +181,11 @@ - - diff --git a/src/Avalonia.Themes.Simple/Controls/CalendarItem.xaml b/src/Avalonia.Themes.Simple/Controls/CalendarItem.xaml index d9acd0d25a..2a9ae7cf8d 100644 --- a/src/Avalonia.Themes.Simple/Controls/CalendarItem.xaml +++ b/src/Avalonia.Themes.Simple/Controls/CalendarItem.xaml @@ -1,7 +1,7 @@ diff --git a/src/Avalonia.Themes.Simple/Controls/DatePicker.xaml b/src/Avalonia.Themes.Simple/Controls/DatePicker.xaml index 317a27435a..b4cf6313a6 100644 --- a/src/Avalonia.Themes.Simple/Controls/DatePicker.xaml +++ b/src/Avalonia.Themes.Simple/Controls/DatePicker.xaml @@ -170,7 +170,7 @@ - diff --git a/src/Avalonia.Themes.Simple/Controls/DateTimePickerShared.xaml b/src/Avalonia.Themes.Simple/Controls/DateTimePickerShared.xaml index 5909a1abbf..8639a2baa2 100644 --- a/src/Avalonia.Themes.Simple/Controls/DateTimePickerShared.xaml +++ b/src/Avalonia.Themes.Simple/Controls/DateTimePickerShared.xaml @@ -1,7 +1,7 @@ diff --git a/src/Avalonia.Themes.Simple/Controls/TimePicker.xaml b/src/Avalonia.Themes.Simple/Controls/TimePicker.xaml index 5aa717e9ee..e02e3d8194 100644 --- a/src/Avalonia.Themes.Simple/Controls/TimePicker.xaml +++ b/src/Avalonia.Themes.Simple/Controls/TimePicker.xaml @@ -189,7 +189,7 @@ - diff --git a/src/Avalonia.X11/X11Atoms.cs b/src/Avalonia.X11/X11Atoms.cs index 424db94e0a..b00879bd1d 100644 --- a/src/Avalonia.X11/X11Atoms.cs +++ b/src/Avalonia.X11/X11Atoms.cs @@ -17,7 +17,7 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -// Copyright (c) 2006 Novell, Inc. (http://www.novell.com) +// Copyright (c) 2006 Novell, Inc. (https://www.novell.com) // // diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 869602e452..55a1014188 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,6 +2,7 @@ + Shared\_ModuleInitializer.cs diff --git a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.CompilationTuning.props b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.CompilationTuning.props deleted file mode 100644 index eb5e5dd733..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.CompilationTuning.props +++ /dev/null @@ -1,7 +0,0 @@ - - - 16777216 - false - false - - diff --git a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj index 693a6a1462..1c31e0eb5d 100644 --- a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj +++ b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj @@ -1,53 +1,40 @@  - net6.0 - enable + net7.0 Avalonia.Web.Blazor - preview - false - true + _IncludeGeneratedAvaloniaStaticFiles;$(ResolveStaticWebAssetsInputsDependsOn) - - - - - true - build\ - - - true - build\;buildTransitive\ - + - - - - - - - - - - + - - - - - - - + + + <_AvaloniaWebAssets Include="$(MSBuildThisFileDirectory)../Avalonia.Web/wwwroot/**/*.*" /> + + + + diff --git a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.props b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.props deleted file mode 100644 index dd96a60c6a..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.props +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaBlazorAppBuilder.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaBlazorAppBuilder.cs deleted file mode 100644 index 11d9bcc98f..0000000000 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaBlazorAppBuilder.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Platform; - -namespace Avalonia.Web.Blazor -{ - public class AvaloniaBlazorAppBuilder : AppBuilderBase - { - public AvaloniaBlazorAppBuilder(IRuntimePlatform platform, Action platformServices) - : base(platform, platformServices) - { - } - - public AvaloniaBlazorAppBuilder() - : base(new StandardRuntimePlatform(), - builder => StandardRuntimePlatformServices.Register(builder.ApplicationType!.Assembly)) - { - UseWindowingSubsystem(BlazorWindowingPlatform.Register); - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.cs new file mode 100644 index 0000000000..909e2dd441 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.cs @@ -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; + } + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor deleted file mode 100644 index 5a3b9d5f71..0000000000 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor +++ /dev/null @@ -1,67 +0,0 @@ -
- - - -
- - -
- - diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs deleted file mode 100644 index 5b5951e800..0000000000 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ /dev/null @@ -1,501 +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? AdditionalAttributes { get; set; } - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - { - AvaloniaLocator.CurrentMutable.Bind().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(); - _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) - { - Console.WriteLine("create glcontext"); - _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); - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs b/src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs index 7970f09a58..f38779f834 100644 --- a/src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs +++ b/src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs @@ -1,31 +1,39 @@ -using Avalonia.Controls; +using System.Runtime.Versioning; + +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Media; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web.Blazor; + +[SupportedOSPlatform("browser")] +public static class WebAppBuilder { - public class BlazorSingleViewLifetime : ISingleViewApplicationLifetime + public static T SetupWithSingleViewLifetime( + this T builder) + where T : AppBuilderBase, new() { - public Control? MainView { get; set; } + return builder.SetupWithLifetime(new BlazorSingleViewLifetime()); } - public static class WebAppBuilder + public static T UseBlazor(this T builder) where T : AppBuilderBase, new() { - public static T SetupWithSingleViewLifetime( - this T builder) - where T : AppBuilderBase, new() - { - return builder.SetupWithLifetime(new BlazorSingleViewLifetime()); - } + return builder + .UseBrowser() + .With(new BrowserPlatformOptions + { + FrameworkAssetPathResolver = new(filePath => $"/_content/Avalonia.Web.Blazor/{filePath}") + }); + } - public static AvaloniaBlazorAppBuilder Configure() - where TApp : Application, new() - { - var builder = AvaloniaBlazorAppBuilder.Configure() - .UseSkia() - .With(new SkiaOptions { CustomGpuFactory = () => new BlazorSkiaGpu() }); + public static AppBuilder Configure() + where TApp : Application, new() + { + return AppBuilder.Configure() + .UseBlazor(); + } - return builder; - } + internal class BlazorSingleViewLifetime : ISingleViewApplicationLifetime + { + public Control? MainView { get; set; } } } diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderSession.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderSession.cs deleted file mode 100644 index 0c53825131..0000000000 --- a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderSession.cs +++ /dev/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; } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs deleted file mode 100644 index fa6a39f210..0000000000 --- a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs +++ /dev/null @@ -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; - } -} diff --git a/src/Web/Avalonia.Web.Blazor/ClipboardImpl.cs b/src/Web/Avalonia.Web.Blazor/ClipboardImpl.cs deleted file mode 100644 index bafc07ca15..0000000000 --- a/src/Web/Avalonia.Web.Blazor/ClipboardImpl.cs +++ /dev/null @@ -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 GetTextAsync() - { - return await AvaloniaLocator.Current.GetRequiredService(). - InvokeAsync("navigator.clipboard.readText"); - } - - public async Task SetTextAsync(string text) - { - await AvaloniaLocator.Current.GetRequiredService(). - InvokeAsync("navigator.clipboard.writeText",text); - } - - public async Task ClearAsync() => await SetTextAsync(""); - - public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; - - public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); - - public Task GetDataAsync(string format) => Task.FromResult(new()); - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/ActionHelper.cs b/src/Web/Avalonia.Web.Blazor/Interop/ActionHelper.cs deleted file mode 100644 index 8bb266a942..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/ActionHelper.cs +++ /dev/null @@ -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 - { - private readonly Action action; - - public ActionHelper(Action action) - { - this.action = action; - } - - [JSInvokable] - public void Invoke(T param1) => action?.Invoke(param1); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - public class ActionHelper - { - private readonly Action action; - - public ActionHelper(Action action) - { - this.action = action; - } - - [JSInvokable] - public void Invoke(T1 p1, T2 p2) => action?.Invoke(p1, p2); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - public class ActionHelper - { - private readonly Action action; - - public ActionHelper(Action 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 - { - private readonly Action action; - - public ActionHelper(Action action) - { - this.action = action; - } - - [JSInvokable] - public void Invoke(T1 p1, T2 p2, T3 p3, T4 p4) => action?.Invoke(p1, p2, p3, p4); - } - - - - - -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs b/src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs deleted file mode 100644 index ff13e95aa7..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs +++ /dev/null @@ -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 ImportAsync(IJSRuntime js) - { - var interop = new AvaloniaModule(js); - await interop.ImportAsync(); - return interop; - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs deleted file mode 100644 index c86c72f29c..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs +++ /dev/null @@ -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? callbacksEvent; - private readonly ActionHelper _callbackHelper; - private readonly AvaloniaModule _module; - - private DotNetObjectReference>? callbackReference; - - public DpiWatcherInterop(AvaloniaModule module, Action? callback = null) - { - _module = module; - _callbackHelper = new ActionHelper((o, n) => callbacksEvent?.Invoke(n)); - - if (callback != null) - Subscribe(callback); - } - - public void Dispose() => Stop(); - - public void Subscribe(Action callback) - { - var shouldStart = callbacksEvent == null; - - callbacksEvent += callback; - - var dpi = shouldStart - ? Start() - : GetDpi(); - - callback(dpi); - } - - public void Unsubscribe(Action callback) - { - callbacksEvent -= callback; - - if (callbacksEvent == null) - Stop(); - } - - private double Start() - { - if (callbackReference != null) - return GetDpi(); - - callbackReference = DotNetObjectReference.Create(_callbackHelper); - - return _module.Invoke(StartSymbol, callbackReference); - } - - private void Stop() - { - if (callbackReference == null) - return; - - _module.Invoke(StopSymbol); - - callbackReference?.Dispose(); - callbackReference = null; - } - - public double GetDpi() => _module.Invoke(GetDpiSymbol); - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/FocusHelperInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/FocusHelperInterop.cs deleted file mode 100644 index 090909f98f..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/FocusHelperInterop.cs +++ /dev/null @@ -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); -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs deleted file mode 100644 index 8872339f91..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs +++ /dev/null @@ -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 _compositionAction; - private readonly ActionHelper _inputAction; - - private DotNetObjectReference>? compositionActionReference; - private DotNetObjectReference>? inputActionReference; - - public InputHelperInterop(AvaloniaModule module, ElementReference inputElement) - { - _module = module; - _inputElement = inputElement; - - _compositionAction = new ActionHelper(OnCompositionEvent); - _inputAction = new ActionHelper(OnInputEvent); - - Start(); - } - - public event EventHandler? CompositionEvent; - public event EventHandler? 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); - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs deleted file mode 100644 index dca1b53650..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Microsoft.JSInterop; - -namespace Avalonia.Web.Blazor.Interop -{ - internal class JSModuleInterop : IDisposable - { - private readonly Task 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("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(string identifier, params object?[]? args) => - Module.Invoke(identifier, args); - - internal ValueTask InvokeAsync(string identifier, params object?[]? args) => - Module.InvokeVoidAsync(identifier, args); - - internal ValueTask InvokeAsync(string identifier, params object?[]? args) => - Module.InvokeAsync(identifier, args); - - protected virtual void OnDisposingModule() { } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs deleted file mode 100644 index cf9350fb62..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs +++ /dev/null @@ -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? 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(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(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); - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs deleted file mode 100644 index e21ae837d0..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs +++ /dev/null @@ -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 _callbackHelper; - - private DotNetObjectReference>? callbackReference; - - public SizeWatcherInterop(AvaloniaModule module, ElementReference element, Action callback) - { - _module = module; - _htmlElement = element; - _htmlElementId = element.Id; - _callbackHelper = new ActionHelper((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; - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs deleted file mode 100644 index 690d9683df..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs +++ /dev/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> 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 ImportAsync(IJSRuntime js) - { - var interop = new StorageProviderInterop(js); - await interop.ImportAsync(); - return interop; - } - - public StorageProviderInterop(IJSRuntime js) - : base(js, JsFilename) - { - } - - public bool CanOpen => Invoke("StorageProvider.canOpen"); - public bool CanSave => Invoke("StorageProvider.canSave"); - public bool CanPickFolder => Invoke("StorageProvider.canPickFolder"); - - public async Task> OpenFilePickerAsync(FilePickerOpenOptions options) - { - try - { - var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; - - var (types, exludeAll) = ConvertFileTypes(options.FileTypeFilter); - var items = await InvokeAsync("StorageProvider.openFileDialog", startIn, options.AllowMultiple, types, exludeAll); - var count = items.Invoke("count"); - - return Enumerable.Range(0, count) - .Select(index => new JSStorageFile(items.Invoke("at", index))) - .ToArray(); - } - catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) - { - return Array.Empty(); - } - } - - public async Task SaveFilePickerAsync(FilePickerSaveOptions options) - { - try - { - var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; - - var (types, exludeAll) = ConvertFileTypes(options.FileTypeChoices); - var item = await InvokeAsync("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> OpenFolderPickerAsync(FolderPickerOpenOptions options) - { - try - { - var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; - - var item = await InvokeAsync("StorageProvider.selectFolderDialog", startIn); - - return item is not null ? new[] { new JSStorageFolder(item) } : Array.Empty(); - } - catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) - { - return Array.Empty(); - } - } - - public async Task OpenFileBookmarkAsync(string bookmark) - { - var item = await InvokeAsync("StorageProvider.openBookmark", bookmark); - return item is not null ? new JSStorageFile(item) : null; - } - - public async Task OpenFolderBookmarkAsync(string bookmark) - { - var item = await InvokeAsync("StorageProvider.openBookmark", bookmark); - return item is not null ? new JSStorageFolder(item) : null; - } - - private static (FilePickerAcceptType[]? types, bool excludeAllOption) ConvertFileTypes(IEnumerable? 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)Array.Empty()))) - .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("getName"); - - public bool TryGetUri([NotNullWhen(true)] out Uri? uri) - { - uri = new Uri(Name, UriKind.Relative); - return false; - } - - public async Task GetBasicPropertiesAsync() - { - var properties = await FileHandle.InvokeAsync("getProperties"); - - return new StorageItemProperties( - properties?.Size, - dateCreated: null, - dateModified: properties?.LastModified > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(properties.LastModified) : null); - } - - public bool CanBookmark => true; - - public Task SaveBookmarkAsync() - { - return FileHandle.InvokeAsync("saveBookmark").AsTask(); - } - - public Task GetParentAsync() - { - return Task.FromResult(null); - } - - public Task ReleaseBookmarkAsync() - { - return FileHandle.InvokeAsync("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 OpenReadAsync() - { - var stream = await FileHandle.InvokeAsync("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 OpenWriteAsync() - { - var properties = await FileHandle.InvokeAsync("getProperties"); - var streamWriter = await FileHandle.InvokeAsync("openWrite"); - - return new JSWriteableStream(streamWriter, (long)(properties?.Size ?? 0)); - } - } - - internal class JSStorageFolder : JSStorageItem, IStorageBookmarkFolder - { - public JSStorageFolder(IJSInProcessObjectReference fileHandle) : base(fileHandle) - { - } - - public async Task> GetItemsAsync() - { - var items = await FileHandle.InvokeAsync("getItems"); - if (items is null) - { - return Array.Empty(); - } - - var count = items.Invoke("count"); - - return Enumerable.Range(0, count) - .Select(index => - { - var reference = items.Invoke("at", index); - return reference.Invoke("getKind") switch - { - "directory" => (IStorageItem)new JSStorageFolder(reference), - "file" => new JSStorageFile(reference), - _ => null - }; - }) - .Where(i => i is not null) - .ToArray()!; - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Storage/WriteableStream.cs b/src/Web/Avalonia.Web.Blazor/Interop/Storage/WriteableStream.cs deleted file mode 100644 index 55a7831b1a..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/Storage/WriteableStream.cs +++ /dev/null @@ -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 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(); - } - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/JSObjectControlHandle.cs b/src/Web/Avalonia.Web.Blazor/JSObjectControlHandle.cs deleted file mode 100644 index 4426c3fbd7..0000000000 --- a/src/Web/Avalonia.Web.Blazor/JSObjectControlHandle.cs +++ /dev/null @@ -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(); - } - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/_Imports.razor b/src/Web/Avalonia.Web.Blazor/_Imports.razor deleted file mode 100644 index 77285129da..0000000000 --- a/src/Web/Avalonia.Web.Blazor/_Imports.razor +++ /dev/null @@ -1 +0,0 @@ -@using Microsoft.AspNetCore.Components.Web diff --git a/src/Web/Avalonia.Web.Blazor/webapp/build.js b/src/Web/Avalonia.Web.Blazor/webapp/build.js deleted file mode 100644 index 9d585dcade..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/build.js +++ /dev/null @@ -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)); \ No newline at end of file diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia.ts deleted file mode 100644 index 369f628a44..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia.ts +++ /dev/null @@ -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" diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/DpiWatcher.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/DpiWatcher.ts deleted file mode 100644 index 06235782f8..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/DpiWatcher.ts +++ /dev/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); - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/FocusHelper.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/FocusHelper.ts deleted file mode 100644 index 96ffee3d53..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/FocusHelper.ts +++ /dev/null @@ -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; - } -} diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts deleted file mode 100644 index 1ea85d84e3..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts +++ /dev/null @@ -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); - } -} - - - diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/NativeControlHost.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/NativeControlHost.ts deleted file mode 100644 index 9e5c3843c8..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/NativeControlHost.ts +++ /dev/null @@ -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; - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SKHtmlCanvas.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SKHtmlCanvas.ts deleted file mode 100644 index e934f74807..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SKHtmlCanvas.ts +++ /dev/null @@ -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; - - 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(); - 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; - } -} diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SizeWatcher.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SizeWatcher.ts deleted file mode 100644 index 715b252988..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SizeWatcher.ts +++ /dev/null @@ -1,67 +0,0 @@ -type SizeWatcherElement = { - SizeWatcher: SizeWatcherInstance; -} & HTMLElement - -type SizeWatcherInstance = { - callback: DotNet.DotNetObject; -} - -export class SizeWatcher { - static observer: ResizeObserver; - static elements: Map; - - 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(); - 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); - } -} diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage.ts deleted file mode 100644 index 2c4654e81b..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage.ts +++ /dev/null @@ -1 +0,0 @@ -export { StorageProvider } from "./Storage/StorageProvider" diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts deleted file mode 100644 index 896e174e43..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - 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 { - // 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 key; - } - finally { - connection.close(); - } - } - - public async deleteBookmark(): Promise { - 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 { - - // '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 { - - 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 { - - 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 { - const connection = await avaloniaDb.connect(); - try { - const handle = await connection.get(fileBookmarksStore, key); - return handle && new StorageItem(handle, key); - } - finally { - connection.close(); - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/webapp/package.json b/src/Web/Avalonia.Web.Blazor/webapp/package.json deleted file mode 100644 index 27e15b0abd..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/package.json +++ /dev/null @@ -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" - } -} diff --git a/src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json b/src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json deleted file mode 100644 index 4e90bc230d..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json +++ /dev/null @@ -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" - ] -} diff --git a/src/Web/Avalonia.Web.Blazor/webapp/types/dotnet/index.d.ts b/src/Web/Avalonia.Web.Blazor/webapp/types/dotnet/index.d.ts deleted file mode 100644 index 932dfa1e1f..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/types/dotnet/index.d.ts +++ /dev/null @@ -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) -// 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(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(assemblyName: string, methodIdentifier: string, ...args: any[]): Promise; - /** - * 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(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(methodIdentifier: string, ...args: any[]): Promise; - } -} diff --git a/src/Web/Avalonia.Web/Avalonia.Web.csproj b/src/Web/Avalonia.Web/Avalonia.Web.csproj new file mode 100644 index 0000000000..cdfa095865 --- /dev/null +++ b/src/Web/Avalonia.Web/Avalonia.Web.csproj @@ -0,0 +1,55 @@ + + + net7.0 + preview + enable + true + + + + + + + + + + + + + + + + + + + true + build\ + + + true + build\;buildTransitive\ + + + true + build/interop.js;buildTransitive/interop.js + + + true + build\wwwroot;buildTransitive\wwwroot + + + + + + + + + + + + + + + + + diff --git a/src/Web/Avalonia.Web/Avalonia.Web.props b/src/Web/Avalonia.Web/Avalonia.Web.props new file mode 100644 index 0000000000..6c975cd284 --- /dev/null +++ b/src/Web/Avalonia.Web/Avalonia.Web.props @@ -0,0 +1,5 @@ + + + $(EmccExtraLDFlags) --js-library="$(MSBuildThisFileDirectory)\interop.js" + + diff --git a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.targets b/src/Web/Avalonia.Web/Avalonia.Web.targets similarity index 55% rename from src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.targets rename to src/Web/Avalonia.Web/Avalonia.Web.targets index e9052fda88..d1bec2aa93 100644 --- a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.targets +++ b/src/Web/Avalonia.Web/Avalonia.Web.targets @@ -1,6 +1,7 @@ - - + + + diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs new file mode 100644 index 0000000000..3a31679424 --- /dev/null +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -0,0 +1,447 @@ +using System; +using System.Runtime.InteropServices.JavaScript; +using Avalonia.Controls; +using Avalonia.Controls.Embedding; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; +using Avalonia.Rendering.Composition; +using Avalonia.Threading; +using Avalonia.Web.Interop; +using Avalonia.Web.Skia; + +using SkiaSharp; + +namespace Avalonia.Web +{ + [System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings + public partial class AvaloniaView : ITextInputMethodImpl + { + private readonly BrowserTopLevelImpl _topLevelImpl; + private EmbeddableControlRoot _topLevel; + + private readonly JSObject _containerElement; + private readonly JSObject _canvas; + private readonly JSObject _nativeControlsContainer; + private readonly JSObject _inputElement; + private readonly JSObject? _splash; + + private GLInfo? _jsGlInfo = null; + private double _dpi = 1; + private Size _canvasSize = new(100.0, 100.0); + + private GRContext? _context; + private GRGlInterface? _glInterface; + private const SKColorType ColorType = SKColorType.Rgba8888; + + private bool _useGL; + private ITextInputMethodClient? _client; + + public AvaloniaView(string divId) + : this(DomHelper.GetElementById(divId) ?? 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) + { + throw new InvalidOperationException("Avalonia WASM host wasn't initialized."); + } + + _containerElement = hostContent.GetPropertyAsJSObject("host") + ?? throw new InvalidOperationException("Host cannot be null"); + _canvas = hostContent.GetPropertyAsJSObject("canvas") + ?? throw new InvalidOperationException("Canvas cannot be null"); + _nativeControlsContainer = hostContent.GetPropertyAsJSObject("nativeHost") + ?? throw new InvalidOperationException("NativeHost cannot be null"); + _inputElement = hostContent.GetPropertyAsJSObject("inputElement") + ?? throw new InvalidOperationException("InputElement cannot be null"); + + _splash = DomHelper.GetElementById("avalonia-splash"); + + _topLevelImpl = new BrowserTopLevelImpl(this); + + _topLevel = new WebEmbeddableControlRoot(_topLevelImpl, () => + { + Dispatcher.UIThread.Post(() => + { + if (_splash != null) + { + DomHelper.AddCssClass(_splash, "splash-close"); + } + }); + }); + + _topLevelImpl.SetCssCursor = (cursor) => + { + InputHelper.SetCursor(_containerElement, cursor); // macOS + InputHelper.SetCursor(_canvas, cursor); // windows + }; + + _topLevel.Prepare(); + + _topLevel.Renderer.Start(); + + InputHelper.SubscribeKeyEvents( + _containerElement, + OnKeyDown, + OnKeyUp); + + InputHelper.SubscribeTextEvents( + _inputElement, + OnTextInput, + OnCompositionStart, + OnCompositionUpdate, + OnCompositionEnd); + + InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp, OnWheel); + + var skiaOptions = AvaloniaLocator.Current.GetService(); + + _dpi = DomHelper.ObserveDpi(OnDpiChanged); + + _useGL = skiaOptions?.CustomGpuFactory != null; + + if (_useGL) + { + _jsGlInfo = CanvasHelper.InitialiseGL(_canvas, OnRenderFrame); + // create the SkiaSharp context + if (_context == null) + { + _glInterface = GRGlInterface.Create(); + _context = GRContext.CreateGl(_glInterface); + + // bump the default resource cache limit + _context.SetResourceCacheLimit(skiaOptions?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024); + } + + _topLevelImpl.Surfaces = new[] { new BrowserSkiaSurface(_context, _jsGlInfo, ColorType, new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, GRSurfaceOrigin.BottomLeft) }; + } + else + { + //var rasterInitialized = _interop.InitRaster(); + //Console.WriteLine("raster initialized: {0}", rasterInitialized); + + //_topLevelImpl.SetSurface(ColorType, + // new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData); + } + + CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + + _topLevelImpl.SetClientSize(_canvasSize, _dpi); + + DomHelper.ObserveSize(host, null, OnSizeChanged); + + CanvasHelper.RequestAnimationFrame(_canvas, true); + } + + private static RawPointerPoint ExtractRawPointerFromJSArgs(JSObject args) + { + var point = new RawPointerPoint + { + Position = new Point(args.GetPropertyAsDouble("offsetX"), args.GetPropertyAsDouble("offsetY")), + Pressure = (float)args.GetPropertyAsDouble("pressure"), + XTilt = (float)args.GetPropertyAsDouble("tiltX"), + YTilt = (float)args.GetPropertyAsDouble("tiltY"), + Twist = (float)args.GetPropertyAsDouble("twist") + }; + + return point; + } + + private bool OnPointerMove(JSObject args) + { + var type = args.GetPropertyAsString("pointertype"); + + var point = ExtractRawPointerFromJSArgs(args); + + return _topLevelImpl.RawPointerEvent(RawPointerEventType.Move, type!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); + } + + private bool OnPointerDown(JSObject args) + { + var pointerType = args.GetPropertyAsString("pointerType"); + + var type = pointerType switch + { + "touch" => RawPointerEventType.TouchBegin, + _ => args.GetPropertyAsInt32("button") switch + { + 0 => RawPointerEventType.LeftButtonDown, + 1 => RawPointerEventType.MiddleButtonDown, + 2 => RawPointerEventType.RightButtonDown, + 3 => RawPointerEventType.XButton1Down, + 4 => RawPointerEventType.XButton2Down, + // 5 => Pen eraser button, + _ => RawPointerEventType.Move + } + }; + + var point = ExtractRawPointerFromJSArgs(args); + + return _topLevelImpl.RawPointerEvent(type, pointerType!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); + } + + private bool OnPointerUp(JSObject args) + { + var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse"; + + var type = pointerType switch + { + "touch" => RawPointerEventType.TouchEnd, + _ => args.GetPropertyAsInt32("button") switch + { + 0 => RawPointerEventType.LeftButtonUp, + 1 => RawPointerEventType.MiddleButtonUp, + 2 => RawPointerEventType.RightButtonUp, + 3 => RawPointerEventType.XButton1Up, + 4 => RawPointerEventType.XButton2Up, + // 5 => Pen eraser button, + _ => RawPointerEventType.Move + } + }; + + var point = ExtractRawPointerFromJSArgs(args); + + return _topLevelImpl.RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); + } + + private bool OnWheel(JSObject args) + { + return _topLevelImpl.RawMouseWheelEvent(new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), + new Vector(-(args.GetPropertyAsDouble("deltaX") / 50), -(args.GetPropertyAsDouble("deltaY") / 50)), GetModifiers(args)); + } + + private static RawInputModifiers GetModifiers(JSObject e) + { + var modifiers = RawInputModifiers.None; + + if (e.GetPropertyAsBoolean("ctrlKey")) + modifiers |= RawInputModifiers.Control; + if (e.GetPropertyAsBoolean("altKey")) + modifiers |= RawInputModifiers.Alt; + if (e.GetPropertyAsBoolean("shiftKey")) + modifiers |= RawInputModifiers.Shift; + if (e.GetPropertyAsBoolean("metaKey")) + modifiers |= RawInputModifiers.Meta; + + var buttons = e.GetPropertyAsInt32("buttons"); + if ((buttons & 1L) == 1) + modifiers |= RawInputModifiers.LeftMouseButton; + + if ((buttons & 2L) == 2) + modifiers |= e.GetPropertyAsString("type") == "pen" ? RawInputModifiers.PenBarrelButton : RawInputModifiers.RightMouseButton; + + if ((buttons & 4L) == 4) + modifiers |= RawInputModifiers.MiddleMouseButton; + + if ((buttons & 8L) == 8) + modifiers |= RawInputModifiers.XButton1MouseButton; + + if ((buttons & 16L) == 16) + modifiers |= RawInputModifiers.XButton2MouseButton; + + if ((buttons & 32L) == 32) + modifiers |= RawInputModifiers.PenEraser; + + return modifiers; + } + + private bool OnKeyDown (string code, string key, int modifier) + { + return _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier); + } + + private bool OnKeyUp(string code, string key, int modifier) + { + return _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)modifier); + } + + private bool OnTextInput (string type, string? data) + { + if(data == null || IsComposing) + { + return false; + } + + return _topLevelImpl.RawTextEvent(data); + } + + private bool OnCompositionStart (JSObject args) + { + if (_client == null) + return false; + + _client.SetPreeditText(null); + IsComposing = true; + + return false; + } + + private bool OnCompositionUpdate(JSObject args) + { + if (_client == null) + return false; + + _client.SetPreeditText(args.GetPropertyAsString("data")); + + return false; + } + + private bool OnCompositionEnd(JSObject args) + { + if (_client == null) + return false; + + IsComposing = false; + _client.SetPreeditText(null); + _topLevelImpl.RawTextEvent(args.GetPropertyAsString("data")!); + + return false; + } + + private void OnRenderFrame() + { + if (_useGL && (_jsGlInfo == null)) + { + Console.WriteLine("nothing to render"); + return; + } + if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0) + { + Console.WriteLine("nothing to render"); + return; + } + + ManualTriggerRenderTimer.Instance.RaiseTick(); + } + + public Control? Content + { + get => (Control)_topLevel.Content!; + set => _topLevel.Content = value; + } + + public bool IsComposing { get; private set; } + + internal INativeControlHostImpl GetNativeControlHostImpl() + { + return new BrowserNativeControlHost(_nativeControlsContainer); + } + + private void ForceBlit() + { + // Note: this is technically a hack, but it's a kinda unique use case when + // we want to blit the previous frame + // renderer doesn't have much control over the render target + // we render on the UI thread + // We also don't want to have it as a meaningful public API. + // Therefore we have InternalsVisibleTo hack here. + + if (_topLevel.Renderer is CompositingRenderer dr) + { + dr.CompositionTarget.ImmediateUIThreadRender(); + } + } + + private void OnDpiChanged(double oldDpi, double newDpi) + { + if (Math.Abs(_dpi - newDpi) > 0.0001) + { + _dpi = newDpi; + + CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + + _topLevelImpl.SetClientSize(_canvasSize, _dpi); + + ForceBlit(); + } + } + + private void OnSizeChanged(int height, int width) + { + var newSize = new Size(height, width); + + if (_canvasSize != newSize) + { + _canvasSize = newSize; + + CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + + _topLevelImpl.SetClientSize(_canvasSize, _dpi); + + ForceBlit(); + } + } + + private void HideIme() + { + InputHelper.HideElement(_inputElement); + InputHelper.FocusElement(_containerElement); + } + + void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) + { + Console.WriteLine("Set Client"); + if (_client != null) + { + _client.SurroundingTextChanged -= SurroundingTextChanged; + } + + if (client != null) + { + client.SurroundingTextChanged += SurroundingTextChanged; + } + + InputHelper.ClearInputElement(_inputElement); + + _client = client; + + if (_client != null) + { + InputHelper.ShowElement(_inputElement); + InputHelper.FocusElement(_inputElement); + + var surroundingText = _client.SurroundingText; + + InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset); + + Console.WriteLine("Shown, focused and surrounded."); + } + else + { + HideIme(); + } + } + + private void SurroundingTextChanged(object? sender, EventArgs e) + { + if (_client != null) + { + var surroundingText = _client.SurroundingText; + + InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset); + } + } + + 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); + } + + void ITextInputMethodImpl.SetOptions(TextInputOptions options) + { + } + + void ITextInputMethodImpl.Reset() + { + InputHelper.ClearInputElement(_inputElement); + InputHelper.SetSurroundingText(_inputElement, "", 0, 0); + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs b/src/Web/Avalonia.Web/BrowserNativeControlHost.cs similarity index 59% rename from src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs rename to src/Web/Avalonia.Web/BrowserNativeControlHost.cs index b824fcae46..4cdcf627e6 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs +++ b/src/Web/Avalonia.Web/BrowserNativeControlHost.cs @@ -1,32 +1,25 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices.JavaScript; using Avalonia.Controls.Platform; using Avalonia.Platform; +using Avalonia.Web.Interop; -using Microsoft.AspNetCore.Components; -using Microsoft.JSInterop; - -namespace Avalonia.Web.Blazor.Interop +namespace Avalonia.Web { - - internal class NativeControlHostInterop : INativeControlHostImpl + internal class BrowserNativeControlHost : 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; + private readonly JSObject _hostElement; - public NativeControlHostInterop(AvaloniaModule module, ElementReference element) + public BrowserNativeControlHost(JSObject element) { - _module = module; _hostElement = element; } public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent) { - var element = _module.Invoke(CreateDefaultChildSymbol); + var element = NativeControlHostHelper.CreateDefaultChild(null); return new JSObjectControlHandle(element); } @@ -35,9 +28,8 @@ namespace Avalonia.Web.Blazor.Interop Attachment? a = null; try { - using var hostElementJsReference = _module.Invoke(GetReferenceSymbol, _hostElement); - var child = create(new JSObjectControlHandle(hostElementJsReference)); - var attachmenetReference = _module.Invoke(CreateAttachmentSymbol); + var child = create(new JSObjectControlHandle(_hostElement)); + var attachmenetReference = NativeControlHostHelper.CreateAttachment(); // It has to be assigned to the variable before property setter is called so we dispose it on exception #pragma warning disable IDE0017 // Simplify object initialization a = new Attachment(attachmenetReference, child); @@ -54,7 +46,7 @@ namespace Avalonia.Web.Blazor.Interop public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle) { - var attachmenetReference = _module.Invoke(CreateAttachmentSymbol); + var attachmenetReference = NativeControlHostHelper.CreateAttachment(); var a = new Attachment(attachmenetReference, handle); a.AttachedTo = this; return a; @@ -62,7 +54,7 @@ namespace Avalonia.Web.Blazor.Interop public bool IsCompatibleWith(IPlatformHandle handle) => handle is JSObjectControlHandle; - class Attachment : INativeControlHostControlTopLevelAttachment + private class Attachment : INativeControlHostControlTopLevelAttachment { private const string InitializeWithChildHandleSymbol = "InitializeWithChildHandle"; private const string AttachToSymbol = "AttachTo"; @@ -70,20 +62,20 @@ namespace Avalonia.Web.Blazor.Interop private const string HideWithSizeSymbol = "HideWithSize"; private const string ReleaseChildSymbol = "ReleaseChild"; - private IJSInProcessObjectReference? _native; - private NativeControlHostInterop? _attachedTo; + private JSObject? _native; + private BrowserNativeControlHost? _attachedTo; - public Attachment(IJSInProcessObjectReference native, IPlatformHandle handle) + public Attachment(JSObject native, IPlatformHandle handle) { _native = native; - _native.InvokeVoid(InitializeWithChildHandleSymbol, ((JSObjectControlHandle)handle).Object); + NativeControlHostHelper.InitializeWithChildHandle(_native, ((JSObjectControlHandle)handle).Object); } public void Dispose() { if (_native != null) { - _native.InvokeVoid(ReleaseChildSymbol); + NativeControlHostHelper.ReleaseChild(_native); _native.Dispose(); _native = null; } @@ -96,20 +88,20 @@ namespace Avalonia.Web.Blazor.Interop { CheckDisposed(); - var host = (NativeControlHostInterop?)value; + var host = (BrowserNativeControlHost?)value; if (host == null) { - _native.InvokeVoid(AttachToSymbol); + NativeControlHostHelper.AttachTo(_native, null); } else { - _native.InvokeVoid(AttachToSymbol, host._hostElement); + NativeControlHostHelper.AttachTo(_native, host._hostElement); } _attachedTo = host; } } - public bool IsCompatibleWith(INativeControlHostImpl host) => host is NativeControlHostInterop; + public bool IsCompatibleWith(INativeControlHostImpl host) => host is BrowserNativeControlHost; public void HideWithSize(Size size) { @@ -117,7 +109,7 @@ namespace Avalonia.Web.Blazor.Interop if (_attachedTo == null) return; - _native.InvokeVoid(HideWithSizeSymbol, Math.Max(1, (float)size.Width), Math.Max(1, (float)size.Height)); + NativeControlHostHelper.HideWithSize(_native, Math.Max(1, size.Width), Math.Max(1, size.Height)); } public void ShowInBounds(Rect bounds) @@ -130,7 +122,7 @@ namespace Avalonia.Web.Blazor.Interop 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); + NativeControlHostHelper.ShowInBounds(_native, bounds.X, bounds.Y, bounds.Width, bounds.Height); } [MemberNotNull(nameof(_native))] diff --git a/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs b/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs new file mode 100644 index 0000000000..00ed961fbe --- /dev/null +++ b/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs @@ -0,0 +1,54 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Web.Skia; +using System.Runtime.Versioning; + +namespace Avalonia.Web; + +[SupportedOSPlatform("browser")] +public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime +{ + public AvaloniaView? View; + + public Control? MainView + { + get => View!.Content; + set => View!.Content = value; + } +} + +public class BrowserPlatformOptions +{ + public Func FrameworkAssetPathResolver { get; set; } = new(fileName => $"./{fileName}"); +} + + +[SupportedOSPlatform("browser")] +public static class WebAppBuilder +{ + public static T SetupBrowserApp( + this T builder, string mainDivId) + where T : AppBuilderBase, new() + { + var lifetime = new BrowserSingleViewLifetime(); + + return builder + .UseBrowser() + .AfterSetup(b => + { + lifetime.View = new AvaloniaView(mainDivId); + }) + .SetupWithLifetime(lifetime); + } + + public static T UseBrowser( + this T builder) + where T : AppBuilderBase, new() + { + return builder + .UseWindowingSubsystem(BrowserWindowingPlatform.Register) + .UseSkia() + .With(new SkiaOptions { CustomGpuFactory = () => new BrowserSkiaGpu() }); + } +} diff --git a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs similarity index 72% rename from src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs rename to src/Web/Avalonia.Web/BrowserTopLevelImpl.cs index 3a09c16932..b955da6df2 100644 --- a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs @@ -1,4 +1,7 @@ +using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Input; @@ -8,17 +11,15 @@ using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Rendering; using Avalonia.Rendering.Composition; -using Avalonia.Web.Blazor.Interop; -using SkiaSharp; +using Avalonia.Web.Skia; +using Avalonia.Web.Storage; -#nullable enable - -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { - internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider + [System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings + internal class BrowserTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider { private Size _clientSize; - private IBlazorSkiaSurface? _currentSurface; private IInputRoot? _inputRoot; private readonly Stopwatch _sw = Stopwatch.StartNew(); private readonly AvaloniaView _avaloniaView; @@ -26,38 +27,26 @@ namespace Avalonia.Web.Blazor private readonly PenDevice _penDevice; private string _currentCursor = CssCursor.Default; - public RazorViewTopLevelImpl(AvaloniaView avaloniaView) + public BrowserTopLevelImpl(AvaloniaView avaloniaView) { + Surfaces = Enumerable.Empty(); _avaloniaView = avaloniaView; TransparencyLevel = WindowTransparencyLevel.None; AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1); _touchDevice = new TouchDevice(); _penDevice = new PenDevice(); + NativeControlHost = _avaloniaView.GetNativeControlHostImpl(); } public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds; - - 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 blitCallback) + public void SetClientSize(Size newSize, double dpi) { - _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 { }) + if (Surfaces.FirstOrDefault() is BrowserSkiaSurface surface) { - _currentSurface.Scaling = dpi; + surface.Scaling = dpi; } ScalingChanged?.Invoke(dpi); @@ -67,16 +56,16 @@ namespace Avalonia.Web.Blazor { _clientSize = newSize; - if (_currentSurface is { }) + if (Surfaces.FirstOrDefault() is BrowserSkiaSurface surface) { - _currentSurface.Size = new PixelSize((int)size.Width, (int)size.Height); + surface.Size = new PixelSize((int)newSize.Width, (int)newSize.Height); } Resized?.Invoke(newSize, PlatformResizeReason.User); } } - public void RawPointerEvent( + public bool RawPointerEvent( RawPointerEventType eventType, string pointerType, RawPointerPoint p, RawInputModifiers modifiers, long touchPointId) { @@ -92,7 +81,11 @@ namespace Avalonia.Web.Blazor }; input.Invoke(args); + + return args.Handled; } + + return false; } private IPointerDevice GetPointerDevice(string pointerType) @@ -105,12 +98,18 @@ namespace Avalonia.Web.Blazor }; } - public void RawMouseWheelEvent(Point p, Vector v, RawInputModifiers modifiers) + public bool RawMouseWheelEvent(Point p, Vector v, RawInputModifiers modifiers) { if (_inputRoot is { }) { - Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, _inputRoot, p, v, modifiers)); + var args = new RawMouseWheelEventArgs(MouseDevice, Timestamp, _inputRoot, p, v, modifiers); + + Input?.Invoke(args); + + return args.Handled; } + + return false; } public bool RawKeyboardEvent(RawKeyEventType type, string code, string key, RawInputModifiers modifiers) @@ -120,9 +119,9 @@ namespace Avalonia.Web.Blazor if (_inputRoot is { }) { var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers); - + Input?.Invoke(args); - + return args.Handled; } } @@ -131,7 +130,7 @@ namespace Avalonia.Web.Blazor if (_inputRoot is { }) { var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers); - + Input?.Invoke(args); return args.Handled; @@ -141,12 +140,17 @@ namespace Avalonia.Web.Blazor return false; } - public void RawTextEvent(string text) + public bool RawTextEvent(string text) { if (_inputRoot is { }) { - Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice, Timestamp, _inputRoot, text)); + var args = new RawTextInputEventArgs(KeyboardDevice, Timestamp, _inputRoot, text); + Input?.Invoke(args); + + return args.Handled; } + + return false; } public void Dispose() @@ -196,9 +200,9 @@ namespace Avalonia.Web.Blazor public Size ClientSize => _clientSize; public Size? FrameSize => null; - public double RenderScaling => _currentSurface?.Scaling ?? 1; + public double RenderScaling => (Surfaces.FirstOrDefault() as BrowserSkiaSurface)?.Scaling ?? 1; - public IEnumerable Surfaces => new object[] { _currentSurface! }; + public IEnumerable Surfaces { get; set; } public Action? SetCssCursor { get; set; } public Action? Input { get; set; } @@ -210,13 +214,13 @@ namespace Avalonia.Web.Blazor public Action? LostFocus { get; set; } public IMouseDevice MouseDevice { get; } = new MouseDevice(); - public IKeyboardDevice KeyboardDevice { get; } = BlazorWindowingPlatform.Keyboard; + public IKeyboardDevice KeyboardDevice { get; } = BrowserWindowingPlatform.Keyboard; public WindowTransparencyLevel TransparencyLevel { get; } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } public ITextInputMethodImpl TextInputMethod => _avaloniaView; - public INativeControlHostImpl? NativeControlHost => _avaloniaView.GetNativeControlHostImpl(); - public IStorageProvider StorageProvider => _avaloniaView.GetStorageProvider(); + public INativeControlHostImpl? NativeControlHost { get; } + public IStorageProvider StorageProvider { get; } = new BrowserStorageProvider(); } } diff --git a/src/Web/Avalonia.Web/ClipboardImpl.cs b/src/Web/Avalonia.Web/ClipboardImpl.cs new file mode 100644 index 0000000000..793099f55a --- /dev/null +++ b/src/Web/Avalonia.Web/ClipboardImpl.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Web.Interop; + +namespace Avalonia.Web +{ + internal class ClipboardImpl : IClipboard + { + public Task GetTextAsync() + { + return InputHelper.ReadClipboardTextAsync(); + } + + public Task SetTextAsync(string text) + { + return InputHelper.WriteClipboardTextAsync(text); + } + + public async Task ClearAsync() => await SetTextAsync(""); + + public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; + + public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); + + public Task GetDataAsync(string format) => Task.FromResult(new()); + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Cursor.cs b/src/Web/Avalonia.Web/Cursor.cs similarity index 97% rename from src/Web/Avalonia.Web.Blazor/Cursor.cs rename to src/Web/Avalonia.Web/Cursor.cs index d921b2fa6c..af7098f800 100644 --- a/src/Web/Avalonia.Web.Blazor/Cursor.cs +++ b/src/Web/Avalonia.Web/Cursor.cs @@ -1,9 +1,11 @@ +using System; +using System.IO; using Avalonia.Input; using Avalonia.Platform; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { - public class CssCursor : ICursorImpl + internal class CssCursor : ICursorImpl { public const string Default = "default"; public string? Value { get; set; } @@ -78,7 +80,7 @@ namespace Avalonia.Web.Blazor { 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); diff --git a/src/Web/Avalonia.Web/Interop/AvaloniaModule.cs b/src/Web/Avalonia.Web/Interop/AvaloniaModule.cs new file mode 100644 index 0000000000..176b8d60fc --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/AvaloniaModule.cs @@ -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() ?? new BrowserPlatformOptions(); + return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver("avalonia.js")); + } + + public static Task ImportStorage() + { + var options = AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions(); + return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver("storage.js")); + } +} diff --git a/src/Web/Avalonia.Web/Interop/CanvasHelper.cs b/src/Web/Avalonia.Web/Interop/CanvasHelper.cs new file mode 100644 index 0000000000..efa94916fa --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/CanvasHelper.cs @@ -0,0 +1,43 @@ +using System; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.JavaScript; + +namespace Avalonia.Web.Interop; + +internal record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth); + +[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings +internal static partial class CanvasHelper +{ + + [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] + static extern JSObject InterceptGLObject(); + + public static GLInfo InitialiseGL(JSObject canvas, Action renderFrameCallback) + { + InterceptGLObject(); + + var info = InitGL(canvas, canvas.GetPropertyAsString("id")!, renderFrameCallback); + + var glInfo = new GLInfo( + info.GetPropertyAsInt32("context"), + (uint)info.GetPropertyAsInt32("fboId"), + info.GetPropertyAsInt32("stencil"), + info.GetPropertyAsInt32("sample"), + info.GetPropertyAsInt32("depth")); + + return glInfo; + } + + [JSImport("Canvas.requestAnimationFrame", AvaloniaModule.MainModuleName)] + public static partial void RequestAnimationFrame(JSObject canvas, bool renderLoop); + + [JSImport("Canvas.setCanvasSize", AvaloniaModule.MainModuleName)] + public static partial void SetCanvasSize(JSObject canvas, int height, int width); + + [JSImport("Canvas.initGL", AvaloniaModule.MainModuleName)] + private static partial JSObject InitGL( + JSObject canvas, + string canvasId, + [JSMarshalAs] Action renderFrameCallback); +} diff --git a/src/Web/Avalonia.Web/Interop/DomHelper.cs b/src/Web/Avalonia.Web/Interop/DomHelper.cs new file mode 100644 index 0000000000..80f146a57a --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/DomHelper.cs @@ -0,0 +1,28 @@ +using System; +using System.Runtime.InteropServices.JavaScript; + +namespace Avalonia.Web.Interop; + +internal static partial class DomHelper +{ + [JSImport("globalThis.document.getElementById")] + internal static partial JSObject? GetElementById(string id); + + [JSImport("AvaloniaDOM.createAvaloniaHost", AvaloniaModule.MainModuleName)] + public static partial JSObject CreateAvaloniaHost(JSObject element); + + [JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)] + public static partial void AddCssClass(JSObject element, string className); + + [JSImport("SizeWatcher.observe", AvaloniaModule.MainModuleName)] + public static partial JSObject ObserveSize( + JSObject canvas, + string? canvasId, + [JSMarshalAs>] + Action onSizeChanged); + + [JSImport("DpiWatcher.start", AvaloniaModule.MainModuleName)] + public static partial double ObserveDpi( + [JSMarshalAs>] + Action onDpiChanged); +} diff --git a/src/Web/Avalonia.Web/Interop/InputHelper.cs b/src/Web/Avalonia.Web/Interop/InputHelper.cs new file mode 100644 index 0000000000..cfec9f30dc --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/InputHelper.cs @@ -0,0 +1,78 @@ +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; + +namespace Avalonia.Web.Interop; + +internal static partial class InputHelper +{ + [JSImport("InputHelper.subscribeKeyEvents", AvaloniaModule.MainModuleName)] + public static partial void SubscribeKeyEvents( + JSObject htmlElement, + [JSMarshalAs>] + Func keyDown, + [JSMarshalAs>] + Func keyUp); + + [JSImport("InputHelper.subscribeTextEvents", AvaloniaModule.MainModuleName)] + public static partial void SubscribeTextEvents( + JSObject htmlElement, + [JSMarshalAs>] + Func onInput, + [JSMarshalAs>] + Func onCompositionStart, + [JSMarshalAs>] + Func onCompositionUpdate, + [JSMarshalAs>] + Func onCompositionEnd); + + [JSImport("InputHelper.subscribePointerEvents", AvaloniaModule.MainModuleName)] + public static partial void SubscribePointerEvents( + JSObject htmlElement, + [JSMarshalAs>] + Func pointerMove, + [JSMarshalAs>] + Func pointerDown, + [JSMarshalAs>] + Func pointerUp, + [JSMarshalAs>] + Func wheel); + + + [JSImport("InputHelper.subscribeInputEvents", AvaloniaModule.MainModuleName)] + public static partial void SubscribeInputEvents( + JSObject htmlElement, + [JSMarshalAs>] + Func input); + + + [JSImport("InputHelper.clearInput", AvaloniaModule.MainModuleName)] + public static partial void ClearInputElement(JSObject htmlElement); + + [JSImport("InputHelper.isInputElement", AvaloniaModule.MainModuleName)] + public static partial void IsInputElement(JSObject htmlElement); + + [JSImport("InputHelper.focusElement", AvaloniaModule.MainModuleName)] + public static partial void FocusElement(JSObject htmlElement); + + [JSImport("InputHelper.setCursor", AvaloniaModule.MainModuleName)] + public static partial void SetCursor(JSObject htmlElement, string kind); + + [JSImport("InputHelper.hide", AvaloniaModule.MainModuleName)] + public static partial void HideElement(JSObject htmlElement); + + [JSImport("InputHelper.show", AvaloniaModule.MainModuleName)] + public static partial void ShowElement(JSObject htmlElement); + + [JSImport("InputHelper.setSurroundingText", AvaloniaModule.MainModuleName)] + public static partial void SetSurroundingText(JSObject htmlElement, string text, int start, int end); + + [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")] + public static partial Task ReadClipboardTextAsync(); + + [JSImport("globalThis.navigator.clipboard.writeText")] + public static partial Task WriteClipboardTextAsync(string text); +} diff --git a/src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs b/src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs new file mode 100644 index 0000000000..d3baaa2533 --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs @@ -0,0 +1,28 @@ +using System; +using System.Runtime.InteropServices.JavaScript; + +namespace Avalonia.Web.Interop; + +internal static partial class NativeControlHostHelper +{ + [JSImport("NativeControlHost.createDefaultChild", AvaloniaModule.MainModuleName)] + internal static partial JSObject CreateDefaultChild(JSObject? parent); + + [JSImport("NativeControlHost.createAttachment", AvaloniaModule.MainModuleName)] + internal static partial JSObject CreateAttachment(); + + [JSImport("NativeControlHost.initializeWithChildHandle", AvaloniaModule.MainModuleName)] + internal static partial void InitializeWithChildHandle(JSObject element, JSObject child); + + [JSImport("NativeControlHost.attachTo", AvaloniaModule.MainModuleName)] + internal static partial void AttachTo(JSObject element, JSObject? host); + + [JSImport("NativeControlHost.showInBounds", AvaloniaModule.MainModuleName)] + internal static partial void ShowInBounds(JSObject element, double x, double y, double width, double height); + + [JSImport("NativeControlHost.hideWithSize", AvaloniaModule.MainModuleName)] + internal static partial void HideWithSize(JSObject element, double width, double height); + + [JSImport("NativeControlHost.releaseChild", AvaloniaModule.MainModuleName)] + internal static partial void ReleaseChild(JSObject element); +} diff --git a/src/Web/Avalonia.Web/Interop/StorageHelper.cs b/src/Web/Avalonia.Web/Interop/StorageHelper.cs new file mode 100644 index 0000000000..9a6cfb9fc2 --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/StorageHelper.cs @@ -0,0 +1,55 @@ +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; + +namespace Avalonia.Web.Interop; + +internal static partial class StorageHelper +{ + [JSImport("Caniuse.canShowOpenFilePicker", AvaloniaModule.MainModuleName)] + public static partial bool CanShowOpenFilePicker(); + + [JSImport("Caniuse.canShowSaveFilePicker", AvaloniaModule.MainModuleName)] + public static partial bool CanShowSaveFilePicker(); + + [JSImport("Caniuse.canShowDirectoryPicker", AvaloniaModule.MainModuleName)] + public static partial bool CanShowDirectoryPicker(); + + [JSImport("StorageProvider.selectFolderDialog", AvaloniaModule.StorageModuleName)] + public static partial Task SelectFolderDialog(JSObject? startIn); + + [JSImport("StorageProvider.openFileDialog", AvaloniaModule.StorageModuleName)] + public static partial Task OpenFileDialog(JSObject? startIn, bool multiple, + [JSMarshalAs>] object[]? types, bool excludeAcceptAllOption); + + [JSImport("StorageProvider.saveFileDialog", AvaloniaModule.StorageModuleName)] + public static partial Task SaveFileDialog(JSObject? startIn, string? suggestedName, + [JSMarshalAs>] object[]? types, bool excludeAcceptAllOption); + + [JSImport("StorageProvider.openBookmark", AvaloniaModule.StorageModuleName)] + public static partial Task OpenBookmark(string key); + + [JSImport("StorageItem.saveBookmark", AvaloniaModule.StorageModuleName)] + public static partial Task SaveBookmark(JSObject item); + + [JSImport("StorageItem.deleteBookmark", AvaloniaModule.StorageModuleName)] + public static partial Task DeleteBookmark(JSObject item); + + [JSImport("StorageItem.getProperties", AvaloniaModule.StorageModuleName)] + public static partial Task GetProperties(JSObject item); + + [JSImport("StorageItem.openWrite", AvaloniaModule.StorageModuleName)] + public static partial Task OpenWrite(JSObject item); + + [JSImport("StorageItem.openRead", AvaloniaModule.StorageModuleName)] + public static partial Task OpenRead(JSObject item); + + [JSImport("StorageItem.getItems", AvaloniaModule.StorageModuleName)] + [return: JSMarshalAs>] + public static partial Task GetItems(JSObject item); + + [JSImport("StorageItems.itemsArray", AvaloniaModule.StorageModuleName)] + public static partial JSObject[] ItemsArray(JSObject item); + + [JSImport("StorageProvider.createAcceptType", AvaloniaModule.StorageModuleName)] + public static partial JSObject CreateAcceptType(string description, string[] mimeTypes); +} diff --git a/src/Web/Avalonia.Web/Interop/StreamHelper.cs b/src/Web/Avalonia.Web/Interop/StreamHelper.cs new file mode 100644 index 0000000000..d9de7bcbd8 --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/StreamHelper.cs @@ -0,0 +1,40 @@ +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; + +namespace Avalonia.Web.Interop; + +/// +/// Set of FileSystemWritableFileStream and Blob methods. +/// +internal static partial class StreamHelper +{ + [JSImport("StreamHelper.seek", AvaloniaModule.MainModuleName)] + public static partial void Seek(JSObject stream, [JSMarshalAs] long position); + + [JSImport("StreamHelper.truncate", AvaloniaModule.MainModuleName)] + public static partial void Truncate(JSObject stream, [JSMarshalAs] long size); + + [JSImport("StreamHelper.write", AvaloniaModule.MainModuleName)] + public static partial Task WriteAsync(JSObject stream, [JSMarshalAs] ArraySegment data); + + [JSImport("StreamHelper.close", AvaloniaModule.MainModuleName)] + public static partial Task CloseAsync(JSObject stream); + + [JSImport("StreamHelper.byteLength", AvaloniaModule.MainModuleName)] + [return: JSMarshalAs] + public static partial long ByteLength(JSObject stream); + + [JSImport("StreamHelper.sliceArrayBuffer", AvaloniaModule.MainModuleName)] + private static partial Task SliceToArrayBuffer(JSObject stream, [JSMarshalAs] long offset, int count); + + [JSImport("StreamHelper.toMemoryView", AvaloniaModule.MainModuleName)] + [return: JSMarshalAs>] + private static partial byte[] ArrayBufferToMemoryView(JSObject stream); + + public static async Task SliceAsync(JSObject stream, long offset, int count) + { + using var buffer = await SliceToArrayBuffer(stream, offset, count); + return ArrayBufferToMemoryView(buffer); + } +} diff --git a/src/Web/Avalonia.Web/JSObjectControlHandle.cs b/src/Web/Avalonia.Web/JSObjectControlHandle.cs new file mode 100644 index 0000000000..e56ca123eb --- /dev/null +++ b/src/Web/Avalonia.Web/JSObjectControlHandle.cs @@ -0,0 +1,30 @@ +using System; +using System.Runtime.InteropServices.JavaScript; + +using Avalonia.Controls.Platform; + +namespace Avalonia.Web; + +public class JSObjectControlHandle : INativeControlHostDestroyableControlHandle +{ + internal const string ElementReferenceDescriptor = "JSObject"; + + public JSObjectControlHandle(JSObject reference) + { + Object = reference; + } + + public JSObject Object { get; } + + public IntPtr Handle => throw new NotSupportedException(); + + public string? HandleDescriptor => ElementReferenceDescriptor; + + public void Destroy() + { + if (Object is JSObject inProcess && !inProcess.IsDisposed) + { + inProcess.Dispose(); + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Keycodes.cs b/src/Web/Avalonia.Web/Keycodes.cs similarity index 98% rename from src/Web/Avalonia.Web.Blazor/Keycodes.cs rename to src/Web/Avalonia.Web/Keycodes.cs index ea30f0a9f0..d1185f6e45 100644 --- a/src/Web/Avalonia.Web.Blazor/Keycodes.cs +++ b/src/Web/Avalonia.Web/Keycodes.cs @@ -1,6 +1,8 @@ +using System.Collections.Generic; + using Avalonia.Input; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { internal static class Keycodes { diff --git a/src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs b/src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs similarity index 79% rename from src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs rename to src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs index 7b9feab2e3..3309a6dd9f 100644 --- a/src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs +++ b/src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs @@ -1,9 +1,10 @@ +using System; using System.Diagnostics; using Avalonia.Rendering; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { - public class ManualTriggerRenderTimer : IRenderTimer + internal class ManualTriggerRenderTimer : IRenderTimer { private static readonly Stopwatch s_sw = Stopwatch.StartNew(); diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpu.cs b/src/Web/Avalonia.Web/Skia/BrowserSkiaGpu.cs similarity index 62% rename from src/Web/Avalonia.Web.Blazor/BlazorSkiaGpu.cs rename to src/Web/Avalonia.Web/Skia/BrowserSkiaGpu.cs index 6fa7bf0bde..f80838232b 100644 --- a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpu.cs +++ b/src/Web/Avalonia.Web/Skia/BrowserSkiaGpu.cs @@ -1,16 +1,17 @@ +using System.Collections.Generic; using Avalonia.Skia; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web.Skia { - public class BlazorSkiaGpu : ISkiaGpu + public class BrowserSkiaGpu : ISkiaGpu { public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable surfaces) { foreach (var surface in surfaces) { - if (surface is BlazorSkiaSurface blazorSkiaSurface) + if (surface is BrowserSkiaSurface browserSkiaSurface) { - return new BlazorSkiaGpuRenderTarget(blazorSkiaSurface); + return new BrowserSkiaGpuRenderTarget(browserSkiaSurface); } } diff --git a/src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderSession.cs b/src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderSession.cs new file mode 100644 index 0000000000..a7f7d9db3d --- /dev/null +++ b/src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderSession.cs @@ -0,0 +1,36 @@ +using Avalonia.Skia; +using SkiaSharp; + +namespace Avalonia.Web.Skia +{ + internal class BrowserSkiaGpuRenderSession : ISkiaGpuRenderSession + { + private readonly SKSurface _surface; + + public BrowserSkiaGpuRenderSession(BrowserSkiaSurface browserSkiaSurface, GRBackendRenderTarget renderTarget) + { + _surface = SKSurface.Create(browserSkiaSurface.Context, renderTarget, browserSkiaSurface.Origin, browserSkiaSurface.ColorType); + + GrContext = browserSkiaSurface.Context; + + ScaleFactor = browserSkiaSurface.Scaling; + + SurfaceOrigin = browserSkiaSurface.Origin; + } + + public void Dispose() + { + _surface.Flush(); + + _surface.Dispose(); + } + + public GRContext GrContext { get; } + + public SKSurface SkSurface => _surface; + + public double ScaleFactor { get; } + + public GRSurfaceOrigin SurfaceOrigin { get; } + } +} diff --git a/src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderTarget.cs b/src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderTarget.cs new file mode 100644 index 0000000000..dba9b34166 --- /dev/null +++ b/src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderTarget.cs @@ -0,0 +1,39 @@ +using Avalonia.Skia; +using SkiaSharp; + +namespace Avalonia.Web.Skia +{ + internal class BrowserSkiaGpuRenderTarget : ISkiaGpuRenderTarget + { + private readonly GRBackendRenderTarget _renderTarget; + private readonly BrowserSkiaSurface _browserSkiaSurface; + private readonly PixelSize _size; + + public BrowserSkiaGpuRenderTarget(BrowserSkiaSurface browserSkiaSurface) + { + _size = browserSkiaSurface.Size; + + var glFbInfo = new GRGlFramebufferInfo(browserSkiaSurface.GlInfo.FboId, browserSkiaSurface.ColorType.ToGlSizedFormat()); + { + _browserSkiaSurface = browserSkiaSurface; + _renderTarget = new GRBackendRenderTarget( + (int)(browserSkiaSurface.Size.Width * browserSkiaSurface.Scaling), + (int)(browserSkiaSurface.Size.Height * browserSkiaSurface.Scaling), + browserSkiaSurface.GlInfo.Samples, + browserSkiaSurface.GlInfo.Stencils, glFbInfo); + } + } + + public void Dispose() + { + _renderTarget.Dispose(); + } + + public ISkiaGpuRenderSession BeginRenderingSession() + { + return new BrowserSkiaGpuRenderSession(_browserSkiaSurface, _renderTarget); + } + + public bool IsCorrupted => _browserSkiaSurface.Size != _size; + } +} diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs b/src/Web/Avalonia.Web/Skia/BrowserSkiaRasterSurface.cs similarity index 92% rename from src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs rename to src/Web/Avalonia.Web/Skia/BrowserSkiaRasterSurface.cs index 603a792de3..c7005583ac 100644 --- a/src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs +++ b/src/Web/Avalonia.Web/Skia/BrowserSkiaRasterSurface.cs @@ -1,12 +1,13 @@ +using System; using System.Runtime.InteropServices; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Platform; using Avalonia.Skia; using SkiaSharp; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web.Skia { - internal class BlazorSkiaRasterSurface : IBlazorSkiaSurface, IFramebufferPlatformSurface, IDisposable + internal class BrowserSkiaRasterSurface : IBrowserSkiaSurface, IFramebufferPlatformSurface, IDisposable { public SKColorType ColorType { get; set; } @@ -18,7 +19,7 @@ namespace Avalonia.Web.Blazor private readonly Action _blitCallback; private readonly Action _onDisposeAction; - public BlazorSkiaRasterSurface( + public BrowserSkiaRasterSurface( SKColorType colorType, PixelSize size, double scaling, Action blitCallback) { ColorType = colorType; diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs b/src/Web/Avalonia.Web/Skia/BrowserSkiaSurface.cs similarity index 56% rename from src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs rename to src/Web/Avalonia.Web/Skia/BrowserSkiaSurface.cs index fb49df338b..27a206c0ec 100644 --- a/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs +++ b/src/Web/Avalonia.Web/Skia/BrowserSkiaSurface.cs @@ -1,11 +1,11 @@ -using Avalonia.Web.Blazor.Interop; +using Avalonia.Web.Interop; using SkiaSharp; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web.Skia { - internal class BlazorSkiaSurface : IBlazorSkiaSurface + internal class BrowserSkiaSurface : IBrowserSkiaSurface { - public BlazorSkiaSurface(GRContext context, SKHtmlCanvasInterop.GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling, GRSurfaceOrigin origin) + public BrowserSkiaSurface(GRContext context, GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling, GRSurfaceOrigin origin) { Context = context; GlInfo = glInfo; @@ -14,7 +14,7 @@ namespace Avalonia.Web.Blazor Scaling = scaling; Origin = origin; } - + public SKColorType ColorType { get; set; } public PixelSize Size { get; set; } @@ -25,6 +25,6 @@ namespace Avalonia.Web.Blazor public double Scaling { get; set; } - public SKHtmlCanvasInterop.GLInfo GlInfo { get; set; } + public GLInfo GlInfo { get; set; } } } diff --git a/src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs b/src/Web/Avalonia.Web/Skia/IBrowserSkiaSurface.cs similarity index 59% rename from src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs rename to src/Web/Avalonia.Web/Skia/IBrowserSkiaSurface.cs index 5463893e27..7301ae45cd 100644 --- a/src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs +++ b/src/Web/Avalonia.Web/Skia/IBrowserSkiaSurface.cs @@ -1,6 +1,6 @@ -namespace Avalonia.Web.Blazor +namespace Avalonia.Web.Skia { - internal interface IBlazorSkiaSurface + internal interface IBrowserSkiaSurface { public PixelSize Size { get; set; } diff --git a/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs b/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs new file mode 100644 index 0000000000..77734ea62f --- /dev/null +++ b/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs @@ -0,0 +1,90 @@ +using System; +using System.IO; +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")] +internal class BlobReadableStream : Stream +{ + private JSObject? _jSReference; + private long _position; + private readonly long _length; + + public BlobReadableStream(JSObject jsStreamReference) + { + _jSReference = jsStreamReference; + _position = 0; + _length = StreamHelper.ByteLength(JSReference); + } + + private JSObject JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(WriteableStream)); + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length => _length; + + public override long Position + { + get => _position; + set => throw new NotSupportedException(); + } + + public override void Flush() { } + + public override long Seek(long offset, SeekOrigin origin) + { + return _position = origin switch + { + SeekOrigin.Current => _position + offset, + SeekOrigin.End => _length + offset, + _ => offset + }; + } + + public override void SetLength(long value) + => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) + => throw new NotSupportedException(); + + public override int Read(byte[] buffer, int offset, int count) + { + throw new InvalidOperationException("Browser supports only ReadAsync"); + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => await ReadAsync(buffer.AsMemory(offset, count), cancellationToken); + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + var numBytesToRead = (int)Math.Min(buffer.Length, Length - _position); + var bytesRead = await StreamHelper.SliceAsync(JSReference, _position, numBytesToRead); + if (bytesRead.Length != numBytesToRead) + { + throw new EndOfStreamException("Failed to read the requested number of bytes from the stream."); + } + + _position += bytesRead.Length; + bytesRead.CopyTo(buffer); + + return bytesRead.Length; + } + + protected override void Dispose(bool disposing) + { + if (_jSReference is { } jsReference) + { + _jSReference = null; + jsReference.Dispose(); + } + } +} diff --git a/src/Web/Avalonia.Web/Storage/BrowserStorageProvider.cs b/src/Web/Avalonia.Web/Storage/BrowserStorageProvider.cs new file mode 100644 index 0000000000..3932b79ad0 --- /dev/null +++ b/src/Web/Avalonia.Web/Storage/BrowserStorageProvider.cs @@ -0,0 +1,257 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.JavaScript; +using System.Runtime.Versioning; +using System.Threading.Tasks; + +using Avalonia.Platform.Storage; +using Avalonia.Web.Interop; + +namespace Avalonia.Web.Storage; + +internal record FilePickerAcceptType(string Description, IReadOnlyDictionary> Accept); + +[SupportedOSPlatform("browser")] +internal class BrowserStorageProvider : IStorageProvider +{ + internal const string PickerCancelMessage = "The user aborted a request"; + internal const string NoPermissionsMessage = "Permissions denied"; + + private readonly Lazy _lazyModule = new(() => AvaloniaModule.ImportStorage()); + + public bool CanOpen => StorageHelper.CanShowOpenFilePicker(); + public bool CanSave => StorageHelper.CanShowSaveFilePicker(); + public bool CanPickFolder => StorageHelper.CanShowDirectoryPicker(); + + public async Task> OpenFilePickerAsync(FilePickerOpenOptions options) + { + await _lazyModule.Value; + var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; + + var (types, exludeAll) = ConvertFileTypes(options.FileTypeFilter); + + try + { + using var items = await StorageHelper.OpenFileDialog(startIn, options.AllowMultiple, types, exludeAll); + if (items is null) + { + return Array.Empty(); + } + + var itemsArray = StorageHelper.ItemsArray(items); + return itemsArray.Select(item => new JSStorageFile(item)).ToArray(); + } + catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) + { + return Array.Empty(); + } + finally + { + if (types is not null) + { + foreach (var type in types) + { + type.Dispose(); + } + } + } + } + + public async Task SaveFilePickerAsync(FilePickerSaveOptions options) + { + await _lazyModule.Value; + var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; + + var (types, exludeAll) = ConvertFileTypes(options.FileTypeChoices); + + try + { + var item = await StorageHelper.SaveFileDialog(startIn, options.SuggestedFileName, types, exludeAll); + return item is not null ? new JSStorageFile(item) : null; + } + catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) + { + return null; + } + finally + { + if (types is not null) + { + foreach (var type in types) + { + type.Dispose(); + } + } + } + } + + public async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) + { + await _lazyModule.Value; + var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; + + try + { + var item = await StorageHelper.SelectFolderDialog(startIn); + return item is not null ? new[] { new JSStorageFolder(item) } : Array.Empty(); + } + catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) + { + return Array.Empty(); + } + } + + public async Task OpenFileBookmarkAsync(string bookmark) + { + await _lazyModule.Value; + var item = await StorageHelper.OpenBookmark(bookmark); + return item is not null ? new JSStorageFile(item) : null; + } + + public async Task OpenFolderBookmarkAsync(string bookmark) + { + await _lazyModule.Value; + var item = await StorageHelper.OpenBookmark(bookmark); + return item is not null ? new JSStorageFolder(item) : null; + } + + private static (JSObject[]? types, bool excludeAllOption) ConvertFileTypes(IEnumerable? input) + { + var types = input? + .Where(t => t.MimeTypes?.Any() == true && t != FilePickerFileTypes.All) + .Select(t => StorageHelper.CreateAcceptType(t.Name, t.MimeTypes!.ToArray())) + .ToArray(); + if (types?.Length == 0) + { + types = null; + } + + var inlcudeAll = input?.Contains(FilePickerFileTypes.All) == true || types is null; + + return (types, !inlcudeAll); + } +} + +internal abstract class JSStorageItem : IStorageBookmarkItem +{ + internal JSObject? _fileHandle; + + protected JSStorageItem(JSObject fileHandle) + { + _fileHandle = fileHandle ?? throw new ArgumentNullException(nameof(fileHandle)); + } + + internal JSObject FileHandle => _fileHandle ?? throw new ObjectDisposedException(nameof(JSStorageItem)); + + public string Name => FileHandle.GetPropertyAsString("name") ?? string.Empty; + + public bool TryGetUri([NotNullWhen(true)] out Uri? uri) + { + uri = new Uri(Name, UriKind.Relative); + return false; + } + + public async Task GetBasicPropertiesAsync() + { + using var properties = await StorageHelper.GetProperties(FileHandle); + var size = (long?)properties?.GetPropertyAsDouble("Size"); + var lastModified = (long?)properties?.GetPropertyAsDouble("LastModified"); + + return new StorageItemProperties( + (ulong?)size, + dateCreated: null, + dateModified: lastModified > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(lastModified.Value) : null); + } + + public bool CanBookmark => true; + + public Task SaveBookmarkAsync() + { + return StorageHelper.SaveBookmark(FileHandle); + } + + public Task GetParentAsync() + { + return Task.FromResult(null); + } + + public Task ReleaseBookmarkAsync() + { + return StorageHelper.DeleteBookmark(FileHandle); + } + + public void Dispose() + { + _fileHandle?.Dispose(); + _fileHandle = null; + } +} + +internal class JSStorageFile : JSStorageItem, IStorageBookmarkFile +{ + public JSStorageFile(JSObject fileHandle) : base(fileHandle) + { + } + + public bool CanOpenRead => true; + public async Task OpenReadAsync() + { + try + { + var blob = await StorageHelper.OpenRead(FileHandle); + return new BlobReadableStream(blob); + } + catch (JSException ex) when (ex.Message == BrowserStorageProvider.NoPermissionsMessage) + { + throw new UnauthorizedAccessException("User denied permissions to open the file", ex); + } + } + + public bool CanOpenWrite => true; + public async Task OpenWriteAsync() + { + try + { + using var properties = await StorageHelper.GetProperties(FileHandle); + var streamWriter = await StorageHelper.OpenWrite(FileHandle); + var size = (long?)properties?.GetPropertyAsDouble("Size") ?? 0; + + return new WriteableStream(streamWriter, size); + } + catch (JSException ex) when (ex.Message == BrowserStorageProvider.NoPermissionsMessage) + { + throw new UnauthorizedAccessException("User denied permissions to open the file", ex); + } + } +} + +internal class JSStorageFolder : JSStorageItem, IStorageBookmarkFolder +{ + public JSStorageFolder(JSObject fileHandle) : base(fileHandle) + { + } + + public async Task> GetItemsAsync() + { + using var items = await StorageHelper.GetItems(FileHandle); + if (items is null) + { + return Array.Empty(); + } + + var itemsArray = StorageHelper.ItemsArray(items); + + return itemsArray + .Select(reference => reference.GetPropertyAsString("kind") switch + { + "directory" => (IStorageItem)new JSStorageFolder(reference), + "file" => new JSStorageFile(reference), + _ => null + }) + .Where(i => i is not null) + .ToArray()!; + } +} diff --git a/src/Web/Avalonia.Web/Storage/WriteableStream.cs b/src/Web/Avalonia.Web/Storage/WriteableStream.cs new file mode 100644 index 0000000000..09e438c34e --- /dev/null +++ b/src/Web/Avalonia.Web/Storage/WriteableStream.cs @@ -0,0 +1,126 @@ +using System; +using System.IO; +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")] +// Loose wrapper implementaion of a stream on top of FileAPI FileSystemWritableFileStream +internal sealed class WriteableStream : Stream +{ + private JSObject? _jSReference; + + // Unfortunatelly we can't read current length/position, so we need to keep it C#-side only. + private long _length, _position; + + internal WriteableStream(JSObject jSReference, long initialLength) + { + _jSReference = jSReference; + _length = initialLength; + } + + private JSObject JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(WriteableStream)); + + public override bool CanRead => false; + + public override bool CanSeek => true; + + public override bool CanWrite => true; + + public override long Length => _length; + + public override long Position + { + get => _position; + set => Seek(_position, SeekOrigin.Begin); + } + + public override void Flush() + { + // no-op + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + var position = origin switch + { + SeekOrigin.Current => _position + offset, + SeekOrigin.End => _length + offset, + _ => offset + }; + StreamHelper.Seek(JSReference, position); + return position; + } + + public override void SetLength(long value) + { + _length = value; + + // See https://docs.w3cub.com/dom/filesystemwritablefilestream/truncate + // If the offset is smaller than the size, it remains unchanged. If the offset is larger than size, the offset is set to that size + if (_position > _length) + { + _position = _length; + } + + StreamHelper.Truncate(JSReference, value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new InvalidOperationException("Browser supports only WriteAsync"); + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + return new ValueTask(WriteAsyncInternal(buffer.ToArray(), cancellationToken)); + } + + private Task WriteAsyncInternal(byte[] buffer, CancellationToken _) + { + _position += buffer.Length; + + return StreamHelper.WriteAsync(JSReference, buffer); + } + + protected override void Dispose(bool disposing) + { + if (_jSReference is { } jsReference) + { + _jSReference = null; + try + { + _ = StreamHelper.CloseAsync(jsReference); + } + finally + { + jsReference.Dispose(); + } + } + } + + public override async ValueTask DisposeAsync() + { + if (_jSReference is { } jsReference) + { + _jSReference = null; + try + { + await StreamHelper.CloseAsync(jsReference); + } + finally + { + jsReference.Dispose(); + } + } + } +} diff --git a/src/Web/Avalonia.Web/WebEmbeddableControlRoot.cs b/src/Web/Avalonia.Web/WebEmbeddableControlRoot.cs new file mode 100644 index 0000000000..19f36403ad --- /dev/null +++ b/src/Web/Avalonia.Web/WebEmbeddableControlRoot.cs @@ -0,0 +1,68 @@ +using System; +using Avalonia.Controls.Embedding; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; + +namespace Avalonia.Web +{ + internal class WebEmbeddableControlRoot : EmbeddableControlRoot + { + class SplashScreenCloseCustomDrawingOperation : ICustomDrawOperation + { + private bool _hasRendered; + private Action _onFirstRender; + + public SplashScreenCloseCustomDrawingOperation(Action onFirstRender) + { + _onFirstRender = onFirstRender; + } + + public Rect Bounds => Rect.Empty; + + public bool HasRendered => _hasRendered; + + public void Dispose() + { + + } + + public bool Equals(ICustomDrawOperation? other) + { + return false; + } + + public bool HitTest(Point p) + { + return false; + } + + public void Render(IDrawingContextImpl context) + { + _hasRendered = true; + _onFirstRender(); + } + } + + public WebEmbeddableControlRoot(ITopLevelImpl impl, Action onFirstRender) : base(impl) + { + _splashCloseOp = new SplashScreenCloseCustomDrawingOperation(() => + { + _splashCloseOp = null; + onFirstRender(); + }); + } + + private SplashScreenCloseCustomDrawingOperation? _splashCloseOp; + + public override void Render(DrawingContext context) + { + base.Render(context); + + if (_splashCloseOp != null) + { + context.Custom(_splashCloseOp); + } + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/WinStubs.cs b/src/Web/Avalonia.Web/WinStubs.cs similarity index 89% rename from src/Web/Avalonia.Web.Blazor/WinStubs.cs rename to src/Web/Avalonia.Web/WinStubs.cs index 808d1526d3..b0961115fe 100644 --- a/src/Web/Avalonia.Web.Blazor/WinStubs.cs +++ b/src/Web/Avalonia.Web/WinStubs.cs @@ -1,12 +1,10 @@ -using Avalonia.Controls; -using Avalonia.Controls.Platform; -using Avalonia.Input; -using Avalonia.Input.Platform; +using System.Collections.Generic; +using System.IO; using Avalonia.Platform; #nullable enable -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { internal class IconLoaderStub : IPlatformIconLoader { diff --git a/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs b/src/Web/Avalonia.Web/WindowingPlatform.cs similarity index 88% rename from src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs rename to src/Web/Avalonia.Web/WindowingPlatform.cs index 46c05d8e8c..7c2a84516b 100644 --- a/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs +++ b/src/Web/Avalonia.Web/WindowingPlatform.cs @@ -1,15 +1,14 @@ -using Avalonia.Controls.Platform; +using System; +using System.Threading; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; -#nullable enable - -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { - public class BlazorWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface + internal class BrowserWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface { private bool _signaled; private static KeyboardDevice? s_keyboard; @@ -27,11 +26,11 @@ namespace Avalonia.Web.Blazor } public static KeyboardDevice Keyboard => s_keyboard ?? - throw new InvalidOperationException("BlazorWindowingPlatform not registered."); + throw new InvalidOperationException("BrowserWindowingPlatform not registered."); public static void Register() { - var instance = new BlazorWindowingPlatform(); + var instance = new BrowserWindowingPlatform(); s_keyboard = new KeyboardDevice(); AvaloniaLocator.CurrentMutable .Bind().ToSingleton() @@ -92,7 +91,7 @@ namespace Avalonia.Web.Blazor { get { - return true; // Blazor is single threaded. + return true; // Browser is single threaded. } } diff --git a/src/Web/Avalonia.Web/interop.js b/src/Web/Avalonia.Web/interop.js new file mode 100644 index 0000000000..c7ae3a56c7 --- /dev/null +++ b/src/Web/Avalonia.Web/interop.js @@ -0,0 +1,13 @@ +var LibraryExample = { + // Internal functions + $EXAMPLE: { + internal_func: function () { + } + }, + InterceptGLObject: function () { + globalThis.AvaloniaGL = GL + } +} + +autoAddDeps(LibraryExample, '$EXAMPLE') +mergeInto(LibraryManager.library, LibraryExample) diff --git a/src/Web/Avalonia.Web/webapp/.eslintrc.json b/src/Web/Avalonia.Web/webapp/.eslintrc.json new file mode 100644 index 0000000000..4b7e24987f --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/.eslintrc.json @@ -0,0 +1,47 @@ +{ + "env": { + "browser": true, + "es6": true + }, + "extends": "standard-with-typescript", + "overrides": [], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module", + "project": [ + "tsconfig.json" + ] + }, + "rules": { + "indent": [ + "warn", + 4 + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "quotes": ["warn", "double"], + "semi": ["error", "always"], + "@typescript-eslint/quotes": ["warn", "double"], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-extraneous-class": "off", + "@typescript-eslint/strict-boolean-expressions": "off", + "@typescript-eslint/space-before-function-paren": "off", + "@typescript-eslint/semi": ["error", "always"], + "@typescript-eslint/member-delimiter-style": [ + "error", + { + "multiline": { + "delimiter": "semi", + "requireLast": true + }, + "singleline": { + "delimiter": "semi", + "requireLast": false + } + } + ] + }, + "ignorePatterns": ["types/*"] +} diff --git a/src/Web/Avalonia.Web/webapp/build.js b/src/Web/Avalonia.Web/webapp/build.js new file mode 100644 index 0000000000..6b6df4c300 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/build.js @@ -0,0 +1,16 @@ +require("esbuild").build({ + entryPoints: [ + "./modules/avalonia.ts", + "./modules/storage.ts" + ], + outdir: "../wwwroot", + bundle: true, + minify: false, + format: "esm", + target: "es2016", + platform: "browser", + sourcemap: "linked", + loader: { ".ts": "ts" } +}) + .then(() => console.log("⚡ Done")) + .catch(() => process.exit(1)); diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts new file mode 100644 index 0000000000..0642bd475d --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts @@ -0,0 +1,32 @@ +import { RuntimeAPI } from "../types/dotnet"; +import { SizeWatcher, DpiWatcher, Canvas } from "./avalonia/canvas"; +import { InputHelper } from "./avalonia/input"; +import { AvaloniaDOM } from "./avalonia/dom"; +import { Caniuse } from "./avalonia/caniuse"; +import { StreamHelper } from "./avalonia/stream"; +import { NativeControlHost } from "./avalonia/nativeControlHost"; + +async function registerAvaloniaModule(api: RuntimeAPI): Promise { + api.setModuleImports("avalonia", { + Caniuse, + Canvas, + InputHelper, + SizeWatcher, + DpiWatcher, + AvaloniaDOM, + StreamHelper, + NativeControlHost + }); +} +export { + Caniuse, + Canvas, + InputHelper, + SizeWatcher, + DpiWatcher, + AvaloniaDOM, + StreamHelper, + NativeControlHost, + + registerAvaloniaModule +}; diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/caniuse.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/caniuse.ts new file mode 100644 index 0000000000..6dedcb724f --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/caniuse.ts @@ -0,0 +1,13 @@ +export class Caniuse { + public static canShowOpenFilePicker(): boolean { + return typeof window.showOpenFilePicker !== "undefined"; + } + + public static canShowSaveFilePicker(): boolean { + return typeof window.showSaveFilePicker !== "undefined"; + } + + public static canShowDirectoryPicker(): boolean { + return typeof window.showDirectoryPicker !== "undefined"; + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts new file mode 100644 index 0000000000..9ae9b3d2a8 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -0,0 +1,303 @@ +interface SKGLViewInfo { + context: WebGLRenderingContext | WebGL2RenderingContext | undefined; + fboId: number; + stencil: number; + sample: number; + depth: number; +} + +type CanvasElement = { + Canvas: Canvas | undefined; +} & HTMLCanvasElement; + +export class Canvas { + static elements: Map; + + htmlCanvas: HTMLCanvasElement; + glInfo?: SKGLViewInfo; + renderFrameCallback: () => void; + renderLoopEnabled: boolean = false; + renderLoopRequest: number = 0; + newWidth?: number; + newHeight?: number; + + public static initGL(element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): SKGLViewInfo | null { + const view = Canvas.init(true, element, elementId, renderFrameCallback); + if (!view || !view.glInfo) { + return null; + } + + return view.glInfo; + } + + static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): Canvas | null { + const htmlCanvas = element as CanvasElement; + if (!htmlCanvas) { + console.error("No canvas element was provided."); + return null; + } + + if (!Canvas.elements) { + Canvas.elements = new Map(); + } + Canvas.elements.set(elementId, element); + + const view = new Canvas(useGL, element, renderFrameCallback); + + htmlCanvas.Canvas = view; + + return view; + } + + public constructor(useGL: boolean, element: HTMLCanvasElement, renderFrameCallback: () => void) { + this.htmlCanvas = element; + this.renderFrameCallback = renderFrameCallback; + + if (useGL) { + const ctx = Canvas.createWebGLContext(element); + if (!ctx) { + console.error("Failed to create WebGL context"); + return; + } + + const GL = (globalThis as any).AvaloniaGL; + + // make current + GL.makeContextCurrent(ctx); + + const GLctx = GL.currentContext.GLctx as WebGLRenderingContext; + + // read values + const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); + + this.glInfo = { + context: ctx, + fboId: fbo ? fbo.id : 0, + stencil: GLctx.getParameter(GLctx.STENCIL_BITS), + sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES) + depth: GLctx.getParameter(GLctx.DEPTH_BITS) + }; + } + } + + public setEnableRenderLoop(enable: boolean): void { + this.renderLoopEnabled = enable; + + // either start the new frame or cancel the existing one + if (enable) { + // console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`); + this.requestAnimationFrame(); + } else if (this.renderLoopRequest !== 0) { + window.cancelAnimationFrame(this.renderLoopRequest); + this.renderLoopRequest = 0; + } + } + + public requestAnimationFrame(renderLoop?: boolean): void { + // optionally update the render loop + if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) { + this.setEnableRenderLoop(renderLoop); + } + + // skip because we have a render loop + if (this.renderLoopRequest !== 0) { + return; + } + + // add the draw to the next frame + this.renderLoopRequest = window.requestAnimationFrame(() => { + if (this.glInfo) { + const GL = (globalThis as any).AvaloniaGL; + // make current + GL.makeContextCurrent(this.glInfo.context); + } + + if (this.htmlCanvas.width !== this.newWidth) { + this.htmlCanvas.width = this.newWidth ?? 0; + } + + if (this.htmlCanvas.height !== this.newHeight) { + this.htmlCanvas.height = this.newHeight ?? 0; + } + + this.renderFrameCallback(); + this.renderLoopRequest = 0; + + // we may want to draw the next frame + if (this.renderLoopEnabled) { + this.requestAnimationFrame(); + } + }); + } + + public setCanvasSize(width: number, height: number): void { + this.newWidth = width; + this.newHeight = height; + + if (this.htmlCanvas.width !== this.newWidth) { + this.htmlCanvas.width = this.newWidth; + } + + if (this.htmlCanvas.height !== this.newHeight) { + this.htmlCanvas.height = this.newHeight; + } + + if (this.glInfo) { + const GL = (globalThis as any).AvaloniaGL; + // make current + GL.makeContextCurrent(this.glInfo.context); + } + } + + public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number): void { + const htmlCanvas = element as CanvasElement; + if (!htmlCanvas || !htmlCanvas.Canvas) { + return; + } + + htmlCanvas.Canvas.setCanvasSize(width, height); + } + + public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean): void { + const htmlCanvas = element as CanvasElement; + if (!htmlCanvas || !htmlCanvas.Canvas) { + return; + } + + htmlCanvas.Canvas.requestAnimationFrame(renderLoop); + } + + static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext { + const contextAttributes = { + alpha: 1, + depth: 1, + stencil: 8, + antialias: 0, + premultipliedAlpha: 1, + preserveDrawingBuffer: 0, + preferLowPowerToHighPerformance: 0, + failIfMajorPerformanceCaveat: 0, + majorVersion: 2, + minorVersion: 0, + enableExtensionsByDefault: 1, + explicitSwapControl: 0, + renderViaOffscreenBackBuffer: 1 + }; + + const GL = (globalThis as any).AvaloniaGL; + + let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes); + + if (!ctx && contextAttributes.majorVersion > 1) { + console.warn("Falling back to WebGL 1.0"); + contextAttributes.majorVersion = 1; + contextAttributes.minorVersion = 0; + ctx = GL.createContext(htmlCanvas, contextAttributes); + } + + return ctx; + } +} + +type SizeWatcherElement = { + SizeWatcher: SizeWatcherInstance; +} & HTMLElement; + +interface SizeWatcherInstance { + callback: (width: number, height: number) => void; +} + +export class SizeWatcher { + static observer: ResizeObserver; + static elements: Map; + + public static observe(element: HTMLElement, elementId: string | undefined, callback: (width: number, height: number) => void): void { + if (!element || !callback) { + return; + } + + SizeWatcher.init(); + + const watcherElement = element as SizeWatcherElement; + watcherElement.SizeWatcher = { + callback + }; + + SizeWatcher.elements.set(elementId ?? element.id, element); + SizeWatcher.observer.observe(element); + + SizeWatcher.invoke(element); + } + + public static unobserve(elementId: string): void { + if (!elementId || !SizeWatcher.observer) { + return; + } + + const element = SizeWatcher.elements.get(elementId); + if (element) { + SizeWatcher.elements.delete(elementId); + SizeWatcher.observer.unobserve(element); + } + } + + static init(): void { + if (SizeWatcher.observer) { + return; + } + + SizeWatcher.elements = new Map(); + SizeWatcher.observer = new ResizeObserver((entries) => { + for (const entry of entries) { + SizeWatcher.invoke(entry.target); + } + }); + } + + static invoke(element: Element): void { + const watcherElement = element as SizeWatcherElement; + const instance = watcherElement.SizeWatcher; + + if (!instance || !instance.callback) { + return; + } + + return instance.callback(element.clientWidth, element.clientHeight); + } +} + +export class DpiWatcher { + static lastDpi: number; + static timerId: number; + static callback: (old: number, newdpi: number) => void; + + public static getDpi(): number { + return window.devicePixelRatio; + } + + public static start(callback: (old: number, newdpi: number) => void): number { + DpiWatcher.lastDpi = window.devicePixelRatio; + DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000); + DpiWatcher.callback = callback; + + return DpiWatcher.lastDpi; + } + + public static stop(): void { + window.clearInterval(DpiWatcher.timerId); + } + + static update(): void { + if (!DpiWatcher.callback) { + return; + } + + const currentDpi = window.devicePixelRatio; + const lastDpi = DpiWatcher.lastDpi; + DpiWatcher.lastDpi = currentDpi; + + if (Math.abs(lastDpi - currentDpi) > 0.001) { + DpiWatcher.callback(lastDpi, currentDpi); + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/caretHelper.ts similarity index 90% rename from src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.ts rename to src/Web/Avalonia.Web/webapp/modules/avalonia/caretHelper.ts index 5709854087..546b41669f 100644 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/caretHelper.ts @@ -1,4 +1,4 @@ -// Based on https://github.com/component/textarea-caret-position/blob/master/index.js +// Based on https://github.com/component/textarea-caret-position/blob/master/index.js export class CaretHelper { public static getCaretCoordinates( element: HTMLInputElement | HTMLTextAreaElement, @@ -11,7 +11,7 @@ export class CaretHelper { ); } - const debug = (options && options.debug) || false; + const debug = options?.debug ?? false; if (debug) { const el = document.querySelector( "#input-textarea-caret-position-mirror-div" @@ -27,7 +27,7 @@ export class CaretHelper { const style = div.style; const computed = window.getComputedStyle ? window.getComputedStyle(element) - : ((element as any)["currentStyle"] as CSSStyleDeclaration); // currentStyle for IE < 9 + : ((element as any).currentStyle as CSSStyleDeclaration); // currentStyle for IE < 9 const isInput = element.nodeName === "INPUT"; // Default textarea styles @@ -51,7 +51,7 @@ export class CaretHelper { parseInt(computed.borderBottomWidth); const targetHeight = outerHeight + parseInt(computed.lineHeight); if (height > targetHeight) { - style.lineHeight = height - outerHeight + "px"; + style.lineHeight = `${height - outerHeight}px`; } else if (height === targetHeight) { style.lineHeight = computed.lineHeight; } else { @@ -67,8 +67,9 @@ export class CaretHelper { 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)) + if (element.scrollHeight > parseInt(computed.height)) { style.overflowY = "scroll"; + } } else { style.overflow = "hidden"; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll' } @@ -88,9 +89,9 @@ export class CaretHelper { div.appendChild(span); const coordinates = { - top: span.offsetTop + parseInt(computed["borderTopWidth"]), - left: span.offsetLeft + parseInt(computed["borderLeftWidth"]), - height: parseInt(computed["lineHeight"]), + top: span.offsetTop + parseInt(computed.borderTopWidth), + left: span.offsetLeft + parseInt(computed.borderLeftWidth), + height: parseInt(computed.lineHeight) }; if (debug) { @@ -103,8 +104,7 @@ export class CaretHelper { } } - -var properties = [ +const properties = [ "direction", // RTL support "boxSizing", "width", // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does @@ -142,7 +142,7 @@ var properties = [ "wordSpacing", "tabSize", - "MozTabSize", + "MozTabSize" ]; const isBrowser = typeof window !== "undefined"; diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts new file mode 100644 index 0000000000..2257d56a92 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts @@ -0,0 +1,67 @@ +export class AvaloniaDOM { + public static addClass(element: HTMLElement, className: string): void { + element.classList.add(className); + } + + 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%"; + canvas.style.height = "100%"; + canvas.style.position = "absolute"; + + // 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"; + nativeHost.style.width = "100%"; + nativeHost.style.height = "100%"; + nativeHost.style.position = "absolute"; + + // IME + const inputElement = document.createElement("input"); + canvas.id = `input${randomIdPart}`; + inputElement.classList.add("avalonia-input-element"); + inputElement.autocapitalize = "none"; + inputElement.type = "text"; + inputElement.spellcheck = false; + inputElement.style.padding = "0"; + inputElement.style.margin = "0"; + inputElement.style.position = "absolute"; + inputElement.style.overflow = "hidden"; + inputElement.style.borderStyle = "hidden"; + inputElement.style.outline = "none"; + inputElement.style.background = "transparent"; + inputElement.style.color = "transparent"; + inputElement.style.display = "none"; + inputElement.style.height = "20px"; + inputElement.style.zIndex = "-1"; + inputElement.onpaste = function () { return false; }; + inputElement.oncopy = function () { return false; }; + inputElement.oncut = function () { return false; }; + + host.prepend(inputElement); + host.prepend(nativeHost); + host.prepend(canvas); + + return { + host, + canvas, + nativeHost, + inputElement + }; + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts new file mode 100644 index 0000000000..768414ccab --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts @@ -0,0 +1,204 @@ +import { CaretHelper } from "./caretHelper"; + +enum RawInputModifiers { + None = 0, + Alt = 1, + Control = 2, + Shift = 4, + Meta = 8, + + LeftMouseButton = 16, + RightMouseButton = 32, + MiddleMouseButton = 64, + XButton1MouseButton = 128, + XButton2MouseButton = 256, + KeyboardMask = Alt | Control | Shift | Meta, + + PenInverted = 512, + PenEraser = 1024, + PenBarrelButton = 2048 +} + +export class InputHelper { + public static subscribeKeyEvents( + element: HTMLInputElement, + keyDownCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, + keyUpCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean) { + const keyDownHandler = (args: KeyboardEvent) => { + if (keyDownCallback(args.code, args.key, this.getModifiers(args))) { + args.preventDefault(); + } + }; + element.addEventListener("keydown", keyDownHandler); + + const keyUpHandler = (args: KeyboardEvent) => { + if (keyUpCallback(args.code, args.key, this.getModifiers(args))) { + args.preventDefault(); + } + }; + + element.addEventListener("keyup", keyUpHandler); + + return () => { + element.removeEventListener("keydown", keyDownHandler); + element.removeEventListener("keyup", keyUpHandler); + }; + } + + public static subscribeTextEvents( + element: HTMLInputElement, + inputCallback: (type: string, data: string | null) => boolean, + compositionStartCallback: (args: CompositionEvent) => boolean, + compositionUpdateCallback: (args: CompositionEvent) => boolean, + compositionEndCallback: (args: CompositionEvent) => boolean) { + const inputHandler = (args: Event) => { + const inputEvent = args as InputEvent; + + // todo check cast + if (inputCallback(inputEvent.type, inputEvent.data)) { + args.preventDefault(); + } + }; + element.addEventListener("input", inputHandler); + + const compositionStartHandler = (args: CompositionEvent) => { + if (compositionStartCallback(args)) { + args.preventDefault(); + } + }; + element.addEventListener("compositionstart", compositionStartHandler); + + const compositionUpdateHandler = (args: CompositionEvent) => { + if (compositionUpdateCallback(args)) { + args.preventDefault(); + } + }; + element.addEventListener("compositionupdate", compositionUpdateHandler); + + const compositionEndHandler = (args: CompositionEvent) => { + if (compositionEndCallback(args)) { + args.preventDefault(); + } + }; + element.addEventListener("compositionend", compositionEndHandler); + + return () => { + element.removeEventListener("input", inputHandler); + element.removeEventListener("compositionstart", compositionStartHandler); + element.removeEventListener("compositionupdate", compositionUpdateHandler); + element.removeEventListener("compositionend", compositionEndHandler); + }; + } + + public static subscribePointerEvents( + element: HTMLInputElement, + pointerMoveCallback: (args: PointerEvent) => boolean, + pointerDownCallback: (args: PointerEvent) => boolean, + pointerUpCallback: (args: PointerEvent) => boolean, + wheelCallback: (args: WheelEvent) => boolean + ) { + const pointerMoveHandler = (args: PointerEvent) => { + if (pointerMoveCallback(args)) { + args.preventDefault(); + } + }; + + const pointerDownHandler = (args: PointerEvent) => { + if (pointerDownCallback(args)) { + args.preventDefault(); + } + }; + + const pointerUpHandler = (args: PointerEvent) => { + if (pointerUpCallback(args)) { + args.preventDefault(); + } + }; + + const wheelHandler = (args: WheelEvent) => { + if (wheelCallback(args)) { + args.preventDefault(); + } + }; + + element.addEventListener("pointermove", pointerMoveHandler); + element.addEventListener("pointerdown", pointerDownHandler); + element.addEventListener("pointerup", pointerUpHandler); + element.addEventListener("wheel", wheelHandler); + + return () => { + element.removeEventListener("pointerover", pointerMoveHandler); + element.removeEventListener("pointerdown", pointerDownHandler); + element.removeEventListener("pointerup", pointerUpHandler); + element.removeEventListener("wheel", wheelHandler); + }; + } + + public static subscribeInputEvents( + element: HTMLInputElement, + inputCallback: (value: string) => boolean + ) { + const inputHandler = (args: Event) => { + if (inputCallback((args as any).value)) { + args.preventDefault(); + } + }; + element.addEventListener("input", inputHandler); + + return () => { + element.removeEventListener("input", inputHandler); + }; + } + + public static clearInput(inputElement: HTMLInputElement) { + inputElement.value = ""; + } + + public static focusElement(inputElement: HTMLElement) { + inputElement.focus(); + } + + public static setCursor(inputElement: HTMLInputElement, kind: string) { + inputElement.style.cursor = kind; + } + + public static setBounds(inputElement: HTMLInputElement, x: number, y: number, caretWidth: number, caretHeight: number, caret: number) { + inputElement.style.left = (x).toFixed(0) + "px"; + inputElement.style.top = (y).toFixed(0) + "px"; + + const { left, top } = CaretHelper.getCaretCoordinates(inputElement, caret); + + inputElement.style.left = (x - left).toFixed(0) + "px"; + inputElement.style.top = (y - top).toFixed(0) + "px"; + } + + public static hide(inputElement: HTMLInputElement) { + inputElement.style.display = "none"; + } + + public static show(inputElement: HTMLInputElement) { + inputElement.style.display = "block"; + } + + public static setSurroundingText(inputElement: HTMLInputElement, text: string, start: number, end: number) { + if (!inputElement) { + return; + } + + inputElement.value = text; + inputElement.setSelectionRange(start, end); + inputElement.style.width = "20px"; + inputElement.style.width = `${inputElement.scrollWidth}px`; + } + + private static getModifiers(args: KeyboardEvent): RawInputModifiers { + let modifiers = RawInputModifiers.None; + + if (args.ctrlKey) { modifiers |= RawInputModifiers.Control; } + if (args.altKey) { modifiers |= RawInputModifiers.Alt; } + if (args.shiftKey) { modifiers |= RawInputModifiers.Shift; } + if (args.metaKey) { modifiers |= RawInputModifiers.Meta; } + + return modifiers; + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/nativeControlHost.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/nativeControlHost.ts new file mode 100644 index 0000000000..c65d5836df --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/nativeControlHost.ts @@ -0,0 +1,55 @@ +class NativeControlHostTopLevelAttachment { + _child?: HTMLElement; + _host?: HTMLElement; +} + +export class NativeControlHost { + public static createDefaultChild(parent?: HTMLElement): HTMLElement { + return document.createElement("div"); + } + + public static createAttachment(): NativeControlHostTopLevelAttachment { + return new NativeControlHostTopLevelAttachment(); + } + + public static initializeWithChildHandle(element: NativeControlHostTopLevelAttachment, child: HTMLElement): void { + element._child = child; + element._child.style.position = "absolute"; + } + + public static attachTo(element: NativeControlHostTopLevelAttachment, host?: HTMLElement): void { + if (element._host && element._child) { + element._host.removeChild(element._child); + } + + element._host = host; + + if (element._host && element._child) { + element._host.appendChild(element._child); + } + } + + public static showInBounds(element: NativeControlHostTopLevelAttachment, x: number, y: number, width: number, height: number): void { + if (element._child) { + element._child.style.top = `${y}px`; + element._child.style.left = `${x}px`; + element._child.style.width = `${width}px`; + element._child.style.height = `${height}px`; + element._child.style.display = "block"; + } + } + + public static hideWithSize(element: NativeControlHostTopLevelAttachment, width: number, height: number): void { + if (element._child) { + element._child.style.width = `${width}px`; + element._child.style.height = `${height}px`; + element._child.style.display = "none"; + } + } + + public static releaseChild(element: NativeControlHostTopLevelAttachment): void { + if (element._child) { + element._child = undefined; + } + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/stream.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/stream.ts new file mode 100644 index 0000000000..1f2c181edc --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/stream.ts @@ -0,0 +1,40 @@ +import { IMemoryView } from "../../types/dotnet"; + +export class StreamHelper { + public static async seek(stream: FileSystemWritableFileStream, position: number) { + return await stream.seek(position); + } + + public static async truncate(stream: FileSystemWritableFileStream, size: number) { + return await stream.truncate(size); + } + + public static async close(stream: FileSystemWritableFileStream) { + return await stream.close(); + } + + public static async write(stream: FileSystemWritableFileStream, span: IMemoryView) { + const array = new Uint8Array(span.byteLength); + span.copyTo(array); + + const data: WriteParams = { + type: "write", + data: array + }; + + return await stream.write(data); + } + + public static byteLength(stream: Blob) { + return stream.size; + } + + public static async sliceArrayBuffer(stream: Blob, offset: number, count: number) { + const buffer = await stream.slice(offset, offset + count).arrayBuffer(); + return new Uint8Array(buffer); + } + + public static toMemoryView(buffer: Uint8Array): Uint8Array { + return buffer; + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/storage.ts b/src/Web/Avalonia.Web/webapp/modules/storage.ts new file mode 100644 index 0000000000..8b9987afa0 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/storage.ts @@ -0,0 +1,2 @@ +export { StorageItem, StorageItems } from "./storage/storageItem"; +export { StorageProvider } from "./storage/storageProvider"; diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/IndexedDbWrapper.ts b/src/Web/Avalonia.Web/webapp/modules/storage/indexedDb.ts similarity index 63% rename from src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/IndexedDbWrapper.ts rename to src/Web/Avalonia.Web/webapp/modules/storage/indexedDb.ts index 2eaa8de2fe..54b095d39e 100644 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/IndexedDbWrapper.ts +++ b/src/Web/Avalonia.Web/webapp/modules/storage/indexedDb.ts @@ -1,15 +1,15 @@ class InnerDbConnection { - constructor(private database: IDBDatabase) { } + constructor(private readonly 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 { + public async put(store: string, obj: any, key?: IDBValidKey): Promise { const os = this.openStore(store, "readwrite"); - return new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { const response = os.put(obj, key); response.onsuccess = () => { resolve(response.result); @@ -34,10 +34,10 @@ class InnerDbConnection { }); } - public delete(store: string, key: IDBValidKey): Promise { + public async delete(store: string, key: IDBValidKey): Promise { const os = this.openStore(store, "readwrite"); - return new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { const response = os.delete(key); response.onsuccess = () => { resolve(); @@ -53,27 +53,32 @@ class InnerDbConnection { } } -export class IndexedDbWrapper { - constructor(private databaseName: string, private objectStores: [string]) { +class IndexedDbWrapper { + constructor(private readonly databaseName: string, private readonly objectStores: [string]) { } - public connect(): Promise { + public async connect(): Promise { const conn = window.indexedDB.open(this.databaseName, 1); conn.onupgradeneeded = event => { - const db = (>event.target).result; + const db = (event.target as IDBRequest).result; this.objectStores.forEach(store => { db.createObjectStore(store); }); }; - return new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { conn.onsuccess = event => { - resolve(new InnerDbConnection((>event.target).result)); + resolve(new InnerDbConnection((event.target as IDBRequest).result)); }; conn.onerror = event => { - reject((>event.target).error); + reject((event.target as IDBRequest).error); }; }); } } + +export const fileBookmarksStore: string = "fileBookmarks"; +export const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [ + fileBookmarksStore +]); diff --git a/src/Web/Avalonia.Web/webapp/modules/storage/storageItem.ts b/src/Web/Avalonia.Web/webapp/modules/storage/storageItem.ts new file mode 100644 index 0000000000..b4cf3d278b --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/storage/storageItem.ts @@ -0,0 +1,111 @@ +import { avaloniaDb, fileBookmarksStore } from "./indexedDb"; + +export class StorageItem { + constructor(public handle: FileSystemHandle, private readonly bookmarkId?: string) { } + + public get name(): string { + return this.handle.name; + } + + public get kind(): string { + return this.handle.kind; + } + + public static async openRead(item: StorageItem): Promise { + if (!(item.handle instanceof FileSystemFileHandle)) { + throw new Error("StorageItem is not a file"); + } + + await item.verityPermissions("read"); + + const file = await item.handle.getFile(); + return file; + } + + public static async openWrite(item: StorageItem): Promise { + if (!(item.handle instanceof FileSystemFileHandle)) { + throw new Error("StorageItem is not a file"); + } + + await item.verityPermissions("readwrite"); + + return await item.handle.createWritable({ keepExistingData: true }); + } + + public static async getProperties(item: StorageItem): Promise<{ Size: number; LastModified: number; Type: string } | null> { + const file = item.handle instanceof FileSystemFileHandle && + await item.handle.getFile(); + + if (!file) { + return null; + } + + return { + Size: file.size, + LastModified: file.lastModified, + Type: file.type + }; + } + + public static async getItems(item: StorageItem): Promise { + if (item.handle.kind !== "directory") { + return new StorageItems([]); + } + + const items: StorageItem[] = []; + for await (const [, value] of (item.handle as any).entries()) { + items.push(new StorageItem(value)); + } + return new StorageItems(items); + } + + private async verityPermissions(mode: FileSystemPermissionMode): Promise { + if (await this.handle.queryPermission({ mode }) === "granted") { + return; + } + + if (await this.handle.requestPermission({ mode }) === "denied") { + throw new Error("Permissions denied"); + } + } + + public static async saveBookmark(item: StorageItem): Promise { + // If file was previously bookmarked, just return old one. + if (item.bookmarkId) { + return item.bookmarkId; + } + + const connection = await avaloniaDb.connect(); + try { + const key = await connection.put(fileBookmarksStore, item.handle, item.generateBookmarkId()); + return key as string; + } finally { + connection.close(); + } + } + + public static async deleteBookmark(item: StorageItem): Promise { + if (!item.bookmarkId) { + return; + } + + const connection = await avaloniaDb.connect(); + try { + await connection.delete(fileBookmarksStore, item.bookmarkId); + } finally { + connection.close(); + } + } + + private generateBookmarkId(): string { + return Date.now().toString(36) + Math.random().toString(36).substring(2); + } +} + +export class StorageItems { + constructor(private readonly items: StorageItem[]) { } + + public static itemsArray(instance: StorageItems): StorageItem[] { + return instance.items; + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/storage/storageProvider.ts b/src/Web/Avalonia.Web/webapp/modules/storage/storageProvider.ts new file mode 100644 index 0000000000..0198f92528 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/storage/storageProvider.ts @@ -0,0 +1,70 @@ +import { avaloniaDb, fileBookmarksStore } from "./indexedDb"; +import { StorageItem, StorageItems } from "./storageItem"; + +declare global { + type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; + type StartInDirectory = WellKnownDirectory | FileSystemHandle; + interface OpenFilePickerOptions { + startIn?: StartInDirectory; + } + interface SaveFilePickerOptions { + startIn?: StartInDirectory; + } +} + +export class StorageProvider { + public static async selectFolderDialog( + startIn: StorageItem | null): Promise { + // '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 { + 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 { + 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 { + const connection = await avaloniaDb.connect(); + try { + const handle = await connection.get(fileBookmarksStore, key); + return handle && new StorageItem(handle, key); + } finally { + connection.close(); + } + } + + public static createAcceptType(description: string, mimeTypes: string[]): FilePickerAcceptType { + const accept: Record = {}; + mimeTypes.forEach(a => { accept[a] = []; }); + return { description, accept }; + } +} diff --git a/src/Web/Avalonia.Web/webapp/package-lock.json b/src/Web/Avalonia.Web/webapp/package-lock.json new file mode 100644 index 0000000000..947f7e12e7 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/package-lock.json @@ -0,0 +1,2234 @@ +{ + "name": "avalonia.web", + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@esbuild/android-arm": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.9.tgz", + "integrity": "sha512-VZPy/ETF3fBG5PiinIkA0W/tlsvlEgJccyN2DzWZEl0DlVKRbu91PvY2D6Lxgluj4w9QtYHjOWjAT44C+oQ+EQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.9.tgz", + "integrity": "sha512-O+NfmkfRrb3uSsTa4jE3WApidSe3N5++fyOVGP1SmMZi4A3BZELkhUUvj5hwmMuNdlpzAZ8iAPz2vmcR7DCFQA==", + "dev": true, + "optional": true + }, + "@eslint/eslintrc": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz", + "integrity": "sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + } + }, + "@humanwhocodes/config-array": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.5.tgz", + "integrity": "sha512-XVVDtp+dVvRxMoxSiSfasYaG02VEe1qH5cKgMQJWhol6HwzbcqoCMJi8dAGoYAO57jhUyhI6cWuRiTcRaDaYug==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/gitignore-to-minimatch": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", + "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", + "dev": true + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@types/emscripten": { + "version": "1.39.6", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.6.tgz", + "integrity": "sha512-H90aoynNhhkQP6DRweEjJp5vfUVdIj7tdPLsu7pq89vODD/lcugKfZOsfgwpvM6XUewEp2N5dCg1Uf3Qe55Dcg==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "@types/wicg-file-system-access": { + "version": "2020.9.5", + "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz", + "integrity": "sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz", + "integrity": "sha512-ky7EFzPhqz3XlhS7vPOoMDaQnQMn+9o5ICR9CPr/6bw8HrFkzhMSxuA3gRfiJVvs7geYrSeawGJjZoZQKCOglQ==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.38.1", + "@typescript-eslint/type-utils": "5.38.1", + "@typescript-eslint/utils": "5.38.1", + "debug": "^4.3.4", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/parser": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.38.1.tgz", + "integrity": "sha512-LDqxZBVFFQnQRz9rUZJhLmox+Ep5kdUmLatLQnCRR6523YV+XhRjfYzStQ4MheFA8kMAfUlclHSbu+RKdRwQKw==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.38.1", + "@typescript-eslint/types": "5.38.1", + "@typescript-eslint/typescript-estree": "5.38.1", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.38.1.tgz", + "integrity": "sha512-BfRDq5RidVU3RbqApKmS7RFMtkyWMM50qWnDAkKgQiezRtLKsoyRKIvz1Ok5ilRWeD9IuHvaidaLxvGx/2eqTQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.38.1", + "@typescript-eslint/visitor-keys": "5.38.1" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.38.1.tgz", + "integrity": "sha512-UU3j43TM66gYtzo15ivK2ZFoDFKKP0k03MItzLdq0zV92CeGCXRfXlfQX5ILdd4/DSpHkSjIgLLLh1NtkOJOAw==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "5.38.1", + "@typescript-eslint/utils": "5.38.1", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/types": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.38.1.tgz", + "integrity": "sha512-QTW1iHq1Tffp9lNfbfPm4WJabbvpyaehQ0SrvVK2yfV79SytD9XDVxqiPvdrv2LK7DGSFo91TB2FgWanbJAZXg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.38.1.tgz", + "integrity": "sha512-99b5e/Enoe8fKMLdSuwrfH/C0EIbpUWmeEKHmQlGZb8msY33qn1KlkFww0z26o5Omx7EVjzVDCWEfrfCDHfE7g==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.38.1", + "@typescript-eslint/visitor-keys": "5.38.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/utils": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.38.1.tgz", + "integrity": "sha512-oIuUiVxPBsndrN81oP8tXnFa/+EcZ03qLqPDfSZ5xIJVm7A9V0rlkQwwBOAGtrdN70ZKDlKv+l1BeT4eSFxwXA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.38.1", + "@typescript-eslint/types": "5.38.1", + "@typescript-eslint/typescript-estree": "5.38.1", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.38.1.tgz", + "integrity": "sha512-bSHr1rRxXt54+j2n4k54p4fj8AHJ49VDWtjpImOpzQj4qjAiOpPni+V1Tyajh19Api1i844F757cur8wH3YvOA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.38.1", + "eslint-visitor-keys": "^3.3.0" + } + }, + "acorn": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "array-includes": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", + "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array.prototype.flat": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", + "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "builtins": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", + "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", + "dev": true, + "requires": { + "semver": "^7.0.0" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.3.tgz", + "integrity": "sha512-AyrnaKVpMzljIdwjzrj+LxGmj8ik2LckwXacHqrJJ/jxz6dDDBcZ7I7nlHM0FvEW8MfbWJwOd+yT2XzYW49Frw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.6", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + } + }, + "es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "esbuild": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.9.tgz", + "integrity": "sha512-OnYr1rkMVxtmMHIAKZLMcEUlJmqcbxBz9QoBU8G9v455na0fuzlT/GLu6l+SRghrk0Mm2fSSciMmzV43Q8e0Gg==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.15.9", + "@esbuild/linux-loong64": "0.15.9", + "esbuild-android-64": "0.15.9", + "esbuild-android-arm64": "0.15.9", + "esbuild-darwin-64": "0.15.9", + "esbuild-darwin-arm64": "0.15.9", + "esbuild-freebsd-64": "0.15.9", + "esbuild-freebsd-arm64": "0.15.9", + "esbuild-linux-32": "0.15.9", + "esbuild-linux-64": "0.15.9", + "esbuild-linux-arm": "0.15.9", + "esbuild-linux-arm64": "0.15.9", + "esbuild-linux-mips64le": "0.15.9", + "esbuild-linux-ppc64le": "0.15.9", + "esbuild-linux-riscv64": "0.15.9", + "esbuild-linux-s390x": "0.15.9", + "esbuild-netbsd-64": "0.15.9", + "esbuild-openbsd-64": "0.15.9", + "esbuild-sunos-64": "0.15.9", + "esbuild-windows-32": "0.15.9", + "esbuild-windows-64": "0.15.9", + "esbuild-windows-arm64": "0.15.9" + } + }, + "esbuild-android-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.9.tgz", + "integrity": "sha512-HQCX7FJn9T4kxZQkhPjNZC7tBWZqJvhlLHPU2SFzrQB/7nDXjmTIFpFTjt7Bd1uFpeXmuwf5h5fZm+x/hLnhbw==", + "dev": true, + "optional": true + }, + "esbuild-android-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.9.tgz", + "integrity": "sha512-E6zbLfqbFVCNEKircSHnPiSTsm3fCRxeIMPfrkS33tFjIAoXtwegQfVZqMGR0FlsvVxp2NEDOUz+WW48COCjSg==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.9.tgz", + "integrity": "sha512-gI7dClcDN/HHVacZhTmGjl0/TWZcGuKJ0I7/xDGJwRQQn7aafZGtvagOFNmuOq+OBFPhlPv1T6JElOXb0unkSQ==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.9.tgz", + "integrity": "sha512-VZIMlcRN29yg/sv7DsDwN+OeufCcoTNaTl3Vnav7dL/nvsApD7uvhVRbgyMzv0zU/PP0xRhhIpTyc7lxEzHGSw==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.9.tgz", + "integrity": "sha512-uM4z5bTvuAXqPxrI204txhlsPIolQPWRMLenvGuCPZTnnGlCMF2QLs0Plcm26gcskhxewYo9LkkmYSS5Czrb5A==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.9.tgz", + "integrity": "sha512-HHDjT3O5gWzicGdgJ5yokZVN9K9KG05SnERwl9nBYZaCjcCgj/sX8Ps1jvoFSfNCO04JSsHSOWo4qvxFuj8FoA==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.9.tgz", + "integrity": "sha512-AQIdE8FugGt1DkcekKi5ycI46QZpGJ/wqcMr7w6YUmOmp2ohQ8eO4sKUsOxNOvYL7hGEVwkndSyszR6HpVHLFg==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.9.tgz", + "integrity": "sha512-4RXjae7g6Qs7StZyiYyXTZXBlfODhb1aBVAjd+ANuPmMhWthQilWo7rFHwJwL7DQu1Fjej2sODAVwLbcIVsAYQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.9.tgz", + "integrity": "sha512-3Zf2GVGUOI7XwChH3qrnTOSqfV1V4CAc/7zLVm4lO6JT6wbJrTgEYCCiNSzziSju+J9Jhf9YGWk/26quWPC6yQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.9.tgz", + "integrity": "sha512-a+bTtxJmYmk9d+s2W4/R1SYKDDAldOKmWjWP0BnrWtDbvUBNOm++du0ysPju4mZVoEFgS1yLNW+VXnG/4FNwdQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.9.tgz", + "integrity": "sha512-Zn9HSylDp89y+TRREMDoGrc3Z4Hs5u56ozZLQCiZAUx2+HdbbXbWdjmw3FdTJ/i7t5Cew6/Q+6kfO3KCcFGlyw==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.9.tgz", + "integrity": "sha512-OEiOxNAMH9ENFYqRsWUj3CWyN3V8P3ZXyfNAtX5rlCEC/ERXrCEFCJji/1F6POzsXAzxvUJrTSTCy7G6BhA6Fw==", + "dev": true, + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.9.tgz", + "integrity": "sha512-ukm4KsC3QRausEFjzTsOZ/qqazw0YvJsKmfoZZm9QW27OHjk2XKSQGGvx8gIEswft/Sadp03/VZvAaqv5AIwNA==", + "dev": true, + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.9.tgz", + "integrity": "sha512-uDOQEH55wQ6ahcIKzQr3VyjGc6Po/xblLGLoUk3fVL1qjlZAibtQr6XRfy5wPJLu/M2o0vQKLq4lyJ2r1tWKcw==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.9.tgz", + "integrity": "sha512-yWgxaYTQz+TqX80wXRq6xAtb7GSBAp6gqLKfOdANg9qEmAI1Bxn04IrQr0Mzm4AhxvGKoHzjHjMgXbCCSSDxcw==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.9.tgz", + "integrity": "sha512-JmS18acQl4iSAjrEha1MfEmUMN4FcnnrtTaJ7Qg0tDCOcgpPPQRLGsZqhes0vmx8VA6IqRyScqXvaL7+Q0Uf3A==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.9.tgz", + "integrity": "sha512-UKynGSWpzkPmXW3D2UMOD9BZPIuRaSqphxSCwScfEE05Be3KAmvjsBhht1fLzKpiFVJb0BYMd4jEbWMyJ/z1hQ==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.9.tgz", + "integrity": "sha512-aqXvu4/W9XyTVqO/hw3rNxKE1TcZiEYHPsXM9LwYmKSX9/hjvfIJzXwQBlPcJ/QOxedfoMVH0YnhhQ9Ffb0RGA==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.9.tgz", + "integrity": "sha512-zm7h91WUmlS4idMtjvCrEeNhlH7+TNOmqw5dJPJZrgFaxoFyqYG6CKDpdFCQXdyKpD5yvzaQBOMVTCBVKGZDEg==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.9.tgz", + "integrity": "sha512-yQEVIv27oauAtvtuhJVfSNMztJJX47ismRS6Sv2QMVV9RM+6xjbMWuuwM2nxr5A2/gj/mu2z9YlQxiwoFRCfZA==", + "dev": true, + "optional": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.24.0.tgz", + "integrity": "sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ==", + "dev": true, + "requires": { + "@eslint/eslintrc": "^1.3.2", + "@humanwhocodes/config-array": "^0.10.5", + "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", + "@humanwhocodes/module-importer": "^1.0.1", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.1", + "globals": "^13.15.0", + "globby": "^11.1.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "dependencies": { + "eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + } + } + }, + "eslint-config-standard": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.0.0.tgz", + "integrity": "sha512-/2ks1GKyqSOkH7JFvXJicu0iMpoojkwB+f5Du/1SC0PtBL+s8v30k9njRZ21pm2drKYm2342jFnGWzttxPmZVg==", + "dev": true + }, + "eslint-config-standard-with-typescript": { + "version": "23.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-23.0.0.tgz", + "integrity": "sha512-iaaWifImn37Z1OXbNW1es7KI+S7D408F9ys0bpaQf2temeBWlvb0Nc5qHkOgYaRb5QxTZT32GGeN1gtswASOXA==", + "dev": true, + "requires": { + "@typescript-eslint/parser": "^5.0.0", + "eslint-config-standard": "17.0.0" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-module-utils": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", + "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==", + "dev": true, + "requires": { + "debug": "^3.2.7" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-plugin-es": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", + "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", + "dev": true, + "requires": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", + "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "dev": true, + "requires": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.3", + "has": "^1.0.3", + "is-core-module": "^2.8.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.5", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "eslint-plugin-n": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.3.0.tgz", + "integrity": "sha512-IyzPnEWHypCWasDpxeJnim60jhlumbmq0pubL6IOcnk8u2y53s5QfT8JnXy7skjHJ44yWHRb11PLtDHuu1kg/Q==", + "dev": true, + "requires": { + "builtins": "^5.0.1", + "eslint-plugin-es": "^4.1.0", + "eslint-utils": "^3.0.0", + "ignore": "^5.1.1", + "is-core-module": "^2.10.0", + "minimatch": "^3.1.2", + "resolve": "^1.22.1", + "semver": "^7.3.7" + } + }, + "eslint-plugin-promise": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.0.1.tgz", + "integrity": "sha512-uM4Tgo5u3UWQiroOyDEsYcVMOo7re3zmno0IZmB5auxoaQNIceAbXEkSt8RNrKtaYehARHG06pYK6K1JhtP0Zw==", + "dev": true + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true + }, + "espree": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", + "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", + "dev": true, + "requires": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + } + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", + "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "js-sdsl": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.4.tgz", + "integrity": "sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "dependencies": { + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + } + } + }, + "regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + } + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, + "semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "shell-quote": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", + "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==", + "dev": true + }, + "string.prototype.padend": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.3.tgz", + "integrity": "sha512-jNIIeokznm8SD/TZISQsZKYu7RJyheFNt84DUPrh482GC8RVp2MKqm2O5oBRdGxbDQoXrhhWtPIWQOiy20svUg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tsconfig-paths": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typescript": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", + "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", + "dev": true + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/src/Web/Avalonia.Web/webapp/package.json b/src/Web/Avalonia.Web/webapp/package.json new file mode 100644 index 0000000000..8845dec604 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/package.json @@ -0,0 +1,22 @@ +{ + "name": "avalonia.web", + "scripts": { + "typecheck": "npx tsc -noEmit", + "eslint": "npx eslint . --fix", + "prebuild": "npm-run-all typecheck eslint", + "build": "node build.js" + }, + "devDependencies": { + "@types/emscripten": "^1.39.6", + "@types/wicg-file-system-access": "^2020.9.5", + "@typescript-eslint/eslint-plugin": "^5.38.1", + "esbuild": "^0.15.7", + "eslint": "^8.24.0", + "eslint-config-standard-with-typescript": "^23.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.3.0", + "eslint-plugin-promise": "^6.0.1", + "npm-run-all": "^4.1.5", + "typescript": "^4.8.3" + } +} diff --git a/src/Web/Avalonia.Web/webapp/tsconfig.json b/src/Web/Avalonia.Web/webapp/tsconfig.json new file mode 100644 index 0000000000..ad0e727150 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "es2020", + "strict": true, + "sourceMap": true, + "noEmitOnError": true, + "isolatedModules": true, // we need it for esbuild + "lib": [ + "dom", + "es2016", + "esnext.asynciterable" + ] + }, + "exclude": [ + "node_modules" + ] + } + \ No newline at end of file diff --git a/src/Web/Avalonia.Web/webapp/types/dotnet.d.ts b/src/Web/Avalonia.Web/webapp/types/dotnet.d.ts new file mode 100644 index 0000000000..0067ee3e0e --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/types/dotnet.d.ts @@ -0,0 +1,270 @@ +// See https://raw.githubusercontent.com/dotnet/runtime/main/src/mono/wasm/runtime/dotnet.d.ts + +//! Licensed to the .NET Foundation under one or more agreements. +//! The .NET Foundation licenses this file to you under the MIT license. +//! +//! This is generated file, see src/mono/wasm/runtime/rollup.config.js + +//! This is not considered public API with backward compatibility guarantees. + +interface DotnetHostBuilder { + withConfig(config: MonoConfig): DotnetHostBuilder; + withConfigSrc(configSrc: string): DotnetHostBuilder; + withApplicationArguments(...args: string[]): DotnetHostBuilder; + withEnvironmentVariable(name: string, value: string): DotnetHostBuilder; + withEnvironmentVariables(variables: { + [i: string]: string; + }): DotnetHostBuilder; + withVirtualWorkingDirectory(vfsPath: string): DotnetHostBuilder; + withDiagnosticTracing(enabled: boolean): DotnetHostBuilder; + withDebugging(level: number): DotnetHostBuilder; + withMainAssembly(mainAssemblyName: string): DotnetHostBuilder; + withApplicationArgumentsFromQuery(): DotnetHostBuilder; + create(): Promise; + run(): Promise; +} + +declare interface NativePointer { + __brandNativePointer: "NativePointer"; +} +declare interface VoidPtr extends NativePointer { + __brand: "VoidPtr"; +} +declare interface CharPtr extends NativePointer { + __brand: "CharPtr"; +} +declare interface Int32Ptr extends NativePointer { + __brand: "Int32Ptr"; +} +declare interface EmscriptenModule { + HEAP8: Int8Array; + HEAP16: Int16Array; + HEAP32: Int32Array; + HEAPU8: Uint8Array; + HEAPU16: Uint16Array; + HEAPU32: Uint32Array; + HEAPF32: Float32Array; + HEAPF64: Float64Array; + _malloc(size: number): VoidPtr; + _free(ptr: VoidPtr): void; + print(message: string): void; + printErr(message: string): void; + ccall(ident: string, returnType?: string | null, argTypes?: string[], args?: any[], opts?: any): T; + cwrap(ident: string, returnType: string, argTypes?: string[], opts?: any): T; + cwrap(ident: string, ...args: any[]): T; + setValue(ptr: VoidPtr, value: number, type: string, noSafe?: number | boolean): void; + setValue(ptr: Int32Ptr, value: number, type: string, noSafe?: number | boolean): void; + getValue(ptr: number, type: string, noSafe?: number | boolean): number; + UTF8ToString(ptr: CharPtr, maxBytesToRead?: number): string; + UTF8ArrayToString(u8Array: Uint8Array, idx?: number, maxBytesToRead?: number): string; + FS_createPath(parent: string, path: string, canRead?: boolean, canWrite?: boolean): string; + FS_createDataFile(parent: string, name: string, data: TypedArray, canRead: boolean, canWrite: boolean, canOwn?: boolean): string; + FS_readFile(filename: string, opts: any): any; + removeRunDependency(id: string): void; + addRunDependency(id: string): void; + stackSave(): VoidPtr; + stackRestore(stack: VoidPtr): void; + stackAlloc(size: number): VoidPtr; + ready: Promise; + instantiateWasm?: InstantiateWasmCallBack; + preInit?: (() => any)[] | (() => any); + preRun?: (() => any)[] | (() => any); + onRuntimeInitialized?: () => any; + postRun?: (() => any)[] | (() => any); + onAbort?: { + (error: any): void; + }; +} +declare type InstantiateWasmSuccessCallback = (instance: WebAssembly.Instance, module: WebAssembly.Module) => void; +declare type InstantiateWasmCallBack = (imports: WebAssembly.Imports, successCallback: InstantiateWasmSuccessCallback) => any; +declare type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array; + +declare type MonoConfig = { + /** + * The subfolder containing managed assemblies and pdbs. This is relative to dotnet.js script. + */ + assemblyRootFolder?: string; + /** + * A list of assets to load along with the runtime. + */ + assets?: AssetEntry[]; + /** + * Additional search locations for assets. + */ + remoteSources?: string[]; + /** + * It will not fail the startup is .pdb files can't be downloaded + */ + ignorePdbLoadErrors?: boolean; + /** + * We are throttling parallel downloads in order to avoid net::ERR_INSUFFICIENT_RESOURCES on chrome. The default value is 16. + */ + maxParallelDownloads?: number; + /** + * Name of the assembly with main entrypoint + */ + mainAssemblyName?: string; + /** + * Configures the runtime's globalization mode + */ + globalizationMode?: GlobalizationMode; + /** + * debugLevel > 0 enables debugging and sets the debug log level to debugLevel + * debugLevel == 0 disables debugging and enables interpreter optimizations + * debugLevel < 0 enabled debugging and disables debug logging. + */ + debugLevel?: number; + /** + * Enables diagnostic log messages during startup + */ + diagnosticTracing?: boolean; + /** + * Dictionary-style Object containing environment variables + */ + environmentVariables?: { + [i: string]: string; + }; + /** + * initial number of workers to add to the emscripten pthread pool + */ + pthreadPoolSize?: number; +}; +interface ResourceRequest { + name: string; + behavior: AssetBehaviours; + resolvedUrl?: string; + hash?: string; +} +interface LoadingResource { + name: string; + url: string; + response: Promise; +} +interface AssetEntry extends ResourceRequest { + /** + * If specified, overrides the path of the asset in the virtual filesystem and similar data structures once downloaded. + */ + virtualPath?: string; + /** + * Culture code + */ + culture?: string; + /** + * If true, an attempt will be made to load the asset from each location in MonoConfig.remoteSources. + */ + loadRemote?: boolean; + /** + * If true, the runtime startup would not fail if the asset download was not successful. + */ + isOptional?: boolean; + /** + * If provided, runtime doesn't have to fetch the data. + * Runtime would set the buffer to null after instantiation to free the memory. + */ + buffer?: ArrayBuffer; + /** + * It's metadata + fetch-like Promise + * If provided, the runtime doesn't have to initiate the download. It would just await the response. + */ + pendingDownload?: LoadingResource; +} +declare type AssetBehaviours = "resource" | "assembly" | "pdb" | "heap" | "icu" | "vfs" | "dotnetwasm" | "js-module-threads"; +declare type GlobalizationMode = "icu" | // load ICU globalization data from any runtime assets with behavior "icu". +"invariant" | // operate in invariant globalization mode. +"auto"; +declare type DotnetModuleConfig = { + disableDotnet6Compatibility?: boolean; + config?: MonoConfig; + configSrc?: string; + onConfigLoaded?: (config: MonoConfig) => void | Promise; + onDotnetReady?: () => void | Promise; + imports?: any; + exports?: string[]; + downloadResource?: (request: ResourceRequest) => LoadingResource | undefined; +} & Partial; +declare type APIType = { + runMain: (mainAssemblyName: string, args: string[]) => Promise; + runMainAndExit: (mainAssemblyName: string, args: string[]) => Promise; + setEnvironmentVariable: (name: string, value: string) => void; + getAssemblyExports(assemblyName: string): Promise; + setModuleImports(moduleName: string, moduleImports: any): void; + getConfig: () => MonoConfig; + setHeapB32: (offset: NativePointer, value: number | boolean) => void; + setHeapU8: (offset: NativePointer, value: number) => void; + setHeapU16: (offset: NativePointer, value: number) => void; + setHeapU32: (offset: NativePointer, value: NativePointer | number) => void; + setHeapI8: (offset: NativePointer, value: number) => void; + setHeapI16: (offset: NativePointer, value: number) => void; + setHeapI32: (offset: NativePointer, value: number) => void; + setHeapI52: (offset: NativePointer, value: number) => void; + setHeapU52: (offset: NativePointer, value: number) => void; + setHeapI64Big: (offset: NativePointer, value: bigint) => void; + setHeapF32: (offset: NativePointer, value: number) => void; + setHeapF64: (offset: NativePointer, value: number) => void; + getHeapB32: (offset: NativePointer) => boolean; + getHeapU8: (offset: NativePointer) => number; + getHeapU16: (offset: NativePointer) => number; + getHeapU32: (offset: NativePointer) => number; + getHeapI8: (offset: NativePointer) => number; + getHeapI16: (offset: NativePointer) => number; + getHeapI32: (offset: NativePointer) => number; + getHeapI52: (offset: NativePointer) => number; + getHeapU52: (offset: NativePointer) => number; + getHeapI64Big: (offset: NativePointer) => bigint; + getHeapF32: (offset: NativePointer) => number; + getHeapF64: (offset: NativePointer) => number; +}; +declare type RuntimeAPI = { + /** + * @deprecated Please use API object instead. See also MONOType in dotnet-legacy.d.ts + */ + MONO: any; + /** + * @deprecated Please use API object instead. See also BINDINGType in dotnet-legacy.d.ts + */ + BINDING: any; + INTERNAL: any; + Module: EmscriptenModule; + runtimeId: number; + runtimeBuildInfo: { + productVersion: string; + gitHash: string; + buildConfiguration: string; + }; +} & APIType; +declare type ModuleAPI = { + dotnet: DotnetHostBuilder; + exit: (code: number, reason?: any) => void; +}; +declare function createDotnetRuntime(moduleFactory: DotnetModuleConfig | ((api: RuntimeAPI) => DotnetModuleConfig)): Promise; +declare type CreateDotnetRuntimeType = typeof createDotnetRuntime; + +declare global { + function getDotnetRuntime(runtimeId: number): RuntimeAPI | undefined; +} + +declare const dotnet: ModuleAPI["dotnet"]; +declare const exit: ModuleAPI["exit"]; + +export { CreateDotnetRuntimeType, DotnetModuleConfig, EmscriptenModule, ModuleAPI, MonoConfig, RuntimeAPI, createDotnetRuntime as default, dotnet, exit }; + +export interface IMemoryView { + /** + * copies elements from provided source to the wasm memory. + * target has to have the elements of the same type as the underlying C# array. + * same as TypedArray.set() + */ + set(source: TypedArray, targetOffset?: number): void; + /** + * copies elements from wasm memory to provided target. + * target has to have the elements of the same type as the underlying C# array. + */ + copyTo(target: TypedArray, sourceOffset?: number): void; + /** + * same as TypedArray.slice() + */ + slice(start?: number, end?: number): TypedArray; + + get length(): number; + get byteLength(): number; +} diff --git a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj index e05566c454..99d4fd2a27 100644 --- a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj +++ b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj @@ -4,6 +4,12 @@ 13.0 true + + + + + + diff --git a/src/iOS/Avalonia.iOS/CombinedSpan3.cs b/src/iOS/Avalonia.iOS/CombinedSpan3.cs index 29d8eb3955..e9f44b7d58 100644 --- a/src/iOS/Avalonia.iOS/CombinedSpan3.cs +++ b/src/iOS/Avalonia.iOS/CombinedSpan3.cs @@ -16,7 +16,7 @@ internal ref struct CombinedSpan3 public int Length => Span1.Length + Span2.Length + Span3.Length; - void CopyFromSpan(ReadOnlySpan from, ref int offset, ref Span to) + void CopyFromSpan(ReadOnlySpan from, int offset, ref Span to) { if(to.Length == 0) return; @@ -33,8 +33,8 @@ internal ref struct CombinedSpan3 public void CopyTo(Span to, int offset) { - CopyFromSpan(Span1, ref offset, ref to); - CopyFromSpan(Span2, ref offset, ref to); - CopyFromSpan(Span3, ref offset, ref to); + CopyFromSpan(Span1, offset, ref to); + CopyFromSpan(Span2, offset, ref to); + CopyFromSpan(Span3, offset, ref to); } } diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index aa32af7e51..21438543d5 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -29,11 +29,36 @@ namespace Avalonia.Controls.UnitTests _helper.Down(target); _helper.Up(target); Assert.True(target.IsDropDownOpen); + Assert.True(target.Classes.Contains(ComboBox.pcDropdownOpen)); _helper.Down(target); _helper.Up(target); Assert.False(target.IsDropDownOpen); + Assert.True(!target.Classes.Contains(ComboBox.pcDropdownOpen)); + } + + [Fact] + public void Clicking_On_Control_PseudoClass() + { + var target = new ComboBox + { + Items = new[] { "Foo", "Bar" }, + }; + + _helper.Down(target); + Assert.True(target.Classes.Contains(ComboBox.pcPressed)); + _helper.Up(target); + Assert.True(!target.Classes.Contains(ComboBox.pcPressed)); + Assert.True(target.Classes.Contains(ComboBox.pcDropdownOpen)); + + _helper.Down(target); + Assert.True(target.Classes.Contains(ComboBox.pcPressed)); + _helper.Up(target); + Assert.True(!target.Classes.Contains(ComboBox.pcPressed)); + + Assert.False(target.IsDropDownOpen); + Assert.True(!target.Classes.Contains(ComboBox.pcDropdownOpen)); } [Fact]