Browse Source

Merge

pull/9028/head
Gillibald 4 years ago
parent
commit
f0a4f0a3ba
  1. 0
      .nuke
  2. 148
      .nuke/build.schema.json
  3. 4
      .nuke/parameters.json
  4. 5
      .nuke/temp/build-attempt.log
  5. 30
      .nuke/temp/build.2022-09-27_08-36-07.log
  6. 0
      .nuke/temp/build.log
  7. 14
      azure-pipelines.yml
  8. 7
      build.cmd
  9. 50
      build.ps1
  10. 60
      build.sh
  11. 6
      nukebuild/Build.cs
  12. 4
      nukebuild/BuildParameters.cs
  13. 12
      nukebuild/BuildTasksPatcher.cs
  14. 4
      nukebuild/DotNetConfigHelper.cs
  15. 4
      nukebuild/_build.csproj
  16. 2
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  17. 2
      src/Avalonia.Base/Platform/Storage/IStorageFile.cs
  18. 2
      src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs
  19. 1
      src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs
  20. 3
      src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
  21. 7
      src/Avalonia.X11/X11IconLoader.cs
  22. 2
      src/Avalonia.X11/X11Platform.cs
  23. 2
      src/Shared/ModuleInitializer.cs
  24. 4
      src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj
  25. 44
      src/Web/Avalonia.Web.Sample/EmbedSample.Browser.cs
  26. 5
      src/Web/Avalonia.Web.Sample/Logo.svg
  27. 9
      src/Web/Avalonia.Web.Sample/Program.cs
  28. 38
      src/Web/Avalonia.Web.Sample/app.css
  29. 11
      src/Web/Avalonia.Web.Sample/embed.js
  30. BIN
      src/Web/Avalonia.Web.Sample/favicon.ico
  31. 13
      src/Web/Avalonia.Web.Sample/index.html
  32. 1
      src/Web/Avalonia.Web/Avalonia.Web.csproj
  33. 28
      src/Web/Avalonia.Web/AvaloniaView.cs
  34. 136
      src/Web/Avalonia.Web/BrowserNativeControlHost.cs
  35. 1
      src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs
  36. 12
      src/Web/Avalonia.Web/BrowserTopLevelImpl.cs
  37. 5
      src/Web/Avalonia.Web/Interop/DomHelper.cs
  38. 8
      src/Web/Avalonia.Web/Interop/InputHelper.cs
  39. 28
      src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs
  40. 55
      src/Web/Avalonia.Web/Interop/StorageHelper.cs
  41. 35
      src/Web/Avalonia.Web/Interop/StreamHelper.cs
  42. 30
      src/Web/Avalonia.Web/JSObjectControlHandle.cs
  43. 2
      src/Web/Avalonia.Web/Skia/BrowserSkiaGpu.cs
  44. 2
      src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderSession.cs
  45. 2
      src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderTarget.cs
  46. 2
      src/Web/Avalonia.Web/Skia/BrowserSkiaRasterSurface.cs
  47. 4
      src/Web/Avalonia.Web/Skia/BrowserSkiaSurface.cs
  48. 2
      src/Web/Avalonia.Web/Skia/IBrowserSkiaSurface.cs
  49. 27
      src/Web/Avalonia.Web/Storage/BlobReadableStream.cs
  50. 257
      src/Web/Avalonia.Web/Storage/BrowserStorageProvider.cs
  51. 27
      src/Web/Avalonia.Web/Storage/WriteableStream.cs
  52. 68
      src/Web/Avalonia.Web/WebEmbeddableControlRoot.cs
  53. 47
      src/Web/Avalonia.Web/webapp/.eslintrc.json
  54. 8
      src/Web/Avalonia.Web/webapp/build.js
  55. 14
      src/Web/Avalonia.Web/webapp/modules/avalonia.ts
  56. 13
      src/Web/Avalonia.Web/webapp/modules/avalonia/caniuse.ts
  57. 137
      src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts
  58. 22
      src/Web/Avalonia.Web/webapp/modules/avalonia/caretHelper.ts
  59. 19
      src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts
  60. 51
      src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts
  61. 55
      src/Web/Avalonia.Web/webapp/modules/avalonia/nativeControlHost.ts
  62. 40
      src/Web/Avalonia.Web/webapp/modules/avalonia/stream.ts
  63. 7
      src/Web/Avalonia.Web/webapp/modules/storage.ts
  64. 84
      src/Web/Avalonia.Web/webapp/modules/storage/indexedDb.ts
  65. 111
      src/Web/Avalonia.Web/webapp/modules/storage/storageItem.ts
  66. 70
      src/Web/Avalonia.Web/webapp/modules/storage/storageProvider.ts
  67. 2330
      src/Web/Avalonia.Web/webapp/package-lock.json
  68. 15
      src/Web/Avalonia.Web/webapp/package.json
  69. 23
      src/Web/Avalonia.Web/webapp/types/dotnet.d.ts
  70. 3
      src/Windows/Avalonia.Win32/Avalonia.Win32.csproj

0
.nuke/temp/build.2022-09-27_08-37-17.log → .nuke

148
.nuke/build.schema.json

@ -1,148 +0,0 @@
{
"$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"
]
}
}
}
}
}

4
.nuke/parameters.json

@ -1,4 +0,0 @@
{
"$schema": "./build.schema.json",
"Solution": ""
}

5
.nuke/temp/build-attempt.log

@ -1,5 +0,0 @@
47be5be6de651ed0c6d6996318ce431a
GenerateCppHeaders
Clean
CompileNative
CompileHtmlPreviewer

30
.nuke/temp/build.2022-09-27_08-36-07.log

@ -1,30 +0,0 @@
V | | ArgumentsFromParametersFile.OnBuildCreated (150)
V | | Passing value for Build.Solution ()
V | | InjectParameterValues.OnBuildCreated (100)
W | | Could not inject value for Build.Solution
System.ArgumentException: Solution 'C:\Users\User\repos\Avalonia\Avalonia.sln' contains duplicated NestedProjects entries:
- {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}
at Nuke.Common.Assert.True(Boolean condition, String message, String argumentExpression) in /_/source/Nuke.Common/Assert.cs:line 34
at Nuke.Common.Utilities.Collections.EnumerableExtensions.ToDictionarySafe[T,TKey,TValue](IEnumerable`1 enumerable, Func`2 keySelector, Func`2 valueSelector, String duplicationMessage) in /_/source/Nuke.Common/Utilities/Collections/Enumerable.ToDictionary.cs:line 46
at Nuke.Common.ProjectModel.SolutionSerializer.GetGlobalSection(String[] lines, String name, String solutionFile) in /_/source/Nuke.Common/ProjectModel/SolutionSerializer.cs:line 92
at Nuke.Common.ProjectModel.SolutionSerializer.DeserializeFromContent[T](String[] content, String solutionFile) in /_/source/Nuke.Common/ProjectModel/SolutionSerializer.cs:line 63
at Nuke.Common.ProjectModel.SolutionSerializer.DeserializeFromFile[T](String solutionFile) in /_/source/Nuke.Common/ProjectModel/SolutionSerializer.cs:line 23
V | | HandleShellCompletion.OnBuildCreated (75)
V | | GenerateBuildServerConfigurations.OnBuildCreated (50)
V | | InvokeBuildServerConfigurationGeneration.OnBuildCreated (45)
V | | UpdateNotification.OnBuildCreated (10)
V | | UnsetVisualStudioEnvironmentVariables.OnBuildCreated (0)
V | | HandleVisualStudioDebugging.OnBuildCreated (0)
V | | HandleSingleFileExecution.OnBuildCreated (-50)
V | | EventInvoker.OnBuildCreated (-3.4028235E+38)
V | | HandleHelpRequests.OnBuildInitialized (5)
V | | Telemetry.OnBuildInitialized (0)
V | | InjectNonParameterValues.OnBuildInitialized (-100)
V | | EventInvoker.OnBuildInitialized (-3.4028235E+38)
V | | Target-unrelated exception was thrown
System.NullReferenceException: Object reference not set to an instance of an object.
at Build.OnBuildInitialized() in C:\Users\User\repos\Avalonia\nukebuild\Build.cs:line 42
at Nuke.Common.Execution.EventInvoker.OnBuildInitialized(NukeBuild build, IReadOnlyCollection`1 executableTargets, IReadOnlyCollection`1 executionPlan) in /_/source/Nuke.Common/Execution/EventInvoker.cs:line 27
at Nuke.Common.Utilities.Collections.EnumerableExtensions.ForEach[T](IEnumerable`1 enumerable, Action`1 action) in /_/source/Nuke.Common/Utilities/Collections/Enumerable.ForEach.cs:line 17
at Nuke.Common.NukeBuild.ExecuteExtension[TExtension](Expression`1 action)
at Nuke.Common.Execution.BuildManager.Execute[T](Expression`1[] defaultTargetExpressions) in /_/source/Nuke.Common/Execution/BuildManager.cs:line 58

0
.nuke/temp/build.log

14
azure-pipelines.yml

@ -30,9 +30,9 @@ jobs:
vmImage: 'ubuntu-20.04'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.423'
displayName: 'Use .NET Core SDK 3.1.418'
inputs:
version: 3.1.423
version: 3.1.418
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.401'
@ -72,9 +72,9 @@ jobs:
vmImage: 'macos-12'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.423'
displayName: 'Use .NET Core SDK 3.1.418'
inputs:
version: 3.1.423
version: 3.1.418
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.401'
@ -155,9 +155,9 @@ jobs:
SolutionDir: '$(Build.SourcesDirectory)'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.423'
displayName: 'Use .NET Core SDK 3.1.418'
inputs:
version: 3.1.423
version: 3.1.418
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.401'
@ -179,7 +179,7 @@ jobs:
displayName: 'Install Nuke'
inputs:
script: |
dotnet tool install --global Nuke.GlobalTool --version 6.2.1
dotnet tool install --global Nuke.GlobalTool --version 0.24.0
- task: CmdLine@2
displayName: 'Run Nuke'

7
build.cmd

@ -1,7 +0,0 @@
:; 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" %*

50
build.ps1

@ -1,12 +1,13 @@
[CmdletBinding()]
Param(
#[switch]$CustomParam,
[Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
[string[]]$BuildArguments
)
Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)"
Write-Output "Windows PowerShell $($Host.Version)"
Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 }
Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { exit 1 }
$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
###########################################################################
@ -14,15 +15,15 @@ $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
###########################################################################
$BuildProjectFile = "$PSScriptRoot\nukebuild\_build.csproj"
$TempDirectory = "$PSScriptRoot\\.nuke\temp"
$TempDirectory = "$PSScriptRoot\\.tmp"
$DotNetGlobalFile = "$PSScriptRoot\\global.json"
$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1"
$DotNetInstallUrl = "https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.ps1"
$DotNetChannel = "Current"
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1
$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1
$env:DOTNET_MULTILEVEL_LOOKUP = 0
$env:NUGET_XMLDOC_MODE = "skip"
###########################################################################
# EXECUTION
@ -33,37 +34,38 @@ function ExecSafe([scriptblock] $cmd) {
if ($LASTEXITCODE) { exit $LASTEXITCODE }
}
# If dotnet CLI is installed globally and it matches requested version, use for execution
# 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 ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and `
$(dotnet --version) -and $LASTEXITCODE -eq 0) {
(!(Test-Path variable:DotNetVersion) -or $(& dotnet --version) -eq $DotNetVersion)) {
$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"
New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
mkdir -force $TempDirectory > $null
(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 { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath }
ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath }
} else {
ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath }
ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath }
}
$env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe"
$env:PATH="$DotNetDirectory;$env:PATH"
}
Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)"
Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)"
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 }
ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile -- $BuildArguments }

60
build.sh

@ -1,6 +1,16 @@
#!/usr/bin/env bash
bash --version 2>&1 | head -n 1
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
set -eo pipefail
SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
@ -10,53 +20,11 @@ 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 DOTNET_MULTILEVEL_LOOKUP=0
export NUGET_XMLDOC_MODE="skip"
###########################################################################
# EXECUTION
###########################################################################
dotnet --info
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 -- "$@"
dotnet run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]}

6
nukebuild/Build.cs

@ -39,7 +39,6 @@ partial class Build : NukeBuild
BuildParameters Parameters { get; set; }
protected override void OnBuildInitialized()
{
Console.WriteLine($"{Solution.Name}");
Parameters = new BuildParameters(this);
Information("Building version {0} of Avalonia ({1}) using version {2} of Nuke.",
Parameters.Version,
@ -144,7 +143,6 @@ partial class Build : NukeBuild
void RunCoreTest(string projectName)
{
Information($"Running tests from {projectName}");
Console.WriteLine(Solution == null);
var project = Solution.GetProject(projectName).NotNull("project != null");
foreach (var fw in project.GetTargetFrameworks())
@ -165,7 +163,7 @@ partial class Build : NukeBuild
.EnableNoBuild()
.EnableNoRestore()
.When(Parameters.PublishTestResults, _ => _
.SetLoggers("trx")
.SetLogger("trx")
.SetResultsDirectory(Parameters.TestResultsRoot)));
}
}
@ -231,7 +229,7 @@ partial class Build : NukeBuild
$"{XunitPath.DoubleQuoteIfNeeded()} --propagate-exit-code -- {testAssembly}",
timeout: 120_000);
}
ControlFlow.ExecuteWithRetry(DoMemoryTest, delay: TimeSpan.FromSeconds(3));
ControlFlow.ExecuteWithRetry(DoMemoryTest, waitInSeconds: 3);
});
Target ZipFiles => _ => _

4
nukebuild/BuildParameters.cs

@ -74,11 +74,11 @@ public partial class Build
MSBuildSolution = RootDirectory / "dirs.proj";
// PARAMETERS
IsLocalBuild = NukeBuild.IsLocalBuild;
IsLocalBuild = Host == HostType.Console;
IsRunningOnUnix = Environment.OSVersion.Platform == PlatformID.Unix ||
Environment.OSVersion.Platform == PlatformID.MacOSX;
IsRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
IsRunningOnAzure = NukeBuild.IsServerBuild ||
IsRunningOnAzure = Host == HostType.AzurePipelines ||
Environment.GetEnvironmentVariable("LOGNAME") == "vsts";
if (IsRunningOnAzure)

12
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
{

4
nukebuild/DotNetConfigHelper.cs

@ -46,7 +46,7 @@ public class DotNetConfigHelper
public DotNetConfigHelper SetVerbosity(DotNetVerbosity verbosity)
{
Build = Build?.SetVerbosity(verbosity);
Pack = Pack?.SetVerbosity(verbosity);
Pack = Pack?.SetVerbostiy(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);
}
}

4
nukebuild/_build.csproj

@ -1,5 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
@ -7,11 +6,10 @@
<RootNamespace></RootNamespace>
<IsPackable>False</IsPackable>
<NoWarn>CS0649;CS0169</NoWarn>
<NukeTelemetryVersion>1</NukeTelemetryVersion>
</PropertyGroup>
<Import Project="..\build\JetBrains.dotMemoryUnit.props" />
<ItemGroup>
<PackageReference Include="Nuke.Common" Version="6.2.1" />
<PackageReference Include="Nuke.Common" Version="5.0.0" />
<PackageReference Include="xunit.runner.console" Version="2.3.1" />
<PackageReference Include="vswhere" Version="2.6.7" Condition=" '$(OS)' == 'Windows_NT' " />
<PackageReference Include="ILRepack.NETStandard" Version="2.0.4" />

2
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

2
src/Avalonia.Base/Platform/Storage/IStorageFile.cs

@ -18,6 +18,7 @@ public interface IStorageFile : IStorageItem
/// <summary>
/// Opens a stream for read access.
/// </summary>
/// <exception cref="System.UnauthorizedAccessException" />
Task<Stream> OpenReadAsync();
/// <summary>
@ -28,5 +29,6 @@ public interface IStorageFile : IStorageItem
/// <summary>
/// Opens stream for writing to the file.
/// </summary>
/// <exception cref="System.UnauthorizedAccessException" />
Task<Stream> OpenWriteAsync();
}

2
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;

1
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;

3
src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj

@ -9,9 +9,6 @@
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
<ProjectReference Include="..\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />

7
src/Avalonia.X11/X11IconLoader.cs

@ -9,13 +9,6 @@ namespace Avalonia.X11
{
class X11IconLoader : IPlatformIconLoader
{
private readonly X11Info _x11;
public X11IconLoader(X11Info x11)
{
_x11 = x11;
}
IWindowIconImpl LoadIcon(Bitmap bitmap)
{
var rv = new X11IconData(bitmap);

2
src/Avalonia.X11/X11Platform.cs

@ -81,7 +81,7 @@ namespace Avalonia.X11
.Bind<ICursorFactory>().ToConstant(new X11CursorFactory(Display))
.Bind<IClipboard>().ToConstant(new X11Clipboard(this))
.Bind<IPlatformSettings>().ToConstant(new PlatformSettingsStub())
.Bind<IPlatformIconLoader>().ToConstant(new X11IconLoader(Info))
.Bind<IPlatformIconLoader>().ToConstant(new X11IconLoader())
.Bind<IMountedVolumeInfoProvider>().ToConstant(new LinuxMountedVolumeInfoProvider())
.Bind<IPlatformLifetimeEventsImpl>().ToConstant(new X11PlatformLifetimeEvents(this));

2
src/Shared/ModuleInitializer.cs

@ -1,6 +1,6 @@
namespace System.Runtime.CompilerServices
{
#if !NET5_0_OR_GREATER
#if NETSTANDARD2_0
internal class ModuleInitializerAttribute : Attribute
{

4
src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj

@ -30,6 +30,10 @@
<ItemGroup>
<WasmExtraFilesToDeploy Include="index.html" />
<WasmExtraFilesToDeploy Include="main.js" />
<WasmExtraFilesToDeploy Include="embed.js" />
<WasmExtraFilesToDeploy Include="favicon.ico" />
<WasmExtraFilesToDeploy Include="Logo.svg" />
<WasmExtraFilesToDeploy Include="app.css" />
</ItemGroup>
<Import Project="..\Avalonia.Web\Avalonia.Web.props" />

44
src/Web/Avalonia.Web.Sample/EmbedSample.Browser.cs

@ -0,0 +1,44 @@
using System;
using System.Runtime.InteropServices.JavaScript;
using Avalonia;
using Avalonia.Platform;
using Avalonia.Web;
using ControlCatalog.Pages;
namespace ControlCatalog.Web;
public class EmbedSampleWeb : INativeDemoControl
{
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault)
{
if (isSecond)
{
var iframe = EmbedInterop.CreateElement("iframe");
iframe.SetProperty("src", "https://www.youtube.com/embed/kZCIporjJ70");
return new JSObjectControlHandle(iframe);
}
else
{
var defaultHandle = (JSObjectControlHandle)createDefault();
_ = JSHost.ImportAsync("embed.js", "./embed.js").ContinueWith(_ =>
{
EmbedInterop.AddAppButton(defaultHandle.Object);
});
return defaultHandle;
}
}
}
internal static partial class EmbedInterop
{
[JSImport("globalThis.document.createElement")]
public static partial JSObject CreateElement(string tagName);
[JSImport("addAppButton", "embed.js")]
public static partial void AddAppButton(JSObject parentObject);
}

5
src/Web/Avalonia.Web.Sample/Logo.svg

@ -0,0 +1,5 @@
<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30.4661 34.928C30.5364 34.928 30.6052 34.928 30.6754 34.928C32.8596 34.928 34.654 33.2918 34.9053 31.1752L34.9356 16.9955C34.6872 7.56697 26.9662 0 17.4777 0C7.83263 0 0.0137329 7.8189 0.0137329 17.464C0.0137329 27.0059 7.66618 34.7631 17.1687 34.928H30.4661Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5239 5.948C12.0268 5.948 7.42967 9.80117 6.286 14.954C7.38092 15.2609 8.18385 16.2664 8.18385 17.4593C8.18385 18.6523 7.38092 19.6577 6.286 19.9647C7.42966 25.1175 12.0268 28.9706 17.5239 28.9706C19.525 28.9706 21.4068 28.4601 23.0462 27.562V28.8927H29.0352V17.9365C29.0407 17.7908 29.0352 17.6063 29.0352 17.4593C29.0352 11.1018 23.8814 5.948 17.5239 5.948ZM12.0098 17.4593C12.0098 14.414 14.4786 11.9452 17.5239 11.9452C20.5693 11.9452 23.038 14.414 23.038 17.4593C23.038 20.5047 20.5693 22.9734 17.5239 22.9734C14.4786 22.9734 12.0098 20.5047 12.0098 17.4593Z" fill="#8B44AC"/>
<path d="M7.36841 17.4517C7.36841 18.4691 6.54368 19.2938 5.52631 19.2938C4.50894 19.2938 3.6842 18.4691 3.6842 17.4517C3.6842 16.4343 4.50894 15.6096 5.52631 15.6096C6.54368 15.6096 7.36841 16.4343 7.36841 17.4517Z" fill="#8B44AC"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

9
src/Web/Avalonia.Web.Sample/Program.cs

@ -1,12 +1,19 @@
using Avalonia;
using Avalonia.Web;
using ControlCatalog;
using ControlCatalog.Web;
internal partial class Program
{
private static void Main(string[] args)
{
Emscripten.Log(EM_LOG.ERROR, "MyError");
Emscripten.Log(EM_LOG.INFO, "Call from Main");
BuildAvaloniaApp()
.AfterSetup(_ =>
{
ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb();
}).SetupBrowserApp("out");
BuildAvaloniaApp().SetupBrowserApp("out");
}

38
src/Web/Avalonia.Web.Sample/app.css

@ -0,0 +1,38 @@
#out {
height: 100vh;
width: 100vw
}
#avalonia-splash {
position: absolute;
height: 100%;
width: 100%;
color: whitesmoke;
background: #171C2C;
font-family: 'Nunito', sans-serif;
}
#avalonia-splash a{
color: whitesmoke;
text-decoration: none;
}
.center {
display: flex;
justify-content: center;
height: 250px;
}
.splash-close {
animation: fadeOut 1s forwards;
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

11
src/Web/Avalonia.Web.Sample/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;
}

BIN
src/Web/Avalonia.Web.Sample/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

13
src/Web/Avalonia.Web.Sample/index.html

@ -10,10 +10,21 @@
<link rel="modulepreload" href="./main.js" />
<link rel="modulepreload" href="./dotnet.js" />
<link rel="modulepreload" href="./avalonia.js" />
<link rel="stylesheet" href="./app.css" />
</head>
<body style="margin: 0px">
<div id="out" style="height: 100vh; width: 100vw"></div>
<div id="out">
<div id="avalonia-splash">
<div class="center">
<h2>Powered by</h2>
<a class="navbar-brand" href="https://www.avaloniaui.net/" target="_blank">
<img src="Logo.svg" alt="Avalonia Logo" width="30" height="24" />
Avalonia
</a>
</div>
</div>
</div>
<script type='module' src="./main.js"></script>
</body>

1
src/Web/Avalonia.Web/Avalonia.Web.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

28
src/Web/Avalonia.Web/AvaloniaView.cs

@ -6,9 +6,11 @@ using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Platform.Storage;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using Avalonia.Web.Interop;
using Avalonia.Web.Skia;
using SkiaSharp;
namespace Avalonia.Web
@ -23,6 +25,7 @@ namespace Avalonia.Web
private readonly JSObject _canvas;
private readonly JSObject _nativeControlsContainer;
private readonly JSObject _inputElement;
private readonly JSObject? _splash;
private GLInfo? _jsGlInfo = null;
private double _dpi = 1;
@ -59,11 +62,23 @@ namespace Avalonia.Web
_inputElement = hostContent.GetPropertyAsJSObject("inputElement")
?? throw new InvalidOperationException("InputElement cannot be null");
_splash = DomHelper.GetElementById("avalonia-splash");
_canvas.SetProperty("id", $"avaloniaCanvas{_canvasCount++}");
_topLevelImpl = new BrowserTopLevelImpl(this);
_topLevel = new EmbeddableControlRoot(_topLevelImpl);
_topLevel = new WebEmbeddableControlRoot(_topLevelImpl, () =>
{
Dispatcher.UIThread.Post(() =>
{
if (_splash != null)
{
DomHelper.AddCssClass(_splash, "splash-close");
}
});
});
_topLevelImpl.SetCssCursor = (cursor) =>
{
InputHelper.SetCursor(_containerElement, cursor); // macOS
@ -318,14 +333,7 @@ namespace Avalonia.Web
internal INativeControlHostImpl GetNativeControlHostImpl()
{
throw new NotImplementedException();
//return _nativeControlHost ?? throw new InvalidOperationException("Blazor View wasn't initialized yet");
}
internal IStorageProvider GetStorageProvider()
{
throw new NotImplementedException();
//return _storageProvider ?? throw new InvalidOperationException("Blazor View wasn't initialized yet");
return new BrowserNativeControlHost(_nativeControlsContainer);
}
private void ForceBlit()

136
src/Web/Avalonia.Web/BrowserNativeControlHost.cs

@ -0,0 +1,136 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices.JavaScript;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using Avalonia.Web.Interop;
namespace Avalonia.Web
{
internal class BrowserNativeControlHost : INativeControlHostImpl
{
private readonly JSObject _hostElement;
public BrowserNativeControlHost(JSObject element)
{
_hostElement = element;
}
public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent)
{
var element = NativeControlHostHelper.CreateDefaultChild(null);
return new JSObjectControlHandle(element);
}
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func<IPlatformHandle, IPlatformHandle> create)
{
Attachment? a = null;
try
{
var child = create(new JSObjectControlHandle(_hostElement));
var attachmenetReference = NativeControlHostHelper.CreateAttachment();
// It has to be assigned to the variable before property setter is called so we dispose it on exception
#pragma warning disable IDE0017 // Simplify object initialization
a = new Attachment(attachmenetReference, child);
#pragma warning restore IDE0017 // Simplify object initialization
a.AttachedTo = this;
return a;
}
catch
{
a?.Dispose();
throw;
}
}
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle)
{
var attachmenetReference = NativeControlHostHelper.CreateAttachment();
var a = new Attachment(attachmenetReference, handle);
a.AttachedTo = this;
return a;
}
public bool IsCompatibleWith(IPlatformHandle handle) => handle is JSObjectControlHandle;
private class Attachment : INativeControlHostControlTopLevelAttachment
{
private const string InitializeWithChildHandleSymbol = "InitializeWithChildHandle";
private const string AttachToSymbol = "AttachTo";
private const string ShowInBoundsSymbol = "ShowInBounds";
private const string HideWithSizeSymbol = "HideWithSize";
private const string ReleaseChildSymbol = "ReleaseChild";
private JSObject? _native;
private BrowserNativeControlHost? _attachedTo;
public Attachment(JSObject native, IPlatformHandle handle)
{
_native = native;
NativeControlHostHelper.InitializeWithChildHandle(_native, ((JSObjectControlHandle)handle).Object);
}
public void Dispose()
{
if (_native != null)
{
NativeControlHostHelper.ReleaseChild(_native);
_native.Dispose();
_native = null;
}
}
public INativeControlHostImpl? AttachedTo
{
get => _attachedTo!;
set
{
CheckDisposed();
var host = (BrowserNativeControlHost?)value;
if (host == null)
{
NativeControlHostHelper.AttachTo(_native, null);
}
else
{
NativeControlHostHelper.AttachTo(_native, host._hostElement);
}
_attachedTo = host;
}
}
public bool IsCompatibleWith(INativeControlHostImpl host) => host is BrowserNativeControlHost;
public void HideWithSize(Size size)
{
CheckDisposed();
if (_attachedTo == null)
return;
NativeControlHostHelper.HideWithSize(_native, Math.Max(1, size.Width), Math.Max(1, size.Height));
}
public void ShowInBounds(Rect bounds)
{
CheckDisposed();
if (_attachedTo == null)
throw new InvalidOperationException("Native control isn't attached to a toplevel");
bounds = new Rect(bounds.X, bounds.Y, Math.Max(1, bounds.Width),
Math.Max(1, bounds.Height));
NativeControlHostHelper.ShowInBounds(_native, bounds.X, bounds.Y, bounds.Width, bounds.Height);
}
[MemberNotNull(nameof(_native))]
private void CheckDisposed()
{
if (_native == null)
throw new ObjectDisposedException(nameof(Attachment));
}
}
}
}

1
src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs

@ -3,6 +3,7 @@ using System;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Media;
using Avalonia.Web.Skia;
namespace Avalonia.Web
{

12
src/Web/Avalonia.Web/BrowserTopLevelImpl.cs

@ -11,8 +11,8 @@ using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
#nullable enable
using Avalonia.Web.Skia;
using Avalonia.Web.Storage;
namespace Avalonia.Web
{
@ -35,13 +35,11 @@ namespace Avalonia.Web
AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1);
_touchDevice = new TouchDevice();
_penDevice = new PenDevice();
NativeControlHost = _avaloniaView.GetNativeControlHostImpl();
}
public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds;
public void SetClientSize(Size newSize, double dpi)
{
if (Math.Abs(RenderScaling - dpi) > 0.0001)
@ -222,7 +220,7 @@ namespace Avalonia.Web
public ITextInputMethodImpl TextInputMethod => _avaloniaView;
public INativeControlHostImpl? NativeControlHost => _avaloniaView.GetNativeControlHostImpl();
public IStorageProvider StorageProvider => _avaloniaView.GetStorageProvider();
public INativeControlHostImpl? NativeControlHost { get; }
public IStorageProvider StorageProvider { get; } = new BrowserStorageProvider();
}
}

5
src/Web/Avalonia.Web/Interop/DomHelper.cs

@ -6,11 +6,14 @@ namespace Avalonia.Web.Interop;
internal static partial class DomHelper
{
[JSImport("globalThis.document.getElementById")]
internal static partial JSObject GetElementById(string id);
internal static partial JSObject? GetElementById(string id);
[JSImport("AvaloniaDOM.createAvaloniaHost", "avalonia.ts")]
public static partial JSObject CreateAvaloniaHost(JSObject element);
[JSImport("AvaloniaDOM.addClass", "avalonia.ts")]
public static partial void AddCssClass(JSObject element, string className);
[JSImport("SizeWatcher.observe", "avalonia.ts")]
public static partial JSObject ObserveSize(
JSObject canvas,

8
src/Web/Avalonia.Web/Interop/InputHelper.cs

@ -1,8 +1,6 @@
using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
using static System.Net.Mime.MediaTypeNames;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace Avalonia.Web.Interop;
@ -39,7 +37,7 @@ internal static partial class InputHelper
Func<JSObject, bool> pointerUp,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> wheel);
[JSImport("InputHelper.subscribeInputEvents", "avalonia.ts")]
public static partial void SubscribeInputEvents(
@ -72,9 +70,9 @@ internal static partial class InputHelper
[JSImport("InputHelper.setBounds", "avalonia.ts")]
public static partial void SetBounds(JSObject htmlElement, int x, int y, int width, int height, int caret);
[JSImport("navigator.clipboard.readText")]
[JSImport("globalThis.navigator.clipboard.readText")]
public static partial Task<string> ReadClipboardTextAsync();
[JSImport("navigator.clipboard.writeText")]
[JSImport("globalThis.navigator.clipboard.writeText")]
public static partial Task WriteClipboardTextAsync(string text);
}

28
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", "avalonia.ts")]
internal static partial JSObject CreateDefaultChild(JSObject? parent);
[JSImport("NativeControlHost.createAttachment", "avalonia.ts")]
internal static partial JSObject CreateAttachment();
[JSImport("NativeControlHost.initializeWithChildHandle", "avalonia.ts")]
internal static partial void InitializeWithChildHandle(JSObject element, JSObject child);
[JSImport("NativeControlHost.attachTo", "avalonia.ts")]
internal static partial void AttachTo(JSObject element, JSObject? host);
[JSImport("NativeControlHost.showInBounds", "avalonia.ts")]
internal static partial void ShowInBounds(JSObject element, double x, double y, double width, double height);
[JSImport("NativeControlHost.hideWithSize", "avalonia.ts")]
internal static partial void HideWithSize(JSObject element, double width, double height);
[JSImport("NativeControlHost.releaseChild", "avalonia.ts")]
internal static partial void ReleaseChild(JSObject element);
}

55
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", "avalonia.ts")]
public static partial bool CanShowOpenFilePicker();
[JSImport("Caniuse.canShowSaveFilePicker", "avalonia.ts")]
public static partial bool CanShowSaveFilePicker();
[JSImport("Caniuse.canShowDirectoryPicker", "avalonia.ts")]
public static partial bool CanShowDirectoryPicker();
[JSImport("StorageProvider.selectFolderDialog", "storage.ts")]
public static partial Task<JSObject?> SelectFolderDialog(JSObject? startIn);
[JSImport("StorageProvider.openFileDialog", "storage.ts")]
public static partial Task<JSObject?> OpenFileDialog(JSObject? startIn, bool multiple,
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption);
[JSImport("StorageProvider.saveFileDialog", "storage.ts")]
public static partial Task<JSObject?> SaveFileDialog(JSObject? startIn, string? suggestedName,
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption);
[JSImport("StorageProvider.openBookmark", "storage.ts")]
public static partial Task<JSObject?> OpenBookmark(string key);
[JSImport("StorageItem.saveBookmark", "storage.ts")]
public static partial Task<string?> SaveBookmark(JSObject item);
[JSImport("StorageItem.deleteBookmark", "storage.ts")]
public static partial Task DeleteBookmark(JSObject item);
[JSImport("StorageItem.getProperties", "storage.ts")]
public static partial Task<JSObject?> GetProperties(JSObject item);
[JSImport("StorageItem.openWrite", "storage.ts")]
public static partial Task<JSObject> OpenWrite(JSObject item);
[JSImport("StorageItem.openRead", "storage.ts")]
public static partial Task<JSObject> OpenRead(JSObject item);
[JSImport("StorageItem.getItems", "storage.ts")]
[return: JSMarshalAs<JSType.Promise<JSType.Object>>]
public static partial Task<JSObject> GetItems(JSObject item);
[JSImport("StorageItems.itemsArray", "storage.ts")]
public static partial JSObject[] ItemsArray(JSObject item);
[JSImport("StorageProvider.createAcceptType", "storage.ts")]
public static partial JSObject CreateAcceptType(string description, string[] mimeTypes);
}

35
src/Web/Avalonia.Web/Interop/StreamHelper.cs

@ -13,43 +13,28 @@ internal static partial class StreamHelper
public static partial void Seek(JSObject stream, [JSMarshalAs<JSType.Number>] long position);
[JSImport("StreamHelper.truncate", "avalonia.ts")]
public static partial void Truncate(JSObject stream, [JSMarshalAs<JSType.Number>] long position);
[JSImport("StreamHelper.write", "avalonia.ts")]
public static partial void Write(JSObject stream, [JSMarshalAs<JSType.MemoryView>] Span<byte> data);
public static partial void Truncate(JSObject stream, [JSMarshalAs<JSType.Number>] long size);
[JSImport("StreamHelper.write", "avalonia.ts")]
public static partial Task WriteAsync(JSObject stream, [JSMarshalAs<JSType.MemoryView>] ArraySegment<byte> data);
[JSImport("StreamHelper.close", "avalonia.ts")]
public static partial void Close(JSObject stream);
[JSImport("StreamHelper.close", "avalonia.ts")]
public static partial Task CloseAsync(JSObject stream);
[JSImport("StreamHelper.size", "avalonia.ts")]
[return: JSMarshalAs<JSType.Number>]
public static partial long Size(JSObject stream);
[JSImport("StreamHelper.byteLength", "avalonia.ts")]
[return: JSMarshalAs<JSType.Number>]
public static partial long ByteLength(JSObject stream);
[JSImport("StreamHelper.sliceToArray", "avalonia.ts")]
[return: JSMarshalAs<JSType.MemoryView>]
public static partial Span<byte> Slice(JSObject stream, [JSMarshalAs<JSType.Number>] long offset, int count);
[JSImport("StreamHelper.sliceArrayBuffer", "avalonia.ts")]
private static partial Task<JSObject> SliceToArrayBuffer(JSObject stream, [JSMarshalAs<JSType.Number>] long offset, int count);
[JSImport("StreamHelper.toMemoryView", "avalonia.ts")]
[return: JSMarshalAs<JSType.Array<JSType.Number>>]
private static partial byte[] ArrayBufferToMemoryView(JSObject stream);
public static async Task<ArraySegment<byte>> SliceAsync(JSObject stream, long offset, int count)
public static async Task<byte[]> SliceAsync(JSObject stream, long offset, int count)
{
using var buffer = await SliceToBufferAsync(stream, offset, count);
return BufferToArray(buffer);
using var buffer = await SliceToArrayBuffer(stream, offset, count);
return ArrayBufferToMemoryView(buffer);
}
[JSImport("StreamHelper.slice", "avalonia.ts")]
[return: JSMarshalAs<JSType.Promise<JSType.Object>>]
private static partial Task<JSObject> SliceToBufferAsync(JSObject stream, [JSMarshalAs<JSType.Number>] long offset, int count);
[JSImport("StreamHelper.toArray", "avalonia.ts")]
[return: JSMarshalAs<JSType.MemoryView>]
private static partial ArraySegment<byte> BufferToArray(JSObject stream);
}

30
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();
}
}
}

2
src/Web/Avalonia.Web/BrowserSkiaGpu.cs → src/Web/Avalonia.Web/Skia/BrowserSkiaGpu.cs

@ -1,7 +1,7 @@
using System.Collections.Generic;
using Avalonia.Skia;
namespace Avalonia.Web
namespace Avalonia.Web.Skia
{
public class BrowserSkiaGpu : ISkiaGpu
{

2
src/Web/Avalonia.Web/BrowserSkiaGpuRenderSession.cs → src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderSession.cs

@ -1,7 +1,7 @@
using Avalonia.Skia;
using SkiaSharp;
namespace Avalonia.Web
namespace Avalonia.Web.Skia
{
internal class BrowserSkiaGpuRenderSession : ISkiaGpuRenderSession
{

2
src/Web/Avalonia.Web/BrowserSkiaGpuRenderTarget.cs → src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderTarget.cs

@ -1,7 +1,7 @@
using Avalonia.Skia;
using SkiaSharp;
namespace Avalonia.Web
namespace Avalonia.Web.Skia
{
internal class BrowserSkiaGpuRenderTarget : ISkiaGpuRenderTarget
{

2
src/Web/Avalonia.Web/BrowserSkiaRasterSurface.cs → src/Web/Avalonia.Web/Skia/BrowserSkiaRasterSurface.cs

@ -5,7 +5,7 @@ using Avalonia.Platform;
using Avalonia.Skia;
using SkiaSharp;
namespace Avalonia.Web
namespace Avalonia.Web.Skia
{
internal class BrowserSkiaRasterSurface : IBrowserSkiaSurface, IFramebufferPlatformSurface, IDisposable
{

4
src/Web/Avalonia.Web/BrowserSkiaSurface.cs → src/Web/Avalonia.Web/Skia/BrowserSkiaSurface.cs

@ -1,7 +1,7 @@
using Avalonia.Web.Interop;
using SkiaSharp;
namespace Avalonia.Web
namespace Avalonia.Web.Skia
{
internal class BrowserSkiaSurface : IBrowserSkiaSurface
{
@ -14,7 +14,7 @@ namespace Avalonia.Web
Scaling = scaling;
Origin = origin;
}
public SKColorType ColorType { get; set; }
public PixelSize Size { get; set; }

2
src/Web/Avalonia.Web/IBrowserSkiaSurface.cs → src/Web/Avalonia.Web/Skia/IBrowserSkiaSurface.cs

@ -1,4 +1,4 @@
namespace Avalonia.Web
namespace Avalonia.Web.Skia
{
internal interface IBrowserSkiaSurface
{

27
src/Web/Avalonia.Web/Storage/BlobReadableStream.cs

@ -6,7 +6,7 @@ using System.Threading.Tasks;
namespace Avalonia.Web.Storage;
[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings
[System.Runtime.Versioning.SupportedOSPlatform("browser")]
internal class BlobReadableStream : Stream
{
private JSObject? _jSReference;
@ -20,7 +20,7 @@ internal class BlobReadableStream : Stream
_length = StreamHelper.ByteLength(JSReference);
}
private JSObject JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(JSWriteableStream));
private JSObject JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(WriteableStream));
public override bool CanRead => true;
@ -55,21 +55,8 @@ internal class BlobReadableStream : Stream
=> throw new NotSupportedException();
public override int Read(byte[] buffer, int offset, int count)
=> Read(buffer.AsSpan(offset, count));
public override int Read(Span<byte> buffer)
{
var numBytesToRead = (int)Math.Min(buffer.Length, Length - _position);
var bytesRead = StreamHelper.Slice(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;
throw new InvalidOperationException("Browser supports only ReadAsync");
}
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
@ -79,15 +66,15 @@ internal class BlobReadableStream : Stream
{
var numBytesToRead = (int)Math.Min(buffer.Length, Length - _position);
var bytesRead = await StreamHelper.SliceAsync(JSReference, _position, numBytesToRead);
if (bytesRead.Count != numBytesToRead)
if (bytesRead.Length != numBytesToRead)
{
throw new EndOfStreamException("Failed to read the requested number of bytes from the stream.");
}
_position += bytesRead.Count;
bytesRead.AsMemory().CopyTo(buffer);
_position += bytesRead.Length;
bytesRead.CopyTo(buffer);
return bytesRead.Count;
return bytesRead.Length;
}
protected override void Dispose(bool disposing)

257
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<string, IReadOnlyList<string>> Accept);
[SupportedOSPlatform("browser")]
internal class BrowserStorageProvider : IStorageProvider
{
internal const string PickerCancelMessage = "The user aborted a request";
internal const string NoPermissionsMessage = "Permissions denied";
private readonly Lazy<Task<JSObject>> _lazyModule = new(() => JSHost.ImportAsync("storage.ts", "./storage.js"));
public bool CanOpen => StorageHelper.CanShowOpenFilePicker();
public bool CanSave => StorageHelper.CanShowSaveFilePicker();
public bool CanPickFolder => StorageHelper.CanShowDirectoryPicker();
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
_ = await _lazyModule.Value;
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
var (types, exludeAll) = ConvertFileTypes(options.FileTypeFilter);
try
{
using var items = await StorageHelper.OpenFileDialog(startIn, options.AllowMultiple, types, exludeAll);
if (items is null)
{
return Array.Empty<IStorageFile>();
}
var itemsArray = StorageHelper.ItemsArray(items);
return itemsArray.Select(item => new JSStorageFile(item)).ToArray();
}
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal))
{
return Array.Empty<IStorageFile>();
}
finally
{
if (types is not null)
{
foreach (var type in types)
{
type.Dispose();
}
}
}
}
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
_ = await _lazyModule.Value;
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
var (types, exludeAll) = ConvertFileTypes(options.FileTypeChoices);
try
{
var item = await StorageHelper.SaveFileDialog(startIn, options.SuggestedFileName, types, exludeAll);
return item is not null ? new JSStorageFile(item) : null;
}
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal))
{
return null;
}
finally
{
if (types is not null)
{
foreach (var type in types)
{
type.Dispose();
}
}
}
}
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
_ = await _lazyModule.Value;
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
try
{
var item = await StorageHelper.SelectFolderDialog(startIn);
return item is not null ? new[] { new JSStorageFolder(item) } : Array.Empty<IStorageFolder>();
}
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal))
{
return Array.Empty<IStorageFolder>();
}
}
public async Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
{
_ = await _lazyModule.Value;
var item = await StorageHelper.OpenBookmark(bookmark);
return item is not null ? new JSStorageFile(item) : null;
}
public async Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
{
_ = await _lazyModule.Value;
var item = await StorageHelper.OpenBookmark(bookmark);
return item is not null ? new JSStorageFolder(item) : null;
}
private static (JSObject[]? types, bool excludeAllOption) ConvertFileTypes(IEnumerable<FilePickerFileType>? input)
{
var types = input?
.Where(t => t.MimeTypes?.Any() == true && t != FilePickerFileTypes.All)
.Select(t => StorageHelper.CreateAcceptType(t.Name, t.MimeTypes!.ToArray()))
.ToArray();
if (types?.Length == 0)
{
types = null;
}
var inlcudeAll = input?.Contains(FilePickerFileTypes.All) == true || types is null;
return (types, !inlcudeAll);
}
}
internal abstract class JSStorageItem : IStorageBookmarkItem
{
internal JSObject? _fileHandle;
protected JSStorageItem(JSObject fileHandle)
{
_fileHandle = fileHandle ?? throw new ArgumentNullException(nameof(fileHandle));
}
internal JSObject FileHandle => _fileHandle ?? throw new ObjectDisposedException(nameof(JSStorageItem));
public string Name => FileHandle.GetPropertyAsString("name") ?? string.Empty;
public bool TryGetUri([NotNullWhen(true)] out Uri? uri)
{
uri = new Uri(Name, UriKind.Relative);
return false;
}
public async Task<StorageItemProperties> GetBasicPropertiesAsync()
{
using var properties = await StorageHelper.GetProperties(FileHandle);
var size = (long?)properties?.GetPropertyAsDouble("Size");
var lastModified = (long?)properties?.GetPropertyAsDouble("LastModified");
return new StorageItemProperties(
(ulong?)size,
dateCreated: null,
dateModified: lastModified > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(lastModified.Value) : null);
}
public bool CanBookmark => true;
public Task<string?> SaveBookmarkAsync()
{
return StorageHelper.SaveBookmark(FileHandle);
}
public Task<IStorageFolder?> GetParentAsync()
{
return Task.FromResult<IStorageFolder?>(null);
}
public Task ReleaseBookmarkAsync()
{
return StorageHelper.DeleteBookmark(FileHandle);
}
public void Dispose()
{
_fileHandle?.Dispose();
_fileHandle = null;
}
}
internal class JSStorageFile : JSStorageItem, IStorageBookmarkFile
{
public JSStorageFile(JSObject fileHandle) : base(fileHandle)
{
}
public bool CanOpenRead => true;
public async Task<Stream> OpenReadAsync()
{
try
{
var blob = await StorageHelper.OpenRead(FileHandle);
return new BlobReadableStream(blob);
}
catch (JSException ex) when (ex.Message == BrowserStorageProvider.NoPermissionsMessage)
{
throw new UnauthorizedAccessException("User denied permissions to open the file", ex);
}
}
public bool CanOpenWrite => true;
public async Task<Stream> OpenWriteAsync()
{
try
{
using var properties = await StorageHelper.GetProperties(FileHandle);
var streamWriter = await StorageHelper.OpenWrite(FileHandle);
var size = (long?)properties?.GetPropertyAsDouble("Size") ?? 0;
return new WriteableStream(streamWriter, size);
}
catch (JSException ex) when (ex.Message == BrowserStorageProvider.NoPermissionsMessage)
{
throw new UnauthorizedAccessException("User denied permissions to open the file", ex);
}
}
}
internal class JSStorageFolder : JSStorageItem, IStorageBookmarkFolder
{
public JSStorageFolder(JSObject fileHandle) : base(fileHandle)
{
}
public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
{
using var items = await StorageHelper.GetItems(FileHandle);
if (items is null)
{
return Array.Empty<IStorageItem>();
}
var itemsArray = StorageHelper.ItemsArray(items);
return itemsArray
.Select(reference => reference.GetPropertyAsString("kind") switch
{
"directory" => (IStorageItem)new JSStorageFolder(reference),
"file" => new JSStorageFile(reference),
_ => null
})
.Where(i => i is not null)
.ToArray()!;
}
}

27
src/Web/Avalonia.Web/Storage/WriteableStream.cs

@ -6,22 +6,22 @@ using System.Threading.Tasks;
namespace Avalonia.Web.Storage;
[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings
[System.Runtime.Versioning.SupportedOSPlatform("browser")]
// Loose wrapper implementaion of a stream on top of FileAPI FileSystemWritableFileStream
internal sealed class JSWriteableStream : Stream
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 JSWriteableStream(JSObject jSReference, long initialLength)
internal WriteableStream(JSObject jSReference, long initialLength)
{
_jSReference = jSReference;
_length = initialLength;
}
private JSObject JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(JSWriteableStream));
private JSObject JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(WriteableStream));
public override bool CanRead => false;
@ -75,20 +75,7 @@ internal sealed class JSWriteableStream : Stream
public override void Write(byte[] buffer, int offset, int count)
{
StreamHelper.Write(JSReference, buffer.AsSpan(offset, count));
}
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);
throw new InvalidOperationException("Browser supports only WriteAsync");
}
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
@ -110,7 +97,7 @@ internal sealed class JSWriteableStream : Stream
_jSReference = null;
try
{
StreamHelper.Close(JSReference);
_ = StreamHelper.CloseAsync(jsReference);
}
finally
{
@ -126,7 +113,7 @@ internal sealed class JSWriteableStream : Stream
_jSReference = null;
try
{
await StreamHelper.CloseAsync(JSReference);
await StreamHelper.CloseAsync(jsReference);
}
finally
{

68
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);
}
}
}
}

47
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/*"]
}

8
src/Web/Avalonia.Web/webapp/build.js

@ -10,7 +10,7 @@ require("esbuild").build({
target: "es2016",
platform: "browser",
sourcemap: "linked",
loader: {".ts": "ts"}
})
.then(() => console.log("⚡ Done"))
.catch(() => process.exit(1));
loader: { ".ts": "ts" }
})
.then(() => console.log("⚡ Done"))
.catch(() => process.exit(1));

14
src/Web/Avalonia.Web/webapp/modules/avalonia.ts

@ -1,18 +1,20 @@
import { RuntimeAPI } from "../types/dotnet";
import { SizeWatcher } from "./avalonia/canvas";
import { DpiWatcher } from "./avalonia/canvas";
import { Canvas } from "./avalonia/canvas";
import { RuntimeAPI } from "../types/dotnet";
import { SizeWatcher, DpiWatcher, Canvas } from "./avalonia/canvas";
import { InputHelper } from "./avalonia/input";
import { AvaloniaDOM } from "./avalonia/dom";
import { CaretHelper } from "./avalonia/CaretHelper"
import { Caniuse } from "./avalonia/caniuse";
import { StreamHelper } from "./avalonia/stream";
import { NativeControlHost } from "./avalonia/NativeControlHost";
export async function createAvaloniaRuntime(api: RuntimeAPI): Promise<void> {
api.setModuleImports("avalonia.ts", {
Caniuse,
Canvas,
InputHelper,
SizeWatcher,
DpiWatcher,
AvaloniaDOM,
CaretHelper
StreamHelper,
NativeControlHost
});
}

13
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";
}
}

137
src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts

@ -1,6 +1,4 @@
declare let Module: EmscriptenModule;
type SKGLViewInfo = {
interface SKGLViewInfo {
context: WebGLRenderingContext | WebGL2RenderingContext | undefined;
fboId: number;
stencil: number;
@ -9,8 +7,8 @@ type SKGLViewInfo = {
}
type CanvasElement = {
Canvas: Canvas | undefined
} & HTMLCanvasElement
Canvas: Canvas | undefined;
} & HTMLCanvasElement;
export class Canvas {
static elements: Map<string, HTMLCanvasElement>;
@ -24,22 +22,24 @@ export class Canvas {
newHeight?: number;
public static initGL(element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): SKGLViewInfo | null {
var view = Canvas.init(true, element, elementId, renderFrameCallback);
if (!view || !view.glInfo)
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 {
var htmlCanvas = element as CanvasElement;
const htmlCanvas = element as CanvasElement;
if (!htmlCanvas) {
console.error(`No canvas element was provided.`);
console.error("No canvas element was provided.");
return null;
}
if (!Canvas.elements)
if (!Canvas.elements) {
Canvas.elements = new Map<string, HTMLCanvasElement>();
}
Canvas.elements.set(elementId, element);
const view = new Canvas(useGL, element, renderFrameCallback);
@ -56,16 +56,16 @@ export class Canvas {
if (useGL) {
const ctx = Canvas.createWebGLContext(element);
if (!ctx) {
console.error(`Failed to create WebGL context: err ${ctx}`);
console.error("Failed to create WebGL context");
return;
}
var GL = (globalThis as any).AvaloniaGL;
const GL = (globalThis as any).AvaloniaGL;
// make current
GL.makeContextCurrent(ctx);
var GLctx = GL.currentContext.GLctx as WebGLRenderingContext;
const GLctx = GL.currentContext.GLctx as WebGLRenderingContext;
// read values
const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING);
@ -75,17 +75,17 @@ export class Canvas {
fboId: fbo ? fbo.id : 0,
stencil: GLctx.getParameter(GLctx.STENCIL_BITS),
sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES)
depth: GLctx.getParameter(GLctx.DEPTH_BITS),
depth: GLctx.getParameter(GLctx.DEPTH_BITS)
};
}
}
public setEnableRenderLoop(enable: boolean) {
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}...`);
// console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`);
this.requestAnimationFrame();
} else if (this.renderLoopRequest !== 0) {
window.cancelAnimationFrame(this.renderLoopRequest);
@ -93,71 +93,76 @@ export class Canvas {
}
}
public requestAnimationFrame(renderLoop?: boolean) {
public requestAnimationFrame(renderLoop?: boolean): void {
// optionally update the render loop
if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop)
if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) {
this.setEnableRenderLoop(renderLoop);
}
// skip because we have a render loop
if (this.renderLoopRequest !== 0)
if (this.renderLoopRequest !== 0) {
return;
}
// add the draw to the next frame
this.renderLoopRequest = window.requestAnimationFrame(() => {
if (this.glInfo) {
var GL = (globalThis as any).AvaloniaGL;
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.width !== this.newWidth) {
this.htmlCanvas.width = this.newWidth ?? 0;
}
if (this.htmlCanvas.height != this.newHeight) {
this.htmlCanvas.height = this.newHeight || 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)
if (this.renderLoopEnabled) {
this.requestAnimationFrame();
}
});
}
public setCanvasSize(width: number, height: number) {
public setCanvasSize(width: number, height: number): void {
this.newWidth = width;
this.newHeight = height;
if (this.htmlCanvas.width != this.newWidth) {
if (this.htmlCanvas.width !== this.newWidth) {
this.htmlCanvas.width = this.newWidth;
}
if (this.htmlCanvas.height != this.newHeight) {
if (this.htmlCanvas.height !== this.newHeight) {
this.htmlCanvas.height = this.newHeight;
}
if (this.glInfo) {
var GL = (globalThis as any).AvaloniaGL;
const GL = (globalThis as any).AvaloniaGL;
// make current
GL.makeContextCurrent(this.glInfo.context);
}
}
public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number) {
public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number): void {
const htmlCanvas = element as CanvasElement;
if (!htmlCanvas || !htmlCanvas.Canvas)
if (!htmlCanvas || !htmlCanvas.Canvas) {
return;
}
htmlCanvas.Canvas.setCanvasSize(width, height);
}
public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean) {
public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean): void {
const htmlCanvas = element as CanvasElement;
if (!htmlCanvas || !htmlCanvas.Canvas)
if (!htmlCanvas || !htmlCanvas.Canvas) {
return;
}
htmlCanvas.Canvas.requestAnimationFrame(renderLoop);
}
@ -176,15 +181,15 @@ export class Canvas {
minorVersion: 0,
enableExtensionsByDefault: 1,
explicitSwapControl: 0,
renderViaOffscreenBackBuffer: 1,
renderViaOffscreenBackBuffer: 1
};
var GL = (globalThis as any).AvaloniaGL;
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');
console.warn("Falling back to WebGL 1.0");
contextAttributes.majorVersion = 1;
contextAttributes.minorVersion = 0;
ctx = GL.createContext(htmlCanvas, contextAttributes);
@ -196,9 +201,9 @@ export class Canvas {
type SizeWatcherElement = {
SizeWatcher: SizeWatcherInstance;
} & HTMLElement
} & HTMLElement;
type SizeWatcherInstance = {
interface SizeWatcherInstance {
callback: (width: number, height: number) => void;
}
@ -206,17 +211,16 @@ export class SizeWatcher {
static observer: ResizeObserver;
static elements: Map<string, HTMLElement>;
public static observe(element: HTMLElement, elementId: string, callback: (width: number, height: number) => void) {
if (!element || !callback)
public static observe(element: HTMLElement, elementId: string, callback: (width: number, height: number) => void): void {
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
callback
};
SizeWatcher.elements.set(elementId, element);
@ -225,38 +229,38 @@ export class SizeWatcher {
SizeWatcher.invoke(element);
}
public static unobserve(elementId: string) {
if (!elementId || !SizeWatcher.observer)
public static unobserve(elementId: string): void {
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);
const element = SizeWatcher.elements.get(elementId);
if (element) {
SizeWatcher.elements.delete(elementId);
SizeWatcher.observer.unobserve(element);
}
}
static init() {
if (SizeWatcher.observer)
static init(): void {
if (SizeWatcher.observer) {
return;
//console.info('Starting size watcher...');
}
SizeWatcher.elements = new Map<string, HTMLElement>();
SizeWatcher.observer = new ResizeObserver((entries) => {
for (let entry of entries) {
for (const entry of entries) {
SizeWatcher.invoke(entry.target);
}
});
}
static invoke(element: Element) {
static invoke(element: Element): void {
const watcherElement = element as SizeWatcherElement;
const instance = watcherElement.SizeWatcher;
if (!instance || !instance.callback)
if (!instance || !instance.callback) {
return;
}
return instance.callback(element.clientWidth, element.clientHeight);
}
@ -267,13 +271,11 @@ export class DpiWatcher {
static timerId: number;
static callback: (old: number, newdpi: number) => void;
public static getDpi() {
public static getDpi(): number {
return window.devicePixelRatio;
}
public static start(callback: (old: number, newdpi: number) => void) : number {
//console.info(`Starting DPI watcher with callback ${callback._id}...`);
public static start(callback: (old: number, newdpi: number) => void): number {
DpiWatcher.lastDpi = window.devicePixelRatio;
DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000);
DpiWatcher.callback = callback;
@ -281,17 +283,14 @@ export class DpiWatcher {
return DpiWatcher.lastDpi;
}
public static stop() {
//console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`);
public static stop(): void {
window.clearInterval(DpiWatcher.timerId);
//DpiWatcher.callback = undefined;
}
static update() {
if (!DpiWatcher.callback)
static update(): void {
if (!DpiWatcher.callback) {
return;
}
const currentDpi = window.devicePixelRatio;
const lastDpi = DpiWatcher.lastDpi;

22
src/Web/Avalonia.Web/webapp/modules/avalonia/CaretHelper.ts → 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";

19
src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts

@ -1,4 +1,8 @@
export class AvaloniaDOM {
public static addClass(element: HTMLElement, className: string): void {
element.classList.add(className);
}
static createAvaloniaHost(host: HTMLElement) {
// Root element
host.classList.add("avalonia-container");
@ -7,26 +11,24 @@ export class AvaloniaDOM {
// Rendering target canvas
const canvas = document.createElement("canvas");
canvas.classList.add('avalonia-canvas');
canvas.classList.add("avalonia-canvas");
canvas.style.backgroundColor = "#ccc";
canvas.style.width = "100%";
canvas.style.height = "100%";
canvas.style.position = "absolute";
host.appendChild(canvas);
// Native controls host
const nativeHost = document.createElement("div");
nativeHost.classList.add('avalonia-native-host');
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";
host.appendChild(nativeHost);
// IME
const inputElement = document.createElement("input");
inputElement.classList.add('avalonia-input-element');
inputElement.classList.add("avalonia-input-element");
inputElement.autocapitalize = "none";
inputElement.type = "text";
inputElement.spellcheck = false;
@ -43,7 +45,10 @@ export class AvaloniaDOM {
inputElement.onpaste = function () { return false; };
inputElement.oncopy = function () { return false; };
inputElement.oncut = function () { return false; };
host.appendChild(inputElement);
host.prepend(inputElement);
host.prepend(nativeHost);
host.prepend(canvas);
return {
host,

51
src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts

@ -1,7 +1,6 @@
import { CaretHelper } from "./CaretHelper"
import { CaretHelper } from "./caretHelper";
enum RawInputModifiers
{
enum RawInputModifiers {
None = 0,
Alt = 1,
Control = 2,
@ -24,12 +23,7 @@ export class InputHelper {
public static subscribeKeyEvents(
element: HTMLInputElement,
keyDownCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean,
keyUpCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean,
inputCallback: (type: string, data: string|null) => boolean,
compositionStartCallback: (args: CompositionEvent) => boolean,
compositionUpdateCallback: (args: CompositionEvent) => boolean,
compositionEndCallback: (args: CompositionEvent) => boolean)
{
keyUpCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean) {
const keyDownHandler = (args: KeyboardEvent) => {
if (keyDownCallback(args.code, args.key, this.getModifiers(args))) {
args.preventDefault();
@ -52,14 +46,13 @@ export class InputHelper {
}
public static subscribeTextEvents(
element: HTMLInputElement,
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) => {
var inputEvent = args as InputEvent;
const inputEvent = args as InputEvent;
// todo check cast
if (inputCallback(inputEvent.type, inputEvent.data)) {
@ -102,31 +95,27 @@ export class InputHelper {
pointerMoveCallback: (args: PointerEvent) => boolean,
pointerDownCallback: (args: PointerEvent) => boolean,
pointerUpCallback: (args: PointerEvent) => boolean,
wheelCallback: (args: WheelEvent) => 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();
}
@ -137,8 +126,6 @@ export class InputHelper {
element.addEventListener("pointerup", pointerUpHandler);
element.addEventListener("wheel", wheelHandler);
return () => {
element.removeEventListener("pointerover", pointerMoveHandler);
element.removeEventListener("pointerdown", pointerDownHandler);
@ -146,7 +133,7 @@ export class InputHelper {
element.removeEventListener("wheel", wheelHandler);
};
}
public static subscribeInputEvents(
element: HTMLInputElement,
inputCallback: (value: string) => boolean
@ -179,18 +166,18 @@ export class InputHelper {
inputElement.style.left = (x).toFixed(0) + "px";
inputElement.style.top = (y).toFixed(0) + "px";
let { height, left, top } = CaretHelper.getCaretCoordinates(inputElement, caret);
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';
inputElement.style.display = "none";
}
public static show(inputElement: HTMLInputElement) {
inputElement.style.display = 'block';
inputElement.style.display = "block";
}
public static setSurroundingText(inputElement: HTMLInputElement, text: string, start: number, end: number) {
@ -201,20 +188,16 @@ export class InputHelper {
inputElement.value = text;
inputElement.setSelectionRange(start, end);
inputElement.style.width = "20px";
inputElement.style.width = inputElement.scrollWidth + "px";
inputElement.style.width = `${inputElement.scrollWidth}px`;
}
private static getModifiers(args: KeyboardEvent): RawInputModifiers {
var 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;
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;
}

55
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;
}
}
}

40
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;
}
}

7
src/Web/Avalonia.Web/webapp/modules/storage.ts

@ -1,5 +1,2 @@
export class StorageProvider {
static isFileApiSupported(): boolean {
return (globalThis as any).showOpenFilePicker !== undefined;
}
}
export { StorageItem, StorageItems } from "./storage/storageItem";
export { StorageProvider } from "./storage/storageProvider";

84
src/Web/Avalonia.Web/webapp/modules/storage/indexedDb.ts

@ -0,0 +1,84 @@
class InnerDbConnection {
constructor(private readonly database: IDBDatabase) { }
private openStore(store: string, mode: IDBTransactionMode): IDBObjectStore {
const tx = this.database.transaction(store, mode);
return tx.objectStore(store);
}
public async put(store: string, obj: any, key?: IDBValidKey): Promise<IDBValidKey> {
const os = this.openStore(store, "readwrite");
return await new Promise((resolve, reject) => {
const response = os.put(obj, key);
response.onsuccess = () => {
resolve(response.result);
};
response.onerror = () => {
reject(response.error);
};
});
}
public get(store: string, key: IDBValidKey): any {
const os = this.openStore(store, "readonly");
return new Promise((resolve, reject) => {
const response = os.get(key);
response.onsuccess = () => {
resolve(response.result);
};
response.onerror = () => {
reject(response.error);
};
});
}
public async delete(store: string, key: IDBValidKey): Promise<void> {
const os = this.openStore(store, "readwrite");
return await new Promise((resolve, reject) => {
const response = os.delete(key);
response.onsuccess = () => {
resolve();
};
response.onerror = () => {
reject(response.error);
};
});
}
public close() {
this.database.close();
}
}
class IndexedDbWrapper {
constructor(private readonly databaseName: string, private readonly objectStores: [string]) {
}
public async connect(): Promise<InnerDbConnection> {
const conn = window.indexedDB.open(this.databaseName, 1);
conn.onupgradeneeded = event => {
const db = (event.target as IDBRequest<IDBDatabase>).result;
this.objectStores.forEach(store => {
db.createObjectStore(store);
});
};
return await new Promise((resolve, reject) => {
conn.onsuccess = event => {
resolve(new InnerDbConnection((event.target as IDBRequest<IDBDatabase>).result));
};
conn.onerror = event => {
reject((event.target as IDBRequest<IDBDatabase>).error);
};
});
}
}
export const fileBookmarksStore: string = "fileBookmarks";
export const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [
fileBookmarksStore
]);

111
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<Blob> {
if (!(item.handle instanceof FileSystemFileHandle)) {
throw new Error("StorageItem is not a file");
}
await item.verityPermissions("read");
const file = await item.handle.getFile();
return file;
}
public static async openWrite(item: StorageItem): Promise<FileSystemWritableFileStream> {
if (!(item.handle instanceof FileSystemFileHandle)) {
throw new Error("StorageItem is not a file");
}
await item.verityPermissions("readwrite");
return await item.handle.createWritable({ keepExistingData: true });
}
public static async getProperties(item: StorageItem): Promise<{ Size: number; LastModified: number; Type: string } | null> {
const file = item.handle instanceof FileSystemFileHandle &&
await item.handle.getFile();
if (!file) {
return null;
}
return {
Size: file.size,
LastModified: file.lastModified,
Type: file.type
};
}
public static async getItems(item: StorageItem): Promise<StorageItems> {
if (item.handle.kind !== "directory") {
return new StorageItems([]);
}
const items: StorageItem[] = [];
for await (const [, value] of (item.handle as any).entries()) {
items.push(new StorageItem(value));
}
return new StorageItems(items);
}
private async verityPermissions(mode: FileSystemPermissionMode): Promise<void | never> {
if (await this.handle.queryPermission({ mode }) === "granted") {
return;
}
if (await this.handle.requestPermission({ mode }) === "denied") {
throw new Error("Permissions denied");
}
}
public static async saveBookmark(item: StorageItem): Promise<string> {
// If file was previously bookmarked, just return old one.
if (item.bookmarkId) {
return item.bookmarkId;
}
const connection = await avaloniaDb.connect();
try {
const key = await connection.put(fileBookmarksStore, item.handle, item.generateBookmarkId());
return key as string;
} finally {
connection.close();
}
}
public static async deleteBookmark(item: StorageItem): Promise<void> {
if (!item.bookmarkId) {
return;
}
const connection = await avaloniaDb.connect();
try {
await connection.delete(fileBookmarksStore, item.bookmarkId);
} finally {
connection.close();
}
}
private generateBookmarkId(): string {
return Date.now().toString(36) + Math.random().toString(36).substring(2);
}
}
export class StorageItems {
constructor(private readonly items: StorageItem[]) { }
public static itemsArray(instance: StorageItems): StorageItem[] {
return instance.items;
}
}

70
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<StorageItem> {
// 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined.
const options: DirectoryPickerOptions = {
startIn: (startIn?.handle ?? undefined)
};
const handle = await window.showDirectoryPicker(options);
return new StorageItem(handle);
}
public static async openFileDialog(
startIn: StorageItem | null, multiple: boolean,
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItems> {
const options: OpenFilePickerOptions = {
startIn: (startIn?.handle ?? undefined),
multiple,
excludeAcceptAllOption,
types: (types ?? undefined)
};
const handles = await window.showOpenFilePicker(options);
return new StorageItems(handles.map((handle: FileSystemHandle) => new StorageItem(handle)));
}
public static async saveFileDialog(
startIn: StorageItem | null, suggestedName: string | null,
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItem> {
const options: SaveFilePickerOptions = {
startIn: (startIn?.handle ?? undefined),
suggestedName: (suggestedName ?? undefined),
excludeAcceptAllOption,
types: (types ?? undefined)
};
const handle = await window.showSaveFilePicker(options);
return new StorageItem(handle);
}
public static async openBookmark(key: string): Promise<StorageItem | null> {
const connection = await avaloniaDb.connect();
try {
const handle = await connection.get(fileBookmarksStore, key);
return handle && new StorageItem(handle, key);
} finally {
connection.close();
}
}
public static createAcceptType(description: string, mimeTypes: string[]): FilePickerAcceptType {
const accept: Record<string, string[]> = {};
mimeTypes.forEach(a => { accept[a] = []; });
return { description, accept };
}
}

2330
src/Web/Avalonia.Web/webapp/package-lock.json

File diff suppressed because it is too large

15
src/Web/Avalonia.Web/webapp/package.json

@ -1,13 +1,22 @@
{
"name": "avalonia.web",
"scripts": {
"prebuild": "node ./node_modules/typescript/bin/tsc -noEmit",
"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": "^4.7.4",
"esbuild": "^0.15.7"
"@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"
}
}

23
src/Web/Avalonia.Web/webapp/types/dotnet.d.ts

@ -246,4 +246,25 @@ declare global {
declare const dotnet: ModuleAPI["dotnet"];
declare const exit: ModuleAPI["exit"];
export { CreateDotnetRuntimeType, DotnetModuleConfig, EmscriptenModule, ModuleAPI, MonoConfig, RuntimeAPI, createDotnetRuntime as default, dotnet, 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;
}

3
src/Windows/Avalonia.Win32/Avalonia.Win32.csproj

@ -16,6 +16,9 @@
<MicroComIdl Include="WinRT\winrt.idl" CSharpInteropPath="WinRT\WinRT.Generated.cs" />
<MicroComIdl Include="Win32Com\win32.idl" CSharpInteropPath="Win32Com\Win32.Generated.cs" />
</ItemGroup>
<ItemGroup>
<Compile Remove="..\..\Shared\ModuleInitializer.cs" />
</ItemGroup>
<Import Project="$(MSBuildThisFileDirectory)\..\..\..\build\System.Drawing.Common.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" />
<PropertyGroup Label="Warnings">

Loading…
Cancel
Save